mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-08 07:56:44 +02:00
Compare commits
233 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df69f88186 | ||
|
|
8d1323f354 | ||
|
|
2f598b618b | ||
|
|
baf0b0b92c | ||
|
|
27a75250a6 | ||
|
|
15f60402a5 | ||
|
|
41c6fc90b3 | ||
|
|
34356b04a8 | ||
|
|
2ca02b6539 | ||
|
|
cd0c71dffb | ||
|
|
a59120fe19 | ||
|
|
fdc35f48ed | ||
|
|
bae9b7ddc3 | ||
|
|
3dd9b7e587 | ||
|
|
44c905bdab | ||
|
|
5214040257 | ||
|
|
7ad9f901dd | ||
|
|
f472d52954 | ||
|
|
1e752af41b | ||
|
|
3ba46c3fc6 | ||
|
|
bf83da476f | ||
|
|
6b8c4cf8d0 | ||
|
|
445329c07a | ||
|
|
8f370e19c6 | ||
|
|
736dbcfb58 | ||
|
|
c1cb7f87e0 | ||
|
|
3c14fcefc9 | ||
|
|
831badf8db | ||
|
|
5f8a6e8d24 | ||
|
|
a3981493f7 | ||
|
|
6919cf5d4d | ||
|
|
f6d1e6bdd6 | ||
|
|
a13ff89acd | ||
|
|
cd5c76279a | ||
|
|
debff5e4b8 | ||
|
|
433e207ec5 | ||
|
|
3775f6a907 | ||
|
|
10d611c0eb | ||
|
|
963bc4d672 | ||
|
|
e68a21ee30 | ||
|
|
d5c083b70f | ||
|
|
2deb9cf417 | ||
|
|
fca0cfcdc7 | ||
|
|
1466e1bdb3 | ||
|
|
dd48bc443a | ||
|
|
f455738e5f | ||
|
|
85193803cd | ||
|
|
4e90a6074a | ||
|
|
ca94fa5184 | ||
|
|
f14a7c996f | ||
|
|
989d22f4d8 | ||
|
|
400a812343 | ||
|
|
97284f1ced | ||
|
|
5e6a0d7e16 | ||
|
|
599e11245f | ||
|
|
538d714c96 | ||
|
|
953915ba2a | ||
|
|
1a2f5da055 | ||
|
|
749a469d37 | ||
|
|
c7d084321a | ||
|
|
00a61cd6cf | ||
|
|
d9c6c13c62 | ||
|
|
5260c5e889 | ||
|
|
1700f96c62 | ||
|
|
5a0f9f8bbb | ||
|
|
8fa22b4de2 | ||
|
|
d17cae16fd | ||
|
|
c4c48962cf | ||
|
|
4140e92f0b | ||
|
|
887e560a1b | ||
|
|
e2d70181e8 | ||
|
|
148c453dbc | ||
|
|
f6ee9d311d | ||
|
|
35209e43bb | ||
|
|
4a3ecf063d | ||
|
|
4c79101624 | ||
|
|
921b01661b | ||
|
|
c63301d8e6 | ||
|
|
c9ed9d2237 | ||
|
|
ca55cbe456 | ||
|
|
d4828613ee | ||
|
|
a0be02ce2f | ||
|
|
9b8016a4d5 | ||
|
|
8fdd3bfd21 | ||
|
|
695a061e3c | ||
|
|
bd50d9218e | ||
|
|
d8f13bc1ce | ||
|
|
ed84d1a3c9 | ||
|
|
3c765d879c | ||
|
|
4c76b6dd96 | ||
|
|
7e66917993 | ||
|
|
eb0f985399 | ||
|
|
3a2908c3a3 | ||
|
|
f13c10859f | ||
|
|
8e8a314c91 | ||
|
|
dad0478b87 | ||
|
|
240b34016d | ||
|
|
2c164d630c | ||
|
|
3d12a9038f | ||
|
|
71fc02b5cd | ||
|
|
74caaa3d94 | ||
|
|
e024c12521 | ||
|
|
1d0c364947 | ||
|
|
88e59405c5 | ||
|
|
8160c93c15 | ||
|
|
3c8113d607 | ||
|
|
8e2f421e76 | ||
|
|
56df833b4a | ||
|
|
119d7d0be0 | ||
|
|
d4d2ca19a1 | ||
|
|
4ce5597d67 | ||
|
|
17888327b3 | ||
|
|
997def54ab | ||
|
|
a407e75459 | ||
|
|
cece53aad9 | ||
|
|
a2d63bc5f8 | ||
|
|
0c81c63ab7 | ||
|
|
3e8f1eee22 | ||
|
|
a91d1def45 | ||
|
|
032e6d1c57 | ||
|
|
93eb133cba | ||
|
|
b7b78842da | ||
|
|
8100ccaf5f | ||
|
|
936e1c0e96 | ||
|
|
1a7274e475 | ||
|
|
ac7b83b6ed | ||
|
|
e298279032 | ||
|
|
f7e58c28c6 | ||
|
|
18f3bc4056 | ||
|
|
4638f82889 | ||
|
|
4396b6eab5 | ||
|
|
594ae2bb7e | ||
|
|
429cf54bd4 | ||
|
|
274357abc4 | ||
|
|
0e21247755 | ||
|
|
37f2868ff3 | ||
|
|
6a85e25a62 | ||
|
|
a91b1c7e79 | ||
|
|
bfd9ecd130 | ||
|
|
ec888e94fd | ||
|
|
a19c80bef2 | ||
|
|
7b9ade2698 | ||
|
|
d03f28eeb9 | ||
|
|
288dd788bc | ||
|
|
2069a5a931 | ||
|
|
8a434cbd15 | ||
|
|
faafe49848 | ||
|
|
17a72292ce | ||
|
|
9ae09f1801 | ||
|
|
abbba17e4b | ||
|
|
479d5da17e | ||
|
|
dec7890d04 | ||
|
|
61910a6667 | ||
|
|
ac2119ac3b | ||
|
|
8c71cd7369 | ||
|
|
ec8b6ff18f | ||
|
|
e214804d57 | ||
|
|
39448f2e3c | ||
|
|
79dc6fc247 | ||
|
|
a800f305f2 | ||
|
|
67e0fb6b3f | ||
|
|
e401d30159 | ||
|
|
504e651828 | ||
|
|
7a8da2f074 | ||
|
|
1e47e96634 | ||
|
|
69502480b4 | ||
|
|
5833e3fcca | ||
|
|
4e47ffb1d2 | ||
|
|
3056333a7c | ||
|
|
ad0cd7f625 | ||
|
|
dedaa263ce | ||
|
|
bccf729102 | ||
|
|
a244c61e56 | ||
|
|
37594935f0 | ||
|
|
e521c2158b | ||
|
|
b1c8a8c76e | ||
|
|
8d92f210f0 | ||
|
|
287281923c | ||
|
|
6f18135574 | ||
|
|
58cf9bec3b | ||
|
|
b7a45e9901 | ||
|
|
f5585b39c6 | ||
|
|
9c3ef37274 | ||
|
|
e904c0b689 | ||
|
|
237f97f069 | ||
|
|
1eb2a04090 | ||
|
|
cda6106858 | ||
|
|
8c4ce5e5f4 | ||
|
|
b5ee6431c4 | ||
|
|
bd06e6d4dc | ||
|
|
3b607e74ed | ||
|
|
dc16474ed3 | ||
|
|
dc0f088be0 | ||
|
|
2718e10044 | ||
|
|
4213dcb8a2 | ||
|
|
39afb31200 | ||
|
|
8e1ce1c96a | ||
|
|
fdfd8293d6 | ||
|
|
b7d3be6a14 | ||
|
|
bb5c809749 | ||
|
|
a2cbe7cbf5 | ||
|
|
b1fb09ac99 | ||
|
|
201ae7e7db | ||
|
|
2978decb84 | ||
|
|
5fbbd4d5f3 | ||
|
|
4abd363f62 | ||
|
|
8288db60a9 | ||
|
|
6cdf679571 | ||
|
|
51179acc12 | ||
|
|
1ff27d3e17 | ||
|
|
1e9bce3d5a | ||
|
|
8811ea2b58 | ||
|
|
be7cdd0a28 | ||
|
|
611a05aa48 | ||
|
|
9f95a757a2 | ||
|
|
58b577453c | ||
|
|
78acfd87e8 | ||
|
|
bf9cb26164 | ||
|
|
68a99ada0b | ||
|
|
1d841ce171 | ||
|
|
ccfdbfd276 | ||
|
|
70d584d56d | ||
|
|
a49ffb07be | ||
|
|
baa9caf010 | ||
|
|
22025c875f | ||
|
|
3806995a50 | ||
|
|
94d2650491 | ||
|
|
844f24afa4 | ||
|
|
083396a8e4 | ||
|
|
e02769056b | ||
|
|
024f8e8d80 | ||
|
|
3b4a7c3c47 | ||
|
|
d9838d9cba |
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -10,9 +10,9 @@ jobs:
|
||||
matrix:
|
||||
java: [8, 11, 17]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Cache
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-sbt-libs
|
||||
with:
|
||||
@@ -22,18 +22,18 @@ jobs:
|
||||
~/.cache/coursier/v1
|
||||
key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }}
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v2
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: ${{ matrix.java }}
|
||||
distribution: adopt
|
||||
- name: Run tests
|
||||
run: sbt scalafmtSbtCheck scalafmtCheck test:scalafmtCheck test
|
||||
- name: Scala 3
|
||||
run: sbt '++ 3.0.1!' update # TODO
|
||||
run: sbt '++ 3.1.2!' update # TODO
|
||||
- name: Build executable
|
||||
run: sbt executable
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: gitbucket-java${{ matrix.java }}-${{ github.sha }}
|
||||
path: ./target/executable/gitbucket.*
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,6 +2,9 @@
|
||||
*.log
|
||||
.ensime
|
||||
.ensime_cache
|
||||
.DS_Store
|
||||
.java-version
|
||||
.tmp
|
||||
|
||||
# sbt specific
|
||||
dist/*
|
||||
|
||||
@@ -3,7 +3,9 @@ updates.limit = 3
|
||||
updates.includeScala = true
|
||||
|
||||
updates.pin = [
|
||||
{ groupId = "org.mockito", version = "4." }
|
||||
{ groupId = "org.eclipse.jetty", version = "9." }
|
||||
{ groupId = "org.eclipse.jgit", version = "5." }
|
||||
{ groupId = "com.zaxxer", version = "4." }
|
||||
{ groupId = "org.mariadb.jdbc", version = "2." }
|
||||
]
|
||||
|
||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -1,14 +1,46 @@
|
||||
# Changelog
|
||||
All changes to the project will be documented in this file.
|
||||
|
||||
### 4.37.2 - 16 Jan 2022
|
||||
- Security fix
|
||||
## 4.39.0 - 29 Apr 2023
|
||||
- Support enum type in custom fields of Issues and Pull requests
|
||||
- Hide large diffs by default
|
||||
- Add new options to make it possible to run GitBucket using multiple machines
|
||||
- Fix many API issues
|
||||
|
||||
### 4.37.1 - 14 Dec 2021
|
||||
## 4.38.4 - 2 Nov 2022
|
||||
- Downgrade MariaDB JDBC drive to avoid unknown error
|
||||
|
||||
## 4.38.3 - 30 Oct 2022
|
||||
- Fix several issues around multiple assignees in issues and pull requests
|
||||
- Fix IllegalStateException when returning unknown avatar image
|
||||
|
||||
## 4.38.2 - 20 Sep 2022
|
||||
- Resurrect assignee icons on the issue list
|
||||
|
||||
## 4.38.1 - 10 Sep 2022
|
||||
- Fix comment diff in Chrome 105
|
||||
- Fix Markdown table CSS
|
||||
- Fix HTML rendering of multiple asignees
|
||||
|
||||
## 4.38.0 - 3 Sep 2022
|
||||
- Support multiple assignees for Issues and Pull requests
|
||||
- Custom fields for issues and pull requests
|
||||
- Reset password by users
|
||||
- Allow to configure Jetty idle timeout in standalone mode
|
||||
- Horizontal scroll for too wide tables in Markdown
|
||||
- Hide header content on signin and register page
|
||||
- Fix the default charset of the online editor in the repository viewer
|
||||
- Fix the milestone count
|
||||
- Some improvements and bugfixes for WebAPI and WebHook
|
||||
|
||||
## 4.37.2 - 16 Jan 2022
|
||||
- Fixed a security issue reported by [Positive Technologies](https://www.ptsecurity.com/ww-en/). Great thanks for their detailed report and close support!
|
||||
|
||||
## 4.37.1 - 14 Dec 2021
|
||||
- Update gist-plugin and notification-plugin
|
||||
- Fix SSHCommand extension point for apache-sshd 2.x
|
||||
|
||||
### 4.37.0 - 11 Dec 2021
|
||||
## 4.37.0 - 11 Dec 2021
|
||||
- Enhance Git Reference APIs
|
||||
- Add milestone data to issue list API
|
||||
- Support "all" in issue list API
|
||||
@@ -17,32 +49,32 @@ All changes to the project will be documented in this file.
|
||||
- Relax max passward length limitation
|
||||
- Relax max webhook url length limitation
|
||||
|
||||
### 4.36.2 - 16 Aug 2021
|
||||
## 4.36.2 - 16 Aug 2021
|
||||
- Escape user name in avatar image tag
|
||||
|
||||
### 4.36.1 - 22 Jul 2021
|
||||
## 4.36.1 - 22 Jul 2021
|
||||
- Bump gitbucket-gist-plugin to 4.21.0
|
||||
|
||||
### 4.36.0 - 17 Jul 2021
|
||||
## 4.36.0 - 17 Jul 2021
|
||||
- Tag selector in the repository viewer
|
||||
- Link issues/pull requests of other repositories
|
||||
- Files and lines can be linked in the diff view
|
||||
- Option to disable XSS protection
|
||||
|
||||
### 4.35.3 - 14 Jan 2021
|
||||
## 4.35.3 - 14 Jan 2021
|
||||
- Fix a bug that Wiki page cannot be deleted
|
||||
- Fix a deployment issue on Tomcat
|
||||
|
||||
### 4.35.2 - 30 Dec 2020
|
||||
## 4.35.2 - 30 Dec 2020
|
||||
- Upgrade gitbucket-notifications-plugin to 1.10.0
|
||||
- Upgrade oauth2-oidc-sdk to 8.29.1 to solve dependency issue
|
||||
|
||||
### 4.35.1 - 29 Dec 2020
|
||||
## 4.35.1 - 29 Dec 2020
|
||||
- Fix database migration issue which happens if webhook is configured
|
||||
- Call push webhook when pull request is merged
|
||||
- Show commit status at commits tab of pull request
|
||||
|
||||
### 4.35.0 - 25 Dec 2020
|
||||
## 4.35.0 - 25 Dec 2020
|
||||
- Editor and source viewer color theme
|
||||
- Auto completion for issues and pull requests
|
||||
- Upload image from clipboard
|
||||
|
||||
28
README.md
28
README.md
@@ -1,4 +1,4 @@
|
||||
GitBucket [](https://gitter.im/gitbucket/gitbucket) [](https://github.com/gitbucket/gitbucket/actions?query=workflow%3Abuild+branch%3Amaster) [](https://maven-badges.herokuapp.com/maven-central/io.github.gitbucket/gitbucket_2.13) [](https://github.com/gitbucket/gitbucket/blob/master/LICENSE)
|
||||
GitBucket [](https://gitter.im/gitbucket/gitbucket) [](https://github.com/gitbucket/gitbucket/actions?query=workflow%3Abuild+branch%3Amaster) [](https://index.scala-lang.org/gitbucket/gitbucket/gitbucket) [](https://github.com/gitbucket/gitbucket/blob/master/LICENSE)
|
||||
=========
|
||||
|
||||
GitBucket is a Git web platform powered by Scala offering:
|
||||
@@ -10,8 +10,6 @@ GitBucket is a Git web platform powered by Scala offering:
|
||||
|
||||

|
||||
|
||||
You can try an [online demo](https://gitbucket.herokuapp.com/) *(ID: root / Pass: root)* of GitBucket, and also get the latest information at [GitBucket News](https://gitbucket.github.io/gitbucket-news/).
|
||||
|
||||
Features
|
||||
--------
|
||||
The current version of GitBucket provides many features such as:
|
||||
@@ -31,7 +29,7 @@ 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`.
|
||||
2. Go to `http://[hostname]:8080/` and log in with ID: **root** / Pass: **root**.
|
||||
|
||||
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). Note that GitBucket doesn't support Jakarta EE yet.
|
||||
|
||||
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).
|
||||
|
||||
@@ -61,22 +59,12 @@ Support
|
||||
- If you can't find same question and report, send it to our [Gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
|
||||
- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles.
|
||||
|
||||
What's New in 4.37.x
|
||||
What's New in 4.39.x
|
||||
-------------
|
||||
### 4.37.2 - 16 Jan 2022
|
||||
- Security fix
|
||||
|
||||
### 4.37.1 - 14 Dec 2021
|
||||
- Update gist-plugin and notification-plugin
|
||||
- Fix SSHCommand extension point for apache-sshd 2.x
|
||||
|
||||
### 4.37.0 - 11 Dec 2021
|
||||
- Enhance Git Reference APIs
|
||||
- Add milestone data to issue list API
|
||||
- Support "all" in issue list API
|
||||
- Support EDDSA in signed commit verification
|
||||
- Support custom SSH url
|
||||
- Relax max passward length limitation
|
||||
- Relax max webhook url length limitation
|
||||
## 4.39.0 - 29 Apr 2023
|
||||
- Support enum type in custom fields of Issues and Pull requests
|
||||
- Hide large diffs by default
|
||||
- Add new options to make it possible to run GitBucket using multiple machines
|
||||
- Fix many API issues
|
||||
|
||||
See the [change log](CHANGELOG.md) for all of the updates.
|
||||
|
||||
59
build.sbt
59
build.sbt
@@ -3,10 +3,10 @@ import com.jsuereth.sbtpgp.PgpKeys._
|
||||
|
||||
val Organization = "io.github.gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val GitBucketVersion = "4.37.2"
|
||||
val ScalatraVersion = "2.8.2"
|
||||
val JettyVersion = "9.4.44.v20210927"
|
||||
val JgitVersion = "5.13.0.202109080827-r"
|
||||
val GitBucketVersion = "4.39.0"
|
||||
val ScalatraVersion = "2.8.4"
|
||||
val JettyVersion = "9.4.51.v20230217"
|
||||
val JgitVersion = "5.13.1.202206130422-r"
|
||||
|
||||
lazy val root = (project in file("."))
|
||||
.enablePlugins(SbtTwirl, ScalatraPlugin)
|
||||
@@ -15,16 +15,15 @@ sourcesInBase := false
|
||||
organization := Organization
|
||||
name := Name
|
||||
version := GitBucketVersion
|
||||
scalaVersion := "2.13.8"
|
||||
scalaVersion := "2.13.10"
|
||||
|
||||
scalafmtOnCompile := true
|
||||
// scalafmtOnCompile := true
|
||||
|
||||
coverageExcludedPackages := ".*\\.html\\..*"
|
||||
|
||||
// dependency settings
|
||||
resolvers ++= Seq(
|
||||
Classpaths.typesafeReleases,
|
||||
Resolver.jcenterRepo,
|
||||
"sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/"
|
||||
)
|
||||
|
||||
@@ -34,42 +33,42 @@ libraryDependencies ++= Seq(
|
||||
"org.scalatra" %% "scalatra" % ScalatraVersion cross CrossVersion.for3Use2_13,
|
||||
"org.scalatra" %% "scalatra-json" % ScalatraVersion cross CrossVersion.for3Use2_13,
|
||||
"org.scalatra" %% "scalatra-forms" % ScalatraVersion cross CrossVersion.for3Use2_13,
|
||||
"org.json4s" %% "json4s-jackson" % "4.0.3" cross CrossVersion.for3Use2_13,
|
||||
"org.json4s" %% "json4s-jackson" % "4.0.6" cross CrossVersion.for3Use2_13,
|
||||
"commons-io" % "commons-io" % "2.11.0",
|
||||
"io.github.gitbucket" % "solidbase" % "1.0.3",
|
||||
"io.github.gitbucket" % "markedj" % "1.0.16",
|
||||
"org.apache.commons" % "commons-compress" % "1.21",
|
||||
"io.github.gitbucket" % "solidbase" % "1.0.5",
|
||||
"io.github.gitbucket" % "markedj" % "1.0.17",
|
||||
"org.apache.commons" % "commons-compress" % "1.23.0",
|
||||
"org.apache.commons" % "commons-email" % "1.5",
|
||||
"commons-net" % "commons-net" % "3.8.0",
|
||||
"org.apache.httpcomponents" % "httpclient" % "4.5.13",
|
||||
"org.apache.sshd" % "apache-sshd" % "2.8.0" exclude ("org.slf4j", "slf4j-jdk14") exclude ("org.apache.sshd", "sshd-mina") exclude ("org.apache.sshd", "sshd-netty"),
|
||||
"org.apache.tika" % "tika-core" % "2.2.1",
|
||||
"commons-net" % "commons-net" % "3.9.0",
|
||||
"org.apache.httpcomponents" % "httpclient" % "4.5.14",
|
||||
"org.apache.sshd" % "apache-sshd" % "2.9.2" exclude ("org.slf4j", "slf4j-jdk14") exclude ("org.apache.sshd", "sshd-mina") exclude ("org.apache.sshd", "sshd-netty"),
|
||||
"org.apache.tika" % "tika-core" % "2.7.0",
|
||||
"com.github.takezoe" %% "blocking-slick-32" % "0.0.12" cross CrossVersion.for3Use2_13,
|
||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||
"com.h2database" % "h2" % "1.4.199",
|
||||
"org.mariadb.jdbc" % "mariadb-java-client" % "2.7.4",
|
||||
"org.postgresql" % "postgresql" % "42.3.1",
|
||||
"ch.qos.logback" % "logback-classic" % "1.2.10",
|
||||
"org.mariadb.jdbc" % "mariadb-java-client" % "2.7.6",
|
||||
"org.postgresql" % "postgresql" % "42.6.0",
|
||||
"ch.qos.logback" % "logback-classic" % "1.3.7",
|
||||
"com.zaxxer" % "HikariCP" % "4.0.3" exclude ("org.slf4j", "slf4j-api"),
|
||||
"com.typesafe" % "config" % "1.4.1",
|
||||
"com.typesafe" % "config" % "1.4.2",
|
||||
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0",
|
||||
"io.github.java-diff-utils" % "java-diff-utils" % "4.11",
|
||||
"io.github.java-diff-utils" % "java-diff-utils" % "4.12",
|
||||
"org.cache2k" % "cache2k-all" % "1.6.0.Final",
|
||||
"net.coobird" % "thumbnailator" % "0.4.16",
|
||||
"net.coobird" % "thumbnailator" % "0.4.19",
|
||||
"com.github.zafarkhaja" % "java-semver" % "0.9.0",
|
||||
"com.nimbusds" % "oauth2-oidc-sdk" % "9.22",
|
||||
"com.nimbusds" % "oauth2-oidc-sdk" % "10.8",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
|
||||
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
|
||||
"junit" % "junit" % "4.13.2" % "test",
|
||||
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test" cross CrossVersion.for3Use2_13,
|
||||
"org.mockito" % "mockito-core" % "4.2.0" % "test",
|
||||
"com.dimafeng" %% "testcontainers-scala" % "0.39.12" % "test",
|
||||
"org.testcontainers" % "mysql" % "1.16.2" % "test",
|
||||
"org.testcontainers" % "postgresql" % "1.16.2" % "test",
|
||||
"org.mockito" % "mockito-core" % "4.11.0" % "test",
|
||||
"com.dimafeng" %% "testcontainers-scala" % "0.40.15" % "test",
|
||||
"org.testcontainers" % "mysql" % "1.18.0" % "test",
|
||||
"org.testcontainers" % "postgresql" % "1.18.0" % "test",
|
||||
"net.i2p.crypto" % "eddsa" % "0.3.0",
|
||||
"is.tagomor.woothee" % "woothee-java" % "1.11.0",
|
||||
"org.ec4j.core" % "ec4j-core" % "0.3.0",
|
||||
"org.kohsuke" % "github-api" % "1.301" % "test"
|
||||
"org.kohsuke" % "github-api" % "1.314" % "test"
|
||||
)
|
||||
|
||||
libraryDependencies ~= {
|
||||
@@ -91,7 +90,6 @@ scalacOptions := Seq(
|
||||
"-Wconf:cat=unused&src=twirl/.*:s,cat=unused&src=scala/gitbucket/core/model/[^/]+\\.scala:s"
|
||||
)
|
||||
compile / javacOptions ++= Seq("-target", "8", "-source", "8")
|
||||
Jetty / javaOptions += "-Dlogback.configurationFile=/logback-dev.xml"
|
||||
|
||||
// Test settings
|
||||
//testOptions in Test += Tests.Argument("-l", "ExternalDBTest")
|
||||
@@ -287,6 +285,9 @@ Test / testOptions ++= {
|
||||
}
|
||||
|
||||
Jetty / javaOptions ++= Seq(
|
||||
"-Dlogback.configurationFile=/logback-dev.xml",
|
||||
"-Xdebug",
|
||||
"-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000"
|
||||
"-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000",
|
||||
"-Dorg.eclipse.jetty.annotations.AnnotationParser.LEVEL=OFF",
|
||||
//"-Ddev-features=keep-session"
|
||||
)
|
||||
|
||||
19
doc/build.md
19
doc/build.md
@@ -18,7 +18,21 @@ $ sbt ~jetty:start
|
||||
|
||||
Then access `http://localhost:8080/` in your browser. The default administrator account is `root` and password is `root`.
|
||||
|
||||
Source code modifications are detected and a reloading happens automatically. You can modify the logging configuration by editing `src/main/resources/logback-dev.xml`.
|
||||
Source code modifications are detected and a reloading happens automatically.
|
||||
You can modify the logging configuration by editing `src/main/resources/logback-dev.xml`.
|
||||
|
||||
Note that HttpSession is cleared when auto-reloading happened.
|
||||
This is a bit annoying when developing features that requires sign-in.
|
||||
You can keep HttpSession even if GitBucket is restarted by enabling this configuration in `build.sbt`:
|
||||
https://github.com/gitbucket/gitbucket/blob/d5c083b70f7f3748d080166252e9a3dcaf579648/build.sbt#L292
|
||||
|
||||
Or by launching GitBucket with the following command:
|
||||
```shell
|
||||
sbt '; set Jetty/javaOptions += "-Ddev-features=keep-session" ; ~jetty:start'
|
||||
```
|
||||
|
||||
Note that this feature serializes HttpSession on the local disk and assigns all requests to the same session
|
||||
which means you cannot test multi users behavior in this mode.
|
||||
|
||||
Build war file
|
||||
--------
|
||||
@@ -37,7 +51,8 @@ To build an executable war file, run
|
||||
$ sbt executable
|
||||
```
|
||||
|
||||
at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. We release this war file as release artifact.
|
||||
at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`.
|
||||
We release this war file as release artifact.
|
||||
|
||||
Run tests spec
|
||||
---------
|
||||
|
||||
@@ -5,13 +5,17 @@ GitBucket persists all data into __HOME/.gitbucket__ in default (In 1.9 or befor
|
||||
This directory has following structure:
|
||||
|
||||
```
|
||||
* /HOME/gitbucket
|
||||
* /HOME/.gitbucket
|
||||
* gitbucket.conf
|
||||
* database.conf
|
||||
* activity.log
|
||||
* data.mv.db, data.trace.db (H2 data files if the default embed H2 is used)
|
||||
* /repositories
|
||||
* /USER_NAME
|
||||
* /REPO_NAME.git (substance of repository. GitServlet sees this directory)
|
||||
* /REPO_NAME.wiki.git (wiki repository)
|
||||
* /REPO_NAME
|
||||
* /issues (files which are attached to issue)
|
||||
* /issues (files attached to issue)
|
||||
* /lfs (LFS managed files)
|
||||
* /data
|
||||
* /USER_NAME
|
||||
@@ -20,6 +24,8 @@ This directory has following structure:
|
||||
* /plugins
|
||||
* plugin.jar
|
||||
* /.installed (copied available plugins from the parent directory automatically)
|
||||
* /sessions
|
||||
* HTTP session data created (if '--save_sessions' option is used in the standalone mode)
|
||||
* /tmp
|
||||
* /_upload
|
||||
* /SESSION_ID (removed at session timeout)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 203 KiB After Width: | Height: | Size: 112 KiB |
@@ -1 +1 @@
|
||||
sbt.version=1.6.1
|
||||
sbt.version=1.7.2
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
|
||||
|
||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
|
||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0")
|
||||
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.5.1")
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0")
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1")
|
||||
addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.4")
|
||||
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2")
|
||||
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1")
|
||||
addSbtPlugin("com.typesafe.sbt" % "sbt-license-report" % "1.2.0")
|
||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3")
|
||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.0")
|
||||
|
||||
addDependencyTreePlugin
|
||||
|
||||
@@ -61,12 +61,19 @@ public class JettyLauncher {
|
||||
String redirectHttps = getEnvironmentVariable("gitbucket.redirectHttps");
|
||||
String contextPath = getEnvironmentVariable("gitbucket.prefix");
|
||||
String tmpDirPath = getEnvironmentVariable("gitbucket.tempDir");
|
||||
String jettyIdleTimeout = getEnvironmentVariable("gitbucket.jettyIdleTimeout");
|
||||
boolean saveSessions = false;
|
||||
|
||||
for(String arg: args) {
|
||||
if(arg.equals("--save_sessions")) {
|
||||
if (arg.equals("--save_sessions")) {
|
||||
saveSessions = true;
|
||||
}
|
||||
if (arg.equals("--disable_news_feed")) {
|
||||
System.setProperty("gitbucket.disableNewsFeed", "true");
|
||||
}
|
||||
if (arg.equals("--disable_cache")) {
|
||||
System.setProperty("gitbucket.disableCache", "true");
|
||||
}
|
||||
if(arg.startsWith("--") && arg.contains("=")) {
|
||||
String[] dim = arg.split("=", 2);
|
||||
if(dim.length == 2) {
|
||||
@@ -107,6 +114,9 @@ public class JettyLauncher {
|
||||
case "--plugin_dir":
|
||||
System.setProperty("gitbucket.pluginDir", dim[1]);
|
||||
break;
|
||||
case "--jetty_idle_timeout":
|
||||
jettyIdleTimeout = dim[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,6 +140,11 @@ public class JettyLauncher {
|
||||
if (connectorsSet.contains(Connectors.HTTPS)) {
|
||||
httpConfig.setSecurePort(fallback(securePort, Defaults.HTTPS_PORT, Integer::parseInt));
|
||||
}
|
||||
if (jettyIdleTimeout != null && jettyIdleTimeout.trim().length() != 0) {
|
||||
httpConfig.setIdleTimeout(Long.parseLong(jettyIdleTimeout.trim()));
|
||||
} else {
|
||||
httpConfig.setIdleTimeout(300000L); // default is 5min
|
||||
}
|
||||
|
||||
if (connectorsSet.contains(Connectors.HTTP)) {
|
||||
final ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</appender>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>gitbucket.log</file>
|
||||
<file>.tmp/gitbucket.log</file>
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
@@ -23,4 +23,4 @@
|
||||
<logger name="scala.slick.jdbc.JdbcBackend.statement" level="DEBUG" />
|
||||
-->
|
||||
|
||||
</configuration>
|
||||
</configuration>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,18 +0,0 @@
|
||||
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
|
||||
SELECT
|
||||
A.USER_NAME,
|
||||
A.REPOSITORY_NAME,
|
||||
A.ISSUE_ID,
|
||||
COALESCE(B.COMMENT_COUNT, 0) + COALESCE(C.COMMENT_COUNT, 0) AS COMMENT_COUNT
|
||||
FROM ISSUE A
|
||||
LEFT OUTER JOIN (
|
||||
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
|
||||
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
|
||||
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
||||
) B
|
||||
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID)
|
||||
LEFT OUTER JOIN (
|
||||
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM COMMIT_COMMENT
|
||||
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
||||
) C
|
||||
ON (A.USER_NAME = C.USER_NAME AND A.REPOSITORY_NAME = C.REPOSITORY_NAME AND A.ISSUE_ID = C.ISSUE_ID);
|
||||
@@ -343,4 +343,28 @@
|
||||
<addPrimaryKey constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_PK" tableName="PROTECTED_BRANCH_REQUIRE_CONTEXT" columnNames="USER_NAME, REPOSITORY_NAME, BRANCH, CONTEXT"/>
|
||||
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_FK0" baseTableName="PROTECTED_BRANCH_REQUIRE_CONTEXT" baseColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" referencedTableName="PROTECTED_BRANCH" referencedColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" onDelete="CASCADE" onUpdate="CASCADE"/>
|
||||
|
||||
<!--================================================================================================-->
|
||||
<!-- ISSUE_OUTLINE_VIEW -->
|
||||
<!--================================================================================================-->
|
||||
<sql>
|
||||
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
|
||||
SELECT
|
||||
A.USER_NAME,
|
||||
A.REPOSITORY_NAME,
|
||||
A.ISSUE_ID,
|
||||
COALESCE(B.COMMENT_COUNT, 0) + COALESCE(C.COMMENT_COUNT, 0) AS COMMENT_COUNT
|
||||
FROM ISSUE A
|
||||
LEFT OUTER JOIN (
|
||||
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
|
||||
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
|
||||
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
||||
) B
|
||||
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID)
|
||||
LEFT OUTER JOIN (
|
||||
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM COMMIT_COMMENT
|
||||
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
||||
) C
|
||||
ON (A.USER_NAME = C.USER_NAME AND A.REPOSITORY_NAME = C.REPOSITORY_NAME AND A.ISSUE_ID = C.ISSUE_ID)
|
||||
</sql>
|
||||
|
||||
</changeSet>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
|
||||
|
||||
SELECT
|
||||
A.USER_NAME,
|
||||
A.REPOSITORY_NAME,
|
||||
A.ISSUE_ID,
|
||||
COALESCE(B.COMMENT_COUNT, 0) + COALESCE(C.COMMENT_COUNT, 0) AS COMMENT_COUNT,
|
||||
COALESCE(D.ORDERING, 9999) AS PRIORITY
|
||||
|
||||
FROM ISSUE A
|
||||
|
||||
LEFT OUTER JOIN (
|
||||
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
|
||||
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
|
||||
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
||||
) B
|
||||
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID)
|
||||
|
||||
LEFT OUTER JOIN (
|
||||
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM COMMIT_COMMENT
|
||||
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
||||
) C
|
||||
ON (A.USER_NAME = C.USER_NAME AND A.REPOSITORY_NAME = C.REPOSITORY_NAME AND A.ISSUE_ID = C.ISSUE_ID)
|
||||
|
||||
LEFT OUTER JOIN PRIORITY D
|
||||
ON (A.PRIORITY_ID = D.PRIORITY_ID);
|
||||
@@ -35,4 +35,28 @@
|
||||
<column name="URL" type="varchar(200)" nullable="false"/>
|
||||
<column name="EVENT" type="varchar(30)" nullable="false"/>
|
||||
</createTable>
|
||||
|
||||
<sql>
|
||||
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
|
||||
SELECT
|
||||
A.USER_NAME,
|
||||
A.REPOSITORY_NAME,
|
||||
A.ISSUE_ID,
|
||||
COALESCE(B.COMMENT_COUNT, 0) + COALESCE(C.COMMENT_COUNT, 0) AS COMMENT_COUNT,
|
||||
COALESCE(D.ORDERING, 9999) AS PRIORITY
|
||||
FROM ISSUE A
|
||||
LEFT OUTER JOIN (
|
||||
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
|
||||
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
|
||||
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
||||
) B
|
||||
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID)
|
||||
LEFT OUTER JOIN (
|
||||
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM COMMIT_COMMENT
|
||||
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
||||
) C
|
||||
ON (A.USER_NAME = C.USER_NAME AND A.REPOSITORY_NAME = C.REPOSITORY_NAME AND A.ISSUE_ID = C.ISSUE_ID)
|
||||
LEFT OUTER JOIN PRIORITY D
|
||||
ON (A.PRIORITY_ID = D.PRIORITY_ID);
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
53
src/main/resources/update/gitbucket-core_4.38.xml
Normal file
53
src/main/resources/update/gitbucket-core_4.38.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<changeSet>
|
||||
<!--================================================================================================-->
|
||||
<!-- CUSTOM_FIELD -->
|
||||
<!--================================================================================================-->
|
||||
<createTable tableName="CUSTOM_FIELD">
|
||||
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||
<column name="FIELD_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
|
||||
<column name="FIELD_NAME" type="varchar(100)" nullable="false"/>
|
||||
<column name="FIELD_TYPE" type="varchar(100)" nullable="false"/>
|
||||
<column name="ENABLE_FOR_ISSUES" type="boolean" nullable="false"/>
|
||||
<column name="ENABLE_FOR_PULL_REQUESTS" type="boolean" nullable="false"/>
|
||||
</createTable>
|
||||
<addPrimaryKey constraintName="IDX_CUSTOM_FIELD_PK" tableName="CUSTOM_FIELD" columnNames="USER_NAME, REPOSITORY_NAME, FIELD_ID"/>
|
||||
<addForeignKeyConstraint constraintName="IDX_CUSTOM_FIELD_FK0" baseTableName="CUSTOM_FIELD" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
|
||||
|
||||
<!--================================================================================================-->
|
||||
<!-- ISSUE_CUSTOM_FIELD -->
|
||||
<!--================================================================================================-->
|
||||
<createTable tableName="ISSUE_CUSTOM_FIELD">
|
||||
<column name="USER_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="FIELD_ID" type="int" nullable="false"/>
|
||||
<column name="VALUE" type="varchar(200)" nullable="true"/>
|
||||
</createTable>
|
||||
<addPrimaryKey constraintName="IDX_ISSUE_CUSTOM_FIELD_PK" tableName="ISSUE_CUSTOM_FIELD" columnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID, FIELD_ID"/>
|
||||
<addForeignKeyConstraint constraintName="IDX_ISSUE_CUSTOM_FIELD_FK0" baseTableName="ISSUE_CUSTOM_FIELD" baseColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID" referencedTableName="ISSUE" referencedColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID"/>
|
||||
<addForeignKeyConstraint constraintName="IDX_ISSUE_CUSTOM_FIELD_FK1" baseTableName="ISSUE_CUSTOM_FIELD" baseColumnNames="USER_NAME, REPOSITORY_NAME, FIELD_ID" referencedTableName="CUSTOM_FIELD" referencedColumnNames="USER_NAME, REPOSITORY_NAME, FIELD_ID"/>
|
||||
|
||||
<!--================================================================================================-->
|
||||
<!-- ISSUE_ASSIGNEE -->
|
||||
<!--================================================================================================-->
|
||||
<createTable tableName="ISSUE_ASSIGNEE">
|
||||
<column name="USER_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="ASSIGNEE_USER_NAME" type="varchar(100)" nullable="false"/>
|
||||
</createTable>
|
||||
<addPrimaryKey constraintName="IDX_ISSUE_ASSIGNEE_PK" tableName="ISSUE_ASSIGNEE" columnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID, ASSIGNEE_USER_NAME"/>
|
||||
<addForeignKeyConstraint constraintName="IDX_ISSUE_ASSIGNEE_FK0" baseTableName="ISSUE_ASSIGNEE" baseColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID" referencedTableName="ISSUE" referencedColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID"/>
|
||||
<!--
|
||||
<addForeignKeyConstraint constraintName="IDX_ISSUE_ASSIGNEE_FK1" baseTableName="ISSUE_ASSIGNEE" baseColumnNames="ASSIGNEE_USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
|
||||
-->
|
||||
|
||||
<sql>
|
||||
INSERT INTO ISSUE_ASSIGNEE (USER_NAME, REPOSITORY_NAME, ISSUE_ID, ASSIGNEE_USER_NAME)
|
||||
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, ASSIGNED_USER_NAME FROM ISSUE WHERE ASSIGNED_USER_NAME IS NOT NULL
|
||||
</sql>
|
||||
|
||||
<dropColumn tableName="ISSUE" columnName="ASSIGNED_USER_NAME"/>
|
||||
</changeSet>
|
||||
6
src/main/resources/update/gitbucket-core_4.39.xml
Normal file
6
src/main/resources/update/gitbucket-core_4.39.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<changeSet>
|
||||
<addColumn tableName="CUSTOM_FIELD">
|
||||
<column name="CONSTRAINTS" type="varchar(200)" nullable="true"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
@@ -1,2 +0,0 @@
|
||||
-- DELETE COLLABORATORS IN GROUP REPOSITORIES
|
||||
DELETE FROM COLLABORATOR WHERE USER_NAME IN (SELECT USER_NAME FROM ACCOUNT WHERE GROUP_ACCOUNT = TRUE)
|
||||
@@ -30,4 +30,9 @@
|
||||
<dropColumn tableName="REPOSITORY" columnName="ENABLE_WIKI"/>
|
||||
<dropColumn tableName="REPOSITORY" columnName="ALLOW_WIKI_EDITING"/>
|
||||
<dropColumn tableName="REPOSITORY" columnName="ENABLE_ISSUES"/>
|
||||
|
||||
<!-- DELETE COLLABORATORS IN GROUP REPOSITORIES -->
|
||||
<sql>
|
||||
DELETE FROM COLLABORATOR WHERE USER_NAME IN (SELECT USER_NAME FROM ACCOUNT WHERE GROUP_ACCOUNT = TRUE)
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
@@ -9,7 +9,7 @@ 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.migration.{LiquibaseMigration, Migration}
|
||||
import io.github.gitbucket.solidbase.model.{Module, Version}
|
||||
import org.json4s.{Formats, NoTypeHints}
|
||||
import org.json4s.jackson.Serialization
|
||||
@@ -20,11 +20,7 @@ import scala.util.Using
|
||||
object GitBucketCoreModule
|
||||
extends Module(
|
||||
"gitbucket-core",
|
||||
new Version(
|
||||
"4.0.0",
|
||||
new LiquibaseMigration("update/gitbucket-core_4.0.xml"),
|
||||
new SqlMigration("update/gitbucket-core_4.0.sql")
|
||||
),
|
||||
new Version("4.0.0", new LiquibaseMigration("update/gitbucket-core_4.0.xml")),
|
||||
new Version("4.1.0"),
|
||||
new Version("4.2.0", new LiquibaseMigration("update/gitbucket-core_4.2.xml")),
|
||||
new Version("4.2.1"),
|
||||
@@ -32,11 +28,7 @@ object GitBucketCoreModule
|
||||
new Version("4.4.0"),
|
||||
new Version("4.5.0"),
|
||||
new Version("4.6.0", new LiquibaseMigration("update/gitbucket-core_4.6.xml")),
|
||||
new Version(
|
||||
"4.7.0",
|
||||
new LiquibaseMigration("update/gitbucket-core_4.7.xml"),
|
||||
new SqlMigration("update/gitbucket-core_4.7.sql")
|
||||
),
|
||||
new Version("4.7.0", new LiquibaseMigration("update/gitbucket-core_4.7.xml")),
|
||||
new Version("4.7.1"),
|
||||
new Version("4.8"),
|
||||
new Version("4.9.0", new LiquibaseMigration("update/gitbucket-core_4.9.xml")),
|
||||
@@ -45,11 +37,7 @@ object GitBucketCoreModule
|
||||
new Version("4.12.0"),
|
||||
new Version("4.12.1"),
|
||||
new Version("4.13.0"),
|
||||
new Version(
|
||||
"4.14.0",
|
||||
new LiquibaseMigration("update/gitbucket-core_4.14.xml"),
|
||||
new SqlMigration("update/gitbucket-core_4.14.sql")
|
||||
),
|
||||
new Version("4.14.0", new LiquibaseMigration("update/gitbucket-core_4.14.xml")),
|
||||
new Version("4.14.1"),
|
||||
new Version("4.15.0"),
|
||||
new Version("4.16.0"),
|
||||
@@ -122,5 +110,11 @@ object GitBucketCoreModule
|
||||
new Version("4.36.2"),
|
||||
new Version("4.37.0", new LiquibaseMigration("update/gitbucket-core_4.37.xml")),
|
||||
new Version("4.37.1"),
|
||||
new Version("4.37.2")
|
||||
new Version("4.37.2"),
|
||||
new Version("4.38.0", new LiquibaseMigration("update/gitbucket-core_4.38.xml")),
|
||||
new Version("4.38.1"),
|
||||
new Version("4.38.2"),
|
||||
new Version("4.38.3"),
|
||||
new Version("4.38.4"),
|
||||
new Version("4.39.0", new LiquibaseMigration("update/gitbucket-core_4.39.xml")),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/issues/labels/#add-labels-to-an-issue
|
||||
*/
|
||||
case class AddLabelsToAnIssue(labels: Seq[String])
|
||||
@@ -97,7 +97,7 @@ object ApiCommits {
|
||||
ApiCommits(
|
||||
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
|
||||
sha = commitInfo.id,
|
||||
html_url = ApiPath(s"${repositoryName.fullName}/commit/${commitInfo.id}"),
|
||||
html_url = ApiPath(s"/${repositoryName.fullName}/commit/${commitInfo.id}"),
|
||||
comment_url = ApiPath(""), // TODO no API for commit comment
|
||||
commit = Commit(
|
||||
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
|
||||
|
||||
@@ -12,7 +12,7 @@ case class ApiIssue(
|
||||
number: Int,
|
||||
title: String,
|
||||
user: ApiUser,
|
||||
assignee: Option[ApiUser],
|
||||
assignees: List[ApiUser],
|
||||
labels: List[ApiLabel],
|
||||
state: String,
|
||||
created_at: Date,
|
||||
@@ -21,7 +21,7 @@ case class ApiIssue(
|
||||
milestone: Option[ApiMilestone]
|
||||
)(repositoryName: RepositoryName, isPullRequest: Boolean) {
|
||||
val id = 0 // dummy id
|
||||
val assignees = List(assignee).flatten
|
||||
val assignee = assignees.headOption
|
||||
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
|
||||
val html_url = ApiPath(s"/${repositoryName.fullName}/${if (isPullRequest) { "pull" } else { "issues" }}/${number}")
|
||||
val pull_request = if (isPullRequest) {
|
||||
@@ -43,7 +43,7 @@ object ApiIssue {
|
||||
issue: Issue,
|
||||
repositoryName: RepositoryName,
|
||||
user: ApiUser,
|
||||
assignee: Option[ApiUser],
|
||||
assignees: List[ApiUser],
|
||||
labels: List[ApiLabel],
|
||||
milestone: Option[ApiMilestone]
|
||||
): ApiIssue =
|
||||
@@ -51,7 +51,7 @@ object ApiIssue {
|
||||
number = issue.issueId,
|
||||
title = issue.title,
|
||||
user = user,
|
||||
assignee = assignee,
|
||||
assignees = assignees,
|
||||
labels = labels,
|
||||
milestone = milestone,
|
||||
state = if (issue.closed) { "closed" } else { "open" },
|
||||
|
||||
@@ -21,10 +21,11 @@ case class ApiPullRequest(
|
||||
body: String,
|
||||
user: ApiUser,
|
||||
labels: List[ApiLabel],
|
||||
assignee: Option[ApiUser],
|
||||
assignees: List[ApiUser],
|
||||
draft: Option[Boolean]
|
||||
) {
|
||||
val id = 0 // dummy id
|
||||
val assignee = assignees.headOption
|
||||
val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
|
||||
//val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff")
|
||||
//val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch")
|
||||
@@ -45,7 +46,7 @@ object ApiPullRequest {
|
||||
baseRepo: ApiRepository,
|
||||
user: ApiUser,
|
||||
labels: List[ApiLabel],
|
||||
assignee: Option[ApiUser],
|
||||
assignees: List[ApiUser],
|
||||
mergedComment: Option[(IssueComment, Account)]
|
||||
): ApiPullRequest =
|
||||
ApiPullRequest(
|
||||
@@ -63,7 +64,7 @@ object ApiPullRequest {
|
||||
body = issue.content.getOrElse(""),
|
||||
user = user,
|
||||
labels = labels,
|
||||
assignee = assignee,
|
||||
assignees = assignees,
|
||||
draft = Some(pullRequest.isDraft)
|
||||
)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ object ApiRef {
|
||||
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/${ref.getName}"),
|
||||
`object` = ApiRefCommit(
|
||||
sha = ref.getObjectId.getName,
|
||||
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/commits/${ref.getObjectId.getName}"),
|
||||
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${ref.getObjectId.getName}"),
|
||||
`type` = "commit"
|
||||
)
|
||||
)
|
||||
@@ -40,10 +40,16 @@ object ApiRef {
|
||||
ApiRef(
|
||||
ref = s"refs/tags/${tagInfo.name}",
|
||||
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/refs/tags/${tagInfo.name}"),
|
||||
//the GH api distinguishes between "releases" and plain git tags
|
||||
//for "releases", the api returns a reference to the release object (with type `tag`)
|
||||
//this would be something like s"/api/v3/repos/${repositoryName.fullName}/git/tags/<hash-of-tag>"
|
||||
//with a hash for the tag, which I do not fully understand
|
||||
//since this is not yet implemented in GB, we always return a link to the plain `commit` object,
|
||||
//which GH does for tags that are not annotated
|
||||
`object` = ApiRefCommit(
|
||||
sha = tagInfo.objectId,
|
||||
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/tags/${tagInfo.objectId}"), // TODO This URL is not yet available?
|
||||
`type` = "tag"
|
||||
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${tagInfo.objectId}"),
|
||||
`type` = "commit"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ case class ApiRepository(
|
||||
val url = ApiPath(s"/api/v3/repos/${full_name}")
|
||||
val clone_url = ApiPath(s"/git/${full_name}.git")
|
||||
val html_url = ApiPath(s"/${full_name}")
|
||||
val ssh_url = Some(SshPath(s":${full_name}.git"))
|
||||
val ssh_url = Some(SshPath(""))
|
||||
}
|
||||
|
||||
object ApiRepository {
|
||||
|
||||
@@ -128,6 +128,15 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
"highlighterTheme" -> trim(label("Theme", text(required)))
|
||||
)(SyntaxHighlighterThemeForm.apply)
|
||||
|
||||
val resetPasswordEmailForm = mapping(
|
||||
"mailAddress" -> trim(label("Email", text(required)))
|
||||
)(ResetPasswordEmailForm.apply)
|
||||
|
||||
val resetPasswordForm = mapping(
|
||||
"token" -> trim(label("Token", text(required))),
|
||||
"password" -> trim(label("Password", text(required, maxlength(40))))
|
||||
)(ResetPasswordForm.apply)
|
||||
|
||||
case class NewGroupForm(
|
||||
groupName: String,
|
||||
description: Option[String],
|
||||
@@ -143,6 +152,13 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
members: String,
|
||||
clearImage: Boolean
|
||||
)
|
||||
case class ResetPasswordEmailForm(
|
||||
mailAddress: String
|
||||
)
|
||||
case class ResetPasswordForm(
|
||||
token: String,
|
||||
password: String
|
||||
)
|
||||
|
||||
val newGroupForm = mapping(
|
||||
"groupName" -> trim(label("Group name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
|
||||
@@ -602,7 +618,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
})
|
||||
|
||||
get("/register") {
|
||||
if (context.settings.allowAccountRegistration) {
|
||||
if (context.settings.basicBehavior.allowAccountRegistration) {
|
||||
if (context.loginAccount.isDefined) {
|
||||
redirect("/")
|
||||
} else {
|
||||
@@ -612,7 +628,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
}
|
||||
|
||||
post("/register", newForm) { form =>
|
||||
if (context.settings.allowAccountRegistration) {
|
||||
if (context.settings.basicBehavior.allowAccountRegistration) {
|
||||
createAccount(
|
||||
form.userName,
|
||||
pbkdf2_sha256(form.password),
|
||||
@@ -628,6 +644,63 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
} else NotFound()
|
||||
}
|
||||
|
||||
get("/reset") {
|
||||
if (context.settings.basicBehavior.allowResetPassword) {
|
||||
html.reset()
|
||||
} else NotFound()
|
||||
}
|
||||
|
||||
post("/reset", resetPasswordEmailForm) { form =>
|
||||
if (context.settings.basicBehavior.allowResetPassword) {
|
||||
getAccountByMailAddress(form.mailAddress).foreach { account =>
|
||||
val token = generateResetPasswordToken(form.mailAddress)
|
||||
val mailer = new Mailer(context.settings)
|
||||
mailer.send(
|
||||
form.mailAddress,
|
||||
"Reset password",
|
||||
s"""Hello, ${account.fullName}!
|
||||
|
|
||||
|You requested to reset the password for your GitBucket account.
|
||||
|If you are not sure about the request, you can ignore this email.
|
||||
|Otherwise, click the following link to set the new password:
|
||||
|${context.baseUrl}/reset/form/${token}
|
||||
|""".stripMargin
|
||||
)
|
||||
}
|
||||
redirect("/reset/sent")
|
||||
} else NotFound()
|
||||
}
|
||||
|
||||
get("/reset/sent") {
|
||||
if (context.settings.basicBehavior.allowResetPassword) {
|
||||
html.resetsent()
|
||||
} else NotFound()
|
||||
}
|
||||
|
||||
get("/reset/form/:token") {
|
||||
if (context.settings.basicBehavior.allowResetPassword) {
|
||||
val token = params("token")
|
||||
decodeResetPasswordToken(token)
|
||||
.map { _ =>
|
||||
html.resetform(token)
|
||||
}
|
||||
.getOrElse(NotFound())
|
||||
} else NotFound()
|
||||
}
|
||||
|
||||
post("/reset/form", resetPasswordForm) { form =>
|
||||
if (context.settings.basicBehavior.allowResetPassword) {
|
||||
decodeResetPasswordToken(form.token)
|
||||
.flatMap { mailAddress =>
|
||||
getAccountByMailAddress(mailAddress).map { account =>
|
||||
updateAccount(account.copy(password = pbkdf2_sha256(form.password)))
|
||||
html.resetcomplete()
|
||||
}
|
||||
}
|
||||
.getOrElse(NotFound())
|
||||
} else NotFound()
|
||||
}
|
||||
|
||||
get("/groups/new")(usersOnly {
|
||||
context.withLoginAccount { loginAccount =>
|
||||
html.creategroup(List(GroupMember("", loginAccount.userName, true)))
|
||||
@@ -713,7 +786,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
*/
|
||||
get("/new")(usersOnly {
|
||||
context.withLoginAccount { loginAccount =>
|
||||
html.newrepo(getGroupsByUserName(loginAccount.userName), context.settings.isCreateRepoOptionPublic)
|
||||
html.newrepo(getGroupsByUserName(loginAccount.userName), context.settings.basicBehavior.isCreateRepoOptionPublic)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -723,7 +796,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
post("/new", newRepositoryForm)(usersOnly { form =>
|
||||
context.withLoginAccount {
|
||||
loginAccount =>
|
||||
if (context.settings.repositoryOperation.create || loginAccount.isAdmin) {
|
||||
if (context.settings.basicBehavior.repositoryOperation.create || loginAccount.isAdmin) {
|
||||
LockUtil.lock(s"${form.owner}/${form.name}") {
|
||||
if (getRepository(form.owner, form.name).isDefined) {
|
||||
// redirect to the repository if repository already exists
|
||||
@@ -753,7 +826,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
|
||||
context.withLoginAccount {
|
||||
loginAccount =>
|
||||
if (repository.repository.options.allowFork && (context.settings.repositoryOperation.fork || loginAccount.isAdmin)) {
|
||||
if (repository.repository.options.allowFork && (context.settings.basicBehavior.repositoryOperation.fork || loginAccount.isAdmin)) {
|
||||
val loginUserName = loginAccount.userName
|
||||
val groups = getGroupsByUserName(loginUserName)
|
||||
groups match {
|
||||
@@ -780,7 +853,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) =>
|
||||
context.withLoginAccount {
|
||||
loginAccount =>
|
||||
if (repository.repository.options.allowFork && (context.settings.repositoryOperation.fork || loginAccount.isAdmin)) {
|
||||
if (repository.repository.options.allowFork && (context.settings.basicBehavior.repositoryOperation.fork || loginAccount.isAdmin)) {
|
||||
val loginUserName = loginAccount.userName
|
||||
val accountName = form.accountName
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import java.io.{File, FileInputStream}
|
||||
|
||||
import java.io.{File, FileInputStream, FileOutputStream}
|
||||
import gitbucket.core.api.{ApiError, JsonFormat}
|
||||
import gitbucket.core.model.Account
|
||||
import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService}
|
||||
@@ -14,9 +13,9 @@ import org.scalatra._
|
||||
import org.scalatra.i18n._
|
||||
import org.scalatra.json._
|
||||
import org.scalatra.forms._
|
||||
|
||||
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
|
||||
import javax.servlet.{FilterChain, ServletRequest, ServletResponse}
|
||||
|
||||
import is.tagomor.woothee.Classifier
|
||||
|
||||
import scala.util.Try
|
||||
@@ -29,6 +28,9 @@ import org.eclipse.jgit.treewalk._
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.json4s.Formats
|
||||
import org.json4s.jackson.Serialization
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/**
|
||||
* Provides generic features for controller implementations.
|
||||
@@ -93,8 +95,16 @@ abstract class ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
private def LoginAccount: Option[Account] =
|
||||
request.getAs[Account](Keys.Session.LoginAccount).orElse(session.getAs[Account](Keys.Session.LoginAccount))
|
||||
private def LoginAccount: Option[Account] = {
|
||||
request
|
||||
.getAs[Account](Keys.Session.LoginAccount)
|
||||
.orElse(session.getAs[Account](Keys.Session.LoginAccount))
|
||||
.orElse {
|
||||
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
|
||||
getLoginAccountFromLocalFile()
|
||||
} else None
|
||||
}
|
||||
}
|
||||
|
||||
def ajaxGet(path: String)(action: => Any): Route =
|
||||
super.get(path) {
|
||||
@@ -277,6 +287,47 @@ abstract class ControllerBase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected object DevFeatures {
|
||||
val KeepSession = "keep-session"
|
||||
}
|
||||
|
||||
private val loginAccountFile = new File(".tmp/login_account.json")
|
||||
|
||||
protected def isDevFeatureEnabled(feature: String): Boolean = {
|
||||
Option(System.getProperty("dev-features")).getOrElse("").split(",").map(_.trim).contains(feature)
|
||||
}
|
||||
|
||||
protected def getLoginAccountFromLocalFile(): Option[Account] = {
|
||||
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
|
||||
if (loginAccountFile.exists()) {
|
||||
Using.resource(new FileInputStream(loginAccountFile)) { in =>
|
||||
val json = IOUtils.toString(in, StandardCharsets.UTF_8)
|
||||
val account = parse(json).extract[Account]
|
||||
session.setAttribute(Keys.Session.LoginAccount, account)
|
||||
Some(parse(json).extract[Account])
|
||||
}
|
||||
} else None
|
||||
|
||||
} else None
|
||||
}
|
||||
|
||||
protected def saveLoginAccountToLocalFile(account: Account): Unit = {
|
||||
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
|
||||
if (!loginAccountFile.getParentFile.exists()) {
|
||||
loginAccountFile.getParentFile.mkdirs()
|
||||
}
|
||||
Using.resource(new FileOutputStream(loginAccountFile)) { in =>
|
||||
in.write(Serialization.write(account).getBytes(StandardCharsets.UTF_8))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected def deleteLoginAccountFromLocalFile(): Unit = {
|
||||
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
|
||||
loginAccountFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ import gitbucket.core.service._
|
||||
import gitbucket.core.util.{Keys, UsersAuthenticator}
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.service.IssuesService._
|
||||
import gitbucket.core.service.ActivityService._
|
||||
|
||||
class DashboardController
|
||||
extends DashboardControllerBase
|
||||
@@ -40,9 +41,9 @@ trait DashboardControllerBase extends ControllerBase {
|
||||
context.loginAccount,
|
||||
None,
|
||||
withoutPhysicalInfo = true,
|
||||
limit = context.settings.limitVisibleRepositories
|
||||
limit = context.settings.basicBehavior.limitVisibleRepositories
|
||||
)
|
||||
html.repos(getGroupNames(loginAccount.userName), repos, repos)
|
||||
html.repos(getGroupNames(loginAccount.userName), repos, repos, isNewsFeedEnabled())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -129,8 +130,9 @@ trait DashboardControllerBase extends ControllerBase {
|
||||
context.loginAccount,
|
||||
None,
|
||||
withoutPhysicalInfo = true,
|
||||
limit = context.settings.limitVisibleRepositories
|
||||
)
|
||||
limit = context.settings.basicBehavior.limitVisibleRepositories
|
||||
),
|
||||
isNewsFeedEnabled()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -171,8 +173,9 @@ trait DashboardControllerBase extends ControllerBase {
|
||||
context.loginAccount,
|
||||
None,
|
||||
withoutPhysicalInfo = true,
|
||||
limit = context.settings.limitVisibleRepositories
|
||||
)
|
||||
limit = context.settings.basicBehavior.limitVisibleRepositories
|
||||
),
|
||||
isNewsFeedEnabled()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import java.net.URI
|
||||
import com.nimbusds.jwt.JWT
|
||||
|
||||
import java.net.URI
|
||||
import com.nimbusds.oauth2.sdk.id.State
|
||||
import com.nimbusds.openid.connect.sdk.Nonce
|
||||
import gitbucket.core.helper.xml
|
||||
@@ -13,6 +14,8 @@ import gitbucket.core.view.helpers._
|
||||
import org.scalatra.Ok
|
||||
import org.scalatra.forms._
|
||||
|
||||
import gitbucket.core.service.ActivityService._
|
||||
|
||||
class IndexController
|
||||
extends IndexControllerBase
|
||||
with RepositoryService
|
||||
@@ -57,30 +60,37 @@ trait IndexControllerBase extends ControllerBase {
|
||||
//
|
||||
// case class SearchForm(query: String, owner: String, repository: String)
|
||||
|
||||
case class OidcContext(state: State, nonce: Nonce, redirectBackURI: String)
|
||||
case class OidcAuthContext(state: State, nonce: Nonce, redirectBackURI: String)
|
||||
case class OidcSessionContext(token: JWT)
|
||||
|
||||
get("/") {
|
||||
context.loginAccount
|
||||
.map { account =>
|
||||
val visibleOwnerSet: Set[String] = Set(account.userName) ++ getGroupsByUserName(account.userName)
|
||||
gitbucket.core.html.index(
|
||||
getRecentActivitiesByOwners(visibleOwnerSet),
|
||||
getVisibleRepositories(
|
||||
Some(account),
|
||||
None,
|
||||
withoutPhysicalInfo = true,
|
||||
limit = context.settings.limitVisibleRepositories
|
||||
),
|
||||
showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken(
|
||||
account.userName
|
||||
if (!isNewsFeedEnabled()) {
|
||||
redirect("/dashboard/repos")
|
||||
} else {
|
||||
gitbucket.core.html.index(
|
||||
activities = getRecentActivitiesByOwners(visibleOwnerSet),
|
||||
recentRepositories = getVisibleRepositories(
|
||||
Some(account),
|
||||
None,
|
||||
withoutPhysicalInfo = true,
|
||||
limit = context.settings.basicBehavior.limitVisibleRepositories
|
||||
),
|
||||
showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken(
|
||||
account.userName
|
||||
),
|
||||
enableNewsFeed = isNewsFeedEnabled()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.getOrElse {
|
||||
gitbucket.core.html.index(
|
||||
getRecentPublicActivities(),
|
||||
getVisibleRepositories(None, withoutPhysicalInfo = true),
|
||||
showBannerToCreatePersonalAccessToken = false
|
||||
activities = getRecentPublicActivities(),
|
||||
recentRepositories = getVisibleRepositories(None, withoutPhysicalInfo = true),
|
||||
showBannerToCreatePersonalAccessToken = false,
|
||||
enableNewsFeed = isNewsFeedEnabled()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -120,8 +130,8 @@ trait IndexControllerBase extends ControllerBase {
|
||||
case _ => "/"
|
||||
}
|
||||
session.setAttribute(
|
||||
Keys.Session.OidcContext,
|
||||
OidcContext(authenticationRequest.getState, authenticationRequest.getNonce, redirectBackURI)
|
||||
Keys.Session.OidcAuthContext,
|
||||
OidcAuthContext(authenticationRequest.getState, authenticationRequest.getNonce, redirectBackURI)
|
||||
)
|
||||
redirect(authenticationRequest.toURI.toString)
|
||||
} getOrElse {
|
||||
@@ -135,10 +145,12 @@ trait IndexControllerBase extends ControllerBase {
|
||||
get("/signin/oidc") {
|
||||
context.settings.oidc.map { oidc =>
|
||||
val redirectURI = new URI(s"$baseUrl/signin/oidc")
|
||||
session.get(Keys.Session.OidcContext) match {
|
||||
case Some(context: OidcContext) =>
|
||||
authenticate(params.toMap, redirectURI, context.state, context.nonce, oidc).map { account =>
|
||||
signin(account, context.redirectBackURI)
|
||||
session.get(Keys.Session.OidcAuthContext) match {
|
||||
case Some(context: OidcAuthContext) =>
|
||||
authenticate(params.toMap, redirectURI, context.state, context.nonce, oidc).map {
|
||||
case (jwt, account) =>
|
||||
session.setAttribute(Keys.Session.OidcSessionContext, OidcSessionContext(jwt))
|
||||
signin(account, context.redirectBackURI)
|
||||
} orElse {
|
||||
flash.update("error", "Sorry, authentication failed. Please try again.")
|
||||
session.invalidate()
|
||||
@@ -155,7 +167,19 @@ trait IndexControllerBase extends ControllerBase {
|
||||
}
|
||||
|
||||
get("/signout") {
|
||||
context.settings.oidc.map { oidc =>
|
||||
session.get(Keys.Session.OidcSessionContext).foreach {
|
||||
case context: OidcSessionContext =>
|
||||
val redirectURI = new URI(baseUrl)
|
||||
val authenticationRequest = createOIDLogoutRequest(oidc.issuer, oidc.clientID, redirectURI, context.token)
|
||||
session.invalidate
|
||||
redirect(authenticationRequest.toURI.toString)
|
||||
}
|
||||
}
|
||||
session.invalidate
|
||||
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
|
||||
deleteLoginAccountFromLocalFile()
|
||||
}
|
||||
redirect("/")
|
||||
}
|
||||
|
||||
@@ -178,6 +202,9 @@ trait IndexControllerBase extends ControllerBase {
|
||||
*/
|
||||
private def signin(account: Account, redirectUrl: String = "/") = {
|
||||
session.setAttribute(Keys.Session.LoginAccount, account)
|
||||
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
|
||||
saveLoginAccountToLocalFile(account)
|
||||
}
|
||||
updateLastLoginDate(account.userName)
|
||||
|
||||
if (LDAPUtil.isDummyMailAddress(account)) {
|
||||
@@ -289,11 +316,11 @@ trait IndexControllerBase extends ControllerBase {
|
||||
context.loginAccount,
|
||||
None,
|
||||
withoutPhysicalInfo = true,
|
||||
limit = context.settings.limitVisibleRepositories
|
||||
limit = context.settings.basicBehavior.limitVisibleRepositories
|
||||
)
|
||||
|
||||
val repositories = {
|
||||
context.settings.limitVisibleRepositories match {
|
||||
context.settings.basicBehavior.limitVisibleRepositories match {
|
||||
case true =>
|
||||
getVisibleRepositories(
|
||||
context.loginAccount,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.issues.html
|
||||
import gitbucket.core.model.Account
|
||||
import gitbucket.core.model.{Account, CustomFieldBehavior}
|
||||
import gitbucket.core.service.IssuesService._
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.util.Implicits._
|
||||
@@ -21,6 +21,7 @@ class IssuesController
|
||||
with ActivityService
|
||||
with HandleCommentService
|
||||
with IssueCreationService
|
||||
with CustomFieldsService
|
||||
with ReadableUsersAuthenticator
|
||||
with ReferrerAuthenticator
|
||||
with WritableUsersAuthenticator
|
||||
@@ -41,6 +42,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
with ActivityService
|
||||
with HandleCommentService
|
||||
with IssueCreationService
|
||||
with CustomFieldsService
|
||||
with ReadableUsersAuthenticator
|
||||
with ReferrerAuthenticator
|
||||
with WritableUsersAuthenticator
|
||||
@@ -51,7 +53,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
case class IssueCreateForm(
|
||||
title: String,
|
||||
content: Option[String],
|
||||
assignedUserName: Option[String],
|
||||
assigneeUserNames: Option[String],
|
||||
milestoneId: Option[Int],
|
||||
priorityId: Option[Int],
|
||||
labelNames: Option[String]
|
||||
@@ -62,7 +64,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
val issueCreateForm = mapping(
|
||||
"title" -> trim(label("Title", text(required))),
|
||||
"content" -> trim(optional(text())),
|
||||
"assignedUserName" -> trim(optional(text())),
|
||||
"assigneeUserNames" -> trim(optional(text())),
|
||||
"milestoneId" -> trim(optional(number())),
|
||||
"priorityId" -> trim(optional(number())),
|
||||
"labelNames" -> trim(optional(text()))
|
||||
@@ -105,10 +107,12 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
issue,
|
||||
getComments(repository.owner, repository.name, issueId.toInt),
|
||||
getIssueLabels(repository.owner, repository.name, issueId.toInt),
|
||||
getIssueAssignees(repository.owner, repository.name, issueId.toInt),
|
||||
getAssignableUserNames(repository.owner, repository.name),
|
||||
getMilestonesWithIssueCount(repository.owner, repository.name),
|
||||
getPriorities(repository.owner, repository.name),
|
||||
getLabels(repository.owner, repository.name),
|
||||
getCustomFieldsWithValue(repository.owner, repository.name, issueId.toInt).filter(_._1.enableForIssues),
|
||||
isIssueEditable(repository),
|
||||
isIssueManageable(repository),
|
||||
isIssueCommentManageable(repository),
|
||||
@@ -126,6 +130,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
getPriorities(repository.owner, repository.name),
|
||||
getDefaultPriority(repository.owner, repository.name),
|
||||
getLabels(repository.owner, repository.name),
|
||||
getCustomFields(repository.owner, repository.name).filter(_.enableForIssues),
|
||||
isIssueManageable(repository),
|
||||
getContentTemplate(repository, "ISSUE_TEMPLATE"),
|
||||
repository
|
||||
@@ -141,12 +146,31 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
repository,
|
||||
form.title,
|
||||
form.content,
|
||||
form.assignedUserName,
|
||||
form.assigneeUserNames.toSeq.flatMap(_.split(",")),
|
||||
form.milestoneId,
|
||||
form.priorityId,
|
||||
form.labelNames.toSeq.flatMap(_.split(",")),
|
||||
loginAccount
|
||||
)
|
||||
|
||||
// Insert custom field values
|
||||
params.toMap.foreach {
|
||||
case (key, value) =>
|
||||
if (key.startsWith("custom-field-")) {
|
||||
getCustomField(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
key.replaceFirst("^custom-field-", "").toInt
|
||||
).foreach { field =>
|
||||
CustomFieldBehavior.validate(field, value, messages) match {
|
||||
case None =>
|
||||
insertOrUpdateCustomFieldValue(field, repository.owner, repository.name, issue.issueId, value)
|
||||
case Some(_) => halt(400)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
redirect(s"/${issue.userName}/${issue.repositoryName}/issues/${issue.issueId}")
|
||||
} else Unauthorized()
|
||||
}
|
||||
@@ -235,7 +259,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
loginAccount =>
|
||||
getComment(repository.owner, repository.name, params("id")).map { comment =>
|
||||
if (isEditableContent(repository.owner, repository.name, comment.commentedUserName, loginAccount)) {
|
||||
updateComment(comment.issueId, comment.commentId, form.content)
|
||||
updateComment(repository.owner, repository.name, comment.issueId, comment.commentId, form.content)
|
||||
redirect(s"/${repository.owner}/${repository.name}/issue_comments/_data/${comment.commentId}")
|
||||
} else Unauthorized()
|
||||
} getOrElse NotFound()
|
||||
@@ -333,15 +357,16 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/:id/assign")(writableUsersOnly { repository =>
|
||||
updateAssignedUserName(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
params("id").toInt,
|
||||
assignedUserName("assignedUserName"),
|
||||
true
|
||||
)
|
||||
Ok("updated")
|
||||
ajaxPost("/:owner/:repository/issues/:id/assignee/new")(writableUsersOnly { repository =>
|
||||
val issueId = params("id").toInt
|
||||
registerIssueAssignee(repository.owner, repository.name, issueId, params("assigneeUserName"), true)
|
||||
Ok()
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/:id/assignee/delete")(writableUsersOnly { repository =>
|
||||
val issueId = params("id").toInt
|
||||
deleteIssueAssignee(repository.owner, repository.name, issueId, params("assigneeUserName"), true)
|
||||
Ok()
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { repository =>
|
||||
@@ -362,6 +387,35 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
Ok("updated")
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/customfield_validation/:fieldId")(writableUsersOnly { repository =>
|
||||
val fieldId = params("fieldId").toInt
|
||||
val value = params("value")
|
||||
getCustomField(repository.owner, repository.name, fieldId)
|
||||
.flatMap { field =>
|
||||
CustomFieldBehavior.validate(field, value, messages).map { error =>
|
||||
Ok(error)
|
||||
}
|
||||
}
|
||||
.getOrElse(Ok())
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/:id/customfield/:fieldId")(writableUsersOnly { repository =>
|
||||
val issueId = params("id").toInt
|
||||
val fieldId = params("fieldId").toInt
|
||||
val value = params("value")
|
||||
|
||||
for {
|
||||
_ <- getIssue(repository.owner, repository.name, issueId.toString)
|
||||
field <- getCustomField(repository.owner, repository.name, fieldId)
|
||||
} {
|
||||
CustomFieldBehavior.validate(field, value, messages) match {
|
||||
case None => insertOrUpdateCustomFieldValue(field, repository.owner, repository.name, issueId, value)
|
||||
case Some(_) => halt(400)
|
||||
}
|
||||
}
|
||||
Ok(value)
|
||||
})
|
||||
|
||||
post("/:owner/:repository/issues/batchedit/state")(writableUsersOnly { repository =>
|
||||
val action = params.get("value")
|
||||
action match {
|
||||
@@ -403,7 +457,13 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository =>
|
||||
val value = assignedUserName("value")
|
||||
executeBatch(repository) {
|
||||
updateAssignedUserName(repository.owner, repository.name, _, value, true)
|
||||
//updateAssignedUserName(repository.owner, repository.name, _, value, true)
|
||||
value match {
|
||||
case Some(assignedUserName) =>
|
||||
registerIssueAssignee(repository.owner, repository.name, _, assignedUserName, true)
|
||||
case None =>
|
||||
deleteAllIssueAssignees(repository.owner, repository.name, _, true)
|
||||
}
|
||||
}
|
||||
if (params("uri").nonEmpty) {
|
||||
redirect(params("uri"))
|
||||
|
||||
@@ -44,7 +44,7 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
|
||||
html.list(
|
||||
getLabels(repository.owner, repository.name),
|
||||
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
||||
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
|
||||
repository,
|
||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||
)
|
||||
@@ -59,7 +59,7 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
html.label(
|
||||
getLabel(repository.owner, repository.name, labelId).get,
|
||||
// TODO futility
|
||||
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
||||
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
|
||||
repository,
|
||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||
)
|
||||
@@ -76,7 +76,7 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
html.label(
|
||||
getLabel(repository.owner, repository.name, params("labelId").toInt).get,
|
||||
// TODO futility
|
||||
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
||||
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
|
||||
repository,
|
||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ trait PreProcessControllerBase extends ControllerBase {
|
||||
* If anonymous access is allowed, pass all requests.
|
||||
* But if it's not allowed, demands authentication except some paths.
|
||||
*/
|
||||
get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) {
|
||||
get(!context.settings.basicBehavior.allowAnonymousAccess, context.loginAccount.isEmpty) {
|
||||
if (!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
|
||||
!context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs") &&
|
||||
!context.currentPath.startsWith("/plugin-assets") &&
|
||||
|
||||
@@ -45,7 +45,7 @@ trait PrioritiesControllerBase extends ControllerBase {
|
||||
get("/:owner/:repository/issues/priorities")(referrersOnly { repository =>
|
||||
html.list(
|
||||
getPriorities(repository.owner, repository.name),
|
||||
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
||||
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
|
||||
repository,
|
||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||
)
|
||||
@@ -60,7 +60,7 @@ trait PrioritiesControllerBase extends ControllerBase {
|
||||
createPriority(repository.owner, repository.name, form.priorityName, form.description, form.color.substring(1))
|
||||
html.priority(
|
||||
getPriority(repository.owner, repository.name, priorityId).get,
|
||||
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
||||
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
|
||||
repository,
|
||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||
)
|
||||
@@ -84,7 +84,7 @@ trait PrioritiesControllerBase extends ControllerBase {
|
||||
)
|
||||
html.priority(
|
||||
getPriority(repository.owner, repository.name, params("priorityId").toInt).get,
|
||||
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
||||
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
|
||||
repository,
|
||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||
)
|
||||
|
||||
@@ -25,6 +25,7 @@ class PullRequestsController
|
||||
with PullRequestService
|
||||
with MilestonesService
|
||||
with LabelsService
|
||||
with CustomFieldsService
|
||||
with CommitsService
|
||||
with ActivityService
|
||||
with WebHookPullRequestService
|
||||
@@ -44,6 +45,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
with IssuesService
|
||||
with MilestonesService
|
||||
with LabelsService
|
||||
with CustomFieldsService
|
||||
with CommitsService
|
||||
with ActivityService
|
||||
with PullRequestService
|
||||
@@ -67,7 +69,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
"commitIdFrom" -> trim(text(required, maxlength(40))),
|
||||
"commitIdTo" -> trim(text(required, maxlength(40))),
|
||||
"isDraft" -> trim(boolean(required)),
|
||||
"assignedUserName" -> trim(optional(text())),
|
||||
"assigneeUserNames" -> trim(optional(text())),
|
||||
"milestoneId" -> trim(optional(number())),
|
||||
"priorityId" -> trim(optional(number())),
|
||||
"labelNames" -> trim(optional(text()))
|
||||
@@ -90,7 +92,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
commitIdFrom: String,
|
||||
commitIdTo: String,
|
||||
isDraft: Boolean,
|
||||
assignedUserName: Option[String],
|
||||
assigneeUserNames: Option[String],
|
||||
milestoneId: Option[Int],
|
||||
priorityId: Option[Int],
|
||||
labelNames: Option[String]
|
||||
@@ -129,10 +131,12 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
getPullRequestComments(repository.owner, repository.name, issue.issueId, commits.flatten),
|
||||
diffs.size,
|
||||
getIssueLabels(repository.owner, repository.name, issueId),
|
||||
getIssueAssignees(repository.owner, repository.name, issueId),
|
||||
getAssignableUserNames(repository.owner, repository.name),
|
||||
getMilestonesWithIssueCount(repository.owner, repository.name),
|
||||
getPriorities(repository.owner, repository.name),
|
||||
getLabels(repository.owner, repository.name),
|
||||
getCustomFieldsWithValue(repository.owner, repository.name, issueId).filter(_._1.enableForPullRequests),
|
||||
isEditable(repository),
|
||||
isManageable(repository),
|
||||
hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount),
|
||||
@@ -140,25 +144,6 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName),
|
||||
flash.iterator.map(f => f._1 -> f._2.toString).toMap
|
||||
)
|
||||
|
||||
// html.pullreq(
|
||||
// issue,
|
||||
// pullreq,
|
||||
// comments,
|
||||
// getIssueLabels(owner, name, issueId),
|
||||
// getAssignableUserNames(owner, name),
|
||||
// getMilestonesWithIssueCount(owner, name),
|
||||
// getPriorities(owner, name),
|
||||
// getLabels(owner, name),
|
||||
// commits,
|
||||
// diffs,
|
||||
// isEditable(repository),
|
||||
// isManageable(repository),
|
||||
// hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount),
|
||||
// repository,
|
||||
// getRepository(pullreq.requestUserName, pullreq.requestRepositoryName),
|
||||
// flash.toMap.map(f => f._1 -> f._2.toString)
|
||||
// )
|
||||
}
|
||||
} getOrElse NotFound()
|
||||
})
|
||||
@@ -392,9 +377,9 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
|
||||
val headBranch: Option[String] = params.get("head")
|
||||
val headBranch = params.get("head")
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(originUserName), Some(originRepositoryName)) => {
|
||||
case (Some(originUserName), Some(originRepositoryName)) =>
|
||||
getRepository(originUserName, originRepositoryName).map {
|
||||
originRepository =>
|
||||
Using.resources(
|
||||
@@ -411,8 +396,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
)
|
||||
}
|
||||
} getOrElse NotFound()
|
||||
}
|
||||
case _ => {
|
||||
case _ =>
|
||||
Using.resource(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))) { git =>
|
||||
JGitUtil.getDefaultBranch(git, forkedRepository).map {
|
||||
case (_, defaultBranch) =>
|
||||
@@ -423,41 +407,48 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
private def getOriginRepositoryName(
|
||||
originOwner: String,
|
||||
forkedOwner: String,
|
||||
forkedRepository: RepositoryInfo
|
||||
): Option[String] = {
|
||||
if (originOwner == forkedOwner) {
|
||||
// Self repository
|
||||
Some(forkedRepository.name)
|
||||
} else if (forkedRepository.repository.originUserName.isEmpty) {
|
||||
// when ForkedRepository is the original repository
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
||||
.find(_.userName == originOwner)
|
||||
.map(_.repositoryName)
|
||||
} else if (Some(originOwner) == forkedRepository.repository.originUserName) {
|
||||
// Original repository
|
||||
forkedRepository.repository.originRepositoryName
|
||||
} else {
|
||||
// Sibling repository
|
||||
getUserRepositories(originOwner)
|
||||
.find { x =>
|
||||
x.repository.originUserName == forkedRepository.repository.originUserName &&
|
||||
x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName
|
||||
}
|
||||
.map(_.repository.repositoryName)
|
||||
}
|
||||
}
|
||||
|
||||
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
|
||||
val Seq(origin, forked) = multiParams("splat")
|
||||
val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner)
|
||||
val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner)
|
||||
|
||||
(for (originRepositoryName <- if (originOwner == forkedOwner) {
|
||||
// Self repository
|
||||
Some(forkedRepository.name)
|
||||
} else if (forkedRepository.repository.originUserName.isEmpty) {
|
||||
// when ForkedRepository is the original repository
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
||||
.find(_.userName == originOwner)
|
||||
.map(_.repositoryName)
|
||||
} else if (Some(originOwner) == forkedRepository.repository.originUserName) {
|
||||
// Original repository
|
||||
forkedRepository.repository.originRepositoryName
|
||||
} else {
|
||||
// Sibling repository
|
||||
getUserRepositories(originOwner)
|
||||
.find { x =>
|
||||
x.repository.originUserName == forkedRepository.repository.originUserName &&
|
||||
x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName
|
||||
}
|
||||
.map(_.repository.repositoryName)
|
||||
};
|
||||
(for (originRepositoryName <- getOriginRepositoryName(originOwner, forkedOwner, forkedRepository);
|
||||
originRepository <- getRepository(originOwner, originRepositoryName)) yield {
|
||||
val (oldId, newId) =
|
||||
getPullRequestCommitFromTo(originRepository, forkedRepository, originId, forkedId)
|
||||
|
||||
(oldId, newId) match {
|
||||
case (Some(oldId), Some(newId)) => {
|
||||
case (Some(oldId), Some(newId)) =>
|
||||
val (commits, diffs) = getRequestCompareInfo(
|
||||
originRepository.owner,
|
||||
originRepository.name,
|
||||
@@ -505,9 +496,9 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
getMilestones(originRepository.owner, originRepository.name),
|
||||
getPriorities(originRepository.owner, originRepository.name),
|
||||
getDefaultPriority(originRepository.owner, originRepository.name),
|
||||
getLabels(originRepository.owner, originRepository.name)
|
||||
getLabels(originRepository.owner, originRepository.name),
|
||||
getCustomFields(originRepository.owner, originRepository.name).filter(_.enableForPullRequests)
|
||||
)
|
||||
}
|
||||
case (oldId, newId) =>
|
||||
redirect(
|
||||
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/" +
|
||||
@@ -519,6 +510,54 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/diff/:id")(referrersOnly { repository =>
|
||||
(for {
|
||||
commitId <- params.get("id")
|
||||
path <- params.get("path")
|
||||
diff <- getSingleDiff(repository.owner, repository.name, commitId, path)
|
||||
} yield {
|
||||
contentType = formats("json")
|
||||
org.json4s.jackson.Serialization.write(
|
||||
Map(
|
||||
"oldContent" -> diff.oldContent,
|
||||
"newContent" -> diff.newContent
|
||||
)
|
||||
)
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/diff/*...*")(referrersOnly { forkedRepository =>
|
||||
val Seq(origin, forked) = multiParams("splat")
|
||||
val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner)
|
||||
val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner)
|
||||
|
||||
(for {
|
||||
path <- params.get("path")
|
||||
originRepositoryName <- getOriginRepositoryName(originOwner, forkedOwner, forkedRepository)
|
||||
originRepository <- getRepository(originOwner, originRepositoryName)
|
||||
(oldId, newId) = getPullRequestCommitFromTo(originRepository, forkedRepository, originId, forkedId)
|
||||
oldId <- oldId
|
||||
newId <- newId
|
||||
diff <- getSingleDiff(
|
||||
originRepository.owner,
|
||||
originRepository.name,
|
||||
oldId.getName,
|
||||
forkedRepository.owner,
|
||||
forkedRepository.name,
|
||||
newId.getName,
|
||||
path
|
||||
)
|
||||
} yield {
|
||||
contentType = formats("json")
|
||||
org.json4s.jackson.Serialization.write(
|
||||
Map(
|
||||
"oldContent" -> diff.oldContent,
|
||||
"newContent" -> diff.newContent
|
||||
)
|
||||
)
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(readableUsersOnly { forkedRepository =>
|
||||
val Seq(origin, forked) = multiParams("splat")
|
||||
val (originOwner, tmpOriginBranch) = parseCompareIdentifier(origin, forkedRepository.owner)
|
||||
@@ -567,7 +606,6 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
loginUser = loginAccount.userName,
|
||||
title = form.title,
|
||||
content = form.content,
|
||||
assignedUserName = if (manageable) form.assignedUserName else None,
|
||||
milestoneId = if (manageable) form.milestoneId else None,
|
||||
priorityId = if (manageable) form.priorityId else None,
|
||||
isPullRequest = true
|
||||
@@ -587,8 +625,14 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
settings = context.settings
|
||||
)
|
||||
|
||||
// insert labels
|
||||
if (manageable) {
|
||||
// insert assignees
|
||||
form.assigneeUserNames.foreach { value =>
|
||||
value.split(",").foreach { userName =>
|
||||
registerIssueAssignee(repository.owner, repository.name, issueId, userName)
|
||||
}
|
||||
}
|
||||
// insert labels
|
||||
form.labelNames.foreach { value =>
|
||||
val labels = getLabels(repository.owner, repository.name)
|
||||
value.split(",").foreach { labelName =>
|
||||
|
||||
@@ -2,7 +2,6 @@ package gitbucket.core.controller
|
||||
|
||||
import java.time.{LocalDateTime, ZoneOffset}
|
||||
import java.util.Date
|
||||
|
||||
import gitbucket.core.settings.html
|
||||
import gitbucket.core.model.{RepositoryWebHook, WebHook}
|
||||
import gitbucket.core.service._
|
||||
@@ -21,7 +20,7 @@ import org.eclipse.jgit.lib.Constants
|
||||
import org.eclipse.jgit.lib.ObjectId
|
||||
|
||||
import scala.util.Using
|
||||
import org.scalatra.Forbidden
|
||||
import org.scalatra.{Forbidden, Ok}
|
||||
|
||||
class RepositorySettingsController
|
||||
extends RepositorySettingsControllerBase
|
||||
@@ -31,6 +30,7 @@ class RepositorySettingsController
|
||||
with ProtectedBranchService
|
||||
with CommitStatusService
|
||||
with DeployKeyService
|
||||
with CustomFieldsService
|
||||
with ActivityService
|
||||
with OwnerAuthenticator
|
||||
with UsersAuthenticator
|
||||
@@ -43,6 +43,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
with ProtectedBranchService
|
||||
with CommitStatusService
|
||||
with DeployKeyService
|
||||
with CustomFieldsService
|
||||
with ActivityService
|
||||
with OwnerAuthenticator
|
||||
with UsersAuthenticator =>
|
||||
@@ -121,6 +122,23 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
"newOwner" -> trim(label("New owner", text(required, transferUser)))
|
||||
)(TransferOwnerShipForm.apply)
|
||||
|
||||
// for custom field
|
||||
case class CustomFieldForm(
|
||||
fieldName: String,
|
||||
fieldType: String,
|
||||
constraints: Option[String],
|
||||
enableForIssues: Boolean,
|
||||
enableForPullRequests: Boolean
|
||||
)
|
||||
|
||||
val customFieldForm = mapping(
|
||||
"fieldName" -> trim(label("Field name", text(required, maxlength(100)))),
|
||||
"fieldType" -> trim(label("Field type", text(required))),
|
||||
"constraints" -> trim(label("Constraints", optional(text()))),
|
||||
"enableForIssues" -> trim(label("Enable for issues", boolean(required))),
|
||||
"enableForPullRequests" -> trim(label("Enable for pull requests", boolean(required))),
|
||||
)(CustomFieldForm.apply)
|
||||
|
||||
/**
|
||||
* Redirect to the Options page.
|
||||
*/
|
||||
@@ -390,7 +408,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) =>
|
||||
context.withLoginAccount {
|
||||
loginAccount =>
|
||||
if (context.settings.repositoryOperation.rename || loginAccount.isAdmin) {
|
||||
if (context.settings.basicBehavior.repositoryOperation.rename || loginAccount.isAdmin) {
|
||||
if (repository.name != form.repositoryName) {
|
||||
// Update database and move git repository
|
||||
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
|
||||
@@ -414,7 +432,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
|
||||
context.withLoginAccount {
|
||||
loginAccount =>
|
||||
if (context.settings.repositoryOperation.transfer || loginAccount.isAdmin) {
|
||||
if (context.settings.basicBehavior.repositoryOperation.transfer || loginAccount.isAdmin) {
|
||||
// Change repository owner
|
||||
if (repository.owner != form.newOwner) {
|
||||
// Update database and move git repository
|
||||
@@ -438,7 +456,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
*/
|
||||
post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
|
||||
context.withLoginAccount { loginAccount =>
|
||||
if (context.settings.repositoryOperation.delete || loginAccount.isAdmin) {
|
||||
if (context.settings.basicBehavior.repositoryOperation.delete || loginAccount.isAdmin) {
|
||||
// Delete the repository and related files
|
||||
deleteRepository(repository.repository)
|
||||
redirect(s"/${repository.owner}")
|
||||
@@ -477,6 +495,60 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/deploykey")
|
||||
})
|
||||
|
||||
/** Custom fields for issues and pull requests */
|
||||
get("/:owner/:repository/settings/issues")(ownerOnly { repository =>
|
||||
val customFields = getCustomFields(repository.owner, repository.name)
|
||||
html.issues(customFields, repository)
|
||||
})
|
||||
|
||||
/** New custom field form */
|
||||
get("/:owner/:repository/settings/issues/fields/new")(ownerOnly { repository =>
|
||||
html.issuesfieldform(None, repository)
|
||||
})
|
||||
|
||||
/** Add custom field */
|
||||
ajaxPost("/:owner/:repository/settings/issues/fields/new", customFieldForm)(ownerOnly { (form, repository) =>
|
||||
val fieldId = createCustomField(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
form.fieldName,
|
||||
form.fieldType,
|
||||
if (form.fieldType == "enum") form.constraints else None,
|
||||
form.enableForIssues,
|
||||
form.enableForPullRequests
|
||||
)
|
||||
html.issuesfield(getCustomField(repository.owner, repository.name, fieldId).get)
|
||||
})
|
||||
|
||||
/** Edit custom field form */
|
||||
ajaxGet("/:owner/:repository/settings/issues/fields/:fieldId/edit")(ownerOnly { repository =>
|
||||
getCustomField(repository.owner, repository.name, params("fieldId").toInt).map { customField =>
|
||||
html.issuesfieldform(Some(customField), repository)
|
||||
} getOrElse NotFound()
|
||||
})
|
||||
|
||||
/** Update custom field */
|
||||
ajaxPost("/:owner/:repository/settings/issues/fields/:fieldId/edit", customFieldForm)(ownerOnly {
|
||||
(form, repository) =>
|
||||
updateCustomField(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
params("fieldId").toInt,
|
||||
form.fieldName,
|
||||
form.fieldType,
|
||||
if (form.fieldType == "enum") form.constraints else None,
|
||||
form.enableForIssues,
|
||||
form.enableForPullRequests
|
||||
)
|
||||
html.issuesfield(getCustomField(repository.owner, repository.name, params("fieldId").toInt).get)
|
||||
})
|
||||
|
||||
/** Delete custom field */
|
||||
ajaxPost("/:owner/:repository/settings/issues/fields/:fieldId/delete")(ownerOnly { repository =>
|
||||
deleteCustomField(repository.owner, repository.name, params("fieldId").toInt)
|
||||
Ok()
|
||||
})
|
||||
|
||||
/**
|
||||
* Provides duplication check for web hook url.
|
||||
*/
|
||||
|
||||
@@ -329,55 +329,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
post("/:owner/:repository/upload", uploadForm)(writableUsersOnly { (form, repository) =>
|
||||
context.withLoginAccount {
|
||||
loginAccount =>
|
||||
val files = form.uploadFiles
|
||||
.split("\n")
|
||||
.map { line =>
|
||||
val i = line.indexOf(':')
|
||||
CommitFile(line.substring(0, i).trim, line.substring(i + 1).trim)
|
||||
}
|
||||
.toSeq
|
||||
|
||||
val newFiles = files.map { file =>
|
||||
file.copy(name = if (form.path.length == 0) file.name else s"${form.path}/${file.name}")
|
||||
}
|
||||
|
||||
if (form.newBranch) {
|
||||
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
|
||||
val objectId = _commit(newBranchName, newFiles, loginAccount)
|
||||
val issueId =
|
||||
createIssueAndPullRequest(
|
||||
repository,
|
||||
form.branch,
|
||||
newBranchName,
|
||||
form.commit,
|
||||
objectId.name,
|
||||
form.message,
|
||||
loginAccount
|
||||
)
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
} else {
|
||||
_commit(form.branch, newFiles, loginAccount)
|
||||
if (form.path.length == 0) {
|
||||
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}")
|
||||
} else {
|
||||
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}/${form.path}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def _commit(
|
||||
branchName: String,
|
||||
//files: Seq[CommitFile],
|
||||
newFiles: Seq[CommitFile],
|
||||
loginAccount: Account
|
||||
): ObjectId = {
|
||||
): Either[String, ObjectId] = {
|
||||
commitFiles(
|
||||
repository = repository,
|
||||
branch = branchName,
|
||||
//path = form.path,
|
||||
//files = files.toIndexedSeq,
|
||||
message = form.message.getOrElse("Add files via upload"),
|
||||
loginAccount = loginAccount,
|
||||
settings = context.settings
|
||||
@@ -399,6 +358,52 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.withLoginAccount {
|
||||
loginAccount =>
|
||||
val files = form.uploadFiles
|
||||
.split("\n")
|
||||
.map { line =>
|
||||
val i = line.indexOf(':')
|
||||
CommitFile(line.substring(0, i).trim, line.substring(i + 1).trim)
|
||||
}
|
||||
.toSeq
|
||||
|
||||
val newFiles = files.map { file =>
|
||||
file.copy(name = if (form.path.length == 0) file.name else s"${form.path}/${file.name}")
|
||||
}
|
||||
|
||||
if (form.newBranch) {
|
||||
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
|
||||
_commit(newBranchName, newFiles, loginAccount) match {
|
||||
case Right(objectId) =>
|
||||
val issueId =
|
||||
createIssueAndPullRequest(
|
||||
repository,
|
||||
form.branch,
|
||||
newBranchName,
|
||||
form.commit,
|
||||
objectId.name,
|
||||
form.message,
|
||||
loginAccount
|
||||
)
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
case Left(error) => Forbidden(gitbucket.core.html.error(error))
|
||||
}
|
||||
} else {
|
||||
_commit(form.branch, newFiles, loginAccount) match {
|
||||
case Right(_) =>
|
||||
if (form.path.length == 0) {
|
||||
redirect(s"/${repository.owner}/${repository.name}/tree/${encodeRefName(form.branch)}")
|
||||
} else {
|
||||
redirect(
|
||||
s"/${repository.owner}/${repository.name}/tree/${encodeRefName(form.branch)}/${encodeRefName(form.path)}"
|
||||
)
|
||||
}
|
||||
case Left(error) => Forbidden(gitbucket.core.html.error(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/edit/*")(writableUsersOnly { repository =>
|
||||
@@ -456,32 +461,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
post("/:owner/:repository/create", editorForm)(writableUsersOnly { (form, repository) =>
|
||||
context.withLoginAccount {
|
||||
loginAccount =>
|
||||
if (form.newBranch) {
|
||||
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
|
||||
val objectId = _commit(newBranchName, loginAccount)
|
||||
val issueId =
|
||||
createIssueAndPullRequest(
|
||||
repository,
|
||||
form.branch,
|
||||
newBranchName,
|
||||
form.commit,
|
||||
objectId.name,
|
||||
form.message,
|
||||
loginAccount
|
||||
)
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
} else {
|
||||
_commit(form.branch, loginAccount)
|
||||
redirect(
|
||||
s"/${repository.owner}/${repository.name}/blob/${form.branch}/${if (form.path.length == 0) urlEncode(form.newFileName)
|
||||
else s"${form.path}/${urlEncode(form.newFileName)}"}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def _commit(branchName: String, loginAccount: Account): ObjectId = {
|
||||
def _commit(branchName: String, loginAccount: Account): Either[String, ObjectId] = {
|
||||
commitFile(
|
||||
repository = repository,
|
||||
branch = branchName,
|
||||
@@ -494,37 +474,48 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
commit = form.commit,
|
||||
loginAccount = loginAccount,
|
||||
settings = context.settings
|
||||
)._1
|
||||
).map(_._1)
|
||||
}
|
||||
})
|
||||
|
||||
post("/:owner/:repository/update", editorForm)(writableUsersOnly { (form, repository) =>
|
||||
context.withLoginAccount {
|
||||
loginAccount =>
|
||||
if (form.newBranch) {
|
||||
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
|
||||
val objectId = _commit(newBranchName, loginAccount)
|
||||
val issueId =
|
||||
createIssueAndPullRequest(
|
||||
repository,
|
||||
form.branch,
|
||||
newBranchName,
|
||||
form.commit,
|
||||
objectId.name,
|
||||
form.message,
|
||||
loginAccount
|
||||
)
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
_commit(newBranchName, loginAccount) match {
|
||||
case Right(objectId) =>
|
||||
val issueId =
|
||||
createIssueAndPullRequest(
|
||||
repository,
|
||||
form.branch,
|
||||
newBranchName,
|
||||
form.commit,
|
||||
objectId.name,
|
||||
form.message,
|
||||
loginAccount
|
||||
)
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
case Left(error) => Forbidden(gitbucket.core.html.error(error))
|
||||
}
|
||||
} else {
|
||||
_commit(form.branch, loginAccount)
|
||||
redirect(
|
||||
s"/${repository.owner}/${repository.name}/blob/${urlEncode(form.branch)}/${if (form.path.length == 0) urlEncode(form.newFileName)
|
||||
else s"${form.path}/${urlEncode(form.newFileName)}"}"
|
||||
)
|
||||
_commit(form.branch, loginAccount) match {
|
||||
case Right(_) =>
|
||||
if (form.path.length == 0) {
|
||||
redirect(
|
||||
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${urlEncode(form.newFileName)}"
|
||||
)
|
||||
} else {
|
||||
redirect(
|
||||
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${encodeRefName(form.path)}/${urlEncode(form.newFileName)}"
|
||||
)
|
||||
}
|
||||
case Left(error) => Forbidden(gitbucket.core.html.error(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
def _commit(branchName: String, loginAccount: Account): ObjectId = {
|
||||
post("/:owner/:repository/update", editorForm)(writableUsersOnly { (form, repository) =>
|
||||
def _commit(branchName: String, loginAccount: Account): Either[String, ObjectId] = {
|
||||
commitFile(
|
||||
repository = repository,
|
||||
branch = branchName,
|
||||
@@ -541,37 +532,48 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
commit = form.commit,
|
||||
loginAccount = loginAccount,
|
||||
settings = context.settings
|
||||
)._1
|
||||
).map(_._1)
|
||||
}
|
||||
})
|
||||
|
||||
post("/:owner/:repository/remove", deleteForm)(writableUsersOnly { (form, repository) =>
|
||||
context.withLoginAccount {
|
||||
loginAccount =>
|
||||
if (form.newBranch) {
|
||||
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
|
||||
val objectId = _commit(newBranchName, loginAccount)
|
||||
val issueId =
|
||||
createIssueAndPullRequest(
|
||||
repository,
|
||||
form.branch,
|
||||
newBranchName,
|
||||
form.commit,
|
||||
objectId.name,
|
||||
form.message,
|
||||
loginAccount
|
||||
)
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
_commit(newBranchName, loginAccount) match {
|
||||
case Right(objectId) =>
|
||||
val issueId =
|
||||
createIssueAndPullRequest(
|
||||
repository,
|
||||
form.branch,
|
||||
newBranchName,
|
||||
form.commit,
|
||||
objectId.name,
|
||||
form.message,
|
||||
loginAccount
|
||||
)
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
case Left(error) => Forbidden(gitbucket.core.html.error(error))
|
||||
}
|
||||
} else {
|
||||
_commit(form.branch, loginAccount)
|
||||
redirect(
|
||||
s"/${repository.owner}/${repository.name}/tree/${form.branch}${if (form.path.length == 0) ""
|
||||
else "/" + form.path}"
|
||||
)
|
||||
_commit(form.branch, loginAccount) match {
|
||||
case Right(_) =>
|
||||
if (form.path.length == 0) {
|
||||
redirect(
|
||||
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${urlEncode(form.newFileName)}"
|
||||
)
|
||||
} else {
|
||||
redirect(
|
||||
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${encodeRefName(form.path)}/${urlEncode(form.newFileName)}"
|
||||
)
|
||||
}
|
||||
case Left(error) => Forbidden(gitbucket.core.html.error(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
def _commit(branchName: String, loginAccount: Account): ObjectId = {
|
||||
post("/:owner/:repository/remove", deleteForm)(writableUsersOnly { (form, repository) =>
|
||||
def _commit(branchName: String, loginAccount: Account): Either[String, ObjectId] = {
|
||||
commitFile(
|
||||
repository = repository,
|
||||
branch = branchName,
|
||||
@@ -584,7 +586,41 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
commit = form.commit,
|
||||
loginAccount = loginAccount,
|
||||
settings = context.settings
|
||||
)._1
|
||||
).map(_._1)
|
||||
}
|
||||
|
||||
context.withLoginAccount {
|
||||
loginAccount =>
|
||||
if (form.newBranch) {
|
||||
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
|
||||
_commit(newBranchName, loginAccount) match {
|
||||
case Right(objectId) =>
|
||||
val issueId =
|
||||
createIssueAndPullRequest(
|
||||
repository,
|
||||
form.branch,
|
||||
newBranchName,
|
||||
form.commit,
|
||||
objectId.name,
|
||||
form.message,
|
||||
loginAccount
|
||||
)
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
case Left(error) => Forbidden(gitbucket.core.html.error(error))
|
||||
}
|
||||
} else {
|
||||
_commit(form.branch, loginAccount) match {
|
||||
case Right(_) =>
|
||||
if (form.path.isEmpty) {
|
||||
redirect(s"/${repository.owner}/${repository.name}/tree/${encodeRefName(form.branch)}")
|
||||
} else {
|
||||
redirect(
|
||||
s"/${repository.owner}/${repository.name}/tree/${encodeRefName(form.branch)}/${encodeRefName(form.path)}"
|
||||
)
|
||||
}
|
||||
case Left(error) => Forbidden(gitbucket.core.html.error(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -640,7 +676,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
loginUser = loginAccount.userName,
|
||||
title = requestBranch,
|
||||
content = commitMessage,
|
||||
assignedUserName = None,
|
||||
milestoneId = None,
|
||||
priorityId = None,
|
||||
isPullRequest = true
|
||||
|
||||
@@ -34,19 +34,22 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
private val form = mapping(
|
||||
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
||||
"information" -> trim(label("Information", optional(text()))),
|
||||
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
|
||||
"allowAnonymousAccess" -> trim(label("Anonymous access", 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())),
|
||||
"notification" -> trim(label("Notification", boolean())),
|
||||
"limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())),
|
||||
"basicBehavior" -> mapping(
|
||||
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
|
||||
"allowResetPassword" -> trim(label("Reset password", boolean())),
|
||||
"allowAnonymousAccess" -> trim(label("Anonymous access", 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())),
|
||||
"notification" -> trim(label("Notification", boolean())),
|
||||
"limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())),
|
||||
)(BasicBehavior.apply),
|
||||
"ssh" -> mapping(
|
||||
"enabled" -> trim(label("SSH access", boolean())),
|
||||
"bindAddress" -> mapping(
|
||||
@@ -334,7 +337,12 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
post("/admin/system/sendmail", sendMailForm)(adminOnly { form =>
|
||||
try {
|
||||
new Mailer(context.settings.copy(smtp = Some(form.smtp), notification = true)).send(
|
||||
new Mailer(
|
||||
context.settings.copy(
|
||||
smtp = Some(form.smtp),
|
||||
basicBehavior = context.settings.basicBehavior.copy(notification = true)
|
||||
)
|
||||
).send(
|
||||
to = form.testAddress,
|
||||
subject = "Test message from GitBucket",
|
||||
textMsg = "This is a test message from GitBucket.",
|
||||
|
||||
@@ -57,22 +57,27 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
|
||||
* iii. Create a reference
|
||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-reference
|
||||
*/
|
||||
post("/api/v3/repos/:owner/:repository/git/refs")(referrersOnly { repository =>
|
||||
post("/api/v3/repos/:owner/:repository/git/refs")(writableUsersOnly { repository =>
|
||||
extractFromJsonBody[CreateARef].map {
|
||||
data =>
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.owner))) { git =>
|
||||
val ref = git.getRepository.findRef(data.ref)
|
||||
if (ref == null) {
|
||||
val update = git.getRepository.updateRef(data.ref)
|
||||
update.setNewObjectId(ObjectId.fromString(data.sha))
|
||||
val result = update.update()
|
||||
result match {
|
||||
case Result.NEW => JsonFormat(ApiRef.fromRef(RepositoryName(repository.owner, repository.name), ref))
|
||||
case _ => UnprocessableEntity(result.name())
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
|
||||
git =>
|
||||
val ref = git.getRepository.findRef(data.ref)
|
||||
if (ref == null) {
|
||||
val update = git.getRepository.updateRef(data.ref)
|
||||
update.setNewObjectId(ObjectId.fromString(data.sha))
|
||||
val result = update.update()
|
||||
result match {
|
||||
case Result.NEW =>
|
||||
JsonFormat(
|
||||
ApiRef
|
||||
.fromRef(RepositoryName(repository.owner, repository.name), git.getRepository.findRef(data.ref))
|
||||
)
|
||||
case _ => UnprocessableEntity(result.name())
|
||||
}
|
||||
} else {
|
||||
UnprocessableEntity("Ref already exists.")
|
||||
}
|
||||
} else {
|
||||
UnprocessableEntity("Ref already exists.")
|
||||
}
|
||||
}
|
||||
} getOrElse BadRequest()
|
||||
})
|
||||
@@ -85,7 +90,7 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
|
||||
val refName = multiParams("splat").mkString("/")
|
||||
extractFromJsonBody[UpdateARef].map {
|
||||
data =>
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.owner))) { git =>
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
val ref = git.getRepository.findRef(refName)
|
||||
if (ref == null) {
|
||||
UnprocessableEntity("Ref does not exist.")
|
||||
@@ -96,7 +101,7 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
|
||||
val result = update.update()
|
||||
result match {
|
||||
case Result.FORCED | Result.FAST_FORWARD | Result.NO_CHANGE =>
|
||||
JsonFormat(ApiRef.fromRef(RepositoryName(repository), update.getRef))
|
||||
JsonFormat(ApiRef.fromRef(RepositoryName(repository), git.getRepository.findRef(refName)))
|
||||
case _ => UnprocessableEntity(result.name())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ trait ApiIssueCommentControllerBase extends ControllerBase {
|
||||
* iv. Delete a comment
|
||||
* https://docs.github.com/en/rest/reference/issues#delete-an-issue-comment
|
||||
*/
|
||||
delete("/api/v3/repos/:owner/:repo/issues/comments/:id")(readableUsersOnly { repository =>
|
||||
delete("/api/v3/repos/:owner/:repository/issues/comments/:id")(readableUsersOnly { repository =>
|
||||
val maybeDeleteResponse: Option[Either[ActionResult, Option[Int]]] =
|
||||
for {
|
||||
commentId <- params("id").toIntOpt
|
||||
|
||||
@@ -29,9 +29,9 @@ trait ApiIssueControllerBase extends ControllerBase {
|
||||
val page = IssueSearchCondition.page(request)
|
||||
// TODO: more api spec condition
|
||||
val condition = IssueSearchCondition(request)
|
||||
val baseOwner = getAccountByUserName(repository.owner).get
|
||||
//val baseOwner = getAccountByUserName(repository.owner).get
|
||||
|
||||
val issues: List[(Issue, Account, Option[Account])] =
|
||||
val issues: List[(Issue, Account, List[Account])] =
|
||||
searchIssueByApi(
|
||||
condition = condition,
|
||||
offset = (page - 1) * PullRequestLimit,
|
||||
@@ -40,12 +40,12 @@ trait ApiIssueControllerBase extends ControllerBase {
|
||||
)
|
||||
|
||||
JsonFormat(issues.map {
|
||||
case (issue, issueUser, assignedUser) =>
|
||||
case (issue, issueUser, assigneeUsers) =>
|
||||
ApiIssue(
|
||||
issue = issue,
|
||||
repositoryName = RepositoryName(repository),
|
||||
user = ApiUser(issueUser),
|
||||
assignee = assignedUser.map(ApiUser(_)),
|
||||
assignees = assigneeUsers.map(ApiUser(_)),
|
||||
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
|
||||
.map(ApiLabel(_, RepositoryName(repository))),
|
||||
issue.milestoneId.flatMap { getApiMilestone(repository, _) }
|
||||
@@ -61,7 +61,8 @@ trait ApiIssueControllerBase extends ControllerBase {
|
||||
(for {
|
||||
issueId <- params("id").toIntOpt
|
||||
issue <- getIssue(repository.owner, repository.name, issueId.toString)
|
||||
users = getAccountsByUserNames(Set(issue.openedUserName) ++ issue.assignedUserName, Set())
|
||||
assigneeUsers = getIssueAssignees(repository.owner, repository.name, issueId)
|
||||
users = getAccountsByUserNames(Set(issue.openedUserName) ++ assigneeUsers.map(_.assigneeUserName), Set())
|
||||
openedUser <- users.get(issue.openedUserName)
|
||||
} yield {
|
||||
JsonFormat(
|
||||
@@ -69,7 +70,7 @@ trait ApiIssueControllerBase extends ControllerBase {
|
||||
issue,
|
||||
RepositoryName(repository),
|
||||
ApiUser(openedUser),
|
||||
issue.assignedUserName.flatMap(users.get(_)).map(ApiUser(_)),
|
||||
assigneeUsers.flatMap(x => users.get(x.assigneeUserName)).map(ApiUser(_)),
|
||||
getIssueLabels(repository.owner, repository.name, issue.issueId).map(ApiLabel(_, RepositoryName(repository))),
|
||||
issue.milestoneId.flatMap { getApiMilestone(repository, _) }
|
||||
)
|
||||
@@ -92,7 +93,7 @@ trait ApiIssueControllerBase extends ControllerBase {
|
||||
repository,
|
||||
data.title,
|
||||
data.body,
|
||||
data.assignees.headOption,
|
||||
data.assignees,
|
||||
milestone.map(_.milestoneId),
|
||||
None,
|
||||
data.labels,
|
||||
@@ -103,7 +104,9 @@ trait ApiIssueControllerBase extends ControllerBase {
|
||||
issue,
|
||||
RepositoryName(repository),
|
||||
ApiUser(loginAccount),
|
||||
issue.assignedUserName.flatMap(getAccountByUserName(_)).map(ApiUser(_)),
|
||||
getIssueAssignees(repository.owner, repository.name, issue.issueId)
|
||||
.flatMap(x => getAccountByUserName(x.assigneeUserName, false))
|
||||
.map(ApiUser.apply),
|
||||
getIssueLabels(repository.owner, repository.name, issue.issueId)
|
||||
.map(ApiLabel(_, RepositoryName(repository))),
|
||||
issue.milestoneId.flatMap { getApiMilestone(repository, _) }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package gitbucket.core.controller.api
|
||||
import gitbucket.core.api.{ApiError, ApiLabel, CreateALabel, JsonFormat}
|
||||
import gitbucket.core.api.{AddLabelsToAnIssue, ApiError, ApiLabel, CreateALabel, JsonFormat}
|
||||
import gitbucket.core.controller.ControllerBase
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.util.Implicits._
|
||||
@@ -121,10 +121,10 @@ trait ApiIssueLabelControllerBase extends ControllerBase {
|
||||
*/
|
||||
post("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository =>
|
||||
JsonFormat(for {
|
||||
data <- extractFromJsonBody[Seq[String]]
|
||||
data <- extractFromJsonBody[AddLabelsToAnIssue]
|
||||
issueId <- params("id").toIntOpt
|
||||
} yield {
|
||||
data.map { labelName =>
|
||||
data.labels.map { labelName =>
|
||||
val label = getLabel(repository.owner, repository.name, labelName).getOrElse(
|
||||
getLabel(
|
||||
repository.owner,
|
||||
@@ -160,11 +160,11 @@ trait ApiIssueLabelControllerBase extends ControllerBase {
|
||||
*/
|
||||
put("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository =>
|
||||
JsonFormat(for {
|
||||
data <- extractFromJsonBody[Seq[String]]
|
||||
data <- extractFromJsonBody[AddLabelsToAnIssue]
|
||||
issueId <- params("id").toIntOpt
|
||||
} yield {
|
||||
deleteAllIssueLabels(repository.owner, repository.name, issueId, true)
|
||||
data.map { labelName =>
|
||||
data.labels.map { labelName =>
|
||||
val label = getLabel(repository.owner, repository.name, labelName).getOrElse(
|
||||
getLabel(
|
||||
repository.owner,
|
||||
|
||||
@@ -40,7 +40,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
|
||||
val condition = IssueSearchCondition(request)
|
||||
val baseOwner = getAccountByUserName(repository.owner).get
|
||||
|
||||
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] =
|
||||
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account, List[Account])] =
|
||||
searchPullRequestByApi(
|
||||
condition = condition,
|
||||
offset = (page - 1) * PullRequestLimit,
|
||||
@@ -49,7 +49,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
|
||||
)
|
||||
|
||||
JsonFormat(issues.map {
|
||||
case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignee) =>
|
||||
case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignees) =>
|
||||
ApiPullRequest(
|
||||
issue = issue,
|
||||
pullRequest = pullRequest,
|
||||
@@ -58,7 +58,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
|
||||
user = ApiUser(issueUser),
|
||||
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
|
||||
.map(ApiLabel(_, RepositoryName(repository))),
|
||||
assignee = assignee.map(ApiUser.apply),
|
||||
assignees = assignees.map(ApiUser.apply),
|
||||
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
|
||||
)
|
||||
})
|
||||
@@ -99,7 +99,6 @@ trait ApiPullRequestControllerBase extends ControllerBase {
|
||||
loginUser = context.loginAccount.get.userName,
|
||||
title = createPullReq.title,
|
||||
content = createPullReq.body,
|
||||
assignedUserName = None,
|
||||
milestoneId = None,
|
||||
priorityId = None,
|
||||
isPullRequest = true
|
||||
@@ -319,8 +318,8 @@ trait ApiPullRequestControllerBase extends ControllerBase {
|
||||
baseOwner <- users.get(repository.owner)
|
||||
headOwner <- users.get(pullRequest.requestUserName)
|
||||
issueUser <- users.get(issue.openedUserName)
|
||||
assignee = issue.assignedUserName.flatMap { userName =>
|
||||
getAccountByUserName(userName, false)
|
||||
assignees = getIssueAssignees(repository.owner, repository.name, issueId).flatMap { assignedUser =>
|
||||
getAccountByUserName(assignedUser.assigneeUserName, false)
|
||||
}
|
||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
|
||||
} yield {
|
||||
@@ -332,7 +331,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
|
||||
user = ApiUser(issueUser),
|
||||
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
|
||||
.map(ApiLabel(_, RepositoryName(repository))),
|
||||
assignee = assignee.map(ApiUser.apply),
|
||||
assignees = assignees.map(ApiUser.apply),
|
||||
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,9 +9,23 @@ import gitbucket.core.util.JGitUtil.{CommitInfo, getBranches, getBranchesOfCommi
|
||||
import gitbucket.core.util.{JGitUtil, ReferrerAuthenticator, RepositoryName}
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.revwalk.RevWalk
|
||||
|
||||
import org.eclipse.jgit.revwalk.filter.{
|
||||
AndRevFilter,
|
||||
AuthorRevFilter,
|
||||
CommitTimeRevFilter,
|
||||
MaxCountRevFilter,
|
||||
RevFilter,
|
||||
SkipRevFilter
|
||||
}
|
||||
import org.eclipse.jgit.treewalk.filter.{AndTreeFilter, PathFilterGroup, TreeFilter}
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.util.Using
|
||||
import math.min
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter._
|
||||
import java.util.Date
|
||||
import java.time.ZoneOffset
|
||||
|
||||
trait ApiRepositoryCommitControllerBase extends ControllerBase {
|
||||
self: AccountService with CommitsService with ProtectedBranchService with ReferrerAuthenticator =>
|
||||
@@ -22,8 +36,13 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
|
||||
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 = params.getOrElse("sha", "refs/heads/master")
|
||||
val sha = params.get("sha").filter(_.nonEmpty).getOrElse("HEAD")
|
||||
val page = params.get("page").filter(_.nonEmpty).getOrElse("1").toInt
|
||||
val per_page = min(params.get("per_page").filter(_.nonEmpty).getOrElse("30").toInt, 100)
|
||||
val author = params.get("author").filter(_.nonEmpty)
|
||||
val path = params.get("path").filter(_.nonEmpty)
|
||||
val since = params.get("since").filter(_.nonEmpty)
|
||||
val until = params.get("until").filter(_.nonEmpty)
|
||||
Using.resource(Git.open(getRepositoryDir(owner, name))) {
|
||||
git =>
|
||||
val repo = git.getRepository
|
||||
@@ -31,7 +50,37 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
|
||||
revWalk =>
|
||||
val objectId = repo.resolve(sha)
|
||||
revWalk.markStart(revWalk.parseCommit(objectId))
|
||||
JsonFormat(revWalk.asScala.take(30).map {
|
||||
if (path.nonEmpty) {
|
||||
revWalk.setTreeFilter(
|
||||
AndTreeFilter.create(PathFilterGroup.createFromStrings(path.get), TreeFilter.ANY_DIFF)
|
||||
)
|
||||
}
|
||||
val revfilters = new ListBuffer[(RevFilter)]()
|
||||
if (author.nonEmpty) {
|
||||
revfilters += AuthorRevFilter.create(author.get)
|
||||
}
|
||||
if (since.nonEmpty) {
|
||||
revfilters += CommitTimeRevFilter.after(
|
||||
Date.from(LocalDateTime.parse(since.get, ISO_DATE_TIME).toInstant(ZoneOffset.UTC))
|
||||
)
|
||||
}
|
||||
if (until.nonEmpty) {
|
||||
revfilters += CommitTimeRevFilter.before(
|
||||
Date.from(LocalDateTime.parse(until.get, ISO_DATE_TIME).toInstant(ZoneOffset.UTC))
|
||||
)
|
||||
}
|
||||
if (page > 1) {
|
||||
revfilters += SkipRevFilter.create(page * per_page - 2)
|
||||
}
|
||||
revfilters += MaxCountRevFilter.create(per_page);
|
||||
revWalk.setRevFilter(
|
||||
if (revfilters.size > 1) {
|
||||
AndRevFilter.create(revfilters.toArray)
|
||||
} else {
|
||||
revfilters(0)
|
||||
}
|
||||
)
|
||||
JsonFormat(revWalk.asScala.map {
|
||||
commit =>
|
||||
val commitInfo = new CommitInfo(commit)
|
||||
ApiCommits(
|
||||
|
||||
@@ -157,7 +157,7 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
|
||||
Some("https://docs.github.com/en/rest/reference/repos#create-or-update-file-contents")
|
||||
)
|
||||
case _ =>
|
||||
val (commitId, blobId) = commitFile(
|
||||
commitFile(
|
||||
repository,
|
||||
branch,
|
||||
path,
|
||||
@@ -170,12 +170,12 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
|
||||
data.committer.map(_.name).getOrElse(loginAccount.fullName),
|
||||
data.committer.map(_.email).getOrElse(loginAccount.mailAddress),
|
||||
context.settings
|
||||
)
|
||||
|
||||
blobId match {
|
||||
case None =>
|
||||
) match {
|
||||
case Left(error) =>
|
||||
ApiError(s"Failed to commit a file: ${error}", None)
|
||||
case Right((_, None)) =>
|
||||
ApiError("Failed to commit a file.", None)
|
||||
case Some(blobId) =>
|
||||
case Right((commitId, Some(blobId))) =>
|
||||
Map(
|
||||
"content" -> ApiContents(
|
||||
"file",
|
||||
|
||||
363
src/main/scala/gitbucket/core/model/CustomField.scala
Normal file
363
src/main/scala/gitbucket/core/model/CustomField.scala
Normal file
@@ -0,0 +1,363 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.util.StringUtil
|
||||
import gitbucket.core.view.helpers
|
||||
import org.scalatra.i18n.Messages
|
||||
import play.twirl.api.Html
|
||||
|
||||
trait CustomFieldComponent extends TemplateComponent { self: Profile =>
|
||||
import profile.api._
|
||||
|
||||
lazy val CustomFields = TableQuery[CustomFields]
|
||||
|
||||
class CustomFields(tag: Tag) extends Table[CustomField](tag, "CUSTOM_FIELD") with BasicTemplate {
|
||||
val fieldId = column[Int]("FIELD_ID", O AutoInc)
|
||||
val fieldName = column[String]("FIELD_NAME")
|
||||
val fieldType = column[String]("FIELD_TYPE")
|
||||
val constraints = column[Option[String]]("CONSTRAINTS")
|
||||
val enableForIssues = column[Boolean]("ENABLE_FOR_ISSUES")
|
||||
val enableForPullRequests = column[Boolean]("ENABLE_FOR_PULL_REQUESTS")
|
||||
def * =
|
||||
(userName, repositoryName, fieldId, fieldName, fieldType, constraints, enableForIssues, enableForPullRequests)
|
||||
.<>(CustomField.tupled, CustomField.unapply)
|
||||
|
||||
def byPrimaryKey(userName: String, repositoryName: String, fieldId: Int) =
|
||||
(this.userName === userName.bind) && (this.repositoryName === repositoryName.bind) && (this.fieldId === fieldId.bind)
|
||||
}
|
||||
}
|
||||
|
||||
case class CustomField(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
fieldId: Int = 0,
|
||||
fieldName: String,
|
||||
fieldType: String, // long, double, string, date, or enum
|
||||
constraints: Option[String],
|
||||
enableForIssues: Boolean,
|
||||
enableForPullRequests: Boolean
|
||||
)
|
||||
|
||||
trait CustomFieldBehavior {
|
||||
def createHtml(repository: RepositoryInfo, fieldId: Int, fieldName: String, constraints: Option[String])(
|
||||
implicit context: Context
|
||||
): String
|
||||
def fieldHtml(
|
||||
repository: RepositoryInfo,
|
||||
issueId: Int,
|
||||
fieldId: Int,
|
||||
fieldName: String,
|
||||
constraints: Option[String],
|
||||
value: String,
|
||||
editable: Boolean
|
||||
)(
|
||||
implicit context: Context
|
||||
): String
|
||||
def validate(name: String, constraints: Option[String], value: String, messages: Messages): Option[String]
|
||||
}
|
||||
|
||||
object CustomFieldBehavior {
|
||||
def validate(field: CustomField, value: String, messages: Messages): Option[String] = {
|
||||
if (value.isEmpty) None
|
||||
else {
|
||||
CustomFieldBehavior(field.fieldType).flatMap { behavior =>
|
||||
behavior.validate(field.fieldName, field.constraints, value, messages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def apply(fieldType: String): Option[CustomFieldBehavior] = {
|
||||
fieldType match {
|
||||
case "long" => Some(LongFieldBehavior)
|
||||
case "double" => Some(DoubleFieldBehavior)
|
||||
case "string" => Some(StringFieldBehavior)
|
||||
case "date" => Some(DateFieldBehavior)
|
||||
case "enum" => Some(EnumFieldBehavior)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
case object LongFieldBehavior extends TextFieldBehavior {
|
||||
override def validate(
|
||||
name: String,
|
||||
constraints: Option[String],
|
||||
value: String,
|
||||
messages: Messages
|
||||
): Option[String] = {
|
||||
try {
|
||||
value.toLong
|
||||
None
|
||||
} catch {
|
||||
case _: NumberFormatException => Some(messages("error.number").format(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
case object DoubleFieldBehavior extends TextFieldBehavior {
|
||||
override def validate(
|
||||
name: String,
|
||||
constraints: Option[String],
|
||||
value: String,
|
||||
messages: Messages
|
||||
): Option[String] = {
|
||||
try {
|
||||
value.toDouble
|
||||
None
|
||||
} catch {
|
||||
case _: NumberFormatException => Some(messages("error.number").format(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
case object StringFieldBehavior extends TextFieldBehavior
|
||||
case object DateFieldBehavior extends TextFieldBehavior {
|
||||
private val pattern = "yyyy-MM-dd"
|
||||
override protected val fieldType: String = "date"
|
||||
|
||||
override def validate(
|
||||
name: String,
|
||||
constraints: Option[String],
|
||||
value: String,
|
||||
messages: Messages
|
||||
): Option[String] = {
|
||||
try {
|
||||
new java.text.SimpleDateFormat(pattern).parse(value)
|
||||
None
|
||||
} catch {
|
||||
case _: java.text.ParseException =>
|
||||
Some(messages("error.datePattern").format(name, pattern))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case object EnumFieldBehavior extends CustomFieldBehavior {
|
||||
override def createHtml(repository: RepositoryInfo, fieldId: Int, fieldName: String, constraints: Option[String])(
|
||||
implicit context: Context
|
||||
): String = {
|
||||
createPulldownHtml(repository, fieldId, fieldName, constraints, None, None)
|
||||
}
|
||||
|
||||
override def fieldHtml(
|
||||
repository: RepositoryInfo,
|
||||
issueId: Int,
|
||||
fieldId: Int,
|
||||
fieldName: String,
|
||||
constraints: Option[String],
|
||||
value: String,
|
||||
editable: Boolean
|
||||
)(implicit context: Context): String = {
|
||||
if (!editable) {
|
||||
val sb = new StringBuilder
|
||||
sb.append("""</div>""")
|
||||
sb.append("""<div>""")
|
||||
if (value == "") {
|
||||
sb.append(s"""<span id="label-custom-field-$fieldId"><span class="muted small">No ${StringUtil.escapeHtml(
|
||||
fieldName
|
||||
)}</span></span>""")
|
||||
} else {
|
||||
sb.append(s"""<span id="label-custom-field-$fieldId"><span class="muted small">${StringUtil
|
||||
.escapeHtml(value)}</span></span>""")
|
||||
}
|
||||
sb.toString()
|
||||
} else {
|
||||
createPulldownHtml(repository, fieldId, fieldName, constraints, Some(issueId), Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
private def createPulldownHtml(
|
||||
repository: RepositoryInfo,
|
||||
fieldId: Int,
|
||||
fieldName: String,
|
||||
constraints: Option[String],
|
||||
issueId: Option[Int],
|
||||
value: Option[String]
|
||||
)(implicit context: Context): String = {
|
||||
val sb = new StringBuilder
|
||||
sb.append("""<div class="pull-right">""")
|
||||
sb.append(
|
||||
gitbucket.core.helper.html
|
||||
.dropdown("Edit", right = true, filter = (fieldName, s"Filter $fieldName")) {
|
||||
val options = new StringBuilder()
|
||||
options.append(
|
||||
s"""<li><a href="javascript:void(0);" class="custom-field-option-$fieldId" data-value=""><i class="octicon octicon-x"></i> Clear ${StringUtil
|
||||
.escapeHtml(fieldName)}</a></li>"""
|
||||
)
|
||||
constraints.foreach {
|
||||
x =>
|
||||
x.split(",").map(_.trim).foreach {
|
||||
item =>
|
||||
options.append(s"""<li>
|
||||
| <a href="javascript:void(0);" class="custom-field-option-$fieldId" data-value="${StringUtil
|
||||
.escapeHtml(item)}">
|
||||
| ${gitbucket.core.helper.html.checkicon(value.contains(item))}
|
||||
| ${StringUtil.escapeHtml(item)}
|
||||
| </a>
|
||||
|</li>
|
||||
|""".stripMargin)
|
||||
}
|
||||
}
|
||||
Html(options.toString())
|
||||
}
|
||||
.toString()
|
||||
)
|
||||
sb.append("""</div>""")
|
||||
sb.append("""</div>""")
|
||||
sb.append("""<div>""")
|
||||
value match {
|
||||
case None =>
|
||||
sb.append(s"""<span id="label-custom-field-$fieldId"><span class="muted small">No ${StringUtil.escapeHtml(
|
||||
fieldName
|
||||
)}</span></span>""")
|
||||
case Some(value) =>
|
||||
sb.append(s"""<span id="label-custom-field-$fieldId"><span class="muted small">${StringUtil
|
||||
.escapeHtml(value)}</span></span>""")
|
||||
}
|
||||
if (value.isEmpty || issueId.isEmpty) {
|
||||
sb.append(s"""<input type="hidden" id="custom-field-$fieldId" name="custom-field-$fieldId" value=""/>""")
|
||||
sb.append(s"""<script>
|
||||
|$$('a.custom-field-option-$fieldId').click(function(){
|
||||
| const value = $$(this).data('value');
|
||||
| $$('a.custom-field-option-$fieldId i.octicon-check').removeClass('octicon-check');
|
||||
| $$('#custom-field-$fieldId').val(value);
|
||||
| if (value == '') {
|
||||
| $$('#label-custom-field-$fieldId').html($$('<span class="muted small">').text('No ${StringUtil
|
||||
.escapeHtml(fieldName)}'));
|
||||
| } else {
|
||||
| $$('#label-custom-field-$fieldId').html($$('<span class="muted small">').text(value));
|
||||
| $$('a.custom-field-option-$fieldId[data-value=' + value + '] i').addClass('octicon-check');
|
||||
| }
|
||||
|});
|
||||
|</script>""".stripMargin)
|
||||
} else {
|
||||
sb.append(s"""<script>
|
||||
|$$('a.custom-field-option-$fieldId').click(function(){
|
||||
| const value = $$(this).data('value');
|
||||
| $$.post('${helpers.url(repository)}/issues/${issueId.get}/customfield/$fieldId',
|
||||
| { value: value },
|
||||
| function(data){
|
||||
| $$('a.custom-field-option-$fieldId i.octicon-check').removeClass('octicon-check');
|
||||
| if (value == '') {
|
||||
| $$('#label-custom-field-$fieldId').html($$('<span class="muted small">').text('No ${StringUtil
|
||||
.escapeHtml(fieldName)}'));
|
||||
| } else {
|
||||
| $$('#label-custom-field-$fieldId').html($$('<span class="muted small">').text(value));
|
||||
| $$('a.custom-field-option-$fieldId[data-value=' + value + '] i').addClass('octicon-check');
|
||||
| }
|
||||
| }
|
||||
| );
|
||||
|});
|
||||
|</script>
|
||||
|""".stripMargin)
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
override def validate(
|
||||
name: String,
|
||||
constraints: Option[String],
|
||||
value: String,
|
||||
messages: Messages
|
||||
): Option[String] = None
|
||||
}
|
||||
|
||||
trait TextFieldBehavior extends CustomFieldBehavior {
|
||||
protected val fieldType = "text"
|
||||
|
||||
override def createHtml(repository: RepositoryInfo, fieldId: Int, fieldName: String, constraints: Option[String])(
|
||||
implicit context: Context
|
||||
): String = {
|
||||
val sb = new StringBuilder
|
||||
sb.append(
|
||||
s"""<input type="$fieldType" class="form-control input-sm" id="custom-field-$fieldId" name="custom-field-$fieldId" data-field-id="$fieldId" style="width: 120px;"/>"""
|
||||
)
|
||||
sb.append(s"""<script>
|
||||
|$$('#custom-field-$fieldId').focusout(function(){
|
||||
| const $$this = $$(this);
|
||||
| $$.post('${helpers.url(repository)}/issues/customfield_validation/$fieldId',
|
||||
| { value: $$this.val() },
|
||||
| function(data){
|
||||
| if (data != '') {
|
||||
| $$('#custom-field-$fieldId-error').text(data);
|
||||
| } else {
|
||||
| $$('#custom-field-$fieldId-error').text('');
|
||||
| }
|
||||
| }
|
||||
| );
|
||||
|});
|
||||
|</script>
|
||||
|""".stripMargin)
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
override def fieldHtml(
|
||||
repository: RepositoryInfo,
|
||||
issueId: Int,
|
||||
fieldId: Int,
|
||||
fieldName: String,
|
||||
constraints: Option[String],
|
||||
value: String,
|
||||
editable: Boolean
|
||||
)(
|
||||
implicit context: Context
|
||||
): String = {
|
||||
val sb = new StringBuilder
|
||||
sb.append(
|
||||
s"""<span id="custom-field-$fieldId-label" class="custom-field-label">${StringUtil
|
||||
.escapeHtml(value)}</span>""".stripMargin
|
||||
)
|
||||
if (editable) {
|
||||
sb.append(
|
||||
s"""<input type="$fieldType" id="custom-field-$fieldId-editor" class="form-control input-sm custom-field-editor" data-field-id="$fieldId" style="width: 120px; display: none;"/>"""
|
||||
)
|
||||
sb.append(s"""<script>
|
||||
|$$('#custom-field-$fieldId-label').click(function(){
|
||||
| const $$this = $$(this);
|
||||
| $$this.hide();
|
||||
| $$this.next().val($$this.text()).show().focus();
|
||||
|});
|
||||
|
|
||||
|$$('#custom-field-$fieldId-editor').focusout(function(){
|
||||
| const $$this = $$(this);
|
||||
| $$.post('${helpers.url(repository)}/issues/customfield_validation/$fieldId',
|
||||
| { value: $$this.val() },
|
||||
| function(data){
|
||||
| if (data != '') {
|
||||
| $$('#custom-field-$fieldId-error').text(data);
|
||||
| } else {
|
||||
| $$('#custom-field-$fieldId-error').text('');
|
||||
| $$.post('${helpers.url(repository)}/issues/$issueId/customfield/$fieldId',
|
||||
| { value: $$this.val() },
|
||||
| function(data){
|
||||
| $$this.hide();
|
||||
| $$this.prev().text(data).show();
|
||||
| }
|
||||
| );
|
||||
| }
|
||||
| }
|
||||
| );
|
||||
|});
|
||||
|
|
||||
|// ESC key handling in text field
|
||||
|$$('#custom-field-$fieldId-editor').keyup(function(e){
|
||||
| if (e.keyCode == 27) {
|
||||
| const $$this = $$(this);
|
||||
| $$this.hide();
|
||||
| $$this.prev().show();
|
||||
| }
|
||||
| if (e.keyCode == 13) {
|
||||
| $$('#custom-field-$fieldId-editor').blur();
|
||||
| }
|
||||
|});
|
||||
|</script>
|
||||
|""".stripMargin)
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
override def validate(
|
||||
name: String,
|
||||
constraints: Option[String],
|
||||
value: String,
|
||||
messages: Messages
|
||||
): Option[String] = None
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ trait IssueComponent extends TemplateComponent { self: Profile =>
|
||||
with MilestoneTemplate
|
||||
with PriorityTemplate {
|
||||
val openedUserName = column[String]("OPENED_USER_NAME")
|
||||
val assignedUserName = column[String]("ASSIGNED_USER_NAME")
|
||||
val title = column[String]("TITLE")
|
||||
val content = column[String]("CONTENT")
|
||||
val closed = column[Boolean]("CLOSED")
|
||||
@@ -42,7 +41,6 @@ trait IssueComponent extends TemplateComponent { self: Profile =>
|
||||
openedUserName,
|
||||
milestoneId.?,
|
||||
priorityId.?,
|
||||
assignedUserName.?,
|
||||
title,
|
||||
content.?,
|
||||
closed,
|
||||
@@ -62,7 +60,6 @@ case class Issue(
|
||||
openedUserName: String,
|
||||
milestoneId: Option[Int],
|
||||
priorityId: Option[Int],
|
||||
assignedUserName: Option[String],
|
||||
title: String,
|
||||
content: Option[String],
|
||||
closed: Boolean,
|
||||
|
||||
26
src/main/scala/gitbucket/core/model/IssueAssignee.scala
Normal file
26
src/main/scala/gitbucket/core/model/IssueAssignee.scala
Normal file
@@ -0,0 +1,26 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
trait IssueAssigneeComponent extends TemplateComponent { self: Profile =>
|
||||
import profile.api._
|
||||
import self._
|
||||
|
||||
lazy val IssueAssignees = TableQuery[IssueAssignees]
|
||||
|
||||
class IssueAssignees(tag: Tag) extends Table[IssueAssignee](tag, "ISSUE_ASSIGNEE") with IssueTemplate {
|
||||
val assigneeUserName = column[String]("ASSIGNEE_USER_NAME")
|
||||
def * =
|
||||
(userName, repositoryName, issueId, assigneeUserName)
|
||||
.<>(IssueAssignee.tupled, IssueAssignee.unapply)
|
||||
|
||||
def byPrimaryKey(owner: String, repository: String, issueId: Int, assigneeUserName: String) = {
|
||||
byIssue(owner, repository, issueId) && this.assigneeUserName === assigneeUserName.bind
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case class IssueAssignee(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
issueId: Int,
|
||||
assigneeUserName: String
|
||||
)
|
||||
31
src/main/scala/gitbucket/core/model/IssueCustomFields.scala
Normal file
31
src/main/scala/gitbucket/core/model/IssueCustomFields.scala
Normal file
@@ -0,0 +1,31 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
trait IssueCustomFieldComponent extends TemplateComponent { self: Profile =>
|
||||
import profile.api._
|
||||
import self._
|
||||
|
||||
lazy val IssueCustomFields = TableQuery[IssueCustomFields]
|
||||
|
||||
class IssueCustomFields(tag: Tag) extends Table[IssueCustomField](tag, "ISSUE_CUSTOM_FIELD") {
|
||||
val userName = column[String]("USER_NAME", O.PrimaryKey)
|
||||
val repositoryName = column[String]("REPOSITORY_NAME", O.PrimaryKey)
|
||||
val issueId = column[Int]("ISSUE_ID", O.PrimaryKey)
|
||||
val fieldId = column[Int]("FIELD_ID", O.PrimaryKey)
|
||||
val value = column[String]("VALUE")
|
||||
def * =
|
||||
(userName, repositoryName, issueId, fieldId, value)
|
||||
.<>(IssueCustomField.tupled, IssueCustomField.unapply)
|
||||
|
||||
def byPrimaryKey(owner: String, repository: String, issueId: Int, fieldId: Int) = {
|
||||
this.userName === owner.bind && this.repositoryName === repository.bind && this.issueId === issueId.bind && this.fieldId === fieldId.bind
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case class IssueCustomField(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
issueId: Int,
|
||||
fieldId: Int,
|
||||
value: String
|
||||
)
|
||||
@@ -73,5 +73,8 @@ trait CoreProfile
|
||||
with ReleaseAssetComponent
|
||||
with AccountExtraMailAddressComponent
|
||||
with AccountPreferenceComponent
|
||||
with CustomFieldComponent
|
||||
with IssueCustomFieldComponent
|
||||
with IssueAssigneeComponent
|
||||
|
||||
object Profile extends CoreProfile
|
||||
|
||||
@@ -7,9 +7,14 @@ import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.model.Profile.dateColumnType
|
||||
import gitbucket.core.util.{LDAPUtil, StringUtil}
|
||||
import StringUtil._
|
||||
import com.nimbusds.jose.{JWSAlgorithm, JWSHeader}
|
||||
import com.nimbusds.jose.crypto.{MACSigner, MACVerifier}
|
||||
import com.nimbusds.jwt.{JWTClaimsSet, SignedJWT}
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||
|
||||
import java.security.SecureRandom
|
||||
|
||||
trait AccountService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[AccountService])
|
||||
@@ -60,28 +65,28 @@ trait AccountService {
|
||||
case Right(ldapUserInfo) => {
|
||||
// Create or update account by LDAP information
|
||||
getAccountByUserName(ldapUserInfo.userName, true) match {
|
||||
case Some(x) if (!x.isRemoved) => {
|
||||
if (settings.ldap.get.mailAttribute.getOrElse("").isEmpty) {
|
||||
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
|
||||
case Some(x) =>
|
||||
if (x.isRemoved) {
|
||||
logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
|
||||
defaultAuthentication(userName, password)
|
||||
} else {
|
||||
updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
|
||||
if (settings.ldap.get.mailAttribute.getOrElse("").isEmpty) {
|
||||
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
|
||||
} else {
|
||||
updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
|
||||
}
|
||||
getAccountByUserName(ldapUserInfo.userName)
|
||||
}
|
||||
getAccountByUserName(ldapUserInfo.userName)
|
||||
}
|
||||
case Some(x) if (x.isRemoved) => {
|
||||
logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
|
||||
defaultAuthentication(userName, password)
|
||||
}
|
||||
case None =>
|
||||
getAccountByMailAddress(ldapUserInfo.mailAddress, true) match {
|
||||
case Some(x) if (!x.isRemoved) => {
|
||||
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
|
||||
getAccountByUserName(ldapUserInfo.userName)
|
||||
}
|
||||
case Some(x) if (x.isRemoved) => {
|
||||
logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
|
||||
defaultAuthentication(userName, password)
|
||||
}
|
||||
case Some(x) =>
|
||||
if (x.isRemoved) {
|
||||
logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
|
||||
defaultAuthentication(userName, password)
|
||||
} else {
|
||||
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
|
||||
getAccountByUserName(ldapUserInfo.userName)
|
||||
}
|
||||
case None => {
|
||||
createAccount(
|
||||
ldapUserInfo.userName,
|
||||
@@ -337,6 +342,33 @@ trait AccountService {
|
||||
}
|
||||
}
|
||||
|
||||
def generateResetPasswordToken(mailAddress: String): String = {
|
||||
val claimsSet = new JWTClaimsSet.Builder()
|
||||
.claim("mailAddress", mailAddress)
|
||||
.expirationTime(new java.util.Date(System.currentTimeMillis() + 10 * 1000))
|
||||
.build()
|
||||
|
||||
val signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet)
|
||||
signedJWT.sign(new MACSigner(AccountService.jwtSecretKey))
|
||||
|
||||
signedJWT.serialize()
|
||||
}
|
||||
|
||||
def decodeResetPasswordToken(token: String): Option[String] = {
|
||||
try {
|
||||
val signedJWT = SignedJWT.parse(token)
|
||||
val verifier = new MACVerifier(AccountService.jwtSecretKey)
|
||||
if (signedJWT.verify(verifier) && new java.util.Date().before(signedJWT.getJWTClaimsSet().getExpirationTime())) {
|
||||
Some(signedJWT.getPayload.toJSONObject.get("mailAddress").toString)
|
||||
} else None
|
||||
} catch {
|
||||
case _: Exception => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object AccountService extends AccountService
|
||||
object AccountService extends AccountService {
|
||||
// 256-bit key for HS256 which must be pre-shared
|
||||
val jwtSecretKey = new Array[Byte](32)
|
||||
new SecureRandom().nextBytes(jwtSecretKey)
|
||||
}
|
||||
|
||||
@@ -9,10 +9,12 @@ import org.json4s.jackson.Serialization.{read, write}
|
||||
import scala.util.Using
|
||||
import java.io.FileOutputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.util.ConfigUtil
|
||||
import org.apache.commons.io.input.ReversedLinesFileReader
|
||||
|
||||
import ActivityService._
|
||||
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
trait ActivityService {
|
||||
@@ -27,7 +29,7 @@ trait ActivityService {
|
||||
}
|
||||
|
||||
def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit context: Context): List[Activity] = {
|
||||
if (!ActivityLog.exists()) {
|
||||
if (!isNewsFeedEnabled() || !ActivityLog.exists()) {
|
||||
List.empty
|
||||
} else {
|
||||
val list = new ListBuffer[Activity]
|
||||
@@ -51,7 +53,7 @@ trait ActivityService {
|
||||
}
|
||||
|
||||
def getRecentPublicActivities()(implicit context: Context): List[Activity] = {
|
||||
if (!ActivityLog.exists()) {
|
||||
if (!isNewsFeedEnabled() || !ActivityLog.exists()) {
|
||||
List.empty
|
||||
} else {
|
||||
val list = new ListBuffer[Activity]
|
||||
@@ -69,7 +71,7 @@ trait ActivityService {
|
||||
}
|
||||
|
||||
def getRecentActivitiesByOwners(owners: Set[String])(implicit context: Context): List[Activity] = {
|
||||
if (!ActivityLog.exists()) {
|
||||
if (!isNewsFeedEnabled() || !ActivityLog.exists()) {
|
||||
List.empty
|
||||
} else {
|
||||
val list = new ListBuffer[Activity]
|
||||
@@ -93,3 +95,8 @@ trait ActivityService {
|
||||
writeLog(info.toActivity)
|
||||
}
|
||||
}
|
||||
|
||||
object ActivityService {
|
||||
def isNewsFeedEnabled(): Boolean =
|
||||
!ConfigUtil.getConfigValue[Boolean]("gitbucket.disableNewsFeed").getOrElse(false)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.model.{CustomField, IssueCustomField}
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
|
||||
trait CustomFieldsService {
|
||||
|
||||
def getCustomFields(owner: String, repository: String)(implicit s: Session): List[CustomField] =
|
||||
CustomFields.filter(_.byRepository(owner, repository)).sortBy(_.fieldId asc).list
|
||||
|
||||
def getCustomFieldsWithValue(owner: String, repository: String, issueId: Int)(
|
||||
implicit s: Session
|
||||
): List[(CustomField, Option[IssueCustomField])] = {
|
||||
CustomFields
|
||||
.filter(_.byRepository(owner, repository))
|
||||
.joinLeft(IssueCustomFields)
|
||||
.on { case (t1, t2) => t1.fieldId === t2.fieldId && t2.issueId === issueId.bind }
|
||||
.sortBy { case (t1, t2) => t1.fieldId }
|
||||
.list
|
||||
}
|
||||
|
||||
def getCustomField(owner: String, repository: String, fieldId: Int)(implicit s: Session): Option[CustomField] =
|
||||
CustomFields.filter(_.byPrimaryKey(owner, repository, fieldId)).firstOption
|
||||
|
||||
def createCustomField(
|
||||
owner: String,
|
||||
repository: String,
|
||||
fieldName: String,
|
||||
fieldType: String,
|
||||
constraints: Option[String],
|
||||
enableForIssues: Boolean,
|
||||
enableForPullRequests: Boolean
|
||||
)(implicit s: Session): Int = {
|
||||
CustomFields returning CustomFields.map(_.fieldId) insert CustomField(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
fieldName = fieldName,
|
||||
fieldType = fieldType,
|
||||
constraints = constraints,
|
||||
enableForIssues = enableForIssues,
|
||||
enableForPullRequests = enableForPullRequests
|
||||
)
|
||||
}
|
||||
|
||||
def updateCustomField(
|
||||
owner: String,
|
||||
repository: String,
|
||||
fieldId: Int,
|
||||
fieldName: String,
|
||||
fieldType: String,
|
||||
constraints: Option[String],
|
||||
enableForIssues: Boolean,
|
||||
enableForPullRequests: Boolean
|
||||
)(
|
||||
implicit s: Session
|
||||
): Unit =
|
||||
CustomFields
|
||||
.filter(_.byPrimaryKey(owner, repository, fieldId))
|
||||
.map(t => (t.fieldName, t.fieldType, t.constraints, t.enableForIssues, t.enableForPullRequests))
|
||||
.update((fieldName, fieldType, constraints, enableForIssues, enableForPullRequests))
|
||||
|
||||
def deleteCustomField(owner: String, repository: String, fieldId: Int)(implicit s: Session): Unit = {
|
||||
IssueCustomFields
|
||||
.filter(t => t.userName === owner.bind && t.repositoryName === repository.bind && t.fieldId === fieldId.bind)
|
||||
.delete
|
||||
CustomFields.filter(_.byPrimaryKey(owner, repository, fieldId)).delete
|
||||
}
|
||||
|
||||
def getCustomFieldValues(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
issueId: Int,
|
||||
)(implicit s: Session): List[IssueCustomField] = {
|
||||
IssueCustomFields
|
||||
.filter(t => t.userName === userName && t.repositoryName === repositoryName.bind && t.issueId === issueId.bind)
|
||||
.list
|
||||
}
|
||||
|
||||
def insertOrUpdateCustomFieldValue(
|
||||
field: CustomField,
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
issueId: Int,
|
||||
value: String
|
||||
)(implicit s: Session): Unit = {
|
||||
IssueCustomFields.insertOrUpdate(
|
||||
IssueCustomField(
|
||||
userName = userName,
|
||||
repositoryName = repositoryName,
|
||||
issueId = issueId,
|
||||
fieldId = field.fieldId,
|
||||
value = value
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -165,7 +165,7 @@ trait HandleCommentService {
|
||||
content match {
|
||||
case Some(content) =>
|
||||
// Update comment
|
||||
val _commentId = Some(updateComment(issue.issueId, commentId.toInt, content))
|
||||
val _commentId = Some(updateComment(owner, name, issue.issueId, commentId.toInt, content))
|
||||
// Record comment activity
|
||||
val commentInfo = if (issue.isPullRequest) {
|
||||
PullRequestCommentInfo(owner, name, userName, content, issue.issueId)
|
||||
|
||||
@@ -16,7 +16,7 @@ trait IssueCreationService {
|
||||
repository: RepositoryInfo,
|
||||
title: String,
|
||||
body: Option[String],
|
||||
assignee: Option[String],
|
||||
assignees: Seq[String],
|
||||
milestoneId: Option[Int],
|
||||
priorityId: Option[Int],
|
||||
labelNames: Seq[String],
|
||||
@@ -35,16 +35,19 @@ trait IssueCreationService {
|
||||
userName,
|
||||
title,
|
||||
body,
|
||||
if (manageable) assignee else None,
|
||||
if (manageable) milestoneId else None,
|
||||
if (manageable) priorityId else None
|
||||
)
|
||||
val issue: Issue = getIssue(owner, name, issueId.toString).get
|
||||
|
||||
// insert labels
|
||||
if (manageable) {
|
||||
// insert assignees
|
||||
assignees.foreach { assignee =>
|
||||
registerIssueAssignee(owner, name, issueId, assignee)
|
||||
}
|
||||
// insert labels
|
||||
val labels = getLabels(owner, name)
|
||||
labelNames.map { labelName =>
|
||||
labelNames.foreach { labelName =>
|
||||
labels.find(_.labelName == labelName).map { label =>
|
||||
registerIssueLabel(owner, name, issueId, label.labelId)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,17 @@ import gitbucket.core.util.StringUtil._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.model.{Account, Issue, IssueComment, IssueLabel, Label, PullRequest, Repository, Role}
|
||||
import gitbucket.core.model.{
|
||||
Account,
|
||||
Issue,
|
||||
IssueAssignee,
|
||||
IssueComment,
|
||||
IssueLabel,
|
||||
Label,
|
||||
PullRequest,
|
||||
Repository,
|
||||
Role
|
||||
}
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.model.Profile.profile._
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
@@ -118,8 +128,7 @@ trait IssuesService {
|
||||
def countIssueGroupByLabels(
|
||||
owner: String,
|
||||
repository: String,
|
||||
condition: IssueSearchCondition,
|
||||
filterUser: Map[String, String]
|
||||
condition: IssueSearchCondition
|
||||
)(implicit s: Session): Map[String, Int] = {
|
||||
|
||||
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), IssueSearchOption.Issues)
|
||||
@@ -138,7 +147,7 @@ trait IssuesService {
|
||||
t3.labelName
|
||||
}
|
||||
.map {
|
||||
case labelName ~ t =>
|
||||
case (labelName, t) =>
|
||||
labelName -> t.length
|
||||
}
|
||||
.list
|
||||
@@ -156,8 +165,7 @@ trait IssuesService {
|
||||
def countIssueGroupByPriorities(
|
||||
owner: String,
|
||||
repository: String,
|
||||
condition: IssueSearchCondition,
|
||||
filterUser: Map[String, String]
|
||||
condition: IssueSearchCondition
|
||||
)(implicit s: Session): Map[String, Int] = {
|
||||
|
||||
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), IssueSearchOption.Issues)
|
||||
@@ -171,7 +179,7 @@ trait IssuesService {
|
||||
t2.priorityName
|
||||
}
|
||||
.map {
|
||||
case priorityName ~ t =>
|
||||
case (priorityName, t) =>
|
||||
priorityName -> t.length
|
||||
}
|
||||
.list
|
||||
@@ -207,9 +215,11 @@ trait IssuesService {
|
||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t1.byPriority(t6.userName, t6.repositoryName, t6.priorityId) }
|
||||
.joinLeft(PullRequests)
|
||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t1.byIssue(t7.userName, t7.repositoryName, t7.issueId) }
|
||||
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => i asc }
|
||||
.joinLeft(IssueAssignees)
|
||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => t1.byIssue(t8.userName, t8.repositoryName, t8.issueId) }
|
||||
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => i asc }
|
||||
.map {
|
||||
case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 =>
|
||||
case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 =>
|
||||
(
|
||||
t1,
|
||||
t2.commentCount,
|
||||
@@ -218,7 +228,8 @@ trait IssuesService {
|
||||
t4.map(_.color),
|
||||
t5.map(_.title),
|
||||
t6.map(_.priorityName),
|
||||
t7.map(_.commitIdTo)
|
||||
t7.map(_.commitIdTo),
|
||||
t8.map(_.assigneeUserName)
|
||||
)
|
||||
}
|
||||
.list
|
||||
@@ -228,36 +239,51 @@ trait IssuesService {
|
||||
|
||||
result.map { issues =>
|
||||
issues.head match {
|
||||
case (issue, commentCount, _, _, _, milestone, priority, commitId) =>
|
||||
case (issue, commentCount, _, _, _, milestone, priority, commitId, _) =>
|
||||
IssueInfo(
|
||||
issue,
|
||||
issues.flatMap { t =>
|
||||
t._3.map(Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get))
|
||||
} toList,
|
||||
issues
|
||||
.flatMap { t =>
|
||||
t._3.map(Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get))
|
||||
}
|
||||
.distinct
|
||||
.toList,
|
||||
milestone,
|
||||
priority,
|
||||
commentCount,
|
||||
commitId
|
||||
commitId,
|
||||
issues.flatMap(_._9).distinct
|
||||
)
|
||||
}
|
||||
} toList
|
||||
}
|
||||
|
||||
/** for api
|
||||
* @return (issue, issueUser, assignedUser)
|
||||
* @return (issue, issueUser, Seq(assigneeUsers))
|
||||
*/
|
||||
def searchIssueByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)(
|
||||
implicit s: Session
|
||||
): List[(Issue, Account, Option[Account])] = {
|
||||
): List[(Issue, Account, List[Account])] = {
|
||||
// get issues and comment count and labels
|
||||
searchIssueQueryBase(condition, IssueSearchOption.Issues, offset, limit, repos)
|
||||
.join(Accounts)
|
||||
.on { case t1 ~ t2 ~ i ~ t3 => t3.userName === t1.openedUserName }
|
||||
.joinLeft(IssueAssignees)
|
||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t4.byIssue(t1.userName, t1.repositoryName, t1.issueId) }
|
||||
.joinLeft(Accounts)
|
||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t4.userName === t1.assignedUserName }
|
||||
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 => i asc }
|
||||
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 => (t1, t3, t4) }
|
||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t4.map(_.assigneeUserName) }
|
||||
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => i asc }
|
||||
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => (t1, t3, t5) }
|
||||
.list
|
||||
.groupBy {
|
||||
case (issue, account, _) =>
|
||||
(issue, account)
|
||||
}
|
||||
.map {
|
||||
case (_, values) =>
|
||||
(values.head._1, values.head._2, values.flatMap(_._3))
|
||||
}
|
||||
.toList
|
||||
}
|
||||
|
||||
/** for api
|
||||
@@ -265,7 +291,7 @@ trait IssuesService {
|
||||
*/
|
||||
def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)(
|
||||
implicit s: Session
|
||||
): List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] = {
|
||||
): List[(Issue, Account, Int, PullRequest, Repository, Account, List[Account])] = {
|
||||
// get issues and comment count and labels
|
||||
searchIssueQueryBase(condition, IssueSearchOption.PullRequests, offset, limit, repos)
|
||||
.join(PullRequests)
|
||||
@@ -276,11 +302,30 @@ trait IssuesService {
|
||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t1.openedUserName }
|
||||
.join(Accounts)
|
||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t6.userName === t4.userName }
|
||||
.joinLeft(IssueAssignees)
|
||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t7.byIssue(t1.userName, t1.repositoryName, t1.issueId) }
|
||||
.joinLeft(Accounts)
|
||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t7.userName === t1.assignedUserName }
|
||||
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => i asc }
|
||||
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => (t1, t5, t2.commentCount, t3, t4, t6, t7) }
|
||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => t8.userName === t7.map(_.assigneeUserName) }
|
||||
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => i asc }
|
||||
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => (t1, t5, t2.commentCount, t3, t4, t6, t8) }
|
||||
.list
|
||||
.groupBy {
|
||||
case (issue, openedUser, commentCount, pullRequest, repository, account, assignedUser) =>
|
||||
(issue, openedUser, commentCount, pullRequest, repository, account)
|
||||
}
|
||||
.map {
|
||||
case (_, values) =>
|
||||
(
|
||||
values.head._1,
|
||||
values.head._2,
|
||||
values.head._3,
|
||||
values.head._4,
|
||||
values.head._5,
|
||||
values.head._6,
|
||||
values.flatMap(_._7)
|
||||
)
|
||||
}
|
||||
.toList
|
||||
}
|
||||
|
||||
private def searchIssueQueryBase(
|
||||
@@ -347,7 +392,7 @@ trait IssuesService {
|
||||
case _ => t1.closed === true || t1.closed === false
|
||||
}).&&(t1.milestoneId.? isEmpty, condition.milestone == Some(None))
|
||||
.&&(t1.priorityId.? isEmpty, condition.priority == Some(None))
|
||||
.&&(t1.assignedUserName.? isEmpty, condition.assigned == Some(None))
|
||||
//.&&(t1.assignedUserName.? isEmpty, condition.assigned == Some(None))
|
||||
.&&(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
|
||||
(searchOption match {
|
||||
case IssueSearchOption.Issues => t1.pullRequest === false
|
||||
@@ -371,7 +416,13 @@ trait IssuesService {
|
||||
condition.priority.flatten.isDefined
|
||||
)
|
||||
// Assignee filter
|
||||
.&&(t1.assignedUserName === condition.assigned.get.get.bind, condition.assigned.flatten.isDefined)
|
||||
.&&(
|
||||
IssueAssignees filter { a =>
|
||||
a.byIssue(t1.userName, t1.repositoryName, t1.issueId) &&
|
||||
a.assigneeUserName === condition.assigned.get.get.bind
|
||||
} exists,
|
||||
condition.assigned.flatten.isDefined
|
||||
)
|
||||
// Label filter
|
||||
.&&(
|
||||
IssueLabels filter { t2 =>
|
||||
@@ -396,7 +447,9 @@ trait IssuesService {
|
||||
.&&(t1.userName inSetBind condition.groups, condition.groups.nonEmpty)
|
||||
// Mentioned filter
|
||||
.&&(
|
||||
(t1.openedUserName === condition.mentioned.get.bind) || t1.assignedUserName === condition.mentioned.get.bind ||
|
||||
(t1.openedUserName === condition.mentioned.get.bind) || (IssueAssignees filter { t1 =>
|
||||
t1.byIssue(t1.userName, t1.repositoryName, t1.issueId) && t1.assigneeUserName === condition.mentioned.get.bind
|
||||
} exists) ||
|
||||
(IssueComments filter { t2 =>
|
||||
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind)
|
||||
} exists),
|
||||
@@ -410,7 +463,6 @@ trait IssuesService {
|
||||
loginUser: String,
|
||||
title: String,
|
||||
content: Option[String],
|
||||
assignedUserName: Option[String],
|
||||
milestoneId: Option[Int],
|
||||
priorityId: Option[Int],
|
||||
isPullRequest: Boolean = false
|
||||
@@ -427,7 +479,6 @@ trait IssuesService {
|
||||
loginUser,
|
||||
milestoneId,
|
||||
priorityId,
|
||||
assignedUserName,
|
||||
title,
|
||||
content,
|
||||
false,
|
||||
@@ -509,7 +560,7 @@ trait IssuesService {
|
||||
content: String,
|
||||
action: String
|
||||
)(implicit s: Session): Int = {
|
||||
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate)
|
||||
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(_.updatedDate).update(currentDate)
|
||||
IssueComments returning IssueComments.map(_.commentId) insert IssueComment(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
@@ -542,35 +593,91 @@ trait IssuesService {
|
||||
.update(true)
|
||||
}
|
||||
|
||||
def updateAssignedUserName(
|
||||
def getIssueAssignees(owner: String, repository: String, issueId: Int)(
|
||||
implicit s: Session
|
||||
): List[IssueAssignee] = {
|
||||
IssueAssignees.filter(_.byIssue(owner, repository, issueId)).sortBy(_.assigneeUserName).list
|
||||
}
|
||||
|
||||
def registerIssueAssignee(
|
||||
owner: String,
|
||||
repository: String,
|
||||
issueId: Int,
|
||||
assignedUserName: Option[String],
|
||||
assigneeUserName: String,
|
||||
insertComment: Boolean = false
|
||||
)(implicit context: Context, s: Session): Int = {
|
||||
val oldAssigned = getIssue(owner, repository, s"${issueId}").get.assignedUserName
|
||||
val assigned = assignedUserName
|
||||
)(
|
||||
implicit context: Context,
|
||||
s: Session
|
||||
): Int = {
|
||||
val assigner = context.loginAccount.map(_.userName)
|
||||
if (insertComment) {
|
||||
IssueComments insert IssueComment(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
issueId = issueId,
|
||||
action = "assign",
|
||||
action = "add_assignee",
|
||||
commentedUserName = assigner.getOrElse("Unknown user"),
|
||||
content = s"""${oldAssigned.getOrElse("Not assigned")}:${assigned.getOrElse("Not assigned")}""",
|
||||
content = assigneeUserName,
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate
|
||||
)
|
||||
}
|
||||
for (issue <- getIssue(owner, repository, issueId.toString); repo <- getRepository(owner, repository)) {
|
||||
PluginRegistry().getIssueHooks.foreach(_.assigned(issue, repo, assigner, assigned, oldAssigned))
|
||||
PluginRegistry().getIssueHooks.foreach(_.assigned(issue, repo, assigner, Some(assigneeUserName), None))
|
||||
}
|
||||
Issues
|
||||
.filter(_.byPrimaryKey(owner, repository, issueId))
|
||||
.map(t => (t.assignedUserName ?, t.updatedDate))
|
||||
.update(assignedUserName, currentDate)
|
||||
IssueAssignees insert IssueAssignee(owner, repository, issueId, assigneeUserName)
|
||||
}
|
||||
|
||||
def deleteIssueAssignee(
|
||||
owner: String,
|
||||
repository: String,
|
||||
issueId: Int,
|
||||
assigneeUserName: String,
|
||||
insertComment: Boolean = false
|
||||
)(
|
||||
implicit context: Context,
|
||||
s: Session
|
||||
): Int = {
|
||||
val assigner = context.loginAccount.map(_.userName)
|
||||
if (insertComment) {
|
||||
IssueComments insert IssueComment(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
issueId = issueId,
|
||||
action = "delete_assignee",
|
||||
commentedUserName = assigner.getOrElse("Unknown user"),
|
||||
content = assigneeUserName,
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate
|
||||
)
|
||||
}
|
||||
|
||||
// TODO Notify plugins of unassignment as doing in registerIssueAssignee()?
|
||||
|
||||
IssueAssignees filter (_.byPrimaryKey(owner, repository, issueId, assigneeUserName)) delete
|
||||
}
|
||||
|
||||
def deleteAllIssueAssignees(owner: String, repository: String, issueId: Int, insertComment: Boolean = false)(
|
||||
implicit context: Context,
|
||||
s: Session
|
||||
): Int = {
|
||||
val assigner = context.loginAccount.map(_.userName)
|
||||
if (insertComment) {
|
||||
IssueComments insert IssueComment(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
issueId = issueId,
|
||||
action = "delete_assign",
|
||||
commentedUserName = assigner.getOrElse("Unknown user"),
|
||||
content = "All assignees",
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate
|
||||
)
|
||||
}
|
||||
|
||||
// TODO Notify plugins of unassignment as doing in registerIssueAssignee()?
|
||||
|
||||
IssueAssignees filter (_.byIssue(owner, repository, issueId)) delete
|
||||
}
|
||||
|
||||
def updateMilestoneId(
|
||||
@@ -635,8 +742,10 @@ trait IssuesService {
|
||||
.update(priorityId, currentDate)
|
||||
}
|
||||
|
||||
def updateComment(issueId: Int, commentId: Int, content: String)(implicit s: Session): Int = {
|
||||
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate)
|
||||
def updateComment(owner: String, repository: String, issueId: Int, commentId: Int, content: String)(
|
||||
implicit s: Session
|
||||
): Int = {
|
||||
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(_.updatedDate).update(currentDate)
|
||||
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.updatedDate)).update(content, currentDate)
|
||||
}
|
||||
|
||||
@@ -644,13 +753,13 @@ trait IssuesService {
|
||||
implicit context: Context,
|
||||
s: Session
|
||||
): Int = {
|
||||
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate)
|
||||
IssueComments.filter(_.byPrimaryKey(commentId)).firstOption match {
|
||||
case Some(c) if c.action == "reopen_comment" =>
|
||||
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(_.updatedDate).update(currentDate)
|
||||
IssueComments.filter(_.byPrimaryKey(commentId)).first match {
|
||||
case c if c.action == "reopen_comment" =>
|
||||
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.action)).update("Reopen", "reopen")
|
||||
case Some(c) if c.action == "close_comment" =>
|
||||
case c if c.action == "close_comment" =>
|
||||
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.action)).update("Close", "close")
|
||||
case Some(_) =>
|
||||
case _ =>
|
||||
IssueComments.filter(_.byPrimaryKey(commentId)).delete
|
||||
IssueComments insert IssueComment(
|
||||
userName = owner,
|
||||
@@ -855,39 +964,39 @@ object IssuesService {
|
||||
|
||||
def nonEmpty: Boolean = !isEmpty
|
||||
|
||||
def toFilterString: String =
|
||||
(
|
||||
List(
|
||||
Some(s"is:${state}"),
|
||||
author.map(author => s"author:${author}"),
|
||||
assigned.map(assignee => s"assignee:${assignee}"),
|
||||
mentioned.map(mentioned => s"mentions:${mentioned}")
|
||||
).flatten ++
|
||||
labels.map(label => s"label:${label}") ++
|
||||
List(
|
||||
milestone.map {
|
||||
case Some(x) => s"milestone:${x}"
|
||||
case None => "no:milestone"
|
||||
},
|
||||
priority.map {
|
||||
case Some(x) => s"priority:${x}"
|
||||
case None => "no:priority"
|
||||
},
|
||||
(sort, direction) match {
|
||||
case ("created", "desc") => None
|
||||
case ("created", "asc") => Some("sort:created-asc")
|
||||
case ("comments", "desc") => Some("sort:comments-desc")
|
||||
case ("comments", "asc") => Some("sort:comments-asc")
|
||||
case ("updated", "desc") => Some("sort:updated-desc")
|
||||
case ("updated", "asc") => Some("sort:updated-asc")
|
||||
case ("priority", "desc") => Some("sort:priority-desc")
|
||||
case ("priority", "asc") => Some("sort:priority-asc")
|
||||
case x => throw new MatchError(x)
|
||||
},
|
||||
visibility.map(visibility => s"visibility:${visibility}")
|
||||
).flatten ++
|
||||
groups.map(group => s"group:${group}")
|
||||
).mkString(" ")
|
||||
// def toFilterString: String =
|
||||
// (
|
||||
// List(
|
||||
// Some(s"is:${state}"),
|
||||
// author.map(author => s"author:${author}"),
|
||||
// assigned.map(assignee => s"assignee:${assignee}"),
|
||||
// mentioned.map(mentioned => s"mentions:${mentioned}")
|
||||
// ).flatten ++
|
||||
// labels.map(label => s"label:${label}") ++
|
||||
// List(
|
||||
// milestone.map {
|
||||
// case Some(x) => s"milestone:${x}"
|
||||
// case None => "no:milestone"
|
||||
// },
|
||||
// priority.map {
|
||||
// case Some(x) => s"priority:${x}"
|
||||
// case None => "no:priority"
|
||||
// },
|
||||
// (sort, direction) match {
|
||||
// case ("created", "desc") => None
|
||||
// case ("created", "asc") => Some("sort:created-asc")
|
||||
// case ("comments", "desc") => Some("sort:comments-desc")
|
||||
// case ("comments", "asc") => Some("sort:comments-asc")
|
||||
// case ("updated", "desc") => Some("sort:updated-desc")
|
||||
// case ("updated", "asc") => Some("sort:updated-asc")
|
||||
// case ("priority", "desc") => Some("sort:priority-desc")
|
||||
// case ("priority", "asc") => Some("sort:priority-asc")
|
||||
// case x => throw new MatchError(x)
|
||||
// },
|
||||
// visibility.map(visibility => s"visibility:${visibility}")
|
||||
// ).flatten ++
|
||||
// groups.map(group => s"group:${group}")
|
||||
// ).mkString(" ")
|
||||
|
||||
def toURL: String =
|
||||
"?" + List(
|
||||
@@ -981,7 +1090,8 @@ object IssuesService {
|
||||
milestone: Option[String],
|
||||
priority: Option[String],
|
||||
commentCount: Int,
|
||||
commitId: Option[String]
|
||||
commitId: Option[String],
|
||||
assignees: Seq[String]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import java.net.URI
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm.Family
|
||||
import com.nimbusds.jose.proc.BadJOSEException
|
||||
import com.nimbusds.jose.util.DefaultResourceRetriever
|
||||
import com.nimbusds.jose.{JOSEException, JWSAlgorithm}
|
||||
import com.nimbusds.jwt.JWT
|
||||
import com.nimbusds.oauth2.sdk._
|
||||
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic
|
||||
import com.nimbusds.oauth2.sdk.id.{ClientID, Issuer, State}
|
||||
@@ -52,6 +52,11 @@ trait OpenIDConnectService {
|
||||
)
|
||||
}
|
||||
|
||||
def createOIDLogoutRequest(issuer: Issuer, clientID: ClientID, redirectURI: URI, token: JWT): LogoutRequest = {
|
||||
val metadata = OIDCProviderMetadata.resolve(issuer)
|
||||
new LogoutRequest(metadata.getEndSessionEndpointURI, token, null, clientID, redirectURI, null, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Proceed the OpenID Connect authentication.
|
||||
*
|
||||
@@ -60,7 +65,7 @@ trait OpenIDConnectService {
|
||||
* @param state State saved in the session
|
||||
* @param nonce Nonce saved in the session
|
||||
* @param oidc OIDC settings
|
||||
* @return ID token
|
||||
* @return (ID token, GitBucket account)
|
||||
*/
|
||||
def authenticate(
|
||||
params: Map[String, String],
|
||||
@@ -68,22 +73,25 @@ trait OpenIDConnectService {
|
||||
state: State,
|
||||
nonce: Nonce,
|
||||
oidc: SystemSettingsService.OIDC
|
||||
)(implicit s: Session): Option[Account] =
|
||||
)(implicit s: Session): Option[(JWT, Account)] =
|
||||
validateOIDCAuthenticationResponse(params, state, redirectURI) flatMap { authenticationResponse =>
|
||||
obtainOIDCToken(authenticationResponse.getAuthorizationCode, nonce, redirectURI, oidc) flatMap { claims =>
|
||||
Seq("email", "preferred_username", "name").map(k => Option(claims.getStringClaim(k))) match {
|
||||
case Seq(Some(email), preferredUsername, name) =>
|
||||
getOrCreateFederatedUser(
|
||||
claims.getIssuer.getValue,
|
||||
claims.getSubject.getValue,
|
||||
email,
|
||||
preferredUsername,
|
||||
name
|
||||
)
|
||||
case _ =>
|
||||
logger.info(s"OIDC ID token must have an email claim: claims=${claims.toJSONObject}")
|
||||
None
|
||||
}
|
||||
obtainOIDCToken(authenticationResponse.getAuthorizationCode, nonce, redirectURI, oidc) flatMap {
|
||||
case (jwt, claims) =>
|
||||
Seq("email", "preferred_username", "name").map(k => Option(claims.getStringClaim(k))) match {
|
||||
case Seq(Some(email), preferredUsername, name) =>
|
||||
getOrCreateFederatedUser(
|
||||
claims.getIssuer.getValue,
|
||||
claims.getSubject.getValue,
|
||||
email,
|
||||
preferredUsername,
|
||||
name
|
||||
).map { account =>
|
||||
(jwt, account)
|
||||
}
|
||||
case _ =>
|
||||
logger.info(s"OIDC ID token must have an email claim: claims=${claims.toJSONObject}")
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +144,7 @@ trait OpenIDConnectService {
|
||||
nonce: Nonce,
|
||||
redirectURI: URI,
|
||||
oidc: SystemSettingsService.OIDC
|
||||
): Option[IDTokenClaimsSet] = {
|
||||
): Option[(JWT, IDTokenClaimsSet)] = {
|
||||
val metadata = OIDCProviderMetadata.resolve(oidc.issuer)
|
||||
val tokenRequest = new TokenRequest(
|
||||
metadata.getTokenEndpointURI,
|
||||
@@ -173,7 +181,7 @@ trait OpenIDConnectService {
|
||||
metadata: OIDCProviderMetadata,
|
||||
nonce: Nonce,
|
||||
oidc: SystemSettingsService.OIDC
|
||||
): Option[IDTokenClaimsSet] =
|
||||
): Option[(JWT, IDTokenClaimsSet)] =
|
||||
Option(response.getOIDCTokens.getIDToken) match {
|
||||
case Some(jwt) =>
|
||||
val validator = oidc.jwsAlgorithm map { jwsAlgorithm =>
|
||||
@@ -188,7 +196,7 @@ trait OpenIDConnectService {
|
||||
new IDTokenValidator(metadata.getIssuer, oidc.clientID)
|
||||
}
|
||||
try {
|
||||
Some(validator.validate(jwt, nonce))
|
||||
Some((jwt, validator.validate(jwt, nonce)))
|
||||
} catch {
|
||||
case e @ (_: BadJOSEException | _: JOSEException) =>
|
||||
logger.info(s"OIDC ID token has error: $e")
|
||||
|
||||
@@ -472,6 +472,40 @@ trait PullRequestService {
|
||||
}
|
||||
}
|
||||
|
||||
def getSingleDiff(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
commitId: String,
|
||||
path: String
|
||||
): Option[DiffInfo] = {
|
||||
Using.resource(
|
||||
Git.open(getRepositoryDir(userName, repositoryName))
|
||||
) { git =>
|
||||
val newId = git.getRepository.resolve(commitId)
|
||||
JGitUtil.getDiff(git, None, newId.getName, path)
|
||||
}
|
||||
}
|
||||
|
||||
def getSingleDiff(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
branch: String,
|
||||
requestUserName: String,
|
||||
requestRepositoryName: String,
|
||||
requestCommitId: String,
|
||||
path: String
|
||||
): Option[DiffInfo] = {
|
||||
Using.resources(
|
||||
Git.open(getRepositoryDir(userName, repositoryName)),
|
||||
Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
|
||||
) { (oldGit, newGit) =>
|
||||
val oldId = oldGit.getRepository.resolve(branch)
|
||||
val newId = newGit.getRepository.resolve(requestCommitId)
|
||||
|
||||
JGitUtil.getDiff(newGit, Some(oldId.getName), newId.getName, path)
|
||||
}
|
||||
}
|
||||
|
||||
def getRequestCompareInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
|
||||
@@ -36,10 +36,10 @@ trait RepositoryCommitFileService {
|
||||
settings: SystemSettings
|
||||
)(
|
||||
f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => Unit
|
||||
)(implicit s: Session, c: JsonFormat.Context): ObjectId = {
|
||||
)(implicit s: Session, c: JsonFormat.Context): Either[String, ObjectId] = {
|
||||
_createFiles(repository, branch, message, loginAccount, loginAccount.fullName, loginAccount.mailAddress, settings)(
|
||||
f
|
||||
)._1
|
||||
).map(_._1)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,7 +58,7 @@ trait RepositoryCommitFileService {
|
||||
commit: String,
|
||||
loginAccount: Account,
|
||||
settings: SystemSettings
|
||||
)(implicit s: Session, c: JsonFormat.Context): (ObjectId, Option[ObjectId]) = {
|
||||
)(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, Option[ObjectId])] = {
|
||||
commitFile(
|
||||
repository,
|
||||
branch,
|
||||
@@ -92,7 +92,7 @@ trait RepositoryCommitFileService {
|
||||
committerName: String,
|
||||
committerMailAddress: String,
|
||||
settings: SystemSettings
|
||||
)(implicit s: Session, c: JsonFormat.Context): (ObjectId, Option[ObjectId]) = {
|
||||
)(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, Option[ObjectId])] = {
|
||||
|
||||
val newPath = newFileName.map { newFileName =>
|
||||
if (path.length == 0) newFileName else s"${path}/${newFileName}"
|
||||
@@ -141,7 +141,7 @@ trait RepositoryCommitFileService {
|
||||
settings: SystemSettings
|
||||
)(
|
||||
f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => R
|
||||
)(implicit s: Session, c: JsonFormat.Context): (ObjectId, R) = {
|
||||
)(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, R)] = {
|
||||
|
||||
LockUtil.lock(s"${repository.owner}/${repository.name}") {
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
@@ -177,11 +177,11 @@ trait RepositoryCommitFileService {
|
||||
error match {
|
||||
case Some(error) =>
|
||||
// commit is rejected
|
||||
// TODO Notify commit failure to edited user
|
||||
val refUpdate = git.getRepository.updateRef(headName)
|
||||
refUpdate.setNewObjectId(headTip)
|
||||
refUpdate.setForceUpdate(true)
|
||||
refUpdate.update()
|
||||
Left(error)
|
||||
|
||||
case None =>
|
||||
// update refs
|
||||
@@ -242,8 +242,8 @@ trait RepositoryCommitFileService {
|
||||
)
|
||||
}
|
||||
}
|
||||
Right((commitId, result))
|
||||
}
|
||||
(commitId, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,6 +254,7 @@ trait RepositoryService {
|
||||
Labels.filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueComments.filter(_.byRepository(userName, repositoryName)).delete
|
||||
PullRequests.filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueAssignees.filter(_.byRepository(userName, repositoryName)).delete
|
||||
Issues.filter(_.byRepository(userName, repositoryName)).delete
|
||||
Priorities.filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueId.filter(_.byRepository(userName, repositoryName)).delete
|
||||
@@ -342,10 +343,7 @@ trait RepositoryService {
|
||||
repository.originUserName.getOrElse(repository.userName),
|
||||
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
||||
),
|
||||
getOpenMilestones(
|
||||
repository.originUserName.getOrElse(repository.userName),
|
||||
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
||||
),
|
||||
getOpenMilestones(repository.userName, repository.repositoryName),
|
||||
getRepositoryManagers(repository.userName, repository.repositoryName)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,17 +18,18 @@ trait SystemSettingsService {
|
||||
val props = new java.util.Properties()
|
||||
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
|
||||
settings.information.foreach(x => props.setProperty(Information, x))
|
||||
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
|
||||
props.setProperty(AllowAnonymousAccess, settings.allowAnonymousAccess.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(Notification, settings.notification.toString)
|
||||
props.setProperty(LimitVisibleRepositories, settings.limitVisibleRepositories.toString)
|
||||
props.setProperty(AllowAccountRegistration, settings.basicBehavior.allowAccountRegistration.toString)
|
||||
props.setProperty(AllowResetPassword, settings.basicBehavior.allowResetPassword.toString)
|
||||
props.setProperty(AllowAnonymousAccess, settings.basicBehavior.allowAnonymousAccess.toString)
|
||||
props.setProperty(IsCreateRepoOptionPublic, settings.basicBehavior.isCreateRepoOptionPublic.toString)
|
||||
props.setProperty(RepositoryOperationCreate, settings.basicBehavior.repositoryOperation.create.toString)
|
||||
props.setProperty(RepositoryOperationDelete, settings.basicBehavior.repositoryOperation.delete.toString)
|
||||
props.setProperty(RepositoryOperationRename, settings.basicBehavior.repositoryOperation.rename.toString)
|
||||
props.setProperty(RepositoryOperationTransfer, settings.basicBehavior.repositoryOperation.transfer.toString)
|
||||
props.setProperty(RepositoryOperationFork, settings.basicBehavior.repositoryOperation.fork.toString)
|
||||
props.setProperty(Gravatar, settings.basicBehavior.gravatar.toString)
|
||||
props.setProperty(Notification, settings.basicBehavior.notification.toString)
|
||||
props.setProperty(LimitVisibleRepositories, settings.basicBehavior.limitVisibleRepositories.toString)
|
||||
props.setProperty(SshEnabled, settings.ssh.enabled.toString)
|
||||
settings.ssh.bindAddress.foreach { bindAddress =>
|
||||
props.setProperty(SshBindAddressHost, bindAddress.host.trim())
|
||||
@@ -109,19 +110,22 @@ trait SystemSettingsService {
|
||||
SystemSettings(
|
||||
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
|
||||
getOptionValue(props, Information, None),
|
||||
getValue(props, AllowAccountRegistration, false),
|
||||
getValue(props, AllowAnonymousAccess, 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)
|
||||
BasicBehavior(
|
||||
getValue(props, AllowAccountRegistration, false),
|
||||
getValue(props, AllowResetPassword, false),
|
||||
getValue(props, AllowAnonymousAccess, 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, Notification, false),
|
||||
getValue(props, LimitVisibleRepositories, false)
|
||||
),
|
||||
getValue(props, Gravatar, false),
|
||||
getValue(props, Notification, false),
|
||||
getValue(props, LimitVisibleRepositories, false),
|
||||
Ssh(
|
||||
enabled = getValue(props, SshEnabled, false),
|
||||
bindAddress = {
|
||||
@@ -214,13 +218,7 @@ object SystemSettingsService {
|
||||
case class SystemSettings(
|
||||
baseUrl: Option[String],
|
||||
information: Option[String],
|
||||
allowAccountRegistration: Boolean,
|
||||
allowAnonymousAccess: Boolean,
|
||||
isCreateRepoOptionPublic: Boolean,
|
||||
repositoryOperation: RepositoryOperation,
|
||||
gravatar: Boolean,
|
||||
notification: Boolean,
|
||||
limitVisibleRepositories: Boolean,
|
||||
basicBehavior: BasicBehavior,
|
||||
ssh: Ssh,
|
||||
useSMTP: Boolean,
|
||||
smtp: Option[Smtp],
|
||||
@@ -264,6 +262,17 @@ object SystemSettingsService {
|
||||
ssh.getUrl(owner: String, name: String)
|
||||
}
|
||||
|
||||
case class BasicBehavior(
|
||||
allowAccountRegistration: Boolean,
|
||||
allowResetPassword: Boolean,
|
||||
allowAnonymousAccess: Boolean,
|
||||
isCreateRepoOptionPublic: Boolean,
|
||||
repositoryOperation: RepositoryOperation,
|
||||
gravatar: Boolean,
|
||||
notification: Boolean,
|
||||
limitVisibleRepositories: Boolean,
|
||||
)
|
||||
|
||||
case class RepositoryOperation(
|
||||
create: Boolean,
|
||||
delete: Boolean,
|
||||
@@ -383,6 +392,7 @@ object SystemSettingsService {
|
||||
private val BaseURL = "base_url"
|
||||
private val Information = "information"
|
||||
private val AllowAccountRegistration = "allow_account_registration"
|
||||
private val AllowResetPassword = "allow_reset_password"
|
||||
private val AllowAnonymousAccess = "allow_anonymous_access"
|
||||
private val IsCreateRepoOptionPublic = "is_create_repository_option_public"
|
||||
private val RepositoryOperationCreate = "repository_operation_create"
|
||||
|
||||
@@ -379,8 +379,12 @@ trait WebHookPullRequestService extends WebHookService {
|
||||
settings: SystemSettings
|
||||
)(implicit s: Session, context: JsonFormat.Context): Unit = {
|
||||
callWebHookOf(repository.owner, repository.name, WebHook.Issues, settings) {
|
||||
val assigneeUsers = getIssueAssignees(repository.owner, repository.name, issue.issueId)
|
||||
val users =
|
||||
getAccountsByUserNames(Set(repository.owner, issue.openedUserName) ++ issue.assignedUserName, Set(sender))
|
||||
getAccountsByUserNames(
|
||||
Set(repository.owner, issue.openedUserName) ++ assigneeUsers.map(_.assigneeUserName),
|
||||
Set(sender)
|
||||
)
|
||||
for {
|
||||
repoOwner <- users.get(repository.owner)
|
||||
issueUser <- users.get(issue.openedUserName)
|
||||
@@ -393,7 +397,7 @@ trait WebHookPullRequestService extends WebHookService {
|
||||
issue,
|
||||
RepositoryName(repository),
|
||||
ApiUser(issueUser),
|
||||
issue.assignedUserName.flatMap(users.get(_)).map(ApiUser(_)),
|
||||
assigneeUsers.flatMap(x => users.get(x.assigneeUserName)).map(ApiUser(_)),
|
||||
getIssueLabels(repository.owner, repository.name, issue.issueId)
|
||||
.map(ApiLabel(_, RepositoryName(repository))),
|
||||
getApiMilestone(repository, issue.milestoneId getOrElse (0))
|
||||
@@ -415,16 +419,15 @@ trait WebHookPullRequestService extends WebHookService {
|
||||
callWebHookOf(repository.owner, repository.name, WebHook.PullRequest, settings) {
|
||||
for {
|
||||
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
|
||||
assignees = getIssueAssignees(repository.owner, repository.name, issueId)
|
||||
users = getAccountsByUserNames(
|
||||
Set(repository.owner, pullRequest.requestUserName, issue.openedUserName),
|
||||
Set(repository.owner, pullRequest.requestUserName, issue.openedUserName) ++ assignees.map(_.assigneeUserName),
|
||||
Set(sender)
|
||||
)
|
||||
baseOwner <- users.get(repository.owner)
|
||||
headOwner <- users.get(pullRequest.requestUserName)
|
||||
issueUser <- users.get(issue.openedUserName)
|
||||
assignee = issue.assignedUserName.flatMap { userName =>
|
||||
getAccountByUserName(userName, false)
|
||||
}
|
||||
assigneeUsers = assignees.flatMap(x => users.get(x.assigneeUserName))
|
||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
|
||||
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
|
||||
.map(ApiLabel(_, RepositoryName(repository)))
|
||||
@@ -433,7 +436,7 @@ trait WebHookPullRequestService extends WebHookService {
|
||||
action = action,
|
||||
issue = issue,
|
||||
issueUser = issueUser,
|
||||
assignee = assignee,
|
||||
assignees = assigneeUsers,
|
||||
pullRequest = pullRequest,
|
||||
headRepository = headRepo,
|
||||
headOwner = headOwner,
|
||||
@@ -481,8 +484,8 @@ trait WebHookPullRequestService extends WebHookService {
|
||||
requestRepository.name,
|
||||
requestBranch
|
||||
)
|
||||
assignee = issue.assignedUserName.flatMap { userName =>
|
||||
getAccountByUserName(userName, false)
|
||||
assignees = getIssueAssignees(requestRepository.owner, requestRepository.name, issue.issueId).flatMap { x =>
|
||||
getAccountByUserName(x.assigneeUserName, false)
|
||||
}
|
||||
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName)
|
||||
labels = getIssueLabels(pullRequest.userName, pullRequest.repositoryName, issue.issueId)
|
||||
@@ -492,7 +495,7 @@ trait WebHookPullRequestService extends WebHookService {
|
||||
action = action,
|
||||
issue = issue,
|
||||
issueUser = issueUser,
|
||||
assignee = assignee,
|
||||
assignees = assignees,
|
||||
pullRequest = pullRequest,
|
||||
headRepository = requestRepository,
|
||||
headOwner = headOwner,
|
||||
@@ -522,15 +525,17 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
|
||||
)(implicit s: Session, c: JsonFormat.Context): Unit = {
|
||||
import WebHookService._
|
||||
callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment, settings) {
|
||||
val assignees = getIssueAssignees(repository.owner, pullRequest.requestUserName, issue.issueId)
|
||||
val users =
|
||||
getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
|
||||
getAccountsByUserNames(
|
||||
Set(repository.owner, pullRequest.requestUserName, issue.openedUserName) ++ assignees.map(_.assigneeUserName),
|
||||
Set(sender)
|
||||
)
|
||||
for {
|
||||
baseOwner <- users.get(repository.owner)
|
||||
headOwner <- users.get(pullRequest.requestUserName)
|
||||
issueUser <- users.get(issue.openedUserName)
|
||||
assignee = issue.assignedUserName.flatMap { userName =>
|
||||
getAccountByUserName(userName, false)
|
||||
}
|
||||
assigneeUsers = assignees.flatMap(x => users.get(x.assigneeUserName))
|
||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
|
||||
labels = getIssueLabels(pullRequest.userName, pullRequest.repositoryName, issue.issueId)
|
||||
.map(ApiLabel(_, RepositoryName(pullRequest.userName, pullRequest.repositoryName)))
|
||||
@@ -540,7 +545,7 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
|
||||
comment = comment,
|
||||
issue = issue,
|
||||
issueUser = issueUser,
|
||||
assignee = assignee,
|
||||
assignees = assigneeUsers,
|
||||
pullRequest = pullRequest,
|
||||
headRepository = headRepo,
|
||||
headOwner = headOwner,
|
||||
@@ -569,14 +574,17 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
|
||||
callWebHookOf(repository.owner, repository.name, WebHook.IssueComment, settings) {
|
||||
for {
|
||||
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
|
||||
assignees = getIssueAssignees(repository.owner, repository.name, issue.issueId)
|
||||
users = getAccountsByUserNames(
|
||||
Set(issue.openedUserName, repository.owner, issueComment.commentedUserName) ++ issue.assignedUserName,
|
||||
Set(issue.openedUserName, repository.owner, issueComment.commentedUserName) ++ assignees.map(
|
||||
_.assigneeUserName
|
||||
),
|
||||
Set(sender)
|
||||
)
|
||||
issueUser <- users.get(issue.openedUserName)
|
||||
repoOwner <- users.get(repository.owner)
|
||||
commenter <- users.get(issueComment.commentedUserName)
|
||||
assignedUser = issue.assignedUserName.flatMap(users.get(_))
|
||||
assigneeUsers = assignees.flatMap(x => users.get(x.assigneeUserName))
|
||||
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
|
||||
milestone = getApiMilestone(repository, issue.milestoneId getOrElse (0))
|
||||
} yield {
|
||||
@@ -587,7 +595,7 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
|
||||
commentUser = commenter,
|
||||
repository = repository,
|
||||
repositoryUser = repoOwner,
|
||||
assignedUser = assignedUser,
|
||||
assignees = assigneeUsers,
|
||||
sender = sender,
|
||||
labels = labels,
|
||||
milestone = milestone
|
||||
@@ -710,7 +718,7 @@ object WebHookService {
|
||||
action: String,
|
||||
issue: Issue,
|
||||
issueUser: Account,
|
||||
assignee: Option[Account],
|
||||
assignees: List[Account],
|
||||
pullRequest: PullRequest,
|
||||
headRepository: RepositoryInfo,
|
||||
headOwner: Account,
|
||||
@@ -731,7 +739,7 @@ object WebHookService {
|
||||
baseRepo = baseRepoPayload,
|
||||
user = ApiUser(issueUser),
|
||||
labels = labels,
|
||||
assignee = assignee.map(ApiUser.apply),
|
||||
assignees = assignees.map(ApiUser.apply),
|
||||
mergedComment = mergedComment
|
||||
)
|
||||
|
||||
@@ -762,7 +770,7 @@ object WebHookService {
|
||||
commentUser: Account,
|
||||
repository: RepositoryInfo,
|
||||
repositoryUser: Account,
|
||||
assignedUser: Option[Account],
|
||||
assignees: List[Account],
|
||||
sender: Account,
|
||||
labels: List[Label],
|
||||
milestone: Option[ApiMilestone]
|
||||
@@ -774,7 +782,7 @@ object WebHookService {
|
||||
issue,
|
||||
RepositoryName(repository),
|
||||
ApiUser(issueUser),
|
||||
assignedUser.map(ApiUser(_)),
|
||||
assignees.map(ApiUser(_)),
|
||||
labels.map(ApiLabel(_, RepositoryName(repository))),
|
||||
milestone
|
||||
),
|
||||
@@ -799,7 +807,7 @@ object WebHookService {
|
||||
comment: CommitComment,
|
||||
issue: Issue,
|
||||
issueUser: Account,
|
||||
assignee: Option[Account],
|
||||
assignees: List[Account],
|
||||
pullRequest: PullRequest,
|
||||
headRepository: RepositoryInfo,
|
||||
headOwner: Account,
|
||||
@@ -828,7 +836,7 @@ object WebHookService {
|
||||
baseRepo = baseRepoPayload,
|
||||
user = ApiUser(issueUser),
|
||||
labels = labels,
|
||||
assignee = assignee.map(ApiUser.apply),
|
||||
assignees = assignees.map(ApiUser.apply),
|
||||
mergedComment = mergedComment
|
||||
),
|
||||
repository = baseRepoPayload,
|
||||
|
||||
@@ -98,29 +98,30 @@ class GitAuthenticationFilter extends Filter with RepositoryService with Account
|
||||
Database() withSession { implicit session =>
|
||||
getRepository(repositoryOwner, repositoryName.replaceFirst("(\\.wiki)?\\.git$", "")) match {
|
||||
case Some(repository) => {
|
||||
val execute = if (!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess) {
|
||||
// Authentication is not required
|
||||
true
|
||||
} else {
|
||||
// Authentication is required
|
||||
val passed = for {
|
||||
authorizationHeader <- Option(request.getHeader("Authorization"))
|
||||
account <- authenticateByHeader(authorizationHeader, settings)
|
||||
} yield
|
||||
if (isUpdating) {
|
||||
if (hasDeveloperRole(repository.owner, repository.name, Some(account))) {
|
||||
request.setAttribute(Keys.Request.UserName, account.userName)
|
||||
request.setAttribute(Keys.Request.RepositoryLockKey, s"${repository.owner}/${repository.name}")
|
||||
true
|
||||
} else false
|
||||
} else if (repository.repository.isPrivate) {
|
||||
if (hasGuestRole(repository.owner, repository.name, Some(account))) {
|
||||
request.setAttribute(Keys.Request.UserName, account.userName)
|
||||
true
|
||||
} else false
|
||||
} else true
|
||||
passed.getOrElse(false)
|
||||
}
|
||||
val execute =
|
||||
if (!isUpdating && !repository.repository.isPrivate && settings.basicBehavior.allowAnonymousAccess) {
|
||||
// Authentication is not required
|
||||
true
|
||||
} else {
|
||||
// Authentication is required
|
||||
val passed = for {
|
||||
authorizationHeader <- Option(request.getHeader("Authorization"))
|
||||
account <- authenticateByHeader(authorizationHeader, settings)
|
||||
} yield
|
||||
if (isUpdating) {
|
||||
if (hasDeveloperRole(repository.owner, repository.name, Some(account))) {
|
||||
request.setAttribute(Keys.Request.UserName, account.userName)
|
||||
request.setAttribute(Keys.Request.RepositoryLockKey, s"${repository.owner}/${repository.name}")
|
||||
true
|
||||
} else false
|
||||
} else if (repository.repository.isPrivate) {
|
||||
if (hasGuestRole(repository.owner, repository.name, Some(account))) {
|
||||
request.setAttribute(Keys.Request.UserName, account.userName)
|
||||
true
|
||||
} else false
|
||||
} else true
|
||||
passed.getOrElse(false)
|
||||
}
|
||||
|
||||
if (execute) { () =>
|
||||
chain.doFilter(request, response)
|
||||
|
||||
@@ -52,15 +52,11 @@ import org.json4s.jackson.Serialization._
|
||||
*/
|
||||
class GitRepositoryServlet extends GitServlet with SystemSettingsService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet])
|
||||
private implicit val jsonFormats: Formats = gitbucket.core.api.JsonFormat.jsonFormats
|
||||
|
||||
override def init(config: ServletConfig): Unit = {
|
||||
setReceivePackFactory(new GitBucketReceivePackFactory())
|
||||
|
||||
val root: File = new File(Directory.RepositoryHome)
|
||||
setRepositoryResolver(new GitBucketRepositoryResolver)
|
||||
|
||||
super.init(config)
|
||||
}
|
||||
|
||||
@@ -420,7 +416,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
pusherAccount,
|
||||
repositoryInfo,
|
||||
ownerAccount,
|
||||
ref = branchName,
|
||||
ref = command.getRefName,
|
||||
refType = refType
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,8 +38,18 @@ object DatabaseConfig {
|
||||
|
||||
private lazy val dbUrl = getValue("db.url", config.getString) //config.getString("db.url")
|
||||
|
||||
def url(directory: Option[String]): String =
|
||||
dbUrl.replace("${DatabaseHome}", directory.getOrElse(DatabaseHome))
|
||||
def url(directory: Option[String]): String = {
|
||||
val sb = new StringBuilder()
|
||||
sb.append(dbUrl)
|
||||
if (dbUrl.startsWith("jdbc:mysql:") && dbUrl.indexOf("permitMysqlScheme") == -1) {
|
||||
if (dbUrl.indexOf("?") == -1) {
|
||||
sb.append("?permitMysqlScheme")
|
||||
} else {
|
||||
sb.append("&permitMysqlScheme")
|
||||
}
|
||||
}
|
||||
sb.toString().replace("${DatabaseHome}", directory.getOrElse(DatabaseHome))
|
||||
}
|
||||
|
||||
lazy val url: String = url(None)
|
||||
lazy val user: String = getValue("db.user", config.getString)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package gitbucket.core.util
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
import scala.jdk.CollectionConverters._
|
||||
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import org.bouncycastle.bcpg.ArmoredInputStream
|
||||
@@ -34,29 +33,33 @@ object GpgUtil {
|
||||
}
|
||||
|
||||
def verifySign(signInfo: JGitUtil.GpgSignInfo)(implicit s: Session): Option[JGitUtil.GpgVerifyInfo] = {
|
||||
new BcPGPObjectFactory(new ArmoredInputStream(new ByteArrayInputStream(signInfo.signArmored)))
|
||||
.iterator()
|
||||
.asScala
|
||||
.flatMap {
|
||||
case signList: PGPSignatureList =>
|
||||
signList
|
||||
.iterator()
|
||||
.asScala
|
||||
.flatMap { sign =>
|
||||
getGpgKey(sign.getKeyID)
|
||||
.map { pubKey =>
|
||||
sign.init(new BcPGPContentVerifierBuilderProvider, pubKey)
|
||||
sign.update(signInfo.target)
|
||||
(sign, pubKey)
|
||||
}
|
||||
.collect {
|
||||
case (sign, pubKey) if sign.verify() =>
|
||||
JGitUtil.GpgVerifyInfo(pubKey.getUserIDs.next, pubKey.getKeyID.toHexString.toUpperCase)
|
||||
}
|
||||
}
|
||||
try {
|
||||
new BcPGPObjectFactory(new ArmoredInputStream(new ByteArrayInputStream(signInfo.signArmored)))
|
||||
.iterator()
|
||||
.asScala
|
||||
.flatMap {
|
||||
case signList: PGPSignatureList =>
|
||||
signList
|
||||
.iterator()
|
||||
.asScala
|
||||
.flatMap { sign =>
|
||||
getGpgKey(sign.getKeyID)
|
||||
.map { pubKey =>
|
||||
sign.init(new BcPGPContentVerifierBuilderProvider, pubKey)
|
||||
sign.update(signInfo.target)
|
||||
(sign, pubKey)
|
||||
}
|
||||
.collect {
|
||||
case (sign, pubKey) if sign.verify() =>
|
||||
JGitUtil.GpgVerifyInfo(pubKey.getUserIDs.next, pubKey.getKeyID.toHexString.toUpperCase)
|
||||
}
|
||||
}
|
||||
}
|
||||
.toList
|
||||
.headOption
|
||||
} catch {
|
||||
case _: Throwable => None
|
||||
}
|
||||
|
||||
}
|
||||
.toList
|
||||
.headOption
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package gitbucket.core.util
|
||||
|
||||
import java.io._
|
||||
|
||||
import gitbucket.core.service.RepositoryService
|
||||
import org.eclipse.jgit.api.Git
|
||||
import Directory._
|
||||
@@ -18,10 +17,10 @@ import org.eclipse.jgit.treewalk.filter._
|
||||
import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
||||
import org.eclipse.jgit.errors.{ConfigInvalidException, IncorrectObjectTypeException, MissingObjectException}
|
||||
import org.eclipse.jgit.transport.RefSpec
|
||||
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import org.cache2k.Cache2kBuilder
|
||||
import org.cache2k.{Cache, Cache2kBuilder}
|
||||
import org.eclipse.jgit.api.errors._
|
||||
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter, RawTextComparator}
|
||||
import org.eclipse.jgit.dircache.DirCacheEntry
|
||||
@@ -37,9 +36,12 @@ object JGitUtil {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(JGitUtil.getClass)
|
||||
|
||||
implicit val objectDatabaseReleasable: Releasable[ObjectDatabase] =
|
||||
private implicit val objectDatabaseReleasable: Releasable[ObjectDatabase] =
|
||||
_.close()
|
||||
|
||||
private def isCacheEnabled(): Boolean =
|
||||
!ConfigUtil.getConfigValue[Boolean]("gitbucket.disableCache").getOrElse(false)
|
||||
|
||||
/**
|
||||
* The repository data.
|
||||
*
|
||||
@@ -284,26 +286,34 @@ object JGitUtil {
|
||||
revCommit
|
||||
}
|
||||
|
||||
private val cache = new Cache2kBuilder[String, Int]() {}
|
||||
.name("commit-count")
|
||||
.expireAfterWrite(24, TimeUnit.HOURS)
|
||||
.entryCapacity(10000)
|
||||
.build()
|
||||
private val cache: Cache[String, Int] = if (isCacheEnabled()) {
|
||||
Cache2kBuilder
|
||||
.of(classOf[String], classOf[Int])
|
||||
.name("commit-count")
|
||||
.expireAfterWrite(24, TimeUnit.HOURS)
|
||||
.entryCapacity(10000)
|
||||
.build()
|
||||
} else null
|
||||
|
||||
private val objectCommitCache = new Cache2kBuilder[ObjectId, RevCommit]() {}
|
||||
.name("object-commit")
|
||||
.entryCapacity(10000)
|
||||
.build()
|
||||
private val objectCommitCache: Cache[ObjectId, RevCommit] = if (isCacheEnabled()) {
|
||||
Cache2kBuilder
|
||||
.of(classOf[ObjectId], classOf[RevCommit])
|
||||
.name("object-commit")
|
||||
.entryCapacity(10000)
|
||||
.build()
|
||||
} else null
|
||||
|
||||
def removeCache(git: Git): Unit = {
|
||||
val dir = git.getRepository.getDirectory
|
||||
val keyPrefix = dir.getAbsolutePath + "@"
|
||||
if (isCacheEnabled()) {
|
||||
val dir = git.getRepository.getDirectory
|
||||
val keyPrefix = dir.getAbsolutePath + "@"
|
||||
|
||||
cache.keys.forEach(key => {
|
||||
if (key.startsWith(keyPrefix)) {
|
||||
cache.remove(key)
|
||||
}
|
||||
})
|
||||
cache.keys.forEach(key => {
|
||||
if (key.startsWith(keyPrefix)) {
|
||||
cache.remove(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -312,16 +322,23 @@ object JGitUtil {
|
||||
*/
|
||||
def getCommitCount(git: Git, branch: String, max: Int = 10001): Int = {
|
||||
val dir = git.getRepository.getDirectory
|
||||
val key = dir.getAbsolutePath + "@" + branch
|
||||
val entry = cache.getEntry(key)
|
||||
|
||||
if (entry == null) {
|
||||
if (isCacheEnabled()) {
|
||||
val key = dir.getAbsolutePath + "@" + branch
|
||||
val entry = cache.getEntry(key)
|
||||
|
||||
if (entry == null) {
|
||||
val commitId = git.getRepository.resolve(branch)
|
||||
val commitCount = git.log.add(commitId).call.iterator.asScala.take(max).size
|
||||
cache.put(key, commitCount)
|
||||
commitCount
|
||||
} else {
|
||||
entry.getValue
|
||||
}
|
||||
} else {
|
||||
val commitId = git.getRepository.resolve(branch)
|
||||
val commitCount = git.log.add(commitId).call.iterator.asScala.take(max).size
|
||||
cache.put(key, commitCount)
|
||||
commitCount
|
||||
} else {
|
||||
entry.getValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,7 +461,7 @@ object JGitUtil {
|
||||
(id, mode, name, path, opt, None)
|
||||
} else if (commitCount < 10000) {
|
||||
(id, mode, name, path, opt, Some(getCommit(path)))
|
||||
} else {
|
||||
} else if (isCacheEnabled()) {
|
||||
// Use in-memory cache if the commit count is too big.
|
||||
val cached = objectCommitCache.getEntry(id)
|
||||
if (cached == null) {
|
||||
@@ -454,6 +471,9 @@ object JGitUtil {
|
||||
} else {
|
||||
(id, mode, name, path, opt, Some(cached.getValue))
|
||||
}
|
||||
} else {
|
||||
val commit = getCommit(path)
|
||||
(id, mode, name, path, opt, Some(commit))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -690,7 +710,7 @@ object JGitUtil {
|
||||
|
||||
val toCommit = revWalk.parseCommit(git.getRepository.resolve(to))
|
||||
(from match {
|
||||
case None => {
|
||||
case None =>
|
||||
toCommit.getParentCount match {
|
||||
case 0 =>
|
||||
df.scan(
|
||||
@@ -700,11 +720,9 @@ object JGitUtil {
|
||||
.asScala
|
||||
case _ => df.scan(toCommit.getParent(0), toCommit.getTree).asScala
|
||||
}
|
||||
}
|
||||
case Some(from) => {
|
||||
case Some(from) =>
|
||||
val fromCommit = revWalk.parseCommit(git.getRepository.resolve(from))
|
||||
df.scan(fromCommit.getTree, toCommit.getTree).asScala
|
||||
}
|
||||
}).toSeq
|
||||
}
|
||||
}
|
||||
@@ -719,6 +737,29 @@ object JGitUtil {
|
||||
}
|
||||
}
|
||||
|
||||
def getDiff(git: Git, from: Option[String], to: String, path: String): Option[DiffInfo] = {
|
||||
getDiffEntries(git, from, to).find(_.getNewPath == path).map { diff =>
|
||||
val oldIsImage = FileUtil.isImage(diff.getOldPath)
|
||||
val newIsImage = FileUtil.isImage(diff.getNewPath)
|
||||
val includeContent = oldIsImage || newIsImage
|
||||
DiffInfo(
|
||||
changeType = diff.getChangeType,
|
||||
oldPath = diff.getOldPath,
|
||||
newPath = diff.getNewPath,
|
||||
oldContent = if (includeContent) None else getTextContent(git, diff.getOldId.toObjectId),
|
||||
newContent = if (includeContent) None else getTextContent(git, diff.getNewId.toObjectId),
|
||||
oldIsImage = oldIsImage,
|
||||
newIsImage = newIsImage,
|
||||
oldObjectId = Option(diff.getOldId).map(_.name),
|
||||
newObjectId = Option(diff.getNewId).map(_.name),
|
||||
oldMode = diff.getOldMode.toString,
|
||||
newMode = diff.getNewMode.toString,
|
||||
tooLarge = false,
|
||||
patch = None
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def getDiffs(
|
||||
git: Git,
|
||||
from: Option[String],
|
||||
@@ -728,7 +769,7 @@ object JGitUtil {
|
||||
): List[DiffInfo] = {
|
||||
val diffs = getDiffEntries(git, from, to)
|
||||
diffs.map { diff =>
|
||||
if (diffs.size > 100) {
|
||||
if (diffs.size > 100) { // Don't show diff if there are more than 100 files
|
||||
DiffInfo(
|
||||
changeType = diff.getChangeType,
|
||||
oldPath = diff.getOldPath,
|
||||
@@ -747,49 +788,35 @@ object JGitUtil {
|
||||
} else {
|
||||
val oldIsImage = FileUtil.isImage(diff.getOldPath)
|
||||
val newIsImage = FileUtil.isImage(diff.getNewPath)
|
||||
if (!fetchContent || oldIsImage || newIsImage) {
|
||||
DiffInfo(
|
||||
changeType = diff.getChangeType,
|
||||
oldPath = diff.getOldPath,
|
||||
newPath = diff.getNewPath,
|
||||
oldContent = None,
|
||||
newContent = None,
|
||||
oldIsImage = oldIsImage,
|
||||
newIsImage = newIsImage,
|
||||
oldObjectId = Option(diff.getOldId).map(_.name),
|
||||
newObjectId = Option(diff.getNewId).map(_.name),
|
||||
oldMode = diff.getOldMode.toString,
|
||||
newMode = diff.getNewMode.toString,
|
||||
tooLarge = false,
|
||||
patch = (if (makePatch) Some(makePatchFromDiffEntry(git, diff)) else None) // TODO use DiffFormatter
|
||||
)
|
||||
} else {
|
||||
DiffInfo(
|
||||
changeType = diff.getChangeType,
|
||||
oldPath = diff.getOldPath,
|
||||
newPath = diff.getNewPath,
|
||||
oldContent = JGitUtil
|
||||
.getContentFromId(git, diff.getOldId.toObjectId, false)
|
||||
.filter(FileUtil.isText)
|
||||
.map(convertFromByteArray),
|
||||
newContent = JGitUtil
|
||||
.getContentFromId(git, diff.getNewId.toObjectId, false)
|
||||
.filter(FileUtil.isText)
|
||||
.map(convertFromByteArray),
|
||||
oldIsImage = oldIsImage,
|
||||
newIsImage = newIsImage,
|
||||
oldObjectId = Option(diff.getOldId).map(_.name),
|
||||
newObjectId = Option(diff.getNewId).map(_.name),
|
||||
oldMode = diff.getOldMode.toString,
|
||||
newMode = diff.getNewMode.toString,
|
||||
tooLarge = false,
|
||||
patch = (if (makePatch) Some(makePatchFromDiffEntry(git, diff)) else None) // TODO use DiffFormatter
|
||||
)
|
||||
}
|
||||
val patch = if (oldIsImage || newIsImage) None else Some(makePatchFromDiffEntry(git, diff)) // TODO use DiffFormatter
|
||||
val tooLarge = patch.exists(_.count(_ == '\n') > 1000) // Don't show diff if the file has more than 1000 lines diff
|
||||
val includeContent = tooLarge || !fetchContent || oldIsImage || newIsImage
|
||||
DiffInfo(
|
||||
changeType = diff.getChangeType,
|
||||
oldPath = diff.getOldPath,
|
||||
newPath = diff.getNewPath,
|
||||
oldContent = if (includeContent) None else getTextContent(git, diff.getOldId.toObjectId),
|
||||
newContent = if (includeContent) None else getTextContent(git, diff.getNewId.toObjectId),
|
||||
oldIsImage = oldIsImage,
|
||||
newIsImage = newIsImage,
|
||||
oldObjectId = Option(diff.getOldId).map(_.name),
|
||||
newObjectId = Option(diff.getNewId).map(_.name),
|
||||
oldMode = diff.getOldMode.toString,
|
||||
newMode = diff.getNewMode.toString,
|
||||
tooLarge = tooLarge,
|
||||
patch = if (makePatch) patch else None
|
||||
)
|
||||
}
|
||||
}.toList
|
||||
}
|
||||
|
||||
private def getTextContent(git: Git, objectId: ObjectId): Option[String] = {
|
||||
JGitUtil
|
||||
.getContentFromId(git, objectId, false)
|
||||
.filter(FileUtil.isText)
|
||||
.map(convertFromByteArray)
|
||||
}
|
||||
|
||||
private def makePatchFromDiffEntry(git: Git, diff: DiffEntry): String = {
|
||||
val out = new ByteArrayOutputStream()
|
||||
Using.resource(new DiffFormatter(out)) { formatter =>
|
||||
|
||||
@@ -28,7 +28,12 @@ object Keys {
|
||||
/**
|
||||
* Session key for the OpenID Connect authentication.
|
||||
*/
|
||||
val OidcContext = "oidcContext"
|
||||
val OidcAuthContext = "oidcAuthContext"
|
||||
|
||||
/**
|
||||
* Session key for the OpenID Connect token.
|
||||
*/
|
||||
val OidcSessionContext = "oidcSessionContext"
|
||||
|
||||
/**
|
||||
* Generate session key for the issue search condition.
|
||||
|
||||
@@ -41,7 +41,7 @@ class Mailer(settings: SystemSettings) {
|
||||
htmlMsg: Option[String] = None,
|
||||
loginAccount: Option[Account] = None
|
||||
): Option[HtmlEmail] = {
|
||||
if (settings.notification) {
|
||||
if (settings.basicBehavior.notification) {
|
||||
settings.smtp.map { smtp =>
|
||||
val email = new HtmlEmail
|
||||
email.setHostName(smtp.host)
|
||||
|
||||
@@ -74,6 +74,11 @@ object StringUtil {
|
||||
|
||||
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")
|
||||
|
||||
/**
|
||||
* URL encode except '/'.
|
||||
*/
|
||||
def encodeRefName(value: String): String = urlEncode(value).replace("%2F", "/")
|
||||
|
||||
def splitWords(value: String): Array[String] = value.split("[ \\t ]+")
|
||||
|
||||
def isInteger(value: String): Boolean = allCatch opt { value.toInt } map (_ => true) getOrElse (false)
|
||||
@@ -93,8 +98,8 @@ object StringUtil {
|
||||
detector.handleData(content, 0, content.length)
|
||||
detector.dataEnd()
|
||||
detector.getDetectedCharset match {
|
||||
case null => "UTF-8"
|
||||
case e => e
|
||||
case null | "US-ASCII" => "UTF-8"
|
||||
case e => e
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ trait AvatarImageProvider { self: RequestCache =>
|
||||
val src = if (mailAddress.isEmpty) {
|
||||
// by user name
|
||||
getAccountByUserNameFromCache(userName).map { account =>
|
||||
if (account.image.isEmpty && context.settings.gravatar) {
|
||||
if (account.image.isEmpty && context.settings.basicBehavior.gravatar) {
|
||||
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
|
||||
} else {
|
||||
s"""${context.path}/${account.userName}/_avatar?${helpers.hashDate(account.updatedDate)}"""
|
||||
@@ -29,13 +29,13 @@ trait AvatarImageProvider { self: RequestCache =>
|
||||
} else {
|
||||
// by mail address
|
||||
getAccountByMailAddressFromCache(mailAddress).map { account =>
|
||||
if (account.image.isEmpty && context.settings.gravatar) {
|
||||
if (account.image.isEmpty && context.settings.basicBehavior.gravatar) {
|
||||
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
|
||||
} else {
|
||||
s"""${context.path}/${account.userName}/_avatar?${helpers.hashDate(account.updatedDate)}"""
|
||||
}
|
||||
} getOrElse {
|
||||
if (context.settings.gravatar) {
|
||||
if (context.settings.basicBehavior.gravatar) {
|
||||
s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
|
||||
} else {
|
||||
s"""${context.path}/_unknown/_avatar"""
|
||||
|
||||
@@ -276,7 +276,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
/**
|
||||
* URL encode except '/'.
|
||||
*/
|
||||
def encodeRefName(value: String): String = StringUtil.urlEncode(value).replace("%2F", "/")
|
||||
def encodeRefName(value: String): String = StringUtil.encodeRefName(value)
|
||||
|
||||
def urlEncode(value: String): String = StringUtil.urlEncode(value)
|
||||
|
||||
|
||||
16
src/main/twirl/gitbucket/core/account/reset.scala.html
Normal file
16
src/main/twirl/gitbucket/core/account/reset.scala.html
Normal file
@@ -0,0 +1,16 @@
|
||||
@()(implicit context: gitbucket.core.controller.Context)
|
||||
@gitbucket.core.html.main("Reset your password"){
|
||||
<div class="content-wrapper main-center">
|
||||
<div class="content body">
|
||||
<h2>Reset your password</h2>
|
||||
<form action="@context.path/reset" method="POST" validate="true" autocomplete="off">
|
||||
<fieldset class="form-group">
|
||||
Enter your email address to reset your password.
|
||||
<input type="text" name="mailAddress" id="mailAddress" class="form-control" value="" value="" style="max-width: 400px;"/>
|
||||
<span id="error-mailAddress" class="error"></span>
|
||||
</fieldset>
|
||||
<input type="submit" class="btn btn-success" value="Submit"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
@()(implicit context: gitbucket.core.controller.Context)
|
||||
@gitbucket.core.html.main("Reset your password"){
|
||||
<div class="content-wrapper main-center">
|
||||
<div class="content body">
|
||||
<h2>Reset your password</h2>
|
||||
<p>
|
||||
Password has been updated. <a href="@context.path/signin">Sign-in</a> with new password.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
17
src/main/twirl/gitbucket/core/account/resetform.scala.html
Normal file
17
src/main/twirl/gitbucket/core/account/resetform.scala.html
Normal file
@@ -0,0 +1,17 @@
|
||||
@(token: String)(implicit context: gitbucket.core.controller.Context)
|
||||
@gitbucket.core.html.main("Reset your password"){
|
||||
<div class="content-wrapper main-center">
|
||||
<div class="content body">
|
||||
<h2>Reset your password</h2>
|
||||
<form action="@context.path/reset/form" method="POST" validate="true" autocomplete="off">
|
||||
<fieldset class="form-group">
|
||||
Enter new password:
|
||||
<input type="password" name="password" id="password" class="form-control" value="" style="max-width: 400px;"/>
|
||||
<span id="error-password" class="error"></span>
|
||||
</fieldset>
|
||||
<input type="submit" class="btn btn-success" value="Submit"/>
|
||||
<input type="hidden" name="token" id="token" value="@token"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
11
src/main/twirl/gitbucket/core/account/resetsent.scala.html
Normal file
11
src/main/twirl/gitbucket/core/account/resetsent.scala.html
Normal file
@@ -0,0 +1,11 @@
|
||||
@()(implicit context: gitbucket.core.controller.Context)
|
||||
@gitbucket.core.html.main("Reset your password"){
|
||||
<div class="content-wrapper main-center">
|
||||
<div class="content body">
|
||||
<h2>Reset your password</h2>
|
||||
<p>
|
||||
Send an email to you. Check your mailbox.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<label class="strong">Services</label>
|
||||
<fieldset>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="gravatar"@if(context.settings.gravatar){ checked}/>
|
||||
<input type="checkbox" name="basicBehavior.gravatar"@if(context.settings.basicBehavior.gravatar){ checked}/>
|
||||
Use Gravatar for profile images
|
||||
</label>
|
||||
</fieldset>
|
||||
@@ -132,7 +132,7 @@
|
||||
<label class="strong">Notifications</label>
|
||||
<fieldset>
|
||||
<label class="checkbox" for="notification">
|
||||
<input type="checkbox" id="notification" name="notification"@if(context.settings.notification){ checked}/>
|
||||
<input type="checkbox" id="notification" name="basicBehavior.notification"@if(context.settings.basicBehavior.notification){ checked}/>
|
||||
Send notifications
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
@@ -108,15 +108,30 @@
|
||||
<label class="strong">Account registration</label>
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="allowAccountRegistration" value="true"@if(context.settings.allowAccountRegistration){ checked}>
|
||||
<input type="radio" name="basicBehavior.allowAccountRegistration" value="true"@if(context.settings.basicBehavior.allowAccountRegistration){ checked}>
|
||||
<span class="strong">Allow</span> <span class="normal">- Users can create accounts by themselves.</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="allowAccountRegistration" value="false"@if(!context.settings.allowAccountRegistration){ checked}>
|
||||
<input type="radio" name="basicBehavior.allowAccountRegistration" value="false"@if(!context.settings.basicBehavior.allowAccountRegistration){ checked}>
|
||||
<span class="strong">Deny</span> <span class="normal">- Only administrators can create accounts.</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Reset password -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">Reset password</label>
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="basicBehavior.allowResetPassword" value="true"@if(context.settings.basicBehavior.allowResetPassword){ checked}>
|
||||
<span class="strong">Allow</span> <span class="normal">- Allow users to reset password. (SMTP setting is required)</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="basicBehavior.allowResetPassword" value="false"@if(!context.settings.basicBehavior.allowResetPassword){ checked}>
|
||||
<span class="strong">Deny</span> <span class="normal">- Doesn't allow users to reset password.</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Repository operations -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
@@ -126,11 +141,11 @@
|
||||
<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}>
|
||||
<input type="radio" name="basicBehavior.repositoryOperation.create" value="true"@if(context.settings.basicBehavior.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}>
|
||||
<input type="radio" name="basicBehavior.repositoryOperation.create" value="false"@if(!context.settings.basicBehavior.repositoryOperation.create){ checked}>
|
||||
<span class="strong">Admin only</span> <span class="normal">- Only administrators can create repository.</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -139,11 +154,11 @@
|
||||
<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}>
|
||||
<input type="radio" name="basicBehavior.repositoryOperation.delete" value="true"@if(context.settings.basicBehavior.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}>
|
||||
<input type="radio" name="basicBehavior.repositoryOperation.delete" value="false"@if(!context.settings.basicBehavior.repositoryOperation.delete){ checked}>
|
||||
<span class="strong">Admin only</span> <span class="normal">- Only administrators can delete repository.</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -152,11 +167,11 @@
|
||||
<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}>
|
||||
<input type="radio" name="basicBehavior.repositoryOperation.rename" value="true"@if(context.settings.basicBehavior.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}>
|
||||
<input type="radio" name="basicBehavior.repositoryOperation.rename" value="false"@if(!context.settings.basicBehavior.repositoryOperation.rename){ checked}>
|
||||
<span class="strong">Admin only</span> <span class="normal">- Only administrators can rename repository.</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -165,11 +180,11 @@
|
||||
<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}>
|
||||
<input type="radio" name="basicBehavior.repositoryOperation.transfer" value="true"@if(context.settings.basicBehavior.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}>
|
||||
<input type="radio" name="basicBehavior.repositoryOperation.transfer" value="false"@if(!context.settings.basicBehavior.repositoryOperation.transfer){ checked}>
|
||||
<span class="strong">Admin only</span> <span class="normal">- Only administrators can transfer repository.</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -178,11 +193,11 @@
|
||||
<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}>
|
||||
<input type="radio" name="basicBehavior.repositoryOperation.fork" value="true"@if(context.settings.basicBehavior.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}>
|
||||
<input type="radio" name="basicBehavior.repositoryOperation.fork" value="false"@if(!context.settings.basicBehavior.repositoryOperation.fork){ checked}>
|
||||
<span class="strong">Admin only</span> <span class="normal">- Only administrators can fork repository.</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -192,11 +207,11 @@
|
||||
<label class="strong">Default visibility when creating a new repository</label>
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="isCreateRepoOptionPublic" value="true"@if(context.settings.isCreateRepoOptionPublic){ checked}>
|
||||
<input type="radio" name="basicBehavior.isCreateRepoOptionPublic" value="true"@if(context.settings.basicBehavior.isCreateRepoOptionPublic){ checked}>
|
||||
<span class="strong">Public</span> <span class="normal">- All users and guests can read the repository.</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="isCreateRepoOptionPublic" value="false"@if(!context.settings.isCreateRepoOptionPublic){ checked}>
|
||||
<input type="radio" name="basicBehavior.isCreateRepoOptionPublic" value="false"@if(!context.settings.basicBehavior.isCreateRepoOptionPublic){ checked}>
|
||||
<span class="strong">Private</span> <span class="normal">- Only collaborators can read the repository.</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
@@ -207,11 +222,11 @@
|
||||
<label class="strong">Anonymous access</label>
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="allowAnonymousAccess" value="true"@if(context.settings.allowAnonymousAccess){ checked}>
|
||||
<input type="radio" name="basicBehavior.allowAnonymousAccess" value="true"@if(context.settings.basicBehavior.allowAnonymousAccess){ checked}>
|
||||
<span class="strong">Allow</span> <span class="normal">- Anyone can view public repositories and user/group profiles.</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="allowAnonymousAccess" value="false"@if(!context.settings.allowAnonymousAccess){ checked}>
|
||||
<input type="radio" name="basicBehavior.allowAnonymousAccess" value="false"@if(!context.settings.basicBehavior.allowAnonymousAccess){ checked}>
|
||||
<span class="strong">Deny</span> <span class="normal">- Users must authenticate before viewing any information.</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
@@ -272,11 +287,11 @@
|
||||
<label><span class="strong">Show repositories in sidebar</span></label>
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="limitVisibleRepositories" value="false"@if(!context.settings.limitVisibleRepositories){ checked}>
|
||||
<input type="radio" name="basicBehavior.limitVisibleRepositories" value="false"@if(!context.settings.basicBehavior.limitVisibleRepositories){ checked}>
|
||||
<span class="strong">All</span> <span class="normal">- Show all visible repositories in sidebar.</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="limitVisibleRepositories" value="true"@if(context.settings.limitVisibleRepositories){ checked}>
|
||||
<input type="radio" name="basicBehavior.limitVisibleRepositories" value="true"@if(context.settings.basicBehavior.limitVisibleRepositories){ checked}>
|
||||
<span class="strong">Limited</span> <span class="normal">- Show only owned repositories in sidebar.</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
|
||||
filter: String,
|
||||
groups: List[String],
|
||||
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context)
|
||||
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
|
||||
enableNewsFeed: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||
@gitbucket.core.html.main("Issues"){
|
||||
@gitbucket.core.dashboard.html.sidebar(recentRepositories){
|
||||
@gitbucket.core.dashboard.html.tab("issues")
|
||||
@gitbucket.core.dashboard.html.tab(enableNewsFeed, "issues")
|
||||
<div class="container">
|
||||
@gitbucket.core.dashboard.html.issuesnavi("issues", filter, openCount, closedCount, condition)
|
||||
@gitbucket.core.dashboard.html.issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId), status) =>
|
||||
@issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId, assignedUserNames), status) =>
|
||||
<tr>
|
||||
<td style="padding-top: 12px; padding-bottom: 12px;">
|
||||
<a href="@context.path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a> ・
|
||||
@@ -33,7 +33,7 @@
|
||||
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
|
||||
}
|
||||
<span class="pull-right muted">
|
||||
@issue.assignedUserName.map { userName =>
|
||||
@assignedUserNames.map { userName =>
|
||||
@helpers.avatar(userName, 20, tooltip = true)
|
||||
}
|
||||
@if(commentCount > 0){
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
|
||||
filter: String,
|
||||
groups: List[String],
|
||||
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context)
|
||||
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
|
||||
enableNewsFeed: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||
@gitbucket.core.html.main("Pull requests"){
|
||||
@gitbucket.core.dashboard.html.sidebar(recentRepositories){
|
||||
@gitbucket.core.dashboard.html.tab("pulls")
|
||||
@gitbucket.core.dashboard.html.tab(enableNewsFeed, "pulls")
|
||||
<div class="container">
|
||||
@gitbucket.core.dashboard.html.issuesnavi("pulls", filter, openCount, closedCount, condition)
|
||||
@gitbucket.core.dashboard.html.issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
@(groups: List[String],
|
||||
visibleRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
|
||||
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context)
|
||||
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
|
||||
enableNewsFeed: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main("Repositories"){
|
||||
@gitbucket.core.dashboard.html.sidebar(recentRepositories){
|
||||
@gitbucket.core.dashboard.html.tab("repos")
|
||||
@gitbucket.core.dashboard.html.tab(enableNewsFeed, "repos")
|
||||
<div class="container">
|
||||
<div class="btn-group" id="owner-dropdown">
|
||||
<button class="dropdown-toggle btn btn-default" data-toggle="dropdown" aria-expanded="false">
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
@(active: String = "")(implicit context: gitbucket.core.controller.Context)
|
||||
<ul class="nav nav-tabs" style="margin-bottom: 20px;">
|
||||
<li @if(active == ""){ class="active"}><a href="@context.path/">News feed</a></li>
|
||||
@if(context.loginAccount.isDefined){
|
||||
<li @if(active == "repos" ){ class="active"}><a href="@context.path/dashboard/repos">Repositories</a></li>
|
||||
<li @if(active == "pulls" ){ class="active"}><a href="@context.path/dashboard/pulls">Pull requests</a></li>
|
||||
<li @if(active == "issues"){ class="active"}><a href="@context.path/dashboard/issues">Issues</a></li>
|
||||
@gitbucket.core.plugin.PluginRegistry().getDashboardTabs.map { tab =>
|
||||
@tab(context).map { link =>
|
||||
<li @if(active == link.id){ class="active"}><a href="@context.path/@link.path">@link.label</a></li>
|
||||
@(enableNewsFeed: Boolean, active: String = "")(implicit context: gitbucket.core.controller.Context)
|
||||
@if(enableNewsFeed || context.loginAccount.isDefined) {
|
||||
<ul class="nav nav-tabs" style="margin-bottom: 20px;">
|
||||
@if(enableNewsFeed) {
|
||||
<li @if(active == "") {class="active"}><a href="@context.path/">News feed</a></li>
|
||||
}
|
||||
@if(context.loginAccount.isDefined) {
|
||||
<li @if(active == "repos") {class="active"}><a href="@context.path/dashboard/repos">Repositories</a></li>
|
||||
<li @if(active == "pulls") {class="active"}><a href="@context.path/dashboard/pulls">Pull requests</a></li>
|
||||
<li @if(active == "issues") {class="active"}><a href="@context.path/dashboard/issues">Issues</a></li>
|
||||
@gitbucket.core.plugin.PluginRegistry().getDashboardTabs.map { tab =>
|
||||
@tab(context).map { link =>
|
||||
<li @if(active == link.id) {class="active"}><a href="@context.path/@link.path">@link.label</a></li>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
</ul>
|
||||
}
|
||||
|
||||
@@ -100,38 +100,48 @@
|
||||
</tr>
|
||||
<tr class="diff-collapse-@i collapse in">
|
||||
<td style="padding: 0;">
|
||||
@if(diff.oldObjectId == diff.newObjectId){
|
||||
@if(diff.oldPath != diff.newPath){
|
||||
<div class="diff-same">File renamed without changes</div>
|
||||
} else {
|
||||
<div class="diff-same">File mode changed</div>
|
||||
}
|
||||
@if(diff.tooLarge) {
|
||||
<div style="padding: 12px;" id="show-diff-@i">
|
||||
@if(oldCommitId.isEmpty && newCommitId.isDefined) {
|
||||
Too large (<a href="javascript:showDiff(@i, '', '@newCommitId', '@diff.newPath')">Show diff</a>)
|
||||
}
|
||||
@if(oldCommitId.isDefined && newCommitId.isDefined) {
|
||||
Too large (<a href="javascript:showDiff(@i, '@oldCommitId', '@newCommitId', '@diff.newPath')">Show diff</a>)
|
||||
}
|
||||
</div>
|
||||
<div id="diffText-@i" class="diffText"></div>
|
||||
<input type="hidden" id="newText-@i" data-file-name="@diff.newPath" data-val="">
|
||||
<input type="hidden" id="oldText-@i" data-file-name="@diff.oldPath" data-val="">
|
||||
} else {
|
||||
@if(diff.newContent != None || diff.oldContent != None){
|
||||
<div id="diffText-@i" class="diffText"></div>
|
||||
<input type="hidden" id="newText-@i" data-file-name="@diff.newPath" data-val="@diff.newContent">
|
||||
<input type="hidden" id="oldText-@i" data-file-name="@diff.oldPath" data-val="@diff.oldContent">
|
||||
} else {
|
||||
@if(diff.newIsImage || diff.oldIsImage){
|
||||
<div class="diff-image-render diff2up">
|
||||
@if(oldCommitId.isDefined && diff.oldIsImage){
|
||||
<div class="diff-image-frame diff-old"><img src="@helpers.url(repository)/raw/@oldCommitId.get/@diff.oldPath" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
|
||||
} else {
|
||||
@if(diff.changeType != ChangeType.ADD){
|
||||
<div style="padding: 12px;">Not supported</div>
|
||||
}
|
||||
}
|
||||
@if(newCommitId.isDefined && diff.newIsImage){
|
||||
<div class="diff-image-frame diff-new"><img src="@helpers.url(repository)/raw/@newCommitId.get/@diff.newPath" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
|
||||
} else {
|
||||
@if(diff.changeType != ChangeType.DELETE){
|
||||
<div style="padding: 12px;">Not supported</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@if(diff.oldObjectId == diff.newObjectId) {
|
||||
@if(diff.oldPath != diff.newPath) {
|
||||
<div class="diff-same">File renamed without changes</div>
|
||||
} else {
|
||||
@if(diff.tooLarge){
|
||||
<div style="padding: 12px;">Too large</div>
|
||||
<div class="diff-same">File mode changed</div>
|
||||
}
|
||||
} else {
|
||||
@if(diff.newContent != None || diff.oldContent != None){
|
||||
<div id="diffText-@i" class="diffText"></div>
|
||||
<input type="hidden" id="newText-@i" data-file-name="@diff.newPath" data-val="@diff.newContent">
|
||||
<input type="hidden" id="oldText-@i" data-file-name="@diff.oldPath" data-val="@diff.oldContent">
|
||||
} else {
|
||||
@if(diff.newIsImage || diff.oldIsImage){
|
||||
<div class="diff-image-render diff2up">
|
||||
@if(oldCommitId.isDefined && diff.oldIsImage){
|
||||
<div class="diff-image-frame diff-old"><img src="@helpers.url(repository)/raw/@oldCommitId.get/@diff.oldPath" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
|
||||
} else {
|
||||
@if(diff.changeType != ChangeType.ADD){
|
||||
<div style="padding: 12px;">Not supported</div>
|
||||
}
|
||||
}
|
||||
@if(newCommitId.isDefined && diff.newIsImage){
|
||||
<div class="diff-image-frame diff-new"><img src="@helpers.url(repository)/raw/@newCommitId.get/@diff.newPath" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
|
||||
} else {
|
||||
@if(diff.changeType != ChangeType.DELETE){
|
||||
<div style="padding: 12px;">Not supported</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
<div style="padding: 12px;">Not supported</div>
|
||||
}
|
||||
@@ -200,53 +210,10 @@ $(function(){
|
||||
renderOneDiff($(this).closest("table").find(".diffText"), window.viewType, $(this).closest("table").find(".file-hash")[0].id);
|
||||
});
|
||||
|
||||
function getInlineContainer(where) {
|
||||
if (window.viewType == 0) {
|
||||
if (where === 'new') {
|
||||
return $('<tr class="not-diff"><td colspan="2"></td><td colspan="2" class="comment-box-container"></td></tr>');
|
||||
} else {
|
||||
return $('<tr class="not-diff"><td colspan="2" class="comment-box-container"></td><td colspan="2"></td></tr>');
|
||||
}
|
||||
}
|
||||
return $('<tr class="not-diff"><td colspan="3" class="comment-box-container"></td></tr>');
|
||||
}
|
||||
|
||||
if (typeof $('#show-notes')[0] !== 'undefined' && !$('#show-notes')[0].checked) {
|
||||
$('#comment-list').children('.inline-comment').hide();
|
||||
}
|
||||
|
||||
function showCommentForm(commitId, fileName, oldLineNumber, newLineNumber, $tr){
|
||||
// assemble Ajax url
|
||||
let url = '@helpers.url(repository)/commit/' + commitId + '/comment/_form?fileName=' + fileName@issueId.map { id => + '&issueId=@id' };
|
||||
if (!isNaN(oldLineNumber) && oldLineNumber) {
|
||||
url += ('&oldLineNumber=' + oldLineNumber)
|
||||
}
|
||||
if (!isNaN(newLineNumber) && newLineNumber) {
|
||||
url += ('&newLineNumber=' + newLineNumber)
|
||||
}
|
||||
// send Ajax request
|
||||
$.get(url, { dataType : 'html' }, function(responseContent) {
|
||||
// create container
|
||||
let tmp;
|
||||
if (!isNaN(oldLineNumber) && oldLineNumber) {
|
||||
if (!isNaN(newLineNumber) && newLineNumber) {
|
||||
tmp = getInlineContainer();
|
||||
} else {
|
||||
tmp = getInlineContainer('old');
|
||||
}
|
||||
} else {
|
||||
tmp = getInlineContainer('new');
|
||||
}
|
||||
// add comment textarea
|
||||
tmp.addClass('inline-comment-form').children('.comment-box-container').html(responseContent);
|
||||
$tr.nextAll(':not(.not-diff):first').before(tmp);
|
||||
// hide reply comment field
|
||||
$(tmp).closest('.not-diff').prev().find('.reply-comment').closest('.not-diff').hide();
|
||||
// focus textarea
|
||||
tmp.find('textarea').focus();
|
||||
});
|
||||
}
|
||||
|
||||
// Add comment button
|
||||
$('.diff-outside').on('click','table.diff .add-comment',function() {
|
||||
const $this = $(this);
|
||||
@@ -302,11 +269,156 @@ $(function(){
|
||||
getSelection().empty();
|
||||
updateHighlighting();
|
||||
});
|
||||
});
|
||||
|
||||
function getInlineContainer(where) {
|
||||
if (window.viewType == 0) {
|
||||
if (where === 'new') {
|
||||
return $('<tr class="not-diff"><td colspan="2"></td><td colspan="2" class="comment-box-container"></td></tr>');
|
||||
} else {
|
||||
return $('<tr class="not-diff"><td colspan="2" class="comment-box-container"></td><td colspan="2"></td></tr>');
|
||||
}
|
||||
}
|
||||
return $('<tr class="not-diff"><td colspan="3" class="comment-box-container"></td></tr>');
|
||||
}
|
||||
|
||||
function showCommentForm(commitId, fileName, oldLineNumber, newLineNumber, $tr){
|
||||
// assemble Ajax url
|
||||
let url = '@helpers.url(repository)/commit/' + commitId + '/comment/_form?fileName=' + fileName@issueId.map { id => + '&issueId=@id' };
|
||||
if (!isNaN(oldLineNumber) && oldLineNumber) {
|
||||
url += ('&oldLineNumber=' + oldLineNumber)
|
||||
}
|
||||
if (!isNaN(newLineNumber) && newLineNumber) {
|
||||
url += ('&newLineNumber=' + newLineNumber)
|
||||
}
|
||||
// send Ajax request
|
||||
$.get(url, { dataType : 'html' }, function(responseContent) {
|
||||
// create container
|
||||
let tmp;
|
||||
if (!isNaN(oldLineNumber) && oldLineNumber) {
|
||||
if (!isNaN(newLineNumber) && newLineNumber) {
|
||||
tmp = getInlineContainer();
|
||||
} else {
|
||||
tmp = getInlineContainer('old');
|
||||
}
|
||||
} else {
|
||||
tmp = getInlineContainer('new');
|
||||
}
|
||||
// add comment textarea
|
||||
tmp.addClass('inline-comment-form').children('.comment-box-container').html(responseContent);
|
||||
$tr.nextAll(':not(.not-diff):first').before(tmp);
|
||||
// hide reply comment field
|
||||
$(tmp).closest('.not-diff').prev().find('.reply-comment').closest('.not-diff').hide();
|
||||
// focus textarea
|
||||
tmp.find('textarea').focus();
|
||||
});
|
||||
}
|
||||
|
||||
function renderOneCommitCommentIntoDiff($v, diff){
|
||||
//var filename = $v.attr('filename');
|
||||
const oldline = $v.attr('oldline');
|
||||
const newline = $v.attr('newline');
|
||||
let tmp;
|
||||
if (typeof oldline !== 'undefined') {
|
||||
if (typeof newline !== 'undefined') {
|
||||
tmp = getInlineContainer();
|
||||
} else {
|
||||
tmp = getInlineContainer('old');
|
||||
}
|
||||
tmp.children('td:first').html($v.clone().show());
|
||||
diff.find('table.diff').find('.oldline[line-number=' + oldline + ']').parent().nextAll(':not(.not-diff):first').before(tmp);
|
||||
} else {
|
||||
tmp = getInlineContainer('new');
|
||||
tmp.children('td:last').html($v.clone().show());
|
||||
diff.find('table.diff').find('.newline[line-number=' + newline + ']').parent().nextAll(':not(.not-diff):first').before(tmp);
|
||||
}
|
||||
if (!diff.find('.toggle-notes').prop('checked')) {
|
||||
tmp.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function renderStatBar(add, del){
|
||||
if(add + del > 5){
|
||||
if(add){
|
||||
if(add < del){
|
||||
add = Math.floor(1 + (add * 4 / (add + del)));
|
||||
} else {
|
||||
add = Math.ceil(1 + (add * 4 / (add + del)));
|
||||
}
|
||||
}
|
||||
del = 5 - add;
|
||||
}
|
||||
const ret = $('<div class="diffstat-bar">');
|
||||
for(let i = 0; i < 5; i++){
|
||||
if(add){
|
||||
ret.append('<span class="text-diff-added">■</span>');
|
||||
add--;
|
||||
} else if(del){
|
||||
ret.append('<span class="text-diff-deleted">■</span>');
|
||||
del--;
|
||||
} else {
|
||||
ret.append('■');
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function renderOneDiff(diffText, viewType, fileHash){
|
||||
const table = diffText.closest("table[data-diff-id]");
|
||||
const i = table.data("diff-id");
|
||||
const ignoreWhiteSpace = table.find('.ignore-whitespace').prop('checked');
|
||||
diffUsingJS('oldText-' + i, 'newText-' + i, diffText.attr('id'), viewType, ignoreWhiteSpace, fileHash);
|
||||
const add = diffText.find("table").attr("add") * 1;
|
||||
const del = diffText.find("table").attr("del") * 1;
|
||||
table.find(".diffstat").text(add + del + " ").append(renderStatBar(add, del)).attr("title", add + " additions & " + del + " deletions").tooltip();
|
||||
$('span.diffstat[data-diff-id="' + i + '"]')
|
||||
.html('<span class="text-diff-added">+' + add + '</span><span class="text-diff-deleted">-' + del + '</span>')
|
||||
.append(renderStatBar(add, del).attr('title', (add + del) + " lines changed").tooltip());
|
||||
|
||||
@if(hasWritePermission) {
|
||||
diffText.find('.body').filter(function(i, e) {
|
||||
return $(e).has('span').length > 0;
|
||||
}).each(function(){
|
||||
$('<b class="add-comment">+</b>').prependTo(this);
|
||||
});
|
||||
}
|
||||
@if(showLineNotes){
|
||||
const fileName = table.attr('filename');
|
||||
$('.inline-comment').each(function(i, v) {
|
||||
if($(this).attr('filename') == fileName){
|
||||
renderOneCommitCommentIntoDiff($(this), table);
|
||||
}
|
||||
});
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
function renderReplyComment($table){
|
||||
const elements = {};
|
||||
let filename, newline, oldline;
|
||||
$table.find('.comment-box-container .inline-comment').each(function(i, e){
|
||||
filename = $(e).attr('filename');
|
||||
newline = $(e).attr('newline');
|
||||
oldline = $(e).attr('oldline');
|
||||
const key = filename + '-' + newline + '-' + oldline;
|
||||
elements[key] = {
|
||||
element: $(e),
|
||||
filename: filename,
|
||||
newline: newline,
|
||||
oldline: oldline
|
||||
};
|
||||
});
|
||||
for(const key in elements){
|
||||
filename = elements[key]['filename'];
|
||||
oldline = elements[key]['oldline']; //? elements[key]['oldline'] : '';
|
||||
newline = elements[key]['newline']; //? elements[key]['newline'] : '';
|
||||
|
||||
const $v = $('<div class="commit-comment-box reply-comment-box">')
|
||||
.append($('<input type="text" class="form-control reply-comment" placeholder="Reply..." '
|
||||
+ 'data-filename="' + filename + '" '
|
||||
+ 'data-newline="' + newline + '" '
|
||||
+ 'data-oldline="' + oldline + '">'));
|
||||
|
||||
function renderOneCommitCommentIntoDiff($v, diff){
|
||||
//var filename = $v.attr('filename');
|
||||
const oldline = $v.attr('oldline');
|
||||
const newline = $v.attr('newline');
|
||||
let tmp;
|
||||
if (typeof oldline !== 'undefined') {
|
||||
if (typeof newline !== 'undefined') {
|
||||
@@ -314,134 +426,51 @@ $(function(){
|
||||
} else {
|
||||
tmp = getInlineContainer('old');
|
||||
}
|
||||
tmp.children('td:first').html($v.clone().show());
|
||||
diff.find('table.diff').find('.oldline[line-number=' + oldline + ']').parent().nextAll(':not(.not-diff):first').before(tmp);
|
||||
tmp.children('td:first').html($v);
|
||||
} else {
|
||||
tmp = getInlineContainer('new');
|
||||
tmp.children('td:last').html($v.clone().show());
|
||||
diff.find('table.diff').find('.newline[line-number=' + newline + ']').parent().nextAll(':not(.not-diff):first').before(tmp);
|
||||
}
|
||||
if (!diff.find('.toggle-notes').prop('checked')) {
|
||||
tmp.hide();
|
||||
tmp.children('td:last').html($v);
|
||||
}
|
||||
elements[key]['element'].closest('.not-diff').after(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
function renderStatBar(add, del){
|
||||
if(add + del > 5){
|
||||
if(add){
|
||||
if(add < del){
|
||||
add = Math.floor(1 + (add * 4 / (add + del)));
|
||||
} else {
|
||||
add = Math.ceil(1 + (add * 4 / (add + del)));
|
||||
}
|
||||
function renderDiffs() {
|
||||
const diffs = $('.diffText');
|
||||
let i = 0;
|
||||
function render(){
|
||||
if (diffs[i]) {
|
||||
const $table = renderOneDiff($(diffs[i]), window.viewType, $('.file-hash')[i].id);
|
||||
@if(hasWritePermission) {
|
||||
renderReplyComment($table);
|
||||
}
|
||||
del = 5 - add;
|
||||
}
|
||||
const ret = $('<div class="diffstat-bar">');
|
||||
for(let i = 0; i < 5; i++){
|
||||
if(add){
|
||||
ret.append('<span class="text-diff-added">■</span>');
|
||||
add--;
|
||||
} else if(del){
|
||||
ret.append('<span class="text-diff-deleted">■</span>');
|
||||
del--;
|
||||
} else {
|
||||
ret.append('■');
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function renderOneDiff(diffText, viewType, fileHash){
|
||||
const table = diffText.closest("table[data-diff-id]");
|
||||
const i = table.data("diff-id");
|
||||
const ignoreWhiteSpace = table.find('.ignore-whitespace').prop('checked');
|
||||
diffUsingJS('oldText-' + i, 'newText-' + i, diffText.attr('id'), viewType, ignoreWhiteSpace, fileHash);
|
||||
const add = diffText.find("table").attr("add") * 1;
|
||||
const del = diffText.find("table").attr("del") * 1;
|
||||
table.find(".diffstat").text(add + del + " ").append(renderStatBar(add, del)).attr("title", add + " additions & " + del + " deletions").tooltip();
|
||||
$('span.diffstat[data-diff-id="' + i + '"]')
|
||||
.html('<span class="text-diff-added">+' + add + '</span><span class="text-diff-deleted">-' + del + '</span>')
|
||||
.append(renderStatBar(add, del).attr('title', (add + del) + " lines changed").tooltip());
|
||||
|
||||
@if(hasWritePermission) {
|
||||
diffText.find('.body').filter(function(i, e) {
|
||||
return $(e).has('span').length > 0;
|
||||
}).each(function(){
|
||||
$('<b class="add-comment">+</b>').prependTo(this);
|
||||
});
|
||||
}
|
||||
@if(showLineNotes){
|
||||
const fileName = table.attr('filename');
|
||||
$('.inline-comment').each(function(i, v) {
|
||||
if($(this).attr('filename') == fileName){
|
||||
renderOneCommitCommentIntoDiff($(this), table);
|
||||
}
|
||||
});
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
function renderReplyComment($table){
|
||||
const elements = {};
|
||||
let filename, newline, oldline;
|
||||
$table.find('.comment-box-container .inline-comment').each(function(i, e){
|
||||
filename = $(e).attr('filename');
|
||||
newline = $(e).attr('newline');
|
||||
oldline = $(e).attr('oldline');
|
||||
const key = filename + '-' + newline + '-' + oldline;
|
||||
elements[key] = {
|
||||
element: $(e),
|
||||
filename: filename,
|
||||
newline: newline,
|
||||
oldline: oldline
|
||||
};
|
||||
});
|
||||
for(const key in elements){
|
||||
filename = elements[key]['filename'];
|
||||
oldline = elements[key]['oldline']; //? elements[key]['oldline'] : '';
|
||||
newline = elements[key]['newline']; //? elements[key]['newline'] : '';
|
||||
|
||||
const $v = $('<div class="commit-comment-box reply-comment-box">')
|
||||
.append($('<input type="text" class="form-control reply-comment" placeholder="Reply..." '
|
||||
+ 'data-filename="' + filename + '" '
|
||||
+ 'data-newline="' + newline + '" '
|
||||
+ 'data-oldline="' + oldline + '">'));
|
||||
|
||||
let tmp;
|
||||
if (typeof oldline !== 'undefined') {
|
||||
if (typeof newline !== 'undefined') {
|
||||
tmp = getInlineContainer();
|
||||
} else {
|
||||
tmp = getInlineContainer('old');
|
||||
}
|
||||
tmp.children('td:first').html($v);
|
||||
} else {
|
||||
tmp = getInlineContainer('new');
|
||||
tmp.children('td:last').html($v);
|
||||
}
|
||||
elements[key]['element'].closest('.not-diff').after(tmp);
|
||||
i++;
|
||||
setTimeout(render);
|
||||
} else {
|
||||
updateHighlighting();
|
||||
}
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
function renderDiffs() {
|
||||
function showDiff(index, fromId, toId, path){
|
||||
let url = '@helpers.url(repository)/diff/';
|
||||
if (fromId == '') {
|
||||
url = url + encodeURIComponent(toId);
|
||||
} else {
|
||||
url = url + encodeURIComponent(fromId) + '...' + encodeURIComponent(toId);
|
||||
}
|
||||
$.get(url, { path : path }, function(data) {
|
||||
$('#oldText-' + index).attr('data-val', data.oldContent);
|
||||
$('#newText-' + index).attr('data-val', data.newContent);
|
||||
const diffs = $('.diffText');
|
||||
let i = 0;
|
||||
function render(){
|
||||
if (diffs[i]) {
|
||||
const $table = renderOneDiff($(diffs[i]), window.viewType, $('.file-hash')[i].id);
|
||||
@if(hasWritePermission) {
|
||||
renderReplyComment($table);
|
||||
}
|
||||
i++;
|
||||
setTimeout(render);
|
||||
} else {
|
||||
updateHighlighting();
|
||||
}
|
||||
const $table = renderOneDiff($(diffs[index]), window.viewType, $('.file-hash')[index].id);
|
||||
@if(hasWritePermission) {
|
||||
renderReplyComment($table);
|
||||
}
|
||||
render();
|
||||
}
|
||||
});
|
||||
$('#show-diff-' + index).hide();
|
||||
});
|
||||
}
|
||||
|
||||
function changeDisplaySetting(key, value){
|
||||
let url = '';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@(activities: List[gitbucket.core.model.Activity],
|
||||
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
|
||||
showBannerToCreatePersonalAccessToken: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||
showBannerToCreatePersonalAccessToken: Boolean,
|
||||
enableNewsFeed: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main("GitBucket"){
|
||||
@gitbucket.core.dashboard.html.sidebar(recentRepositories){
|
||||
@@ -19,12 +20,26 @@
|
||||
</a> and use it in place of a password on the <code>git</code> command line.
|
||||
</div>
|
||||
}
|
||||
@gitbucket.core.dashboard.html.tab()
|
||||
@gitbucket.core.dashboard.html.tab(enableNewsFeed)
|
||||
<div class="container">
|
||||
<div class="pull-right">
|
||||
<a href="@context.path/activities.atom"><img src="@helpers.assets("/common/images/feed.png")" alt="activities"></a>
|
||||
</div>
|
||||
@gitbucket.core.helper.html.activities(activities)
|
||||
@if(enableNewsFeed) {
|
||||
<div class="pull-right">
|
||||
<a href="@context.path/activities.atom"><img src="@helpers.assets("/common/images/feed.png")" alt="activities"></a>
|
||||
</div>
|
||||
@gitbucket.core.helper.html.activities(activities)
|
||||
} else {
|
||||
<div class="signin-form">
|
||||
@if(context.settings.basicBehavior.allowAnonymousAccess){
|
||||
@context.settings.information.map { information =>
|
||||
<div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
@Html(information)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@gitbucket.core.html.signinform(context.settings)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-tag"></i></span>
|
||||
@helpers.avatarLink(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
add the <code>@comment.content</code> label
|
||||
added the <code>@comment.content</code> label
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,7 +222,7 @@
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-flame"></i></span>
|
||||
@helpers.avatarLink(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
change priority from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
|
||||
changed priority from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
@@ -233,18 +233,40 @@
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-milestone"></i></span>
|
||||
@helpers.avatarLink(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
change milestone from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
|
||||
changed milestone from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
case "assign" => {
|
||||
case "assign" => { @* for backward compatibility *@
|
||||
<div class="discussion-item discussion-item-assign">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-person"></i></span>
|
||||
@helpers.avatarLink(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
change assignee from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
|
||||
changed assignee from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
case "add_assignee" => {
|
||||
<div class="discussion-item discussion-item-assign">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-person"></i></span>
|
||||
@helpers.avatarLink(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
assigned <code>@comment.content</code>
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
case "delete_assignee" => {
|
||||
<div class="discussion-item discussion-item-assign">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-person"></i></span>
|
||||
@helpers.avatarLink(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
unassigned <code>@comment.content</code></code>
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
@@ -255,7 +277,7 @@
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-pencil"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
change title from <code>@convertLineSeparator(comment.content, "LF").split("\n")(0)</code> to <code>@convertLineSeparator(comment.content, "LF").split("\n")(1)</code>
|
||||
changed title from <code>@convertLineSeparator(comment.content, "LF").split("\n")(0)</code> to <code>@convertLineSeparator(comment.content, "LF").split("\n")(1)</code>
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
@@ -266,7 +288,7 @@
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-pencil"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
change base branch from <code>@convertLineSeparator(comment.content, "LF").split("\n")(0)</code> to <code>@convertLineSeparator(comment.content, "LF").split("\n")(1)</code>
|
||||
changed base branch from <code>@convertLineSeparator(comment.content, "LF").split("\n")(0)</code> to <code>@convertLineSeparator(comment.content, "LF").split("\n")(1)</code>
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
@@ -328,9 +350,9 @@
|
||||
$(function(){
|
||||
@if(issue.isDefined){
|
||||
$('.issue-comment-box i.octicon-pencil').click(function(){
|
||||
var id = $(this).closest('a').data('comment-id');
|
||||
var url = '@helpers.url(repository)/issue_comments/_data/' + id;
|
||||
var $content = $('#commentContent-' + id);
|
||||
let id = $(this).closest('a').data('comment-id');
|
||||
let url = '@helpers.url(repository)/issue_comments/_data/' + id;
|
||||
let $content = $('#commentContent-' + id);
|
||||
|
||||
if(!id){
|
||||
id = $(this).closest('a').data('issue-id');
|
||||
@@ -345,7 +367,7 @@ $(function(){
|
||||
});
|
||||
$('.issue-comment-box i.octicon-x').click(function(){
|
||||
if(confirm('Are you sure you want to delete this?')) {
|
||||
var id = $(this).closest('a').data('comment-id');
|
||||
const id = $(this).closest('a').data('comment-id');
|
||||
$.post('@helpers.url(repository)/issue_comments/delete/' + id, function(data){
|
||||
if(data > 0) {
|
||||
$('#comment-' + id).remove();
|
||||
@@ -356,9 +378,9 @@ $(function(){
|
||||
});
|
||||
}
|
||||
$(document).on('click', '.commit-comment-box i.octicon-pencil', function(){
|
||||
var id = $(this).closest('a').data('comment-id');
|
||||
var url = '@helpers.url(repository)/commit_comments/_data/' + id;
|
||||
var $content = $('.commit-commentContent-' + id, $(this).closest('.commit-comment-box'));
|
||||
const id = $(this).closest('a').data('comment-id');
|
||||
const url = '@helpers.url(repository)/commit_comments/_data/' + id;
|
||||
const $content = $('.commit-commentContent-' + id, $(this).closest('.commit-comment-box'));
|
||||
|
||||
$.get(url, { dataType : 'html' }, function(data){
|
||||
$content.empty().html(data);
|
||||
@@ -369,14 +391,14 @@ $(function(){
|
||||
|
||||
$(document).on('click', '.commit-comment-box i.octicon-x', function(){
|
||||
if(confirm('Are you sure you want to delete this?')) {
|
||||
var id = $(this).closest('a').data('comment-id');
|
||||
const id = $(this).closest('a').data('comment-id');
|
||||
$.post('@helpers.url(repository)/commit_comments/delete/' + id,
|
||||
function(data){
|
||||
if(data > 0) {
|
||||
var comment = $('.commit-comment-' + id);
|
||||
const comment = $('.commit-comment-' + id);
|
||||
|
||||
// diff view
|
||||
var tr = comment.closest('.not-diff');
|
||||
const tr = comment.closest('.not-diff');
|
||||
if(tr.length > 0){
|
||||
if(tr.prev('.not-diff').length == 0){
|
||||
tr.next('.not-diff:has(.reply-comment)').remove();
|
||||
@@ -385,7 +407,7 @@ $(function(){
|
||||
}
|
||||
|
||||
// comment list view
|
||||
var panel = comment.closest('div.panel:has(.commit-comment-box)');
|
||||
const panel = comment.closest('div.panel:has(.commit-comment-box)');
|
||||
if(panel.length > 0){
|
||||
comment.parent('.commit-comment-box').remove();
|
||||
if(panel.has('.commit-comment-box').length == 0){
|
||||
@@ -401,7 +423,7 @@ $(function(){
|
||||
});
|
||||
|
||||
$('div[class*=commit-commentContent-]').on('click', ':checkbox', function(ev){
|
||||
var $commentContent = $(ev.target).parents('div[class*=commit-commentContent-]'),
|
||||
const $commentContent = $(ev.target).parents('div[class*=commit-commentContent-]'),
|
||||
commentId = $commentContent.attr('class').match(/commit-commentContent-.+/)[0].replace(/commit-commentContent-/, ''),
|
||||
checkboxes = $commentContent.find(':checkbox');
|
||||
$.get('@helpers.url(repository)/commit_comments/_data/' + commentId, { dataType : 'html' },
|
||||
@@ -421,9 +443,9 @@ $(function(){
|
||||
);
|
||||
});
|
||||
|
||||
@if(issue.isDefined){
|
||||
@if(issue.isDefined){
|
||||
$('#issueContent').on('click', ':checkbox', function(ev){
|
||||
var checkboxes = $('#issueContent :checkbox');
|
||||
const checkboxes = $('#issueContent :checkbox');
|
||||
$.get('@helpers.url(repository)/issues/_data/@issue.get.issueId', { dataType : 'html' },
|
||||
function(responseContent){
|
||||
$.ajax({
|
||||
@@ -439,7 +461,7 @@ $(function(){
|
||||
});
|
||||
|
||||
$('div[id^=commentContent-]').on('click', ':checkbox', function(ev){
|
||||
var $commentContent = $(ev.target).parents('div[id^=commentContent-]'),
|
||||
const $commentContent = $(ev.target).parents('div[id^=commentContent-]'),
|
||||
commentId = $commentContent.attr('id').replace(/commentContent-/, ''),
|
||||
checkboxes = $commentContent.find(':checkbox');
|
||||
$.get('@helpers.url(repository)/issue_comments/_data/' + commentId, { dataType : 'html' },
|
||||
@@ -455,9 +477,7 @@ $(function(){
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
priorities: List[gitbucket.core.model.Priority],
|
||||
defaultPriority: Option[gitbucket.core.model.Priority],
|
||||
labels: List[gitbucket.core.model.Label],
|
||||
customFields: List[gitbucket.core.model.CustomField],
|
||||
isManageable: Boolean,
|
||||
content: String,
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||
@@ -27,13 +28,37 @@
|
||||
elastic = true
|
||||
)
|
||||
<div class="align-right">
|
||||
<input type="submit" class="btn btn-success" value="Submit new issue"/>
|
||||
<input type="submit" class="btn btn-success" value="Submit new issue" onclick="javascript:return checkCustomFieldErrors();"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
@gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map(x => (x, 0, 0)), priorities, defaultPriority, labels, isManageable, repository)
|
||||
@gitbucket.core.issues.html.issueinfo(
|
||||
issue = None,
|
||||
comments = Nil,
|
||||
issueLabels = Nil,
|
||||
issueAssignees = Nil,
|
||||
collaborators = collaborators,
|
||||
milestones = milestones.map(x => (x, 0, 0)),
|
||||
priorities= priorities,
|
||||
defaultPriority = defaultPriority,
|
||||
labels = labels,
|
||||
customFields = customFields.map((_, None)),
|
||||
isManageable = isManageable,
|
||||
repository = repository
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
function checkCustomFieldErrors() {
|
||||
let error = false;
|
||||
$('.custom-field-error').each(function(i, e) {
|
||||
if ($(e).text() != '') {
|
||||
error = true;
|
||||
}
|
||||
});
|
||||
return !error;
|
||||
}
|
||||
</script>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
@(issue: gitbucket.core.model.Issue,
|
||||
comments: List[gitbucket.core.model.IssueComment],
|
||||
issueLabels: List[gitbucket.core.model.Label],
|
||||
issueAssignees: List[gitbucket.core.model.IssueAssignee],
|
||||
collaborators: List[String],
|
||||
milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
|
||||
priorities: List[gitbucket.core.model.Priority],
|
||||
labels: List[gitbucket.core.model.Label],
|
||||
customFields: List[(gitbucket.core.model.CustomField, Option[gitbucket.core.model.IssueCustomField])],
|
||||
isEditable: Boolean,
|
||||
isManageable: Boolean,
|
||||
isCommentManageable: Boolean,
|
||||
@@ -56,7 +58,20 @@
|
||||
@gitbucket.core.issues.html.commentform(issue, true, isEditable, isManageable, repository)
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
@gitbucket.core.issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, priorities, None, labels, isManageable, repository)
|
||||
@gitbucket.core.issues.html.issueinfo(
|
||||
issue = Some(issue),
|
||||
comments = comments,
|
||||
issueLabels = issueLabels,
|
||||
issueAssignees = issueAssignees,
|
||||
collaborators = collaborators,
|
||||
milestones = milestones,
|
||||
priorities = priorities,
|
||||
defaultPriority = None,
|
||||
labels = labels,
|
||||
customFields = customFields,
|
||||
isManageable = isManageable,
|
||||
repository = repository
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
@import org.json4s.scalap.scalasig.ClassFileParser.field
|
||||
@(issue: Option[gitbucket.core.model.Issue],
|
||||
comments: List[gitbucket.core.model.Comment],
|
||||
issueLabels: List[gitbucket.core.model.Label],
|
||||
issueAssignees: List[gitbucket.core.model.IssueAssignee],
|
||||
collaborators: List[String],
|
||||
milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
|
||||
priorities: List[gitbucket.core.model.Priority],
|
||||
defaultPriority: Option[gitbucket.core.model.Priority],
|
||||
labels: List[gitbucket.core.model.Label],
|
||||
customFields: List[(gitbucket.core.model.CustomField, Option[gitbucket.core.model.IssueCustomField])],
|
||||
isManageable: Boolean,
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@@ -125,11 +128,10 @@
|
||||
@if(isManageable){
|
||||
<div class="pull-right">
|
||||
@gitbucket.core.helper.html.dropdown("Edit", right = true, filter = ("assignee", "Filter Assignee")) {
|
||||
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="octicon octicon-x"></i> Clear assignee</a></li>
|
||||
@collaborators.map { collaborator =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
|
||||
@gitbucket.core.helper.html.checkicon(issue.exists(_.assignedUserName.exists(_ == collaborator)))@helpers.avatar(collaborator, 20) @collaborator
|
||||
<a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator">
|
||||
@gitbucket.core.helper.html.checkicon(issueAssignees.exists(_.assigneeUserName == collaborator))@helpers.avatar(collaborator, 20) @collaborator
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
@@ -138,15 +140,35 @@
|
||||
}
|
||||
</div>
|
||||
<span id="label-assigned">
|
||||
@issue.flatMap(_.assignedUserName).map { userName =>
|
||||
@helpers.avatarLink(userName, 20) @helpers.user(userName, styleClass="username strong small")
|
||||
}.getOrElse{
|
||||
<span class="muted small">No one</span>
|
||||
@issueAssignees.map { assignee =>
|
||||
<div>@helpers.avatarLink(assignee.assigneeUserName, 20) @helpers.user(assignee.assigneeUserName, styleClass="username strong small")</div>
|
||||
}
|
||||
@if(issueAssignees.isEmpty) {
|
||||
<span class="muted small">No one assigned</span>
|
||||
}
|
||||
</span>
|
||||
@if(issue.isEmpty){
|
||||
<input type="hidden" name="assignedUserName" value=""/>
|
||||
<input type="hidden" name="assigneeUserNames" value=""/>
|
||||
}
|
||||
|
||||
@customFields.map { case (field, value) =>
|
||||
<hr/>
|
||||
<div style="margin-bottom: 14px;">
|
||||
<span class="muted small strong">@field.fieldName</span>
|
||||
<div class="pull-right">
|
||||
@gitbucket.core.model.CustomFieldBehavior(field.fieldType).map { behavior =>
|
||||
@if(issue.nonEmpty) {
|
||||
@Html(behavior.fieldHtml(repository, issue.get.issueId, field.fieldId, field.fieldName, field.constraints, value.map(_.value).getOrElse(""), isManageable))
|
||||
}
|
||||
@if(issue.isEmpty) {
|
||||
@Html(behavior.createHtml(repository, field.fieldId, field.fieldName, field.constraints))
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<span id="custom-field-@field.fieldId-error" class="error custom-field-error"></span>
|
||||
}
|
||||
|
||||
@issue.map { issue =>
|
||||
@gitbucket.core.plugin.PluginRegistry().getIssueSidebars.map { sidebarComponent =>
|
||||
@sidebarComponent(issue, repository, context)
|
||||
@@ -167,7 +189,7 @@
|
||||
$(function(){
|
||||
@issue.map { issue =>
|
||||
$('a.toggle-label').click(function(){
|
||||
var path = switchLabel($(this));
|
||||
const path = switchToggleOptions($(this));
|
||||
$.post('@helpers.url(repository)/issues/@issue.issueId/label/' + path,
|
||||
{ labelId : $(this).data('label-id') },
|
||||
function(data){
|
||||
@@ -178,8 +200,8 @@ $(function(){
|
||||
});
|
||||
|
||||
$('a.milestone').click(function(){
|
||||
var title = $(this).data('title');
|
||||
var milestoneId = $(this).data('id');
|
||||
const title = $(this).data('title');
|
||||
const milestoneId = $(this).data('id');
|
||||
$.post('@helpers.url(repository)/issues/@issue.issueId/milestone',
|
||||
{ milestoneId: milestoneId },
|
||||
function(data){
|
||||
@@ -189,11 +211,11 @@ $(function(){
|
||||
});
|
||||
|
||||
$('a.priority').click(function(){
|
||||
var priorityName = $(this).data('name');
|
||||
var priorityId = $(this).data('id');
|
||||
var description = $(this).attr('title');
|
||||
var color = $(this).data('color');
|
||||
var fontColor = $(this).data('font-color');
|
||||
const priorityName = $(this).data('name');
|
||||
const priorityId = $(this).data('id');
|
||||
const description = $(this).attr('title');
|
||||
const color = $(this).data('color');
|
||||
const fontColor = $(this).data('font-color');
|
||||
$.post('@helpers.url(repository)/issues/@issue.issueId/priority',
|
||||
{ priorityId: priorityId },
|
||||
function(data){
|
||||
@@ -202,20 +224,26 @@ $(function(){
|
||||
);
|
||||
});
|
||||
|
||||
$('a.assign').click(function(){
|
||||
var $this = $(this);
|
||||
var userName = $this.data('name');
|
||||
$.post('@helpers.url(repository)/issues/@issue.issueId/assign',
|
||||
{ assignedUserName: userName },
|
||||
function(){
|
||||
displayAssignee($this, userName);
|
||||
$('a.toggle-assign').click(function(){
|
||||
const path = switchToggleOptions($(this));
|
||||
$.post('@helpers.url(repository)/issues/@issue.issueId/assignee/' + path,
|
||||
{ assigneeUserName : $(this).data('name') },
|
||||
function(data){
|
||||
const assignees = Array();
|
||||
$('a.toggle-assign').each(function(i, e){
|
||||
if($(e).children('i').hasClass('octicon-check') == true){
|
||||
assignees.push($(e).text().trim());
|
||||
}
|
||||
});
|
||||
displayAssignee(assignees);
|
||||
}
|
||||
);
|
||||
return false;
|
||||
});
|
||||
}.getOrElse {
|
||||
$('a.toggle-label').click(function(){
|
||||
switchLabel($(this));
|
||||
var labelNames = Array();
|
||||
switchToggleOptions($(this));
|
||||
const labelNames = Array();
|
||||
$('a.toggle-label').each(function(i, e){
|
||||
if($(e).children('i').hasClass('octicon-check') == true){
|
||||
labelNames.push($(e).text().trim());
|
||||
@@ -232,32 +260,37 @@ $(function(){
|
||||
});
|
||||
|
||||
$('a.milestone').click(function(){
|
||||
var title = $(this).data('title');
|
||||
var milestoneId = $(this).data('id');
|
||||
const title = $(this).data('title');
|
||||
const milestoneId = $(this).data('id');
|
||||
displayMilestone(title, milestoneId);
|
||||
$('input[name=milestoneId]').val(milestoneId);
|
||||
});
|
||||
|
||||
$('a.priority').click(function(){
|
||||
var priorityName = $(this).data('name');
|
||||
var priorityId = $(this).data('id');
|
||||
var description = $(this).attr('title');
|
||||
var color = $(this).data('color');
|
||||
var fontColor = $(this).data('font-color');
|
||||
const priorityName = $(this).data('name');
|
||||
const priorityId = $(this).data('id');
|
||||
const description = $(this).attr('title');
|
||||
const color = $(this).data('color');
|
||||
const fontColor = $(this).data('font-color');
|
||||
displayPriority(priorityName, priorityId, description, color, fontColor);
|
||||
$('input[name=priorityId]').val(priorityId);
|
||||
});
|
||||
|
||||
$('a.assign').click(function(){
|
||||
var $this = $(this);
|
||||
var userName = $this.data('name');
|
||||
displayAssignee($this, userName);
|
||||
$('input[name=assignedUserName]').val(userName);
|
||||
$('a.toggle-assign').click(function(){
|
||||
switchToggleOptions($(this));
|
||||
const assignees = Array();
|
||||
$('a.toggle-assign').each(function(i, e){
|
||||
if($(e).children('i').hasClass('octicon-check') == true){
|
||||
assignees.push($(e).text().trim());
|
||||
}
|
||||
});
|
||||
$('input[name=assigneeUserNames]').val(assignees.join(','));
|
||||
displayAssignee(assignees);
|
||||
});
|
||||
}
|
||||
|
||||
function switchLabel($this){
|
||||
var i = $this.children('i');
|
||||
function switchToggleOptions($this){
|
||||
const i = $this.children('i');
|
||||
if(i.hasClass('octicon-check')){
|
||||
i.removeClass('octicon-check');
|
||||
return 'delete';
|
||||
@@ -299,15 +332,18 @@ $(function(){
|
||||
}
|
||||
}
|
||||
|
||||
function displayAssignee($this, userName){
|
||||
function displayAssignee(assignees){
|
||||
$('a.assign i.octicon-check').removeClass('octicon-check');
|
||||
if(userName == ''){
|
||||
$('#label-assigned').html($('<span class="muted small">').text('No one'));
|
||||
if(assignees.length == 0){
|
||||
$('#label-assigned').html($('<span class="muted small">').text('No one assigned'));
|
||||
} else {
|
||||
$('#label-assigned').empty()
|
||||
.append($this.find('img.avatar-mini').clone(false)).append(' ')
|
||||
.append($('<a class="username strong small">').attr('href', '@context.path/' + userName).text(userName));
|
||||
$('a.assign[data-name=' + jqSelectorEscape(userName.toString()) + '] i').addClass('octicon-check');
|
||||
$('#label-assigned').empty();
|
||||
for (const userName of assignees) {
|
||||
$('#label-assigned').append($('<div>').append(
|
||||
$('a.toggle-assign').parent().find("img.avatar-mini[alt='@@" + userName + "']").clone(false),
|
||||
' ',
|
||||
$('<a class="username strong small">').attr('href', '@context.path/' + userName).text(userName)));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -206,7 +206,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId), status) =>
|
||||
@issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId, assignedUserNames), status) =>
|
||||
<tr>
|
||||
<td style="padding-top: 12px; padding-bottom: 12px;">
|
||||
@if(isManageable){
|
||||
@@ -230,7 +230,7 @@
|
||||
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
|
||||
}
|
||||
<span class="pull-right small">
|
||||
@issue.assignedUserName.map { userName =>
|
||||
@assignedUserNames.map { userName =>
|
||||
@helpers.avatar(userName, 20, tooltip = true)
|
||||
}
|
||||
@if(commentCount > 0){
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId), status) =>
|
||||
@issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId, assignedUserNames), status) =>
|
||||
<tr>
|
||||
<td style="padding-top: 12px; padding-bottom: 12px;">
|
||||
@if(isManageable){
|
||||
@@ -90,7 +90,7 @@
|
||||
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
|
||||
}
|
||||
<span class="pull-right small">
|
||||
@issue.assignedUserName.map { userName =>
|
||||
@assignedUserNames.map { userName =>
|
||||
@helpers.avatar(userName, 20, tooltip = true)
|
||||
}
|
||||
@if(commentCount > 0){
|
||||
|
||||
@@ -73,27 +73,27 @@
|
||||
</a>
|
||||
<nav class="navbar navbar-static-top" role="navigation">
|
||||
<!-- 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" title="Toggle navigation">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
</a>
|
||||
}
|
||||
<form id="search" action="@context.path/search" method="GET" class="pc navbar-form navbar-left" role="search">
|
||||
<div class="form-group">
|
||||
<input type="text" name="query" id="navbar-search-input" class="form-control" placeholder="Find a repository" aria-label="Search"/>
|
||||
</div>
|
||||
</form>
|
||||
<ul class="pc nav navbar-nav">
|
||||
@if(context.loginAccount.isDefined){
|
||||
@if(!(context.loginAccount.isEmpty && !context.settings.basicBehavior.allowAnonymousAccess)) {
|
||||
<form id="search" action="@context.path/search" method="GET" class="pc navbar-form navbar-left" role="search">
|
||||
<div class="form-group">
|
||||
<input type="text" name="query" id="navbar-search-input" class="form-control" placeholder="Find a repository" aria-label="Search"/>
|
||||
</div>
|
||||
</form>
|
||||
<ul class="pc nav navbar-nav">
|
||||
<li><a href="@context.path/dashboard/pulls">Pull requests</a></li>
|
||||
<li><a href="@context.path/dashboard/issues">Issues</a></li>
|
||||
}
|
||||
@gitbucket.core.plugin.PluginRegistry().getGlobalMenus.map { menu =>
|
||||
@menu(context).map { link =>
|
||||
<li><a href="@context.path/@link.path">@link.label</a></li>
|
||||
@gitbucket.core.plugin.PluginRegistry().getGlobalMenus.map { menu =>
|
||||
@menu(context).map { link =>
|
||||
<li><a href="@context.path/@link.path">@link.label</a></li>
|
||||
}
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
</ul>
|
||||
}
|
||||
<div class="navbar-custom-menu">
|
||||
<ul class="nav navbar-nav">
|
||||
@if(context.loginAccount.isDefined){
|
||||
@@ -104,7 +104,7 @@
|
||||
<ul class="dropdown-menu pull-right" style="width: auto;">
|
||||
<li>
|
||||
<ul class="menu">
|
||||
@if(context.settings.repositoryOperation.create || context.loginAccount.get.isAdmin){
|
||||
@if(context.settings.basicBehavior.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>
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
@gitbucket.core.plugin.PluginRegistry().getRepositoryHeaders.map { repositoryHeaderComponent =>
|
||||
@repositoryHeaderComponent(repository, context)
|
||||
}
|
||||
@if(repository.repository.options.allowFork && (context.settings.repositoryOperation.fork || context.loginAccount.map(_.isAdmin).getOrElse(false))) {
|
||||
@if(repository.repository.options.allowFork && (context.settings.basicBehavior.repositoryOperation.fork || context.loginAccount.map(_.isAdmin).getOrElse(false))) {
|
||||
@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}")">
|
||||
<span class="strong"><i class="octicon octicon-repo-forked"></i>Fork</span><span class="muted">: @repository.forkedCount</span>
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
milestones: List[gitbucket.core.model.Milestone],
|
||||
priorities: List[gitbucket.core.model.Priority],
|
||||
defaultPriority: Option[gitbucket.core.model.Priority],
|
||||
labels: List[gitbucket.core.model.Label])(implicit context: gitbucket.core.controller.Context)
|
||||
labels: List[gitbucket.core.model.Label],
|
||||
customFields: List[gitbucket.core.model.CustomField])(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main(s"Pull requests - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@gitbucket.core.html.menu("pulls", repository){
|
||||
@@ -101,7 +102,20 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
@gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map((_, 0, 0)), priorities, defaultPriority, labels, hasOriginWritePermission, repository)
|
||||
@gitbucket.core.issues.html.issueinfo(
|
||||
issue = None,
|
||||
comments = Nil,
|
||||
issueLabels = Nil,
|
||||
issueAssignees = Nil,
|
||||
collaborators = collaborators,
|
||||
milestones = milestones.map((_, 0, 0)),
|
||||
priorities = priorities,
|
||||
defaultPriority = defaultPriority,
|
||||
labels = labels,
|
||||
customFields = customFields.map((_, None)),
|
||||
isManageable = hasOriginWritePermission,
|
||||
repository = repository
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
comments: Seq[gitbucket.core.model.Comment],
|
||||
changedFileSize: Int,
|
||||
issueLabels: List[gitbucket.core.model.Label],
|
||||
issueAssignees: List[gitbucket.core.model.IssueAssignee],
|
||||
collaborators: List[String],
|
||||
milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
|
||||
priorities: List[gitbucket.core.model.Priority],
|
||||
labels: List[gitbucket.core.model.Label],
|
||||
customFields: List[(gitbucket.core.model.CustomField, Option[gitbucket.core.model.IssueCustomField])],
|
||||
isEditable: Boolean,
|
||||
isManageable: Boolean,
|
||||
isManageableForkedRepository: Boolean,
|
||||
@@ -50,7 +52,20 @@
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
@gitbucket.core.issues.html.issueinfo(Some(issue), comments.toList, issueLabels, collaborators, milestones, priorities, None, labels, isManageable, repository)
|
||||
@gitbucket.core.issues.html.issueinfo(
|
||||
Some(issue),
|
||||
comments.toList,
|
||||
issueLabels,
|
||||
issueAssignees,
|
||||
collaborators,
|
||||
milestones,
|
||||
priorities,
|
||||
None,
|
||||
labels,
|
||||
customFields,
|
||||
isManageable,
|
||||
repository
|
||||
)
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
|
||||
@@ -55,10 +55,10 @@
|
||||
});
|
||||
|
||||
@if(newLineNumber.isDefined){
|
||||
var diff = getDiffData($('table[filename="@fileName"] table.diff tr:has(th.line-num.newline[line-number=@newLineNumber])'));
|
||||
var diff = getDiffData($('table[filename="@fileName"] table.diff tr:has(th.line-num.newline[line-number="@newLineNumber"])'));
|
||||
param['diff'] = JSON.stringify(diff);
|
||||
} else if(oldLineNumber.isDefined){
|
||||
var diff = getDiffData($('table[filename="@fileName"] table.diff tr:has(th.line-num.oldline[line-number=@oldLineNumber])'));
|
||||
var diff = getDiffData($('table[filename="@fileName"] table.diff tr:has(th.line-num.oldline[line-number="@oldLineNumber"])'));
|
||||
param['diff'] = JSON.stringify(diff);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
@if(context.settings.repositoryOperation.rename || context.loginAccount.get.isAdmin){
|
||||
@if(context.settings.basicBehavior.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>
|
||||
@@ -33,7 +33,7 @@
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
@if(context.settings.repositoryOperation.transfer || context.loginAccount.get.isAdmin){
|
||||
@if(context.settings.basicBehavior.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>
|
||||
@@ -50,7 +50,7 @@
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
@if(context.settings.repositoryOperation.delete || context.loginAccount.get.isAdmin){
|
||||
@if(context.settings.basicBehavior.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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user