mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-09 04:26:32 +02:00
Compare commits
282 Commits
optimize-c
...
4.39.0
| 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 | ||
|
|
b3ca4468cd | ||
|
|
452e41603a | ||
|
|
b41de4a770 | ||
|
|
3423d2f2f3 | ||
|
|
9269acc64d | ||
|
|
18b5f20f1c | ||
|
|
68d592a8ff | ||
|
|
ee18d96f98 | ||
|
|
6d3687cbdf | ||
|
|
1ef1a2983b | ||
|
|
47850fe0fa | ||
|
|
055a37cac0 | ||
|
|
333deb4756 | ||
|
|
34cde4b09a | ||
|
|
5091b4838c | ||
|
|
2e83289061 | ||
|
|
e11f651002 | ||
|
|
be684a567d | ||
|
|
97e552b10f | ||
|
|
5bb8046c32 | ||
|
|
e7192655f7 | ||
|
|
19ba09740c | ||
|
|
d169777722 | ||
|
|
ff8a5f6b77 | ||
|
|
ec953df156 | ||
|
|
d6a191d95b | ||
|
|
aba428bba1 | ||
|
|
6ab37fd596 | ||
|
|
73fc70f55b | ||
|
|
aad18b7a50 | ||
|
|
cc278be5cd | ||
|
|
d0f4f82a0f | ||
|
|
1dcbf386b1 | ||
|
|
414afd285c | ||
|
|
35b645d8b5 | ||
|
|
b3cba53866 | ||
|
|
a4773bb3ca | ||
|
|
863d8a4af5 | ||
|
|
3fccd7b53c | ||
|
|
dd2760eaf7 | ||
|
|
824bafa739 | ||
|
|
60cdaec05f | ||
|
|
c204a435b3 | ||
|
|
37accd92d6 | ||
|
|
01fd0ee1f0 | ||
|
|
fab1c74473 | ||
|
|
0d8fcfd28d | ||
|
|
b91a7c32a6 | ||
|
|
e7a6f0930b |
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -10,9 +10,9 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
java: [8, 11, 17]
|
java: [8, 11, 17]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Cache
|
- name: Cache
|
||||||
uses: actions/cache@v2.1.6
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: cache-sbt-libs
|
cache-name: cache-sbt-libs
|
||||||
with:
|
with:
|
||||||
@@ -22,18 +22,18 @@ jobs:
|
|||||||
~/.cache/coursier/v1
|
~/.cache/coursier/v1
|
||||||
key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }}
|
key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }}
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: ${{ matrix.java }}
|
java-version: ${{ matrix.java }}
|
||||||
distribution: adopt
|
distribution: adopt
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: sbt scalafmtSbtCheck scalafmtCheck test:scalafmtCheck test
|
run: sbt scalafmtSbtCheck scalafmtCheck test:scalafmtCheck test
|
||||||
- name: Scala 3
|
- name: Scala 3
|
||||||
run: sbt '++ 3.0.1!' update # TODO
|
run: sbt '++ 3.1.2!' update # TODO
|
||||||
- name: Build executable
|
- name: Build executable
|
||||||
run: sbt executable
|
run: sbt executable
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: gitbucket-java${{ matrix.java }}-${{ github.sha }}
|
name: gitbucket-java${{ matrix.java }}-${{ github.sha }}
|
||||||
path: ./target/executable/gitbucket.*
|
path: ./target/executable/gitbucket.*
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,6 +2,9 @@
|
|||||||
*.log
|
*.log
|
||||||
.ensime
|
.ensime
|
||||||
.ensime_cache
|
.ensime_cache
|
||||||
|
.DS_Store
|
||||||
|
.java-version
|
||||||
|
.tmp
|
||||||
|
|
||||||
# sbt specific
|
# sbt specific
|
||||||
dist/*
|
dist/*
|
||||||
|
|||||||
@@ -3,5 +3,9 @@ updates.limit = 3
|
|||||||
updates.includeScala = true
|
updates.includeScala = true
|
||||||
|
|
||||||
updates.pin = [
|
updates.pin = [
|
||||||
|
{ groupId = "org.mockito", version = "4." }
|
||||||
{ groupId = "org.eclipse.jetty", version = "9." }
|
{ groupId = "org.eclipse.jetty", version = "9." }
|
||||||
|
{ groupId = "org.eclipse.jgit", version = "5." }
|
||||||
|
{ groupId = "com.zaxxer", version = "4." }
|
||||||
|
{ groupId = "org.mariadb.jdbc", version = "2." }
|
||||||
]
|
]
|
||||||
|
|||||||
62
CHANGELOG.md
62
CHANGELOG.md
@@ -1,32 +1,80 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
All changes to the project will be documented in this file.
|
All changes to the project will be documented in this file.
|
||||||
|
|
||||||
### 4.36.2 - 16 Aug 2021
|
## 4.39.0 - 29 Apr 2023
|
||||||
|
- Support enum type in custom fields of Issues and Pull requests
|
||||||
|
- Hide large diffs by default
|
||||||
|
- Add new options to make it possible to run GitBucket using multiple machines
|
||||||
|
- Fix many API issues
|
||||||
|
|
||||||
|
## 4.38.4 - 2 Nov 2022
|
||||||
|
- Downgrade MariaDB JDBC drive to avoid unknown error
|
||||||
|
|
||||||
|
## 4.38.3 - 30 Oct 2022
|
||||||
|
- Fix several issues around multiple assignees in issues and pull requests
|
||||||
|
- Fix IllegalStateException when returning unknown avatar image
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- 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.36.2 - 16 Aug 2021
|
||||||
- Escape user name in avatar image tag
|
- 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
|
- 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
|
- Tag selector in the repository viewer
|
||||||
- Link issues/pull requests of other repositories
|
- Link issues/pull requests of other repositories
|
||||||
- Files and lines can be linked in the diff view
|
- Files and lines can be linked in the diff view
|
||||||
- Option to disable XSS protection
|
- 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 bug that Wiki page cannot be deleted
|
||||||
- Fix a deployment issue on Tomcat
|
- 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 gitbucket-notifications-plugin to 1.10.0
|
||||||
- Upgrade oauth2-oidc-sdk to 8.29.1 to solve dependency issue
|
- 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
|
- Fix database migration issue which happens if webhook is configured
|
||||||
- Call push webhook when pull request is merged
|
- Call push webhook when pull request is merged
|
||||||
- Show commit status at commits tab of pull request
|
- 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
|
- Editor and source viewer color theme
|
||||||
- Auto completion for issues and pull requests
|
- Auto completion for issues and pull requests
|
||||||
- Upload image from clipboard
|
- Upload image from clipboard
|
||||||
|
|||||||
24
README.md
24
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:
|
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
|
Features
|
||||||
--------
|
--------
|
||||||
The current version of GitBucket provides many features such as:
|
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`.
|
1. Download the latest **gitbucket.war** from [the releases page](https://github.com/gitbucket/gitbucket/releases) and run it by `java -jar gitbucket.war`.
|
||||||
2. Go to `http://[hostname]:8080/` and log in with ID: **root** / Pass: **root**.
|
2. Go to `http://[hostname]:8080/` and log in with ID: **root** / Pass: **root**.
|
||||||
|
|
||||||
You can 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).
|
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,18 +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.
|
- If you can't find same question and report, send it to our [Gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
|
||||||
- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles.
|
- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles.
|
||||||
|
|
||||||
What's New in 4.36.x
|
What's New in 4.39.x
|
||||||
-------------
|
-------------
|
||||||
### 4.36.2 - 16 Aug 2021
|
## 4.39.0 - 29 Apr 2023
|
||||||
- Escape user name in avatar image tag
|
- Support enum type in custom fields of Issues and Pull requests
|
||||||
|
- Hide large diffs by default
|
||||||
### 4.36.1 - 22 Jul 2021
|
- Add new options to make it possible to run GitBucket using multiple machines
|
||||||
- Bump gitbucket-gist-plugin to 4.21.0
|
- Fix many API issues
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
See the [change log](CHANGELOG.md) for all of the updates.
|
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 Organization = "io.github.gitbucket"
|
||||||
val Name = "gitbucket"
|
val Name = "gitbucket"
|
||||||
val GitBucketVersion = "4.36.2"
|
val GitBucketVersion = "4.39.0"
|
||||||
val ScalatraVersion = "2.8.2"
|
val ScalatraVersion = "2.8.4"
|
||||||
val JettyVersion = "9.4.44.v20210927"
|
val JettyVersion = "9.4.51.v20230217"
|
||||||
val JgitVersion = "5.13.0.202109080827-r"
|
val JgitVersion = "5.13.1.202206130422-r"
|
||||||
|
|
||||||
lazy val root = (project in file("."))
|
lazy val root = (project in file("."))
|
||||||
.enablePlugins(SbtTwirl, ScalatraPlugin)
|
.enablePlugins(SbtTwirl, ScalatraPlugin)
|
||||||
@@ -15,16 +15,15 @@ sourcesInBase := false
|
|||||||
organization := Organization
|
organization := Organization
|
||||||
name := Name
|
name := Name
|
||||||
version := GitBucketVersion
|
version := GitBucketVersion
|
||||||
scalaVersion := "2.13.7"
|
scalaVersion := "2.13.10"
|
||||||
|
|
||||||
scalafmtOnCompile := true
|
// scalafmtOnCompile := true
|
||||||
|
|
||||||
coverageExcludedPackages := ".*\\.html\\..*"
|
coverageExcludedPackages := ".*\\.html\\..*"
|
||||||
|
|
||||||
// dependency settings
|
// dependency settings
|
||||||
resolvers ++= Seq(
|
resolvers ++= Seq(
|
||||||
Classpaths.typesafeReleases,
|
Classpaths.typesafeReleases,
|
||||||
Resolver.jcenterRepo,
|
|
||||||
"sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/"
|
"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" % ScalatraVersion cross CrossVersion.for3Use2_13,
|
||||||
"org.scalatra" %% "scalatra-json" % ScalatraVersion cross CrossVersion.for3Use2_13,
|
"org.scalatra" %% "scalatra-json" % ScalatraVersion cross CrossVersion.for3Use2_13,
|
||||||
"org.scalatra" %% "scalatra-forms" % 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",
|
"commons-io" % "commons-io" % "2.11.0",
|
||||||
"io.github.gitbucket" % "solidbase" % "1.0.3",
|
"io.github.gitbucket" % "solidbase" % "1.0.5",
|
||||||
"io.github.gitbucket" % "markedj" % "1.0.16",
|
"io.github.gitbucket" % "markedj" % "1.0.17",
|
||||||
"org.apache.commons" % "commons-compress" % "1.21",
|
"org.apache.commons" % "commons-compress" % "1.23.0",
|
||||||
"org.apache.commons" % "commons-email" % "1.5",
|
"org.apache.commons" % "commons-email" % "1.5",
|
||||||
"commons-net" % "commons-net" % "3.8.0",
|
"commons-net" % "commons-net" % "3.9.0",
|
||||||
"org.apache.httpcomponents" % "httpclient" % "4.5.13",
|
"org.apache.httpcomponents" % "httpclient" % "4.5.14",
|
||||||
"org.apache.sshd" % "apache-sshd" % "2.1.0" exclude ("org.slf4j", "slf4j-jdk14") exclude ("org.apache.sshd", "sshd-mina") exclude ("org.apache.sshd", "sshd-netty"),
|
"org.apache.sshd" % "apache-sshd" % "2.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.1.0",
|
"org.apache.tika" % "tika-core" % "2.7.0",
|
||||||
"com.github.takezoe" %% "blocking-slick-32" % "0.0.12" cross CrossVersion.for3Use2_13,
|
"com.github.takezoe" %% "blocking-slick-32" % "0.0.12" cross CrossVersion.for3Use2_13,
|
||||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||||
"com.h2database" % "h2" % "1.4.199",
|
"com.h2database" % "h2" % "1.4.199",
|
||||||
"org.mariadb.jdbc" % "mariadb-java-client" % "2.7.4",
|
"org.mariadb.jdbc" % "mariadb-java-client" % "2.7.6",
|
||||||
"org.postgresql" % "postgresql" % "42.3.1",
|
"org.postgresql" % "postgresql" % "42.6.0",
|
||||||
"ch.qos.logback" % "logback-classic" % "1.2.6",
|
"ch.qos.logback" % "logback-classic" % "1.3.7",
|
||||||
"com.zaxxer" % "HikariCP" % "4.0.3" exclude ("org.slf4j", "slf4j-api"),
|
"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",
|
"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",
|
"org.cache2k" % "cache2k-all" % "1.6.0.Final",
|
||||||
"net.coobird" % "thumbnailator" % "0.4.14",
|
"net.coobird" % "thumbnailator" % "0.4.19",
|
||||||
"com.github.zafarkhaja" % "java-semver" % "0.9.0",
|
"com.github.zafarkhaja" % "java-semver" % "0.9.0",
|
||||||
"com.nimbusds" % "oauth2-oidc-sdk" % "9.19",
|
"com.nimbusds" % "oauth2-oidc-sdk" % "10.8",
|
||||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
|
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
|
||||||
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
|
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
|
||||||
"junit" % "junit" % "4.13.2" % "test",
|
"junit" % "junit" % "4.13.2" % "test",
|
||||||
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test" cross CrossVersion.for3Use2_13,
|
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test" cross CrossVersion.for3Use2_13,
|
||||||
"org.mockito" % "mockito-core" % "4.0.0" % "test",
|
"org.mockito" % "mockito-core" % "4.11.0" % "test",
|
||||||
"com.dimafeng" %% "testcontainers-scala" % "0.39.11" % "test",
|
"com.dimafeng" %% "testcontainers-scala" % "0.40.15" % "test",
|
||||||
"org.testcontainers" % "mysql" % "1.16.2" % "test",
|
"org.testcontainers" % "mysql" % "1.18.0" % "test",
|
||||||
"org.testcontainers" % "postgresql" % "1.16.2" % "test",
|
"org.testcontainers" % "postgresql" % "1.18.0" % "test",
|
||||||
"net.i2p.crypto" % "eddsa" % "0.3.0",
|
"net.i2p.crypto" % "eddsa" % "0.3.0",
|
||||||
"is.tagomor.woothee" % "woothee-java" % "1.11.0",
|
"is.tagomor.woothee" % "woothee-java" % "1.11.0",
|
||||||
"org.ec4j.core" % "ec4j-core" % "0.3.0",
|
"org.ec4j.core" % "ec4j-core" % "0.3.0",
|
||||||
"org.kohsuke" % "github-api" % "1.135" % "test"
|
"org.kohsuke" % "github-api" % "1.314" % "test"
|
||||||
)
|
)
|
||||||
|
|
||||||
libraryDependencies ~= {
|
libraryDependencies ~= {
|
||||||
@@ -91,7 +90,6 @@ scalacOptions := Seq(
|
|||||||
"-Wconf:cat=unused&src=twirl/.*:s,cat=unused&src=scala/gitbucket/core/model/[^/]+\\.scala:s"
|
"-Wconf:cat=unused&src=twirl/.*:s,cat=unused&src=scala/gitbucket/core/model/[^/]+\\.scala:s"
|
||||||
)
|
)
|
||||||
compile / javacOptions ++= Seq("-target", "8", "-source", "8")
|
compile / javacOptions ++= Seq("-target", "8", "-source", "8")
|
||||||
Jetty / javaOptions += "-Dlogback.configurationFile=/logback-dev.xml"
|
|
||||||
|
|
||||||
// Test settings
|
// Test settings
|
||||||
//testOptions in Test += Tests.Argument("-l", "ExternalDBTest")
|
//testOptions in Test += Tests.Argument("-l", "ExternalDBTest")
|
||||||
@@ -287,6 +285,9 @@ Test / testOptions ++= {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Jetty / javaOptions ++= Seq(
|
Jetty / javaOptions ++= Seq(
|
||||||
|
"-Dlogback.configurationFile=/logback-dev.xml",
|
||||||
"-Xdebug",
|
"-Xdebug",
|
||||||
"-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000"
|
"-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000",
|
||||||
|
"-Dorg.eclipse.jetty.annotations.AnnotationParser.LEVEL=OFF",
|
||||||
|
//"-Ddev-features=keep-session"
|
||||||
)
|
)
|
||||||
|
|||||||
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`.
|
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
|
Build war file
|
||||||
--------
|
--------
|
||||||
@@ -37,7 +51,8 @@ To build an executable war file, run
|
|||||||
$ sbt executable
|
$ sbt executable
|
||||||
```
|
```
|
||||||
|
|
||||||
at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. 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
|
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:
|
This directory has following structure:
|
||||||
|
|
||||||
```
|
```
|
||||||
* /HOME/gitbucket
|
* /HOME/.gitbucket
|
||||||
|
* gitbucket.conf
|
||||||
|
* database.conf
|
||||||
|
* activity.log
|
||||||
|
* data.mv.db, data.trace.db (H2 data files if the default embed H2 is used)
|
||||||
* /repositories
|
* /repositories
|
||||||
* /USER_NAME
|
* /USER_NAME
|
||||||
* /REPO_NAME.git (substance of repository. GitServlet sees this directory)
|
* /REPO_NAME.git (substance of repository. GitServlet sees this directory)
|
||||||
* /REPO_NAME.wiki.git (wiki repository)
|
* /REPO_NAME.wiki.git (wiki repository)
|
||||||
* /REPO_NAME
|
* /REPO_NAME
|
||||||
* /issues (files which are attached to issue)
|
* /issues (files attached to issue)
|
||||||
* /lfs (LFS managed files)
|
* /lfs (LFS managed files)
|
||||||
* /data
|
* /data
|
||||||
* /USER_NAME
|
* /USER_NAME
|
||||||
@@ -20,6 +24,8 @@ This directory has following structure:
|
|||||||
* /plugins
|
* /plugins
|
||||||
* plugin.jar
|
* plugin.jar
|
||||||
* /.installed (copied available plugins from the parent directory automatically)
|
* /.installed (copied available plugins from the parent directory automatically)
|
||||||
|
* /sessions
|
||||||
|
* HTTP session data created (if '--save_sessions' option is used in the standalone mode)
|
||||||
* /tmp
|
* /tmp
|
||||||
* /_upload
|
* /_upload
|
||||||
* /SESSION_ID (removed at session timeout)
|
* /SESSION_ID (removed at session timeout)
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 203 KiB After Width: | Height: | Size: 112 KiB |
@@ -1 +1 @@
|
|||||||
sbt.version=1.5.5
|
sbt.version=1.7.2
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
|
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
|
||||||
|
|
||||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.3")
|
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0")
|
||||||
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.5.1")
|
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("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("com.typesafe.sbt" % "sbt-license-report" % "1.2.0")
|
||||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.2")
|
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.0")
|
||||||
|
|
||||||
addDependencyTreePlugin
|
addDependencyTreePlugin
|
||||||
|
|||||||
@@ -61,12 +61,19 @@ public class JettyLauncher {
|
|||||||
String redirectHttps = getEnvironmentVariable("gitbucket.redirectHttps");
|
String redirectHttps = getEnvironmentVariable("gitbucket.redirectHttps");
|
||||||
String contextPath = getEnvironmentVariable("gitbucket.prefix");
|
String contextPath = getEnvironmentVariable("gitbucket.prefix");
|
||||||
String tmpDirPath = getEnvironmentVariable("gitbucket.tempDir");
|
String tmpDirPath = getEnvironmentVariable("gitbucket.tempDir");
|
||||||
|
String jettyIdleTimeout = getEnvironmentVariable("gitbucket.jettyIdleTimeout");
|
||||||
boolean saveSessions = false;
|
boolean saveSessions = false;
|
||||||
|
|
||||||
for(String arg: args) {
|
for(String arg: args) {
|
||||||
if(arg.equals("--save_sessions")) {
|
if (arg.equals("--save_sessions")) {
|
||||||
saveSessions = true;
|
saveSessions = true;
|
||||||
}
|
}
|
||||||
|
if (arg.equals("--disable_news_feed")) {
|
||||||
|
System.setProperty("gitbucket.disableNewsFeed", "true");
|
||||||
|
}
|
||||||
|
if (arg.equals("--disable_cache")) {
|
||||||
|
System.setProperty("gitbucket.disableCache", "true");
|
||||||
|
}
|
||||||
if(arg.startsWith("--") && arg.contains("=")) {
|
if(arg.startsWith("--") && arg.contains("=")) {
|
||||||
String[] dim = arg.split("=", 2);
|
String[] dim = arg.split("=", 2);
|
||||||
if(dim.length == 2) {
|
if(dim.length == 2) {
|
||||||
@@ -107,6 +114,9 @@ public class JettyLauncher {
|
|||||||
case "--plugin_dir":
|
case "--plugin_dir":
|
||||||
System.setProperty("gitbucket.pluginDir", dim[1]);
|
System.setProperty("gitbucket.pluginDir", dim[1]);
|
||||||
break;
|
break;
|
||||||
|
case "--jetty_idle_timeout":
|
||||||
|
jettyIdleTimeout = dim[1];
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,6 +140,11 @@ public class JettyLauncher {
|
|||||||
if (connectorsSet.contains(Connectors.HTTPS)) {
|
if (connectorsSet.contains(Connectors.HTTPS)) {
|
||||||
httpConfig.setSecurePort(fallback(securePort, Defaults.HTTPS_PORT, Integer::parseInt));
|
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)) {
|
if (connectorsSet.contains(Connectors.HTTP)) {
|
||||||
final ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
|
final ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
notifications:1.10.0
|
notifications:1.11.0
|
||||||
gist:4.21.0
|
gist:4.22.0
|
||||||
emoji:4.6.0
|
emoji:4.6.0
|
||||||
pages:1.10.0
|
pages:1.10.0
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||||
<file>gitbucket.log</file>
|
<file>.tmp/gitbucket.log</file>
|
||||||
<encoder>
|
<encoder>
|
||||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
|
|||||||
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"/>
|
<addPrimaryKey constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_PK" tableName="PROTECTED_BRANCH_REQUIRE_CONTEXT" columnNames="USER_NAME, REPOSITORY_NAME, BRANCH, CONTEXT"/>
|
||||||
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_FK0" baseTableName="PROTECTED_BRANCH_REQUIRE_CONTEXT" baseColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" referencedTableName="PROTECTED_BRANCH" referencedColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" onDelete="CASCADE" onUpdate="CASCADE"/>
|
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_FK0" baseTableName="PROTECTED_BRANCH_REQUIRE_CONTEXT" baseColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" referencedTableName="PROTECTED_BRANCH" referencedColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" onDelete="CASCADE" onUpdate="CASCADE"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- 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>
|
</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="URL" type="varchar(200)" nullable="false"/>
|
||||||
<column name="EVENT" type="varchar(30)" nullable="false"/>
|
<column name="EVENT" type="varchar(30)" nullable="false"/>
|
||||||
</createTable>
|
</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>
|
</changeSet>
|
||||||
|
|||||||
7
src/main/resources/update/gitbucket-core_4.37.xml
Normal file
7
src/main/resources/update/gitbucket-core_4.37.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<changeSet>
|
||||||
|
<dropForeignKeyConstraint constraintName="IDX_WEB_HOOK_EVENT_FK0" baseTableName="WEB_HOOK_EVENT"/>
|
||||||
|
<modifyDataType columnName="URL" newDataType="varchar(400)" tableName="WEB_HOOK_EVENT"/>
|
||||||
|
<modifyDataType columnName="URL" newDataType="varchar(400)" tableName="WEB_HOOK"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_WEB_HOOK_EVENT_FK0" baseTableName="WEB_HOOK_EVENT" baseColumnNames="USER_NAME, REPOSITORY_NAME, URL" referencedTableName="WEB_HOOK" referencedColumnNames="USER_NAME, REPOSITORY_NAME, URL" onDelete="CASCADE" onUpdate="CASCADE"/>
|
||||||
|
</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="ENABLE_WIKI"/>
|
||||||
<dropColumn tableName="REPOSITORY" columnName="ALLOW_WIKI_EDITING"/>
|
<dropColumn tableName="REPOSITORY" columnName="ALLOW_WIKI_EDITING"/>
|
||||||
<dropColumn tableName="REPOSITORY" columnName="ENABLE_ISSUES"/>
|
<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>
|
</changeSet>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import gitbucket.core.model.Activity
|
|||||||
import gitbucket.core.util.Directory.ActivityLog
|
import gitbucket.core.util.Directory.ActivityLog
|
||||||
import gitbucket.core.util.JDBCUtil
|
import gitbucket.core.util.JDBCUtil
|
||||||
import io.github.gitbucket.solidbase.Solidbase
|
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 io.github.gitbucket.solidbase.model.{Module, Version}
|
||||||
import org.json4s.{Formats, NoTypeHints}
|
import org.json4s.{Formats, NoTypeHints}
|
||||||
import org.json4s.jackson.Serialization
|
import org.json4s.jackson.Serialization
|
||||||
@@ -20,11 +20,7 @@ import scala.util.Using
|
|||||||
object GitBucketCoreModule
|
object GitBucketCoreModule
|
||||||
extends Module(
|
extends Module(
|
||||||
"gitbucket-core",
|
"gitbucket-core",
|
||||||
new Version(
|
new Version("4.0.0", new LiquibaseMigration("update/gitbucket-core_4.0.xml")),
|
||||||
"4.0.0",
|
|
||||||
new LiquibaseMigration("update/gitbucket-core_4.0.xml"),
|
|
||||||
new SqlMigration("update/gitbucket-core_4.0.sql")
|
|
||||||
),
|
|
||||||
new Version("4.1.0"),
|
new Version("4.1.0"),
|
||||||
new Version("4.2.0", new LiquibaseMigration("update/gitbucket-core_4.2.xml")),
|
new Version("4.2.0", new LiquibaseMigration("update/gitbucket-core_4.2.xml")),
|
||||||
new Version("4.2.1"),
|
new Version("4.2.1"),
|
||||||
@@ -32,11 +28,7 @@ object GitBucketCoreModule
|
|||||||
new Version("4.4.0"),
|
new Version("4.4.0"),
|
||||||
new Version("4.5.0"),
|
new Version("4.5.0"),
|
||||||
new Version("4.6.0", new LiquibaseMigration("update/gitbucket-core_4.6.xml")),
|
new Version("4.6.0", new LiquibaseMigration("update/gitbucket-core_4.6.xml")),
|
||||||
new Version(
|
new Version("4.7.0", new LiquibaseMigration("update/gitbucket-core_4.7.xml")),
|
||||||
"4.7.0",
|
|
||||||
new LiquibaseMigration("update/gitbucket-core_4.7.xml"),
|
|
||||||
new SqlMigration("update/gitbucket-core_4.7.sql")
|
|
||||||
),
|
|
||||||
new Version("4.7.1"),
|
new Version("4.7.1"),
|
||||||
new Version("4.8"),
|
new Version("4.8"),
|
||||||
new Version("4.9.0", new LiquibaseMigration("update/gitbucket-core_4.9.xml")),
|
new Version("4.9.0", new LiquibaseMigration("update/gitbucket-core_4.9.xml")),
|
||||||
@@ -45,11 +37,7 @@ object GitBucketCoreModule
|
|||||||
new Version("4.12.0"),
|
new Version("4.12.0"),
|
||||||
new Version("4.12.1"),
|
new Version("4.12.1"),
|
||||||
new Version("4.13.0"),
|
new Version("4.13.0"),
|
||||||
new Version(
|
new Version("4.14.0", new LiquibaseMigration("update/gitbucket-core_4.14.xml")),
|
||||||
"4.14.0",
|
|
||||||
new LiquibaseMigration("update/gitbucket-core_4.14.xml"),
|
|
||||||
new SqlMigration("update/gitbucket-core_4.14.sql")
|
|
||||||
),
|
|
||||||
new Version("4.14.1"),
|
new Version("4.14.1"),
|
||||||
new Version("4.15.0"),
|
new Version("4.15.0"),
|
||||||
new Version("4.16.0"),
|
new Version("4.16.0"),
|
||||||
@@ -119,5 +107,14 @@ object GitBucketCoreModule
|
|||||||
new Version("4.35.3"),
|
new Version("4.35.3"),
|
||||||
new Version("4.36.0", new LiquibaseMigration("update/gitbucket-core_4.36.xml")),
|
new Version("4.36.0", new LiquibaseMigration("update/gitbucket-core_4.36.xml")),
|
||||||
new Version("4.36.1"),
|
new Version("4.36.1"),
|
||||||
new Version("4.36.2")
|
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.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(
|
ApiCommits(
|
||||||
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
|
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
|
||||||
sha = commitInfo.id,
|
sha = commitInfo.id,
|
||||||
html_url = ApiPath(s"${repositoryName.fullName}/commit/${commitInfo.id}"),
|
html_url = ApiPath(s"/${repositoryName.fullName}/commit/${commitInfo.id}"),
|
||||||
comment_url = ApiPath(""), // TODO no API for commit comment
|
comment_url = ApiPath(""), // TODO no API for commit comment
|
||||||
commit = Commit(
|
commit = Commit(
|
||||||
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
|
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ case class ApiIssue(
|
|||||||
number: Int,
|
number: Int,
|
||||||
title: String,
|
title: String,
|
||||||
user: ApiUser,
|
user: ApiUser,
|
||||||
assignee: Option[ApiUser],
|
assignees: List[ApiUser],
|
||||||
labels: List[ApiLabel],
|
labels: List[ApiLabel],
|
||||||
state: String,
|
state: String,
|
||||||
created_at: Date,
|
created_at: Date,
|
||||||
@@ -21,7 +21,7 @@ case class ApiIssue(
|
|||||||
milestone: Option[ApiMilestone]
|
milestone: Option[ApiMilestone]
|
||||||
)(repositoryName: RepositoryName, isPullRequest: Boolean) {
|
)(repositoryName: RepositoryName, isPullRequest: Boolean) {
|
||||||
val id = 0 // dummy id
|
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 comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
|
||||||
val html_url = ApiPath(s"/${repositoryName.fullName}/${if (isPullRequest) { "pull" } else { "issues" }}/${number}")
|
val html_url = ApiPath(s"/${repositoryName.fullName}/${if (isPullRequest) { "pull" } else { "issues" }}/${number}")
|
||||||
val pull_request = if (isPullRequest) {
|
val pull_request = if (isPullRequest) {
|
||||||
@@ -43,7 +43,7 @@ object ApiIssue {
|
|||||||
issue: Issue,
|
issue: Issue,
|
||||||
repositoryName: RepositoryName,
|
repositoryName: RepositoryName,
|
||||||
user: ApiUser,
|
user: ApiUser,
|
||||||
assignee: Option[ApiUser],
|
assignees: List[ApiUser],
|
||||||
labels: List[ApiLabel],
|
labels: List[ApiLabel],
|
||||||
milestone: Option[ApiMilestone]
|
milestone: Option[ApiMilestone]
|
||||||
): ApiIssue =
|
): ApiIssue =
|
||||||
@@ -51,7 +51,7 @@ object ApiIssue {
|
|||||||
number = issue.issueId,
|
number = issue.issueId,
|
||||||
title = issue.title,
|
title = issue.title,
|
||||||
user = user,
|
user = user,
|
||||||
assignee = assignee,
|
assignees = assignees,
|
||||||
labels = labels,
|
labels = labels,
|
||||||
milestone = milestone,
|
milestone = milestone,
|
||||||
state = if (issue.closed) { "closed" } else { "open" },
|
state = if (issue.closed) { "closed" } else { "open" },
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ case class ApiPullRequest(
|
|||||||
body: String,
|
body: String,
|
||||||
user: ApiUser,
|
user: ApiUser,
|
||||||
labels: List[ApiLabel],
|
labels: List[ApiLabel],
|
||||||
assignee: Option[ApiUser],
|
assignees: List[ApiUser],
|
||||||
draft: Option[Boolean]
|
draft: Option[Boolean]
|
||||||
) {
|
) {
|
||||||
val id = 0 // dummy id
|
val id = 0 // dummy id
|
||||||
|
val assignee = assignees.headOption
|
||||||
val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
|
val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
|
||||||
//val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff")
|
//val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff")
|
||||||
//val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch")
|
//val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch")
|
||||||
@@ -45,7 +46,7 @@ object ApiPullRequest {
|
|||||||
baseRepo: ApiRepository,
|
baseRepo: ApiRepository,
|
||||||
user: ApiUser,
|
user: ApiUser,
|
||||||
labels: List[ApiLabel],
|
labels: List[ApiLabel],
|
||||||
assignee: Option[ApiUser],
|
assignees: List[ApiUser],
|
||||||
mergedComment: Option[(IssueComment, Account)]
|
mergedComment: Option[(IssueComment, Account)]
|
||||||
): ApiPullRequest =
|
): ApiPullRequest =
|
||||||
ApiPullRequest(
|
ApiPullRequest(
|
||||||
@@ -63,7 +64,7 @@ object ApiPullRequest {
|
|||||||
body = issue.content.getOrElse(""),
|
body = issue.content.getOrElse(""),
|
||||||
user = user,
|
user = user,
|
||||||
labels = labels,
|
labels = labels,
|
||||||
assignee = assignee,
|
assignees = assignees,
|
||||||
draft = Some(pullRequest.isDraft)
|
draft = Some(pullRequest.isDraft)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,55 @@
|
|||||||
package gitbucket.core.api
|
package gitbucket.core.api
|
||||||
|
|
||||||
case class ApiObject(sha: String)
|
import gitbucket.core.util.JGitUtil.TagInfo
|
||||||
|
import gitbucket.core.util.RepositoryName
|
||||||
|
import org.eclipse.jgit.lib.Ref
|
||||||
|
|
||||||
case class ApiRef(ref: String, `object`: ApiObject)
|
case class ApiRefCommit(
|
||||||
|
sha: String,
|
||||||
|
`type`: String,
|
||||||
|
url: ApiPath
|
||||||
|
)
|
||||||
|
|
||||||
|
case class ApiRef(
|
||||||
|
ref: String,
|
||||||
|
node_id: String = "",
|
||||||
|
url: ApiPath,
|
||||||
|
`object`: ApiRefCommit,
|
||||||
|
)
|
||||||
|
|
||||||
|
object ApiRef {
|
||||||
|
|
||||||
|
def fromRef(
|
||||||
|
repositoryName: RepositoryName,
|
||||||
|
ref: Ref
|
||||||
|
): ApiRef =
|
||||||
|
ApiRef(
|
||||||
|
ref = ref.getName,
|
||||||
|
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}/commits/${ref.getObjectId.getName}"),
|
||||||
|
`type` = "commit"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def fromTag(
|
||||||
|
repositoryName: RepositoryName,
|
||||||
|
tagInfo: TagInfo
|
||||||
|
): 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}/commits/${tagInfo.objectId}"),
|
||||||
|
`type` = "commit"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ case class ApiRepository(
|
|||||||
val url = ApiPath(s"/api/v3/repos/${full_name}")
|
val url = ApiPath(s"/api/v3/repos/${full_name}")
|
||||||
val clone_url = ApiPath(s"/git/${full_name}.git")
|
val clone_url = ApiPath(s"/git/${full_name}.git")
|
||||||
val html_url = ApiPath(s"/${full_name}")
|
val html_url = ApiPath(s"/${full_name}")
|
||||||
val ssh_url = Some(SshPath(s":${full_name}.git"))
|
val ssh_url = Some(SshPath(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
object ApiRepository {
|
object ApiRepository {
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
package gitbucket.core.api
|
|
||||||
|
|
||||||
import gitbucket.core.util.RepositoryName
|
|
||||||
|
|
||||||
case class ApiTagCommit(
|
|
||||||
sha: String,
|
|
||||||
url: ApiPath
|
|
||||||
)
|
|
||||||
|
|
||||||
case class ApiTag(
|
|
||||||
name: String,
|
|
||||||
commit: ApiTagCommit,
|
|
||||||
zipball_url: ApiPath,
|
|
||||||
tarball_url: ApiPath
|
|
||||||
)
|
|
||||||
|
|
||||||
object ApiTag {
|
|
||||||
def apply(
|
|
||||||
tagName: String,
|
|
||||||
repositoryName: RepositoryName,
|
|
||||||
commitId: String
|
|
||||||
): ApiTag =
|
|
||||||
ApiTag(
|
|
||||||
name = tagName,
|
|
||||||
commit = ApiTagCommit(sha = commitId, url = ApiPath(s"/${repositoryName.fullName}/commits/${commitId}")),
|
|
||||||
zipball_url = ApiPath(s"/${repositoryName.fullName}/archive/${tagName}.zip"),
|
|
||||||
tarball_url = ApiPath(s"/${repositoryName.fullName}/archive/${tagName}.tar.gz")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -128,6 +128,15 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
"highlighterTheme" -> trim(label("Theme", text(required)))
|
"highlighterTheme" -> trim(label("Theme", text(required)))
|
||||||
)(SyntaxHighlighterThemeForm.apply)
|
)(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(
|
case class NewGroupForm(
|
||||||
groupName: String,
|
groupName: String,
|
||||||
description: Option[String],
|
description: Option[String],
|
||||||
@@ -143,6 +152,13 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
members: String,
|
members: String,
|
||||||
clearImage: Boolean
|
clearImage: Boolean
|
||||||
)
|
)
|
||||||
|
case class ResetPasswordEmailForm(
|
||||||
|
mailAddress: String
|
||||||
|
)
|
||||||
|
case class ResetPasswordForm(
|
||||||
|
token: String,
|
||||||
|
password: String
|
||||||
|
)
|
||||||
|
|
||||||
val newGroupForm = mapping(
|
val newGroupForm = mapping(
|
||||||
"groupName" -> trim(label("Group name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
|
"groupName" -> trim(label("Group name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
|
||||||
@@ -602,7 +618,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
get("/register") {
|
get("/register") {
|
||||||
if (context.settings.allowAccountRegistration) {
|
if (context.settings.basicBehavior.allowAccountRegistration) {
|
||||||
if (context.loginAccount.isDefined) {
|
if (context.loginAccount.isDefined) {
|
||||||
redirect("/")
|
redirect("/")
|
||||||
} else {
|
} else {
|
||||||
@@ -612,7 +628,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
post("/register", newForm) { form =>
|
post("/register", newForm) { form =>
|
||||||
if (context.settings.allowAccountRegistration) {
|
if (context.settings.basicBehavior.allowAccountRegistration) {
|
||||||
createAccount(
|
createAccount(
|
||||||
form.userName,
|
form.userName,
|
||||||
pbkdf2_sha256(form.password),
|
pbkdf2_sha256(form.password),
|
||||||
@@ -628,6 +644,63 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
} else NotFound()
|
} 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 {
|
get("/groups/new")(usersOnly {
|
||||||
context.withLoginAccount { loginAccount =>
|
context.withLoginAccount { loginAccount =>
|
||||||
html.creategroup(List(GroupMember("", loginAccount.userName, true)))
|
html.creategroup(List(GroupMember("", loginAccount.userName, true)))
|
||||||
@@ -713,7 +786,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
*/
|
*/
|
||||||
get("/new")(usersOnly {
|
get("/new")(usersOnly {
|
||||||
context.withLoginAccount { loginAccount =>
|
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 =>
|
post("/new", newRepositoryForm)(usersOnly { form =>
|
||||||
context.withLoginAccount {
|
context.withLoginAccount {
|
||||||
loginAccount =>
|
loginAccount =>
|
||||||
if (context.settings.repositoryOperation.create || loginAccount.isAdmin) {
|
if (context.settings.basicBehavior.repositoryOperation.create || loginAccount.isAdmin) {
|
||||||
LockUtil.lock(s"${form.owner}/${form.name}") {
|
LockUtil.lock(s"${form.owner}/${form.name}") {
|
||||||
if (getRepository(form.owner, form.name).isDefined) {
|
if (getRepository(form.owner, form.name).isDefined) {
|
||||||
// redirect to the repository if repository already exists
|
// redirect to the repository if repository already exists
|
||||||
@@ -753,7 +826,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
|
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
|
||||||
context.withLoginAccount {
|
context.withLoginAccount {
|
||||||
loginAccount =>
|
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 loginUserName = loginAccount.userName
|
||||||
val groups = getGroupsByUserName(loginUserName)
|
val groups = getGroupsByUserName(loginUserName)
|
||||||
groups match {
|
groups match {
|
||||||
@@ -780,7 +853,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) =>
|
post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) =>
|
||||||
context.withLoginAccount {
|
context.withLoginAccount {
|
||||||
loginAccount =>
|
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 loginUserName = loginAccount.userName
|
||||||
val accountName = form.accountName
|
val accountName = form.accountName
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
import java.io.{File, FileInputStream}
|
import java.io.{File, FileInputStream, FileOutputStream}
|
||||||
|
|
||||||
import gitbucket.core.api.{ApiError, JsonFormat}
|
import gitbucket.core.api.{ApiError, JsonFormat}
|
||||||
import gitbucket.core.model.Account
|
import gitbucket.core.model.Account
|
||||||
import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService}
|
import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService}
|
||||||
@@ -14,9 +13,9 @@ import org.scalatra._
|
|||||||
import org.scalatra.i18n._
|
import org.scalatra.i18n._
|
||||||
import org.scalatra.json._
|
import org.scalatra.json._
|
||||||
import org.scalatra.forms._
|
import org.scalatra.forms._
|
||||||
|
|
||||||
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
|
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
|
||||||
import javax.servlet.{FilterChain, ServletRequest, ServletResponse}
|
import javax.servlet.{FilterChain, ServletRequest, ServletResponse}
|
||||||
|
|
||||||
import is.tagomor.woothee.Classifier
|
import is.tagomor.woothee.Classifier
|
||||||
|
|
||||||
import scala.util.Try
|
import scala.util.Try
|
||||||
@@ -29,6 +28,9 @@ import org.eclipse.jgit.treewalk._
|
|||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.json4s.Formats
|
import org.json4s.Formats
|
||||||
|
import org.json4s.jackson.Serialization
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides generic features for controller implementations.
|
* Provides generic features for controller implementations.
|
||||||
@@ -93,8 +95,16 @@ abstract class ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def LoginAccount: Option[Account] =
|
private def LoginAccount: Option[Account] = {
|
||||||
request.getAs[Account](Keys.Session.LoginAccount).orElse(session.getAs[Account](Keys.Session.LoginAccount))
|
request
|
||||||
|
.getAs[Account](Keys.Session.LoginAccount)
|
||||||
|
.orElse(session.getAs[Account](Keys.Session.LoginAccount))
|
||||||
|
.orElse {
|
||||||
|
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
|
||||||
|
getLoginAccountFromLocalFile()
|
||||||
|
} else None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def ajaxGet(path: String)(action: => Any): Route =
|
def ajaxGet(path: String)(action: => Any): Route =
|
||||||
super.get(path) {
|
super.get(path) {
|
||||||
@@ -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.{Keys, UsersAuthenticator}
|
||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
import gitbucket.core.service.IssuesService._
|
import gitbucket.core.service.IssuesService._
|
||||||
|
import gitbucket.core.service.ActivityService._
|
||||||
|
|
||||||
class DashboardController
|
class DashboardController
|
||||||
extends DashboardControllerBase
|
extends DashboardControllerBase
|
||||||
@@ -40,9 +41,9 @@ trait DashboardControllerBase extends ControllerBase {
|
|||||||
context.loginAccount,
|
context.loginAccount,
|
||||||
None,
|
None,
|
||||||
withoutPhysicalInfo = true,
|
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,
|
context.loginAccount,
|
||||||
None,
|
None,
|
||||||
withoutPhysicalInfo = true,
|
withoutPhysicalInfo = true,
|
||||||
limit = context.settings.limitVisibleRepositories
|
limit = context.settings.basicBehavior.limitVisibleRepositories
|
||||||
)
|
),
|
||||||
|
isNewsFeedEnabled()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,8 +173,9 @@ trait DashboardControllerBase extends ControllerBase {
|
|||||||
context.loginAccount,
|
context.loginAccount,
|
||||||
None,
|
None,
|
||||||
withoutPhysicalInfo = true,
|
withoutPhysicalInfo = true,
|
||||||
limit = context.settings.limitVisibleRepositories
|
limit = context.settings.basicBehavior.limitVisibleRepositories
|
||||||
)
|
),
|
||||||
|
isNewsFeedEnabled()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package gitbucket.core.controller
|
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.oauth2.sdk.id.State
|
||||||
import com.nimbusds.openid.connect.sdk.Nonce
|
import com.nimbusds.openid.connect.sdk.Nonce
|
||||||
import gitbucket.core.helper.xml
|
import gitbucket.core.helper.xml
|
||||||
@@ -13,6 +14,8 @@ import gitbucket.core.view.helpers._
|
|||||||
import org.scalatra.Ok
|
import org.scalatra.Ok
|
||||||
import org.scalatra.forms._
|
import org.scalatra.forms._
|
||||||
|
|
||||||
|
import gitbucket.core.service.ActivityService._
|
||||||
|
|
||||||
class IndexController
|
class IndexController
|
||||||
extends IndexControllerBase
|
extends IndexControllerBase
|
||||||
with RepositoryService
|
with RepositoryService
|
||||||
@@ -57,30 +60,37 @@ trait IndexControllerBase extends ControllerBase {
|
|||||||
//
|
//
|
||||||
// case class SearchForm(query: String, owner: String, repository: String)
|
// case class SearchForm(query: String, owner: String, repository: String)
|
||||||
|
|
||||||
case class OidcContext(state: State, nonce: Nonce, redirectBackURI: String)
|
case class OidcAuthContext(state: State, nonce: Nonce, redirectBackURI: String)
|
||||||
|
case class OidcSessionContext(token: JWT)
|
||||||
|
|
||||||
get("/") {
|
get("/") {
|
||||||
context.loginAccount
|
context.loginAccount
|
||||||
.map { account =>
|
.map { account =>
|
||||||
val visibleOwnerSet: Set[String] = Set(account.userName) ++ getGroupsByUserName(account.userName)
|
val visibleOwnerSet: Set[String] = Set(account.userName) ++ getGroupsByUserName(account.userName)
|
||||||
gitbucket.core.html.index(
|
if (!isNewsFeedEnabled()) {
|
||||||
getRecentActivitiesByOwners(visibleOwnerSet),
|
redirect("/dashboard/repos")
|
||||||
getVisibleRepositories(
|
} else {
|
||||||
Some(account),
|
gitbucket.core.html.index(
|
||||||
None,
|
activities = getRecentActivitiesByOwners(visibleOwnerSet),
|
||||||
withoutPhysicalInfo = true,
|
recentRepositories = getVisibleRepositories(
|
||||||
limit = context.settings.limitVisibleRepositories
|
Some(account),
|
||||||
),
|
None,
|
||||||
showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken(
|
withoutPhysicalInfo = true,
|
||||||
account.userName
|
limit = context.settings.basicBehavior.limitVisibleRepositories
|
||||||
|
),
|
||||||
|
showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken(
|
||||||
|
account.userName
|
||||||
|
),
|
||||||
|
enableNewsFeed = isNewsFeedEnabled()
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
.getOrElse {
|
.getOrElse {
|
||||||
gitbucket.core.html.index(
|
gitbucket.core.html.index(
|
||||||
getRecentPublicActivities(),
|
activities = getRecentPublicActivities(),
|
||||||
getVisibleRepositories(None, withoutPhysicalInfo = true),
|
recentRepositories = getVisibleRepositories(None, withoutPhysicalInfo = true),
|
||||||
showBannerToCreatePersonalAccessToken = false
|
showBannerToCreatePersonalAccessToken = false,
|
||||||
|
enableNewsFeed = isNewsFeedEnabled()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,8 +130,8 @@ trait IndexControllerBase extends ControllerBase {
|
|||||||
case _ => "/"
|
case _ => "/"
|
||||||
}
|
}
|
||||||
session.setAttribute(
|
session.setAttribute(
|
||||||
Keys.Session.OidcContext,
|
Keys.Session.OidcAuthContext,
|
||||||
OidcContext(authenticationRequest.getState, authenticationRequest.getNonce, redirectBackURI)
|
OidcAuthContext(authenticationRequest.getState, authenticationRequest.getNonce, redirectBackURI)
|
||||||
)
|
)
|
||||||
redirect(authenticationRequest.toURI.toString)
|
redirect(authenticationRequest.toURI.toString)
|
||||||
} getOrElse {
|
} getOrElse {
|
||||||
@@ -135,10 +145,12 @@ trait IndexControllerBase extends ControllerBase {
|
|||||||
get("/signin/oidc") {
|
get("/signin/oidc") {
|
||||||
context.settings.oidc.map { oidc =>
|
context.settings.oidc.map { oidc =>
|
||||||
val redirectURI = new URI(s"$baseUrl/signin/oidc")
|
val redirectURI = new URI(s"$baseUrl/signin/oidc")
|
||||||
session.get(Keys.Session.OidcContext) match {
|
session.get(Keys.Session.OidcAuthContext) match {
|
||||||
case Some(context: OidcContext) =>
|
case Some(context: OidcAuthContext) =>
|
||||||
authenticate(params.toMap, redirectURI, context.state, context.nonce, oidc).map { account =>
|
authenticate(params.toMap, redirectURI, context.state, context.nonce, oidc).map {
|
||||||
signin(account, context.redirectBackURI)
|
case (jwt, account) =>
|
||||||
|
session.setAttribute(Keys.Session.OidcSessionContext, OidcSessionContext(jwt))
|
||||||
|
signin(account, context.redirectBackURI)
|
||||||
} orElse {
|
} orElse {
|
||||||
flash.update("error", "Sorry, authentication failed. Please try again.")
|
flash.update("error", "Sorry, authentication failed. Please try again.")
|
||||||
session.invalidate()
|
session.invalidate()
|
||||||
@@ -155,7 +167,19 @@ trait IndexControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get("/signout") {
|
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
|
session.invalidate
|
||||||
|
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
|
||||||
|
deleteLoginAccountFromLocalFile()
|
||||||
|
}
|
||||||
redirect("/")
|
redirect("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +202,9 @@ trait IndexControllerBase extends ControllerBase {
|
|||||||
*/
|
*/
|
||||||
private def signin(account: Account, redirectUrl: String = "/") = {
|
private def signin(account: Account, redirectUrl: String = "/") = {
|
||||||
session.setAttribute(Keys.Session.LoginAccount, account)
|
session.setAttribute(Keys.Session.LoginAccount, account)
|
||||||
|
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
|
||||||
|
saveLoginAccountToLocalFile(account)
|
||||||
|
}
|
||||||
updateLastLoginDate(account.userName)
|
updateLastLoginDate(account.userName)
|
||||||
|
|
||||||
if (LDAPUtil.isDummyMailAddress(account)) {
|
if (LDAPUtil.isDummyMailAddress(account)) {
|
||||||
@@ -289,11 +316,11 @@ trait IndexControllerBase extends ControllerBase {
|
|||||||
context.loginAccount,
|
context.loginAccount,
|
||||||
None,
|
None,
|
||||||
withoutPhysicalInfo = true,
|
withoutPhysicalInfo = true,
|
||||||
limit = context.settings.limitVisibleRepositories
|
limit = context.settings.basicBehavior.limitVisibleRepositories
|
||||||
)
|
)
|
||||||
|
|
||||||
val repositories = {
|
val repositories = {
|
||||||
context.settings.limitVisibleRepositories match {
|
context.settings.basicBehavior.limitVisibleRepositories match {
|
||||||
case true =>
|
case true =>
|
||||||
getVisibleRepositories(
|
getVisibleRepositories(
|
||||||
context.loginAccount,
|
context.loginAccount,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
import gitbucket.core.issues.html
|
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.IssuesService._
|
||||||
import gitbucket.core.service._
|
import gitbucket.core.service._
|
||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
@@ -21,6 +21,7 @@ class IssuesController
|
|||||||
with ActivityService
|
with ActivityService
|
||||||
with HandleCommentService
|
with HandleCommentService
|
||||||
with IssueCreationService
|
with IssueCreationService
|
||||||
|
with CustomFieldsService
|
||||||
with ReadableUsersAuthenticator
|
with ReadableUsersAuthenticator
|
||||||
with ReferrerAuthenticator
|
with ReferrerAuthenticator
|
||||||
with WritableUsersAuthenticator
|
with WritableUsersAuthenticator
|
||||||
@@ -41,6 +42,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
with ActivityService
|
with ActivityService
|
||||||
with HandleCommentService
|
with HandleCommentService
|
||||||
with IssueCreationService
|
with IssueCreationService
|
||||||
|
with CustomFieldsService
|
||||||
with ReadableUsersAuthenticator
|
with ReadableUsersAuthenticator
|
||||||
with ReferrerAuthenticator
|
with ReferrerAuthenticator
|
||||||
with WritableUsersAuthenticator
|
with WritableUsersAuthenticator
|
||||||
@@ -51,7 +53,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
case class IssueCreateForm(
|
case class IssueCreateForm(
|
||||||
title: String,
|
title: String,
|
||||||
content: Option[String],
|
content: Option[String],
|
||||||
assignedUserName: Option[String],
|
assigneeUserNames: Option[String],
|
||||||
milestoneId: Option[Int],
|
milestoneId: Option[Int],
|
||||||
priorityId: Option[Int],
|
priorityId: Option[Int],
|
||||||
labelNames: Option[String]
|
labelNames: Option[String]
|
||||||
@@ -62,7 +64,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
val issueCreateForm = mapping(
|
val issueCreateForm = mapping(
|
||||||
"title" -> trim(label("Title", text(required))),
|
"title" -> trim(label("Title", text(required))),
|
||||||
"content" -> trim(optional(text())),
|
"content" -> trim(optional(text())),
|
||||||
"assignedUserName" -> trim(optional(text())),
|
"assigneeUserNames" -> trim(optional(text())),
|
||||||
"milestoneId" -> trim(optional(number())),
|
"milestoneId" -> trim(optional(number())),
|
||||||
"priorityId" -> trim(optional(number())),
|
"priorityId" -> trim(optional(number())),
|
||||||
"labelNames" -> trim(optional(text()))
|
"labelNames" -> trim(optional(text()))
|
||||||
@@ -105,10 +107,12 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
issue,
|
issue,
|
||||||
getComments(repository.owner, repository.name, issueId.toInt),
|
getComments(repository.owner, repository.name, issueId.toInt),
|
||||||
getIssueLabels(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),
|
getAssignableUserNames(repository.owner, repository.name),
|
||||||
getMilestonesWithIssueCount(repository.owner, repository.name),
|
getMilestonesWithIssueCount(repository.owner, repository.name),
|
||||||
getPriorities(repository.owner, repository.name),
|
getPriorities(repository.owner, repository.name),
|
||||||
getLabels(repository.owner, repository.name),
|
getLabels(repository.owner, repository.name),
|
||||||
|
getCustomFieldsWithValue(repository.owner, repository.name, issueId.toInt).filter(_._1.enableForIssues),
|
||||||
isIssueEditable(repository),
|
isIssueEditable(repository),
|
||||||
isIssueManageable(repository),
|
isIssueManageable(repository),
|
||||||
isIssueCommentManageable(repository),
|
isIssueCommentManageable(repository),
|
||||||
@@ -126,6 +130,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
getPriorities(repository.owner, repository.name),
|
getPriorities(repository.owner, repository.name),
|
||||||
getDefaultPriority(repository.owner, repository.name),
|
getDefaultPriority(repository.owner, repository.name),
|
||||||
getLabels(repository.owner, repository.name),
|
getLabels(repository.owner, repository.name),
|
||||||
|
getCustomFields(repository.owner, repository.name).filter(_.enableForIssues),
|
||||||
isIssueManageable(repository),
|
isIssueManageable(repository),
|
||||||
getContentTemplate(repository, "ISSUE_TEMPLATE"),
|
getContentTemplate(repository, "ISSUE_TEMPLATE"),
|
||||||
repository
|
repository
|
||||||
@@ -141,12 +146,31 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
repository,
|
repository,
|
||||||
form.title,
|
form.title,
|
||||||
form.content,
|
form.content,
|
||||||
form.assignedUserName,
|
form.assigneeUserNames.toSeq.flatMap(_.split(",")),
|
||||||
form.milestoneId,
|
form.milestoneId,
|
||||||
form.priorityId,
|
form.priorityId,
|
||||||
form.labelNames.toSeq.flatMap(_.split(",")),
|
form.labelNames.toSeq.flatMap(_.split(",")),
|
||||||
loginAccount
|
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}")
|
redirect(s"/${issue.userName}/${issue.repositoryName}/issues/${issue.issueId}")
|
||||||
} else Unauthorized()
|
} else Unauthorized()
|
||||||
}
|
}
|
||||||
@@ -235,7 +259,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
loginAccount =>
|
loginAccount =>
|
||||||
getComment(repository.owner, repository.name, params("id")).map { comment =>
|
getComment(repository.owner, repository.name, params("id")).map { comment =>
|
||||||
if (isEditableContent(repository.owner, repository.name, comment.commentedUserName, loginAccount)) {
|
if (isEditableContent(repository.owner, repository.name, comment.commentedUserName, loginAccount)) {
|
||||||
updateComment(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}")
|
redirect(s"/${repository.owner}/${repository.name}/issue_comments/_data/${comment.commentId}")
|
||||||
} else Unauthorized()
|
} else Unauthorized()
|
||||||
} getOrElse NotFound()
|
} getOrElse NotFound()
|
||||||
@@ -333,15 +357,16 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
||||||
})
|
})
|
||||||
|
|
||||||
ajaxPost("/:owner/:repository/issues/:id/assign")(writableUsersOnly { repository =>
|
ajaxPost("/:owner/:repository/issues/:id/assignee/new")(writableUsersOnly { repository =>
|
||||||
updateAssignedUserName(
|
val issueId = params("id").toInt
|
||||||
repository.owner,
|
registerIssueAssignee(repository.owner, repository.name, issueId, params("assigneeUserName"), true)
|
||||||
repository.name,
|
Ok()
|
||||||
params("id").toInt,
|
})
|
||||||
assignedUserName("assignedUserName"),
|
|
||||||
true
|
ajaxPost("/:owner/:repository/issues/:id/assignee/delete")(writableUsersOnly { repository =>
|
||||||
)
|
val issueId = params("id").toInt
|
||||||
Ok("updated")
|
deleteIssueAssignee(repository.owner, repository.name, issueId, params("assigneeUserName"), true)
|
||||||
|
Ok()
|
||||||
})
|
})
|
||||||
|
|
||||||
ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { repository =>
|
ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { repository =>
|
||||||
@@ -362,6 +387,35 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
Ok("updated")
|
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 =>
|
post("/:owner/:repository/issues/batchedit/state")(writableUsersOnly { repository =>
|
||||||
val action = params.get("value")
|
val action = params.get("value")
|
||||||
action match {
|
action match {
|
||||||
@@ -403,7 +457,13 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository =>
|
post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository =>
|
||||||
val value = assignedUserName("value")
|
val value = assignedUserName("value")
|
||||||
executeBatch(repository) {
|
executeBatch(repository) {
|
||||||
updateAssignedUserName(repository.owner, repository.name, _, value, true)
|
//updateAssignedUserName(repository.owner, repository.name, _, value, true)
|
||||||
|
value match {
|
||||||
|
case Some(assignedUserName) =>
|
||||||
|
registerIssueAssignee(repository.owner, repository.name, _, assignedUserName, true)
|
||||||
|
case None =>
|
||||||
|
deleteAllIssueAssignees(repository.owner, repository.name, _, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (params("uri").nonEmpty) {
|
if (params("uri").nonEmpty) {
|
||||||
redirect(params("uri"))
|
redirect(params("uri"))
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ trait LabelsControllerBase extends ControllerBase {
|
|||||||
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
|
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
|
||||||
html.list(
|
html.list(
|
||||||
getLabels(repository.owner, repository.name),
|
getLabels(repository.owner, repository.name),
|
||||||
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
|
||||||
repository,
|
repository,
|
||||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||||
)
|
)
|
||||||
@@ -59,7 +59,7 @@ trait LabelsControllerBase extends ControllerBase {
|
|||||||
html.label(
|
html.label(
|
||||||
getLabel(repository.owner, repository.name, labelId).get,
|
getLabel(repository.owner, repository.name, labelId).get,
|
||||||
// TODO futility
|
// TODO futility
|
||||||
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
|
||||||
repository,
|
repository,
|
||||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||||
)
|
)
|
||||||
@@ -76,7 +76,7 @@ trait LabelsControllerBase extends ControllerBase {
|
|||||||
html.label(
|
html.label(
|
||||||
getLabel(repository.owner, repository.name, params("labelId").toInt).get,
|
getLabel(repository.owner, repository.name, params("labelId").toInt).get,
|
||||||
// TODO futility
|
// TODO futility
|
||||||
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
|
||||||
repository,
|
repository,
|
||||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ trait PreProcessControllerBase extends ControllerBase {
|
|||||||
* If anonymous access is allowed, pass all requests.
|
* If anonymous access is allowed, pass all requests.
|
||||||
* But if it's not allowed, demands authentication except some paths.
|
* But if it's not allowed, demands authentication except some paths.
|
||||||
*/
|
*/
|
||||||
get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) {
|
get(!context.settings.basicBehavior.allowAnonymousAccess, context.loginAccount.isEmpty) {
|
||||||
if (!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
|
if (!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
|
||||||
!context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs") &&
|
!context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs") &&
|
||||||
!context.currentPath.startsWith("/plugin-assets") &&
|
!context.currentPath.startsWith("/plugin-assets") &&
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ trait PrioritiesControllerBase extends ControllerBase {
|
|||||||
get("/:owner/:repository/issues/priorities")(referrersOnly { repository =>
|
get("/:owner/:repository/issues/priorities")(referrersOnly { repository =>
|
||||||
html.list(
|
html.list(
|
||||||
getPriorities(repository.owner, repository.name),
|
getPriorities(repository.owner, repository.name),
|
||||||
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
|
||||||
repository,
|
repository,
|
||||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
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))
|
createPriority(repository.owner, repository.name, form.priorityName, form.description, form.color.substring(1))
|
||||||
html.priority(
|
html.priority(
|
||||||
getPriority(repository.owner, repository.name, priorityId).get,
|
getPriority(repository.owner, repository.name, priorityId).get,
|
||||||
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
|
||||||
repository,
|
repository,
|
||||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||||
)
|
)
|
||||||
@@ -84,7 +84,7 @@ trait PrioritiesControllerBase extends ControllerBase {
|
|||||||
)
|
)
|
||||||
html.priority(
|
html.priority(
|
||||||
getPriority(repository.owner, repository.name, params("priorityId").toInt).get,
|
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,
|
repository,
|
||||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class PullRequestsController
|
|||||||
with PullRequestService
|
with PullRequestService
|
||||||
with MilestonesService
|
with MilestonesService
|
||||||
with LabelsService
|
with LabelsService
|
||||||
|
with CustomFieldsService
|
||||||
with CommitsService
|
with CommitsService
|
||||||
with ActivityService
|
with ActivityService
|
||||||
with WebHookPullRequestService
|
with WebHookPullRequestService
|
||||||
@@ -44,6 +45,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
with IssuesService
|
with IssuesService
|
||||||
with MilestonesService
|
with MilestonesService
|
||||||
with LabelsService
|
with LabelsService
|
||||||
|
with CustomFieldsService
|
||||||
with CommitsService
|
with CommitsService
|
||||||
with ActivityService
|
with ActivityService
|
||||||
with PullRequestService
|
with PullRequestService
|
||||||
@@ -67,7 +69,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
"commitIdFrom" -> trim(text(required, maxlength(40))),
|
"commitIdFrom" -> trim(text(required, maxlength(40))),
|
||||||
"commitIdTo" -> trim(text(required, maxlength(40))),
|
"commitIdTo" -> trim(text(required, maxlength(40))),
|
||||||
"isDraft" -> trim(boolean(required)),
|
"isDraft" -> trim(boolean(required)),
|
||||||
"assignedUserName" -> trim(optional(text())),
|
"assigneeUserNames" -> trim(optional(text())),
|
||||||
"milestoneId" -> trim(optional(number())),
|
"milestoneId" -> trim(optional(number())),
|
||||||
"priorityId" -> trim(optional(number())),
|
"priorityId" -> trim(optional(number())),
|
||||||
"labelNames" -> trim(optional(text()))
|
"labelNames" -> trim(optional(text()))
|
||||||
@@ -90,7 +92,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
commitIdFrom: String,
|
commitIdFrom: String,
|
||||||
commitIdTo: String,
|
commitIdTo: String,
|
||||||
isDraft: Boolean,
|
isDraft: Boolean,
|
||||||
assignedUserName: Option[String],
|
assigneeUserNames: Option[String],
|
||||||
milestoneId: Option[Int],
|
milestoneId: Option[Int],
|
||||||
priorityId: Option[Int],
|
priorityId: Option[Int],
|
||||||
labelNames: Option[String]
|
labelNames: Option[String]
|
||||||
@@ -129,10 +131,12 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
getPullRequestComments(repository.owner, repository.name, issue.issueId, commits.flatten),
|
getPullRequestComments(repository.owner, repository.name, issue.issueId, commits.flatten),
|
||||||
diffs.size,
|
diffs.size,
|
||||||
getIssueLabels(repository.owner, repository.name, issueId),
|
getIssueLabels(repository.owner, repository.name, issueId),
|
||||||
|
getIssueAssignees(repository.owner, repository.name, issueId),
|
||||||
getAssignableUserNames(repository.owner, repository.name),
|
getAssignableUserNames(repository.owner, repository.name),
|
||||||
getMilestonesWithIssueCount(repository.owner, repository.name),
|
getMilestonesWithIssueCount(repository.owner, repository.name),
|
||||||
getPriorities(repository.owner, repository.name),
|
getPriorities(repository.owner, repository.name),
|
||||||
getLabels(repository.owner, repository.name),
|
getLabels(repository.owner, repository.name),
|
||||||
|
getCustomFieldsWithValue(repository.owner, repository.name, issueId).filter(_._1.enableForPullRequests),
|
||||||
isEditable(repository),
|
isEditable(repository),
|
||||||
isManageable(repository),
|
isManageable(repository),
|
||||||
hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount),
|
hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount),
|
||||||
@@ -140,25 +144,6 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName),
|
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName),
|
||||||
flash.iterator.map(f => f._1 -> f._2.toString).toMap
|
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()
|
} getOrElse NotFound()
|
||||||
})
|
})
|
||||||
@@ -392,9 +377,9 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
|
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 {
|
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||||
case (Some(originUserName), Some(originRepositoryName)) => {
|
case (Some(originUserName), Some(originRepositoryName)) =>
|
||||||
getRepository(originUserName, originRepositoryName).map {
|
getRepository(originUserName, originRepositoryName).map {
|
||||||
originRepository =>
|
originRepository =>
|
||||||
Using.resources(
|
Using.resources(
|
||||||
@@ -411,8 +396,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} getOrElse NotFound()
|
} getOrElse NotFound()
|
||||||
}
|
case _ =>
|
||||||
case _ => {
|
|
||||||
Using.resource(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))) { git =>
|
Using.resource(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))) { git =>
|
||||||
JGitUtil.getDefaultBranch(git, forkedRepository).map {
|
JGitUtil.getDefaultBranch(git, forkedRepository).map {
|
||||||
case (_, defaultBranch) =>
|
case (_, defaultBranch) =>
|
||||||
@@ -423,41 +407,48 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
|
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 =>
|
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
|
||||||
val Seq(origin, forked) = multiParams("splat")
|
val Seq(origin, forked) = multiParams("splat")
|
||||||
val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner)
|
val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner)
|
||||||
val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner)
|
val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner)
|
||||||
|
|
||||||
(for (originRepositoryName <- if (originOwner == forkedOwner) {
|
(for (originRepositoryName <- getOriginRepositoryName(originOwner, forkedOwner, forkedRepository);
|
||||||
// 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)
|
|
||||||
};
|
|
||||||
originRepository <- getRepository(originOwner, originRepositoryName)) yield {
|
originRepository <- getRepository(originOwner, originRepositoryName)) yield {
|
||||||
val (oldId, newId) =
|
val (oldId, newId) =
|
||||||
getPullRequestCommitFromTo(originRepository, forkedRepository, originId, forkedId)
|
getPullRequestCommitFromTo(originRepository, forkedRepository, originId, forkedId)
|
||||||
|
|
||||||
(oldId, newId) match {
|
(oldId, newId) match {
|
||||||
case (Some(oldId), Some(newId)) => {
|
case (Some(oldId), Some(newId)) =>
|
||||||
val (commits, diffs) = getRequestCompareInfo(
|
val (commits, diffs) = getRequestCompareInfo(
|
||||||
originRepository.owner,
|
originRepository.owner,
|
||||||
originRepository.name,
|
originRepository.name,
|
||||||
@@ -505,9 +496,9 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
getMilestones(originRepository.owner, originRepository.name),
|
getMilestones(originRepository.owner, originRepository.name),
|
||||||
getPriorities(originRepository.owner, originRepository.name),
|
getPriorities(originRepository.owner, originRepository.name),
|
||||||
getDefaultPriority(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) =>
|
case (oldId, newId) =>
|
||||||
redirect(
|
redirect(
|
||||||
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/" +
|
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/" +
|
||||||
@@ -519,6 +510,54 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
}) getOrElse NotFound()
|
}) 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 =>
|
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(readableUsersOnly { forkedRepository =>
|
||||||
val Seq(origin, forked) = multiParams("splat")
|
val Seq(origin, forked) = multiParams("splat")
|
||||||
val (originOwner, tmpOriginBranch) = parseCompareIdentifier(origin, forkedRepository.owner)
|
val (originOwner, tmpOriginBranch) = parseCompareIdentifier(origin, forkedRepository.owner)
|
||||||
@@ -567,7 +606,6 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
loginUser = loginAccount.userName,
|
loginUser = loginAccount.userName,
|
||||||
title = form.title,
|
title = form.title,
|
||||||
content = form.content,
|
content = form.content,
|
||||||
assignedUserName = if (manageable) form.assignedUserName else None,
|
|
||||||
milestoneId = if (manageable) form.milestoneId else None,
|
milestoneId = if (manageable) form.milestoneId else None,
|
||||||
priorityId = if (manageable) form.priorityId else None,
|
priorityId = if (manageable) form.priorityId else None,
|
||||||
isPullRequest = true
|
isPullRequest = true
|
||||||
@@ -587,8 +625,14 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
settings = context.settings
|
settings = context.settings
|
||||||
)
|
)
|
||||||
|
|
||||||
// insert labels
|
|
||||||
if (manageable) {
|
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 =>
|
form.labelNames.foreach { value =>
|
||||||
val labels = getLabels(repository.owner, repository.name)
|
val labels = getLabels(repository.owner, repository.name)
|
||||||
value.split(",").foreach { labelName =>
|
value.split(",").foreach { labelName =>
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ trait ReleaseControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
get("/:owner/:repository/changelog/*...*")(writableUsersOnly { repository =>
|
get("/:owner/:repository/changelog/*...*")(writableUsersOnly { repository =>
|
||||||
val Seq(previousTag, currentTag) = multiParams("splat")
|
val Seq(previousTag, currentTag) = multiParams("splat")
|
||||||
val previousTagId = repository.tags.collectFirst { case x if x.name == previousTag => x.id }.getOrElse("")
|
val previousTagId = repository.tags.collectFirst { case x if x.name == previousTag => x.commitId }.getOrElse("")
|
||||||
|
|
||||||
val commitLog = Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
val commitLog = Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||||
val commits = JGitUtil.getCommitLog(git, previousTagId, currentTag).reverse
|
val commits = JGitUtil.getCommitLog(git, previousTagId, currentTag).reverse
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package gitbucket.core.controller
|
|||||||
|
|
||||||
import java.time.{LocalDateTime, ZoneOffset}
|
import java.time.{LocalDateTime, ZoneOffset}
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
import gitbucket.core.settings.html
|
import gitbucket.core.settings.html
|
||||||
import gitbucket.core.model.{RepositoryWebHook, WebHook}
|
import gitbucket.core.model.{RepositoryWebHook, WebHook}
|
||||||
import gitbucket.core.service._
|
import gitbucket.core.service._
|
||||||
@@ -21,7 +20,7 @@ import org.eclipse.jgit.lib.Constants
|
|||||||
import org.eclipse.jgit.lib.ObjectId
|
import org.eclipse.jgit.lib.ObjectId
|
||||||
|
|
||||||
import scala.util.Using
|
import scala.util.Using
|
||||||
import org.scalatra.Forbidden
|
import org.scalatra.{Forbidden, Ok}
|
||||||
|
|
||||||
class RepositorySettingsController
|
class RepositorySettingsController
|
||||||
extends RepositorySettingsControllerBase
|
extends RepositorySettingsControllerBase
|
||||||
@@ -31,6 +30,7 @@ class RepositorySettingsController
|
|||||||
with ProtectedBranchService
|
with ProtectedBranchService
|
||||||
with CommitStatusService
|
with CommitStatusService
|
||||||
with DeployKeyService
|
with DeployKeyService
|
||||||
|
with CustomFieldsService
|
||||||
with ActivityService
|
with ActivityService
|
||||||
with OwnerAuthenticator
|
with OwnerAuthenticator
|
||||||
with UsersAuthenticator
|
with UsersAuthenticator
|
||||||
@@ -43,6 +43,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
with ProtectedBranchService
|
with ProtectedBranchService
|
||||||
with CommitStatusService
|
with CommitStatusService
|
||||||
with DeployKeyService
|
with DeployKeyService
|
||||||
|
with CustomFieldsService
|
||||||
with ActivityService
|
with ActivityService
|
||||||
with OwnerAuthenticator
|
with OwnerAuthenticator
|
||||||
with UsersAuthenticator =>
|
with UsersAuthenticator =>
|
||||||
@@ -121,6 +122,23 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
"newOwner" -> trim(label("New owner", text(required, transferUser)))
|
"newOwner" -> trim(label("New owner", text(required, transferUser)))
|
||||||
)(TransferOwnerShipForm.apply)
|
)(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.
|
* Redirect to the Options page.
|
||||||
*/
|
*/
|
||||||
@@ -390,7 +408,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) =>
|
post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) =>
|
||||||
context.withLoginAccount {
|
context.withLoginAccount {
|
||||||
loginAccount =>
|
loginAccount =>
|
||||||
if (context.settings.repositoryOperation.rename || loginAccount.isAdmin) {
|
if (context.settings.basicBehavior.repositoryOperation.rename || loginAccount.isAdmin) {
|
||||||
if (repository.name != form.repositoryName) {
|
if (repository.name != form.repositoryName) {
|
||||||
// Update database and move git repository
|
// Update database and move git repository
|
||||||
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
|
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) =>
|
post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
|
||||||
context.withLoginAccount {
|
context.withLoginAccount {
|
||||||
loginAccount =>
|
loginAccount =>
|
||||||
if (context.settings.repositoryOperation.transfer || loginAccount.isAdmin) {
|
if (context.settings.basicBehavior.repositoryOperation.transfer || loginAccount.isAdmin) {
|
||||||
// Change repository owner
|
// Change repository owner
|
||||||
if (repository.owner != form.newOwner) {
|
if (repository.owner != form.newOwner) {
|
||||||
// Update database and move git repository
|
// Update database and move git repository
|
||||||
@@ -438,7 +456,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
*/
|
*/
|
||||||
post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
|
post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
|
||||||
context.withLoginAccount { loginAccount =>
|
context.withLoginAccount { loginAccount =>
|
||||||
if (context.settings.repositoryOperation.delete || loginAccount.isAdmin) {
|
if (context.settings.basicBehavior.repositoryOperation.delete || loginAccount.isAdmin) {
|
||||||
// Delete the repository and related files
|
// Delete the repository and related files
|
||||||
deleteRepository(repository.repository)
|
deleteRepository(repository.repository)
|
||||||
redirect(s"/${repository.owner}")
|
redirect(s"/${repository.owner}")
|
||||||
@@ -477,6 +495,60 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
redirect(s"/${repository.owner}/${repository.name}/settings/deploykey")
|
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.
|
* Provides duplication check for web hook url.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -329,55 +329,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/upload", uploadForm)(writableUsersOnly { (form, repository) =>
|
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(
|
def _commit(
|
||||||
branchName: String,
|
branchName: String,
|
||||||
//files: Seq[CommitFile],
|
|
||||||
newFiles: Seq[CommitFile],
|
newFiles: Seq[CommitFile],
|
||||||
loginAccount: Account
|
loginAccount: Account
|
||||||
): ObjectId = {
|
): Either[String, ObjectId] = {
|
||||||
commitFiles(
|
commitFiles(
|
||||||
repository = repository,
|
repository = repository,
|
||||||
branch = branchName,
|
branch = branchName,
|
||||||
//path = form.path,
|
|
||||||
//files = files.toIndexedSeq,
|
|
||||||
message = form.message.getOrElse("Add files via upload"),
|
message = form.message.getOrElse("Add files via upload"),
|
||||||
loginAccount = loginAccount,
|
loginAccount = loginAccount,
|
||||||
settings = context.settings
|
settings = context.settings
|
||||||
@@ -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 =>
|
get("/:owner/:repository/edit/*")(writableUsersOnly { repository =>
|
||||||
@@ -456,32 +461,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/create", editorForm)(writableUsersOnly { (form, repository) =>
|
post("/:owner/:repository/create", editorForm)(writableUsersOnly { (form, repository) =>
|
||||||
context.withLoginAccount {
|
def _commit(branchName: String, loginAccount: Account): Either[String, ObjectId] = {
|
||||||
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 = {
|
|
||||||
commitFile(
|
commitFile(
|
||||||
repository = repository,
|
repository = repository,
|
||||||
branch = branchName,
|
branch = branchName,
|
||||||
@@ -494,37 +474,48 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
commit = form.commit,
|
commit = form.commit,
|
||||||
loginAccount = loginAccount,
|
loginAccount = loginAccount,
|
||||||
settings = context.settings
|
settings = context.settings
|
||||||
)._1
|
).map(_._1)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
post("/:owner/:repository/update", editorForm)(writableUsersOnly { (form, repository) =>
|
|
||||||
context.withLoginAccount {
|
context.withLoginAccount {
|
||||||
loginAccount =>
|
loginAccount =>
|
||||||
if (form.newBranch) {
|
if (form.newBranch) {
|
||||||
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
|
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
|
||||||
val objectId = _commit(newBranchName, loginAccount)
|
_commit(newBranchName, loginAccount) match {
|
||||||
val issueId =
|
case Right(objectId) =>
|
||||||
createIssueAndPullRequest(
|
val issueId =
|
||||||
repository,
|
createIssueAndPullRequest(
|
||||||
form.branch,
|
repository,
|
||||||
newBranchName,
|
form.branch,
|
||||||
form.commit,
|
newBranchName,
|
||||||
objectId.name,
|
form.commit,
|
||||||
form.message,
|
objectId.name,
|
||||||
loginAccount
|
form.message,
|
||||||
)
|
loginAccount
|
||||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
)
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||||
|
case Left(error) => Forbidden(gitbucket.core.html.error(error))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_commit(form.branch, loginAccount)
|
_commit(form.branch, loginAccount) match {
|
||||||
redirect(
|
case Right(_) =>
|
||||||
s"/${repository.owner}/${repository.name}/blob/${urlEncode(form.branch)}/${if (form.path.length == 0) urlEncode(form.newFileName)
|
if (form.path.length == 0) {
|
||||||
else s"${form.path}/${urlEncode(form.newFileName)}"}"
|
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(
|
commitFile(
|
||||||
repository = repository,
|
repository = repository,
|
||||||
branch = branchName,
|
branch = branchName,
|
||||||
@@ -541,37 +532,48 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
commit = form.commit,
|
commit = form.commit,
|
||||||
loginAccount = loginAccount,
|
loginAccount = loginAccount,
|
||||||
settings = context.settings
|
settings = context.settings
|
||||||
)._1
|
).map(_._1)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
post("/:owner/:repository/remove", deleteForm)(writableUsersOnly { (form, repository) =>
|
|
||||||
context.withLoginAccount {
|
context.withLoginAccount {
|
||||||
loginAccount =>
|
loginAccount =>
|
||||||
if (form.newBranch) {
|
if (form.newBranch) {
|
||||||
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
|
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
|
||||||
val objectId = _commit(newBranchName, loginAccount)
|
_commit(newBranchName, loginAccount) match {
|
||||||
val issueId =
|
case Right(objectId) =>
|
||||||
createIssueAndPullRequest(
|
val issueId =
|
||||||
repository,
|
createIssueAndPullRequest(
|
||||||
form.branch,
|
repository,
|
||||||
newBranchName,
|
form.branch,
|
||||||
form.commit,
|
newBranchName,
|
||||||
objectId.name,
|
form.commit,
|
||||||
form.message,
|
objectId.name,
|
||||||
loginAccount
|
form.message,
|
||||||
)
|
loginAccount
|
||||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
)
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||||
|
case Left(error) => Forbidden(gitbucket.core.html.error(error))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_commit(form.branch, loginAccount)
|
_commit(form.branch, loginAccount) match {
|
||||||
redirect(
|
case Right(_) =>
|
||||||
s"/${repository.owner}/${repository.name}/tree/${form.branch}${if (form.path.length == 0) ""
|
if (form.path.length == 0) {
|
||||||
else "/" + form.path}"
|
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(
|
commitFile(
|
||||||
repository = repository,
|
repository = repository,
|
||||||
branch = branchName,
|
branch = branchName,
|
||||||
@@ -584,7 +586,41 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
commit = form.commit,
|
commit = form.commit,
|
||||||
loginAccount = loginAccount,
|
loginAccount = loginAccount,
|
||||||
settings = context.settings
|
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,
|
loginUser = loginAccount.userName,
|
||||||
title = requestBranch,
|
title = requestBranch,
|
||||||
content = commitMessage,
|
content = commitMessage,
|
||||||
assignedUserName = None,
|
|
||||||
milestoneId = None,
|
milestoneId = None,
|
||||||
priorityId = None,
|
priorityId = None,
|
||||||
isPullRequest = true
|
isPullRequest = true
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
|
||||||
import gitbucket.core.admin.html
|
import gitbucket.core.admin.html
|
||||||
import gitbucket.core.plugin.PluginRegistry
|
import gitbucket.core.plugin.PluginRegistry
|
||||||
import gitbucket.core.service.SystemSettingsService._
|
import gitbucket.core.service.SystemSettingsService._
|
||||||
@@ -35,23 +34,38 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
|||||||
private val form = mapping(
|
private val form = mapping(
|
||||||
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
||||||
"information" -> trim(label("Information", optional(text()))),
|
"information" -> trim(label("Information", optional(text()))),
|
||||||
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
|
"basicBehavior" -> mapping(
|
||||||
"allowAnonymousAccess" -> trim(label("Anonymous access", boolean())),
|
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
|
||||||
"isCreateRepoOptionPublic" -> trim(label("Default visibility of new repository", boolean())),
|
"allowResetPassword" -> trim(label("Reset password", boolean())),
|
||||||
"repositoryOperation" -> mapping(
|
"allowAnonymousAccess" -> trim(label("Anonymous access", boolean())),
|
||||||
"create" -> trim(label("Allow all users to create repository", boolean())),
|
"isCreateRepoOptionPublic" -> trim(label("Default visibility of new repository", boolean())),
|
||||||
"delete" -> trim(label("Allow all users to delete repository", boolean())),
|
"repositoryOperation" -> mapping(
|
||||||
"rename" -> trim(label("Allow all users to rename repository", boolean())),
|
"create" -> trim(label("Allow all users to create repository", boolean())),
|
||||||
"transfer" -> trim(label("Allow all users to transfer repository", boolean())),
|
"delete" -> trim(label("Allow all users to delete repository", boolean())),
|
||||||
"fork" -> trim(label("Allow all users to fork repository", boolean()))
|
"rename" -> trim(label("Allow all users to rename repository", boolean())),
|
||||||
)(RepositoryOperation.apply),
|
"transfer" -> trim(label("Allow all users to transfer repository", boolean())),
|
||||||
"gravatar" -> trim(label("Gravatar", boolean())),
|
"fork" -> trim(label("Allow all users to fork repository", boolean()))
|
||||||
"notification" -> trim(label("Notification", boolean())),
|
)(RepositoryOperation.apply),
|
||||||
"limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())),
|
"gravatar" -> trim(label("Gravatar", boolean())),
|
||||||
|
"notification" -> trim(label("Notification", boolean())),
|
||||||
|
"limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())),
|
||||||
|
)(BasicBehavior.apply),
|
||||||
"ssh" -> mapping(
|
"ssh" -> mapping(
|
||||||
"enabled" -> trim(label("SSH access", boolean())),
|
"enabled" -> trim(label("SSH access", boolean())),
|
||||||
"host" -> trim(label("SSH host", optional(text()))),
|
"bindAddress" -> mapping(
|
||||||
"port" -> trim(label("SSH port", optional(number())))
|
"host" -> trim(label("Bind SSH host", optional(text()))),
|
||||||
|
"port" -> trim(label("Bind SSH port", optional(number()))),
|
||||||
|
)(
|
||||||
|
(hostOption, portOption) =>
|
||||||
|
hostOption.map(h => SshAddress(h, portOption.getOrElse(DefaultSshPort), GenericSshUser))
|
||||||
|
),
|
||||||
|
"publicAddress" -> mapping(
|
||||||
|
"host" -> trim(label("Public SSH host", optional(text()))),
|
||||||
|
"port" -> trim(label("Public SSH port", optional(number()))),
|
||||||
|
)(
|
||||||
|
(hostOption, portOption) =>
|
||||||
|
hostOption.map(h => SshAddress(h, portOption.getOrElse(PublicSshPort), GenericSshUser))
|
||||||
|
),
|
||||||
)(Ssh.apply),
|
)(Ssh.apply),
|
||||||
"useSMTP" -> trim(label("SMTP", boolean())),
|
"useSMTP" -> trim(label("SMTP", boolean())),
|
||||||
"smtp" -> optionalIfNotChecked(
|
"smtp" -> optionalIfNotChecked(
|
||||||
@@ -116,8 +130,8 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
|||||||
if (settings.ssh.enabled && settings.baseUrl.isEmpty) {
|
if (settings.ssh.enabled && settings.baseUrl.isEmpty) {
|
||||||
Some("baseUrl" -> "Base URL is required if SSH access is enabled.")
|
Some("baseUrl" -> "Base URL is required if SSH access is enabled.")
|
||||||
} else None,
|
} else None,
|
||||||
if (settings.ssh.enabled && settings.ssh.sshHost.isEmpty) {
|
if (settings.ssh.enabled && settings.ssh.bindAddress.isEmpty) {
|
||||||
Some("sshHost" -> "SSH host is required if SSH access is enabled.")
|
Some("ssh.bindAddress.host" -> "SSH bind host is required if SSH access is enabled.")
|
||||||
} else None
|
} else None
|
||||||
).flatten
|
).flatten
|
||||||
}
|
}
|
||||||
@@ -308,12 +322,13 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
|||||||
post("/admin/system", form)(adminOnly { form =>
|
post("/admin/system", form)(adminOnly { form =>
|
||||||
saveSystemSettings(form)
|
saveSystemSettings(form)
|
||||||
|
|
||||||
if (form.sshAddress != context.settings.sshAddress) {
|
if (form.ssh.bindAddress != context.settings.sshBindAddress || form.ssh.publicAddress != context.settings.sshPublicAddress) {
|
||||||
SshServer.stop()
|
SshServer.stop()
|
||||||
for {
|
for {
|
||||||
sshAddress <- form.sshAddress
|
bindAddress <- form.ssh.bindAddress
|
||||||
|
publicAddress <- form.ssh.publicAddress.orElse(form.ssh.bindAddress)
|
||||||
baseUrl <- form.baseUrl
|
baseUrl <- form.baseUrl
|
||||||
} SshServer.start(sshAddress, baseUrl)
|
} SshServer.start(bindAddress, publicAddress, baseUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
flash.update("info", "System settings has been updated.")
|
flash.update("info", "System settings has been updated.")
|
||||||
@@ -322,7 +337,12 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
|||||||
|
|
||||||
post("/admin/system/sendmail", sendMailForm)(adminOnly { form =>
|
post("/admin/system/sendmail", sendMailForm)(adminOnly { form =>
|
||||||
try {
|
try {
|
||||||
new Mailer(context.settings.copy(smtp = Some(form.smtp), notification = true)).send(
|
new Mailer(
|
||||||
|
context.settings.copy(
|
||||||
|
smtp = Some(form.smtp),
|
||||||
|
basicBehavior = context.settings.basicBehavior.copy(notification = true)
|
||||||
|
)
|
||||||
|
).send(
|
||||||
to = form.testAddress,
|
to = form.testAddress,
|
||||||
subject = "Test message from GitBucket",
|
subject = "Test message from GitBucket",
|
||||||
textMsg = "This is a test message from GitBucket.",
|
textMsg = "This is a test message from GitBucket.",
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package gitbucket.core.controller.api
|
package gitbucket.core.controller.api
|
||||||
import gitbucket.core.api.{ApiObject, ApiRef, CreateARef, JsonFormat, UpdateARef}
|
import gitbucket.core.api.{ApiError, ApiRef, CreateARef, JsonFormat, UpdateARef}
|
||||||
import gitbucket.core.controller.ControllerBase
|
import gitbucket.core.controller.ControllerBase
|
||||||
|
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||||
import gitbucket.core.util.Directory.getRepositoryDir
|
import gitbucket.core.util.Directory.getRepositoryDir
|
||||||
import gitbucket.core.util.ReferrerAuthenticator
|
|
||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
|
import gitbucket.core.util.{ReferrerAuthenticator, RepositoryName, WritableUsersAuthenticator}
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import org.eclipse.jgit.lib.ObjectId
|
import org.eclipse.jgit.lib.ObjectId
|
||||||
import org.eclipse.jgit.lib.RefUpdate.Result
|
import org.eclipse.jgit.lib.RefUpdate.Result
|
||||||
@@ -14,47 +15,38 @@ import scala.jdk.CollectionConverters._
|
|||||||
import scala.util.Using
|
import scala.util.Using
|
||||||
|
|
||||||
trait ApiGitReferenceControllerBase extends ControllerBase {
|
trait ApiGitReferenceControllerBase extends ControllerBase {
|
||||||
self: ReferrerAuthenticator =>
|
self: ReferrerAuthenticator with WritableUsersAuthenticator =>
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[ApiGitReferenceControllerBase])
|
private val logger = LoggerFactory.getLogger(classOf[ApiGitReferenceControllerBase])
|
||||||
|
|
||||||
|
get("/api/v3/repos/:owner/:repository/git/refs")(referrersOnly { repository =>
|
||||||
|
val result = Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||||
|
val refs = git
|
||||||
|
.getRepository()
|
||||||
|
.getRefDatabase()
|
||||||
|
.getRefsByPrefix("refs")
|
||||||
|
.asScala
|
||||||
|
|
||||||
|
refs.map(ApiRef.fromRef(RepositoryName(s"${repository.owner}/${repository.name}"), _))
|
||||||
|
}
|
||||||
|
JsonFormat(result)
|
||||||
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* i. Get a reference
|
* i. Get a reference
|
||||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#get-a-reference
|
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#get-a-reference
|
||||||
*/
|
*/
|
||||||
get("/api/v3/repos/:owner/:repository/git/ref/*")(referrersOnly { repository =>
|
get("/api/v3/repos/:owner/:repository/git/ref/*")(referrersOnly { repository =>
|
||||||
getRef()
|
val revstr = multiParams("splat").head
|
||||||
|
getRef(revstr, repository)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Some versions of GHE support this path
|
// Some versions of GHE support this path
|
||||||
get("/api/v3/repos/:owner/:repository/git/refs/*")(referrersOnly { repository =>
|
get("/api/v3/repos/:owner/:repository/git/refs/*")(referrersOnly { repository =>
|
||||||
logger.warn("git/refs/ endpoint may not be compatible with GitHub API v3. Consider using git/ref/ endpoint instead")
|
logger.warn("git/refs/ endpoint may not be compatible with GitHub API v3. Consider using git/ref/ endpoint instead")
|
||||||
getRef()
|
|
||||||
})
|
|
||||||
|
|
||||||
private def getRef() = {
|
|
||||||
val revstr = multiParams("splat").head
|
val revstr = multiParams("splat").head
|
||||||
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
|
getRef(revstr, repository)
|
||||||
val ref = git.getRepository().findRef(revstr)
|
})
|
||||||
|
|
||||||
if (ref != null) {
|
|
||||||
val sha = ref.getObjectId().name()
|
|
||||||
JsonFormat(ApiRef(revstr, ApiObject(sha)))
|
|
||||||
|
|
||||||
} else {
|
|
||||||
val refs = git
|
|
||||||
.getRepository()
|
|
||||||
.getRefDatabase()
|
|
||||||
.getRefsByPrefix("refs/")
|
|
||||||
.asScala
|
|
||||||
|
|
||||||
JsonFormat(refs.map { ref =>
|
|
||||||
val sha = ref.getObjectId().name()
|
|
||||||
ApiRef(revstr, ApiObject(sha))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ii. Get all references
|
* ii. Get all references
|
||||||
@@ -65,22 +57,27 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
|
|||||||
* iii. Create a reference
|
* iii. Create a reference
|
||||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-reference
|
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-reference
|
||||||
*/
|
*/
|
||||||
post("/api/v3/repos/:owner/:repository/git/refs")(referrersOnly { _ =>
|
post("/api/v3/repos/:owner/:repository/git/refs")(writableUsersOnly { repository =>
|
||||||
extractFromJsonBody[CreateARef].map {
|
extractFromJsonBody[CreateARef].map {
|
||||||
data =>
|
data =>
|
||||||
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
|
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
|
||||||
val ref = git.getRepository.findRef(data.ref)
|
git =>
|
||||||
if (ref == null) {
|
val ref = git.getRepository.findRef(data.ref)
|
||||||
val update = git.getRepository.updateRef(data.ref)
|
if (ref == null) {
|
||||||
update.setNewObjectId(ObjectId.fromString(data.sha))
|
val update = git.getRepository.updateRef(data.ref)
|
||||||
val result = update.update()
|
update.setNewObjectId(ObjectId.fromString(data.sha))
|
||||||
result match {
|
val result = update.update()
|
||||||
case Result.NEW => JsonFormat(ApiRef(update.getName, ApiObject(update.getNewObjectId.getName)))
|
result match {
|
||||||
case _ => UnprocessableEntity(result.name())
|
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()
|
} getOrElse BadRequest()
|
||||||
})
|
})
|
||||||
@@ -89,11 +86,11 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
|
|||||||
* iv. Update a reference
|
* iv. Update a reference
|
||||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#update-a-reference
|
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#update-a-reference
|
||||||
*/
|
*/
|
||||||
patch("/api/v3/repos/:owner/:repository/git/refs/*")(referrersOnly { _ =>
|
patch("/api/v3/repos/:owner/:repository/git/refs/*")(writableUsersOnly { repository =>
|
||||||
val refName = multiParams("splat").mkString("/")
|
val refName = multiParams("splat").mkString("/")
|
||||||
extractFromJsonBody[UpdateARef].map {
|
extractFromJsonBody[UpdateARef].map {
|
||||||
data =>
|
data =>
|
||||||
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
|
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||||
val ref = git.getRepository.findRef(refName)
|
val ref = git.getRepository.findRef(refName)
|
||||||
if (ref == null) {
|
if (ref == null) {
|
||||||
UnprocessableEntity("Ref does not exist.")
|
UnprocessableEntity("Ref does not exist.")
|
||||||
@@ -104,7 +101,7 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
|
|||||||
val result = update.update()
|
val result = update.update()
|
||||||
result match {
|
result match {
|
||||||
case Result.FORCED | Result.FAST_FORWARD | Result.NO_CHANGE =>
|
case Result.FORCED | Result.FAST_FORWARD | Result.NO_CHANGE =>
|
||||||
JsonFormat(ApiRef(update.getName, ApiObject(update.getNewObjectId.getName)))
|
JsonFormat(ApiRef.fromRef(RepositoryName(repository), git.getRepository.findRef(refName)))
|
||||||
case _ => UnprocessableEntity(result.name())
|
case _ => UnprocessableEntity(result.name())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +113,7 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
|
|||||||
* v. Delete a reference
|
* v. Delete a reference
|
||||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#delete-a-reference
|
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#delete-a-reference
|
||||||
*/
|
*/
|
||||||
delete("/api/v3/repos/:owner/:repository/git/refs/*")(referrersOnly { _ =>
|
delete("/api/v3/repos/:owner/:repository/git/refs/*")(writableUsersOnly { _ =>
|
||||||
val refName = multiParams("splat").mkString("/")
|
val refName = multiParams("splat").mkString("/")
|
||||||
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
|
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
|
||||||
val ref = git.getRepository.findRef(refName)
|
val ref = git.getRepository.findRef(refName)
|
||||||
@@ -133,4 +130,34 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
private def notFound(): ApiError = {
|
||||||
|
response.setStatus(404)
|
||||||
|
ApiError("Not Found")
|
||||||
|
}
|
||||||
|
|
||||||
|
protected def getRef(revstr: String, repository: RepositoryInfo): AnyRef = {
|
||||||
|
logger.debug(s"getRef: path '${revstr}'")
|
||||||
|
|
||||||
|
val name = RepositoryName(repository)
|
||||||
|
val result = JsonFormat(revstr match {
|
||||||
|
case "tags" => repository.tags.map(ApiRef.fromTag(name, _))
|
||||||
|
case x if x.startsWith("tags/") =>
|
||||||
|
val tagName = x.substring("tags/".length)
|
||||||
|
repository.tags.find(_.name == tagName) match {
|
||||||
|
case Some(tagInfo) => ApiRef.fromTag(name, tagInfo)
|
||||||
|
case None => notFound()
|
||||||
|
}
|
||||||
|
case other =>
|
||||||
|
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||||
|
git.getRepository().findRef(other) match {
|
||||||
|
case null => notFound()
|
||||||
|
case ref => ApiRef.fromRef(name, ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.debug(s"json result: $result")
|
||||||
|
result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ trait ApiIssueCommentControllerBase extends ControllerBase {
|
|||||||
* iv. Delete a comment
|
* iv. Delete a comment
|
||||||
* https://docs.github.com/en/rest/reference/issues#delete-an-issue-comment
|
* https://docs.github.com/en/rest/reference/issues#delete-an-issue-comment
|
||||||
*/
|
*/
|
||||||
delete("/api/v3/repos/:owner/:repo/issues/comments/:id")(readableUsersOnly { repository =>
|
delete("/api/v3/repos/:owner/:repository/issues/comments/:id")(readableUsersOnly { repository =>
|
||||||
val maybeDeleteResponse: Option[Either[ActionResult, Option[Int]]] =
|
val maybeDeleteResponse: Option[Either[ActionResult, Option[Int]]] =
|
||||||
for {
|
for {
|
||||||
commentId <- params("id").toIntOpt
|
commentId <- params("id").toIntOpt
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ trait ApiIssueControllerBase extends ControllerBase {
|
|||||||
val page = IssueSearchCondition.page(request)
|
val page = IssueSearchCondition.page(request)
|
||||||
// TODO: more api spec condition
|
// TODO: more api spec condition
|
||||||
val condition = IssueSearchCondition(request)
|
val condition = IssueSearchCondition(request)
|
||||||
val baseOwner = getAccountByUserName(repository.owner).get
|
//val baseOwner = getAccountByUserName(repository.owner).get
|
||||||
|
|
||||||
val issues: List[(Issue, Account, Option[Account])] =
|
val issues: List[(Issue, Account, List[Account])] =
|
||||||
searchIssueByApi(
|
searchIssueByApi(
|
||||||
condition = condition,
|
condition = condition,
|
||||||
offset = (page - 1) * PullRequestLimit,
|
offset = (page - 1) * PullRequestLimit,
|
||||||
@@ -40,12 +40,12 @@ trait ApiIssueControllerBase extends ControllerBase {
|
|||||||
)
|
)
|
||||||
|
|
||||||
JsonFormat(issues.map {
|
JsonFormat(issues.map {
|
||||||
case (issue, issueUser, assignedUser) =>
|
case (issue, issueUser, assigneeUsers) =>
|
||||||
ApiIssue(
|
ApiIssue(
|
||||||
issue = issue,
|
issue = issue,
|
||||||
repositoryName = RepositoryName(repository),
|
repositoryName = RepositoryName(repository),
|
||||||
user = ApiUser(issueUser),
|
user = ApiUser(issueUser),
|
||||||
assignee = assignedUser.map(ApiUser(_)),
|
assignees = assigneeUsers.map(ApiUser(_)),
|
||||||
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
|
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
|
||||||
.map(ApiLabel(_, RepositoryName(repository))),
|
.map(ApiLabel(_, RepositoryName(repository))),
|
||||||
issue.milestoneId.flatMap { getApiMilestone(repository, _) }
|
issue.milestoneId.flatMap { getApiMilestone(repository, _) }
|
||||||
@@ -61,7 +61,8 @@ trait ApiIssueControllerBase extends ControllerBase {
|
|||||||
(for {
|
(for {
|
||||||
issueId <- params("id").toIntOpt
|
issueId <- params("id").toIntOpt
|
||||||
issue <- getIssue(repository.owner, repository.name, issueId.toString)
|
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)
|
openedUser <- users.get(issue.openedUserName)
|
||||||
} yield {
|
} yield {
|
||||||
JsonFormat(
|
JsonFormat(
|
||||||
@@ -69,7 +70,7 @@ trait ApiIssueControllerBase extends ControllerBase {
|
|||||||
issue,
|
issue,
|
||||||
RepositoryName(repository),
|
RepositoryName(repository),
|
||||||
ApiUser(openedUser),
|
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))),
|
getIssueLabels(repository.owner, repository.name, issue.issueId).map(ApiLabel(_, RepositoryName(repository))),
|
||||||
issue.milestoneId.flatMap { getApiMilestone(repository, _) }
|
issue.milestoneId.flatMap { getApiMilestone(repository, _) }
|
||||||
)
|
)
|
||||||
@@ -92,7 +93,7 @@ trait ApiIssueControllerBase extends ControllerBase {
|
|||||||
repository,
|
repository,
|
||||||
data.title,
|
data.title,
|
||||||
data.body,
|
data.body,
|
||||||
data.assignees.headOption,
|
data.assignees,
|
||||||
milestone.map(_.milestoneId),
|
milestone.map(_.milestoneId),
|
||||||
None,
|
None,
|
||||||
data.labels,
|
data.labels,
|
||||||
@@ -103,7 +104,9 @@ trait ApiIssueControllerBase extends ControllerBase {
|
|||||||
issue,
|
issue,
|
||||||
RepositoryName(repository),
|
RepositoryName(repository),
|
||||||
ApiUser(loginAccount),
|
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)
|
getIssueLabels(repository.owner, repository.name, issue.issueId)
|
||||||
.map(ApiLabel(_, RepositoryName(repository))),
|
.map(ApiLabel(_, RepositoryName(repository))),
|
||||||
issue.milestoneId.flatMap { getApiMilestone(repository, _) }
|
issue.milestoneId.flatMap { getApiMilestone(repository, _) }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package gitbucket.core.controller.api
|
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.controller.ControllerBase
|
||||||
import gitbucket.core.service._
|
import gitbucket.core.service._
|
||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
@@ -121,10 +121,10 @@ trait ApiIssueLabelControllerBase extends ControllerBase {
|
|||||||
*/
|
*/
|
||||||
post("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository =>
|
post("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository =>
|
||||||
JsonFormat(for {
|
JsonFormat(for {
|
||||||
data <- extractFromJsonBody[Seq[String]]
|
data <- extractFromJsonBody[AddLabelsToAnIssue]
|
||||||
issueId <- params("id").toIntOpt
|
issueId <- params("id").toIntOpt
|
||||||
} yield {
|
} yield {
|
||||||
data.map { labelName =>
|
data.labels.map { labelName =>
|
||||||
val label = getLabel(repository.owner, repository.name, labelName).getOrElse(
|
val label = getLabel(repository.owner, repository.name, labelName).getOrElse(
|
||||||
getLabel(
|
getLabel(
|
||||||
repository.owner,
|
repository.owner,
|
||||||
@@ -160,11 +160,11 @@ trait ApiIssueLabelControllerBase extends ControllerBase {
|
|||||||
*/
|
*/
|
||||||
put("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository =>
|
put("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository =>
|
||||||
JsonFormat(for {
|
JsonFormat(for {
|
||||||
data <- extractFromJsonBody[Seq[String]]
|
data <- extractFromJsonBody[AddLabelsToAnIssue]
|
||||||
issueId <- params("id").toIntOpt
|
issueId <- params("id").toIntOpt
|
||||||
} yield {
|
} yield {
|
||||||
deleteAllIssueLabels(repository.owner, repository.name, issueId, true)
|
deleteAllIssueLabels(repository.owner, repository.name, issueId, true)
|
||||||
data.map { labelName =>
|
data.labels.map { labelName =>
|
||||||
val label = getLabel(repository.owner, repository.name, labelName).getOrElse(
|
val label = getLabel(repository.owner, repository.name, labelName).getOrElse(
|
||||||
getLabel(
|
getLabel(
|
||||||
repository.owner,
|
repository.owner,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package gitbucket.core.controller.api
|
|||||||
import gitbucket.core.api._
|
import gitbucket.core.api._
|
||||||
import gitbucket.core.controller.ControllerBase
|
import gitbucket.core.controller.ControllerBase
|
||||||
import gitbucket.core.service.MilestonesService
|
import gitbucket.core.service.MilestonesService
|
||||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
|
||||||
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
|
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
|
||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
import org.scalatra.NoContent
|
import org.scalatra.NoContent
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
|
|||||||
val condition = IssueSearchCondition(request)
|
val condition = IssueSearchCondition(request)
|
||||||
val baseOwner = getAccountByUserName(repository.owner).get
|
val baseOwner = getAccountByUserName(repository.owner).get
|
||||||
|
|
||||||
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] =
|
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account, List[Account])] =
|
||||||
searchPullRequestByApi(
|
searchPullRequestByApi(
|
||||||
condition = condition,
|
condition = condition,
|
||||||
offset = (page - 1) * PullRequestLimit,
|
offset = (page - 1) * PullRequestLimit,
|
||||||
@@ -49,7 +49,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
|
|||||||
)
|
)
|
||||||
|
|
||||||
JsonFormat(issues.map {
|
JsonFormat(issues.map {
|
||||||
case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignee) =>
|
case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignees) =>
|
||||||
ApiPullRequest(
|
ApiPullRequest(
|
||||||
issue = issue,
|
issue = issue,
|
||||||
pullRequest = pullRequest,
|
pullRequest = pullRequest,
|
||||||
@@ -58,7 +58,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
|
|||||||
user = ApiUser(issueUser),
|
user = ApiUser(issueUser),
|
||||||
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
|
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
|
||||||
.map(ApiLabel(_, RepositoryName(repository))),
|
.map(ApiLabel(_, RepositoryName(repository))),
|
||||||
assignee = assignee.map(ApiUser.apply),
|
assignees = assignees.map(ApiUser.apply),
|
||||||
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
|
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -99,7 +99,6 @@ trait ApiPullRequestControllerBase extends ControllerBase {
|
|||||||
loginUser = context.loginAccount.get.userName,
|
loginUser = context.loginAccount.get.userName,
|
||||||
title = createPullReq.title,
|
title = createPullReq.title,
|
||||||
content = createPullReq.body,
|
content = createPullReq.body,
|
||||||
assignedUserName = None,
|
|
||||||
milestoneId = None,
|
milestoneId = None,
|
||||||
priorityId = None,
|
priorityId = None,
|
||||||
isPullRequest = true
|
isPullRequest = true
|
||||||
@@ -319,8 +318,8 @@ trait ApiPullRequestControllerBase extends ControllerBase {
|
|||||||
baseOwner <- users.get(repository.owner)
|
baseOwner <- users.get(repository.owner)
|
||||||
headOwner <- users.get(pullRequest.requestUserName)
|
headOwner <- users.get(pullRequest.requestUserName)
|
||||||
issueUser <- users.get(issue.openedUserName)
|
issueUser <- users.get(issue.openedUserName)
|
||||||
assignee = issue.assignedUserName.flatMap { userName =>
|
assignees = getIssueAssignees(repository.owner, repository.name, issueId).flatMap { assignedUser =>
|
||||||
getAccountByUserName(userName, false)
|
getAccountByUserName(assignedUser.assigneeUserName, false)
|
||||||
}
|
}
|
||||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
|
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
|
||||||
} yield {
|
} yield {
|
||||||
@@ -332,7 +331,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
|
|||||||
user = ApiUser(issueUser),
|
user = ApiUser(issueUser),
|
||||||
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
|
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
|
||||||
.map(ApiLabel(_, RepositoryName(repository))),
|
.map(ApiLabel(_, RepositoryName(repository))),
|
||||||
assignee = assignee.map(ApiUser.apply),
|
assignees = assignees.map(ApiUser.apply),
|
||||||
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
|
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 gitbucket.core.util.{JGitUtil, ReferrerAuthenticator, RepositoryName}
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import org.eclipse.jgit.revwalk.RevWalk
|
import org.eclipse.jgit.revwalk.RevWalk
|
||||||
|
import org.eclipse.jgit.revwalk.filter.{
|
||||||
|
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.jdk.CollectionConverters._
|
||||||
import scala.util.Using
|
import scala.util.Using
|
||||||
|
import math.min
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter._
|
||||||
|
import java.util.Date
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
trait ApiRepositoryCommitControllerBase extends ControllerBase {
|
trait ApiRepositoryCommitControllerBase extends ControllerBase {
|
||||||
self: AccountService with CommitsService with ProtectedBranchService with ReferrerAuthenticator =>
|
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 =>
|
get("/api/v3/repos/:owner/:repository/commits")(referrersOnly { repository =>
|
||||||
val owner = repository.owner
|
val owner = repository.owner
|
||||||
val name = repository.name
|
val name = repository.name
|
||||||
// TODO: The following parameters need to be implemented. [:path, :author, :since, :until]
|
val sha = params.get("sha").filter(_.nonEmpty).getOrElse("HEAD")
|
||||||
val sha = params.getOrElse("sha", "refs/heads/master")
|
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))) {
|
Using.resource(Git.open(getRepositoryDir(owner, name))) {
|
||||||
git =>
|
git =>
|
||||||
val repo = git.getRepository
|
val repo = git.getRepository
|
||||||
@@ -31,7 +50,37 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
|
|||||||
revWalk =>
|
revWalk =>
|
||||||
val objectId = repo.resolve(sha)
|
val objectId = repo.resolve(sha)
|
||||||
revWalk.markStart(revWalk.parseCommit(objectId))
|
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 =>
|
commit =>
|
||||||
val commitInfo = new CommitInfo(commit)
|
val commitInfo = new CommitInfo(commit)
|
||||||
ApiCommits(
|
ApiCommits(
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
|
|||||||
Some("https://docs.github.com/en/rest/reference/repos#create-or-update-file-contents")
|
Some("https://docs.github.com/en/rest/reference/repos#create-or-update-file-contents")
|
||||||
)
|
)
|
||||||
case _ =>
|
case _ =>
|
||||||
val (commitId, blobId) = commitFile(
|
commitFile(
|
||||||
repository,
|
repository,
|
||||||
branch,
|
branch,
|
||||||
path,
|
path,
|
||||||
@@ -170,12 +170,12 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
|
|||||||
data.committer.map(_.name).getOrElse(loginAccount.fullName),
|
data.committer.map(_.name).getOrElse(loginAccount.fullName),
|
||||||
data.committer.map(_.email).getOrElse(loginAccount.mailAddress),
|
data.committer.map(_.email).getOrElse(loginAccount.mailAddress),
|
||||||
context.settings
|
context.settings
|
||||||
)
|
) match {
|
||||||
|
case Left(error) =>
|
||||||
blobId match {
|
ApiError(s"Failed to commit a file: ${error}", None)
|
||||||
case None =>
|
case Right((_, None)) =>
|
||||||
ApiError("Failed to commit a file.", None)
|
ApiError("Failed to commit a file.", None)
|
||||||
case Some(blobId) =>
|
case Right((commitId, Some(blobId))) =>
|
||||||
Map(
|
Map(
|
||||||
"content" -> ApiContents(
|
"content" -> ApiContents(
|
||||||
"file",
|
"file",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import scala.util.Using
|
|||||||
|
|
||||||
trait ApiRepositoryControllerBase extends ControllerBase {
|
trait ApiRepositoryControllerBase extends ControllerBase {
|
||||||
self: RepositoryService
|
self: RepositoryService
|
||||||
|
with ApiGitReferenceControllerBase
|
||||||
with RepositoryCreationService
|
with RepositoryCreationService
|
||||||
with AccountService
|
with AccountService
|
||||||
with OwnerAuthenticator
|
with OwnerAuthenticator
|
||||||
@@ -184,9 +185,11 @@ trait ApiRepositoryControllerBase extends ControllerBase {
|
|||||||
* https://docs.github.com/en/rest/reference/repos#list-repository-tags
|
* https://docs.github.com/en/rest/reference/repos#list-repository-tags
|
||||||
*/
|
*/
|
||||||
get("/api/v3/repos/:owner/:repository/tags")(referrersOnly { repository =>
|
get("/api/v3/repos/:owner/:repository/tags")(referrersOnly { repository =>
|
||||||
JsonFormat(
|
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||||
repository.tags.map(tagInfo => ApiTag(tagInfo.name, RepositoryName(repository), tagInfo.id))
|
JsonFormat(
|
||||||
)
|
self.getRef("tags", repository)
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
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 MilestoneTemplate
|
||||||
with PriorityTemplate {
|
with PriorityTemplate {
|
||||||
val openedUserName = column[String]("OPENED_USER_NAME")
|
val openedUserName = column[String]("OPENED_USER_NAME")
|
||||||
val assignedUserName = column[String]("ASSIGNED_USER_NAME")
|
|
||||||
val title = column[String]("TITLE")
|
val title = column[String]("TITLE")
|
||||||
val content = column[String]("CONTENT")
|
val content = column[String]("CONTENT")
|
||||||
val closed = column[Boolean]("CLOSED")
|
val closed = column[Boolean]("CLOSED")
|
||||||
@@ -42,7 +41,6 @@ trait IssueComponent extends TemplateComponent { self: Profile =>
|
|||||||
openedUserName,
|
openedUserName,
|
||||||
milestoneId.?,
|
milestoneId.?,
|
||||||
priorityId.?,
|
priorityId.?,
|
||||||
assignedUserName.?,
|
|
||||||
title,
|
title,
|
||||||
content.?,
|
content.?,
|
||||||
closed,
|
closed,
|
||||||
@@ -62,7 +60,6 @@ case class Issue(
|
|||||||
openedUserName: String,
|
openedUserName: String,
|
||||||
milestoneId: Option[Int],
|
milestoneId: Option[Int],
|
||||||
priorityId: Option[Int],
|
priorityId: Option[Int],
|
||||||
assignedUserName: Option[String],
|
|
||||||
title: String,
|
title: String,
|
||||||
content: Option[String],
|
content: Option[String],
|
||||||
closed: Boolean,
|
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 ReleaseAssetComponent
|
||||||
with AccountExtraMailAddressComponent
|
with AccountExtraMailAddressComponent
|
||||||
with AccountPreferenceComponent
|
with AccountPreferenceComponent
|
||||||
|
with CustomFieldComponent
|
||||||
|
with IssueCustomFieldComponent
|
||||||
|
with IssueAssigneeComponent
|
||||||
|
|
||||||
object Profile extends CoreProfile
|
object Profile extends CoreProfile
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
package gitbucket.core.plugin
|
package gitbucket.core.plugin
|
||||||
|
|
||||||
import javax.servlet.ServletContext
|
import javax.servlet.ServletContext
|
||||||
|
|
||||||
import gitbucket.core.controller.{Context, ControllerBase}
|
import gitbucket.core.controller.{Context, ControllerBase}
|
||||||
import gitbucket.core.model.{Account, Issue}
|
import gitbucket.core.model.{Account, Issue}
|
||||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||||
import io.github.gitbucket.solidbase.model.Version
|
import io.github.gitbucket.solidbase.model.Version
|
||||||
|
import org.apache.sshd.server.channel.ChannelSession
|
||||||
import org.apache.sshd.server.command.Command
|
import org.apache.sshd.server.command.Command
|
||||||
import play.twirl.api.Html
|
import play.twirl.api.Html
|
||||||
|
|
||||||
import scala.util.Using
|
import scala.util.Using
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -323,7 +324,7 @@ abstract class Plugin {
|
|||||||
/**
|
/**
|
||||||
* Override to add ssh command providers.
|
* Override to add ssh command providers.
|
||||||
*/
|
*/
|
||||||
val sshCommandProviders: Seq[PartialFunction[String, Command]] = Nil
|
val sshCommandProviders: Seq[PartialFunction[String, ChannelSession => Command]] = Nil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override to add ssh command providers.
|
* Override to add ssh command providers.
|
||||||
@@ -332,7 +333,7 @@ abstract class Plugin {
|
|||||||
registry: PluginRegistry,
|
registry: PluginRegistry,
|
||||||
context: ServletContext,
|
context: ServletContext,
|
||||||
settings: SystemSettings
|
settings: SystemSettings
|
||||||
): Seq[PartialFunction[String, Command]] = Nil
|
): Seq[PartialFunction[String, ChannelSession => Command]] = Nil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is invoked in initialization of plugin system.
|
* This method is invoked in initialization of plugin system.
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import java.nio.file.{Files, Paths, StandardWatchEventKinds}
|
|||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
import javax.servlet.ServletContext
|
import javax.servlet.ServletContext
|
||||||
import com.github.zafarkhaja.semver.Version
|
import com.github.zafarkhaja.semver.Version
|
||||||
import gitbucket.core.controller.{Context, ControllerBase}
|
import gitbucket.core.controller.{Context, ControllerBase}
|
||||||
@@ -21,6 +20,7 @@ import io.github.gitbucket.solidbase.Solidbase
|
|||||||
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
|
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
|
||||||
import io.github.gitbucket.solidbase.model.Module
|
import io.github.gitbucket.solidbase.model.Module
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.sshd.server.channel.ChannelSession
|
||||||
import org.apache.sshd.server.command.Command
|
import org.apache.sshd.server.command.Command
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import play.twirl.api.Html
|
import play.twirl.api.Html
|
||||||
@@ -58,7 +58,7 @@ class PluginRegistry {
|
|||||||
private val suggestionProviders = new ConcurrentLinkedQueue[SuggestionProvider]
|
private val suggestionProviders = new ConcurrentLinkedQueue[SuggestionProvider]
|
||||||
suggestionProviders.add(new UserNameSuggestionProvider())
|
suggestionProviders.add(new UserNameSuggestionProvider())
|
||||||
suggestionProviders.add(new IssueSuggestionProvider())
|
suggestionProviders.add(new IssueSuggestionProvider())
|
||||||
private val sshCommandProviders = new ConcurrentLinkedQueue[PartialFunction[String, Command]]()
|
private val sshCommandProviders = new ConcurrentLinkedQueue[PartialFunction[String, ChannelSession => Command]]()
|
||||||
|
|
||||||
def addPlugin(pluginInfo: PluginInfo): Unit = plugins.add(pluginInfo)
|
def addPlugin(pluginInfo: PluginInfo): Unit = plugins.add(pluginInfo)
|
||||||
|
|
||||||
@@ -177,10 +177,11 @@ class PluginRegistry {
|
|||||||
|
|
||||||
def getSuggestionProviders: Seq[SuggestionProvider] = suggestionProviders.asScala.toSeq
|
def getSuggestionProviders: Seq[SuggestionProvider] = suggestionProviders.asScala.toSeq
|
||||||
|
|
||||||
def addSshCommandProvider(sshCommandProvider: PartialFunction[String, Command]): Unit =
|
def addSshCommandProvider(sshCommandProvider: PartialFunction[String, ChannelSession => Command]): Unit =
|
||||||
sshCommandProviders.add(sshCommandProvider)
|
sshCommandProviders.add(sshCommandProvider)
|
||||||
|
|
||||||
def getSshCommandProviders: Seq[PartialFunction[String, Command]] = sshCommandProviders.asScala.toSeq
|
def getSshCommandProviders: Seq[PartialFunction[String, ChannelSession => Command]] =
|
||||||
|
sshCommandProviders.asScala.toSeq
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,9 +7,14 @@ import gitbucket.core.model.Profile.profile.blockingApi._
|
|||||||
import gitbucket.core.model.Profile.dateColumnType
|
import gitbucket.core.model.Profile.dateColumnType
|
||||||
import gitbucket.core.util.{LDAPUtil, StringUtil}
|
import gitbucket.core.util.{LDAPUtil, StringUtil}
|
||||||
import 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.plugin.PluginRegistry
|
||||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||||
|
|
||||||
|
import java.security.SecureRandom
|
||||||
|
|
||||||
trait AccountService {
|
trait AccountService {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[AccountService])
|
private val logger = LoggerFactory.getLogger(classOf[AccountService])
|
||||||
@@ -60,28 +65,28 @@ trait AccountService {
|
|||||||
case Right(ldapUserInfo) => {
|
case Right(ldapUserInfo) => {
|
||||||
// Create or update account by LDAP information
|
// Create or update account by LDAP information
|
||||||
getAccountByUserName(ldapUserInfo.userName, true) match {
|
getAccountByUserName(ldapUserInfo.userName, true) match {
|
||||||
case Some(x) if (!x.isRemoved) => {
|
case Some(x) =>
|
||||||
if (settings.ldap.get.mailAttribute.getOrElse("").isEmpty) {
|
if (x.isRemoved) {
|
||||||
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
|
logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
|
||||||
|
defaultAuthentication(userName, password)
|
||||||
} else {
|
} 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 =>
|
case None =>
|
||||||
getAccountByMailAddress(ldapUserInfo.mailAddress, true) match {
|
getAccountByMailAddress(ldapUserInfo.mailAddress, true) match {
|
||||||
case Some(x) if (!x.isRemoved) => {
|
case Some(x) =>
|
||||||
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
|
if (x.isRemoved) {
|
||||||
getAccountByUserName(ldapUserInfo.userName)
|
logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
|
||||||
}
|
defaultAuthentication(userName, password)
|
||||||
case Some(x) if (x.isRemoved) => {
|
} else {
|
||||||
logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
|
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
|
||||||
defaultAuthentication(userName, password)
|
getAccountByUserName(ldapUserInfo.userName)
|
||||||
}
|
}
|
||||||
case None => {
|
case None => {
|
||||||
createAccount(
|
createAccount(
|
||||||
ldapUserInfo.userName,
|
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 scala.util.Using
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
import gitbucket.core.controller.Context
|
import gitbucket.core.controller.Context
|
||||||
|
import gitbucket.core.util.ConfigUtil
|
||||||
import org.apache.commons.io.input.ReversedLinesFileReader
|
import org.apache.commons.io.input.ReversedLinesFileReader
|
||||||
|
|
||||||
|
import ActivityService._
|
||||||
|
|
||||||
import scala.collection.mutable.ListBuffer
|
import scala.collection.mutable.ListBuffer
|
||||||
|
|
||||||
trait ActivityService {
|
trait ActivityService {
|
||||||
@@ -27,7 +29,7 @@ trait ActivityService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit context: Context): List[Activity] = {
|
def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit context: Context): List[Activity] = {
|
||||||
if (!ActivityLog.exists()) {
|
if (!isNewsFeedEnabled() || !ActivityLog.exists()) {
|
||||||
List.empty
|
List.empty
|
||||||
} else {
|
} else {
|
||||||
val list = new ListBuffer[Activity]
|
val list = new ListBuffer[Activity]
|
||||||
@@ -51,7 +53,7 @@ trait ActivityService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def getRecentPublicActivities()(implicit context: Context): List[Activity] = {
|
def getRecentPublicActivities()(implicit context: Context): List[Activity] = {
|
||||||
if (!ActivityLog.exists()) {
|
if (!isNewsFeedEnabled() || !ActivityLog.exists()) {
|
||||||
List.empty
|
List.empty
|
||||||
} else {
|
} else {
|
||||||
val list = new ListBuffer[Activity]
|
val list = new ListBuffer[Activity]
|
||||||
@@ -69,7 +71,7 @@ trait ActivityService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def getRecentActivitiesByOwners(owners: Set[String])(implicit context: Context): List[Activity] = {
|
def getRecentActivitiesByOwners(owners: Set[String])(implicit context: Context): List[Activity] = {
|
||||||
if (!ActivityLog.exists()) {
|
if (!isNewsFeedEnabled() || !ActivityLog.exists()) {
|
||||||
List.empty
|
List.empty
|
||||||
} else {
|
} else {
|
||||||
val list = new ListBuffer[Activity]
|
val list = new ListBuffer[Activity]
|
||||||
@@ -93,3 +95,8 @@ trait ActivityService {
|
|||||||
writeLog(info.toActivity)
|
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 {
|
content match {
|
||||||
case Some(content) =>
|
case Some(content) =>
|
||||||
// Update comment
|
// 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
|
// Record comment activity
|
||||||
val commentInfo = if (issue.isPullRequest) {
|
val commentInfo = if (issue.isPullRequest) {
|
||||||
PullRequestCommentInfo(owner, name, userName, content, issue.issueId)
|
PullRequestCommentInfo(owner, name, userName, content, issue.issueId)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ trait IssueCreationService {
|
|||||||
repository: RepositoryInfo,
|
repository: RepositoryInfo,
|
||||||
title: String,
|
title: String,
|
||||||
body: Option[String],
|
body: Option[String],
|
||||||
assignee: Option[String],
|
assignees: Seq[String],
|
||||||
milestoneId: Option[Int],
|
milestoneId: Option[Int],
|
||||||
priorityId: Option[Int],
|
priorityId: Option[Int],
|
||||||
labelNames: Seq[String],
|
labelNames: Seq[String],
|
||||||
@@ -35,16 +35,19 @@ trait IssueCreationService {
|
|||||||
userName,
|
userName,
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
if (manageable) assignee else None,
|
|
||||||
if (manageable) milestoneId else None,
|
if (manageable) milestoneId else None,
|
||||||
if (manageable) priorityId else None
|
if (manageable) priorityId else None
|
||||||
)
|
)
|
||||||
val issue: Issue = getIssue(owner, name, issueId.toString).get
|
val issue: Issue = getIssue(owner, name, issueId.toString).get
|
||||||
|
|
||||||
// insert labels
|
|
||||||
if (manageable) {
|
if (manageable) {
|
||||||
|
// insert assignees
|
||||||
|
assignees.foreach { assignee =>
|
||||||
|
registerIssueAssignee(owner, name, issueId, assignee)
|
||||||
|
}
|
||||||
|
// insert labels
|
||||||
val labels = getLabels(owner, name)
|
val labels = getLabels(owner, name)
|
||||||
labelNames.map { labelName =>
|
labelNames.foreach { labelName =>
|
||||||
labels.find(_.labelName == labelName).map { label =>
|
labels.find(_.labelName == labelName).map { label =>
|
||||||
registerIssueLabel(owner, name, issueId, label.labelId)
|
registerIssueLabel(owner, name, issueId, label.labelId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,17 @@ import gitbucket.core.util.StringUtil._
|
|||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
import gitbucket.core.util.SyntaxSugars._
|
import gitbucket.core.util.SyntaxSugars._
|
||||||
import gitbucket.core.controller.Context
|
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._
|
||||||
import gitbucket.core.model.Profile.profile._
|
import gitbucket.core.model.Profile.profile._
|
||||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||||
@@ -118,8 +128,7 @@ trait IssuesService {
|
|||||||
def countIssueGroupByLabels(
|
def countIssueGroupByLabels(
|
||||||
owner: String,
|
owner: String,
|
||||||
repository: String,
|
repository: String,
|
||||||
condition: IssueSearchCondition,
|
condition: IssueSearchCondition
|
||||||
filterUser: Map[String, String]
|
|
||||||
)(implicit s: Session): Map[String, Int] = {
|
)(implicit s: Session): Map[String, Int] = {
|
||||||
|
|
||||||
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), IssueSearchOption.Issues)
|
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), IssueSearchOption.Issues)
|
||||||
@@ -138,7 +147,7 @@ trait IssuesService {
|
|||||||
t3.labelName
|
t3.labelName
|
||||||
}
|
}
|
||||||
.map {
|
.map {
|
||||||
case labelName ~ t =>
|
case (labelName, t) =>
|
||||||
labelName -> t.length
|
labelName -> t.length
|
||||||
}
|
}
|
||||||
.list
|
.list
|
||||||
@@ -156,8 +165,7 @@ trait IssuesService {
|
|||||||
def countIssueGroupByPriorities(
|
def countIssueGroupByPriorities(
|
||||||
owner: String,
|
owner: String,
|
||||||
repository: String,
|
repository: String,
|
||||||
condition: IssueSearchCondition,
|
condition: IssueSearchCondition
|
||||||
filterUser: Map[String, String]
|
|
||||||
)(implicit s: Session): Map[String, Int] = {
|
)(implicit s: Session): Map[String, Int] = {
|
||||||
|
|
||||||
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), IssueSearchOption.Issues)
|
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), IssueSearchOption.Issues)
|
||||||
@@ -171,7 +179,7 @@ trait IssuesService {
|
|||||||
t2.priorityName
|
t2.priorityName
|
||||||
}
|
}
|
||||||
.map {
|
.map {
|
||||||
case priorityName ~ t =>
|
case (priorityName, t) =>
|
||||||
priorityName -> t.length
|
priorityName -> t.length
|
||||||
}
|
}
|
||||||
.list
|
.list
|
||||||
@@ -207,9 +215,11 @@ trait IssuesService {
|
|||||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t1.byPriority(t6.userName, t6.repositoryName, t6.priorityId) }
|
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t1.byPriority(t6.userName, t6.repositoryName, t6.priorityId) }
|
||||||
.joinLeft(PullRequests)
|
.joinLeft(PullRequests)
|
||||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t1.byIssue(t7.userName, t7.repositoryName, t7.issueId) }
|
.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 {
|
.map {
|
||||||
case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 =>
|
case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 =>
|
||||||
(
|
(
|
||||||
t1,
|
t1,
|
||||||
t2.commentCount,
|
t2.commentCount,
|
||||||
@@ -218,7 +228,8 @@ trait IssuesService {
|
|||||||
t4.map(_.color),
|
t4.map(_.color),
|
||||||
t5.map(_.title),
|
t5.map(_.title),
|
||||||
t6.map(_.priorityName),
|
t6.map(_.priorityName),
|
||||||
t7.map(_.commitIdTo)
|
t7.map(_.commitIdTo),
|
||||||
|
t8.map(_.assigneeUserName)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.list
|
.list
|
||||||
@@ -228,36 +239,51 @@ trait IssuesService {
|
|||||||
|
|
||||||
result.map { issues =>
|
result.map { issues =>
|
||||||
issues.head match {
|
issues.head match {
|
||||||
case (issue, commentCount, _, _, _, milestone, priority, commitId) =>
|
case (issue, commentCount, _, _, _, milestone, priority, commitId, _) =>
|
||||||
IssueInfo(
|
IssueInfo(
|
||||||
issue,
|
issue,
|
||||||
issues.flatMap { t =>
|
issues
|
||||||
t._3.map(Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get))
|
.flatMap { t =>
|
||||||
} toList,
|
t._3.map(Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get))
|
||||||
|
}
|
||||||
|
.distinct
|
||||||
|
.toList,
|
||||||
milestone,
|
milestone,
|
||||||
priority,
|
priority,
|
||||||
commentCount,
|
commentCount,
|
||||||
commitId
|
commitId,
|
||||||
|
issues.flatMap(_._9).distinct
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} toList
|
} toList
|
||||||
}
|
}
|
||||||
|
|
||||||
/** for api
|
/** for api
|
||||||
* @return (issue, issueUser, assignedUser)
|
* @return (issue, issueUser, Seq(assigneeUsers))
|
||||||
*/
|
*/
|
||||||
def searchIssueByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)(
|
def searchIssueByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)(
|
||||||
implicit s: Session
|
implicit s: Session
|
||||||
): List[(Issue, Account, Option[Account])] = {
|
): List[(Issue, Account, List[Account])] = {
|
||||||
// get issues and comment count and labels
|
// get issues and comment count and labels
|
||||||
searchIssueQueryBase(condition, IssueSearchOption.Issues, offset, limit, repos)
|
searchIssueQueryBase(condition, IssueSearchOption.Issues, offset, limit, repos)
|
||||||
.join(Accounts)
|
.join(Accounts)
|
||||||
.on { case t1 ~ t2 ~ i ~ t3 => t3.userName === t1.openedUserName }
|
.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)
|
.joinLeft(Accounts)
|
||||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t4.userName === t1.assignedUserName }
|
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t4.map(_.assigneeUserName) }
|
||||||
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 => i asc }
|
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => i asc }
|
||||||
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 => (t1, t3, t4) }
|
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => (t1, t3, t5) }
|
||||||
.list
|
.list
|
||||||
|
.groupBy {
|
||||||
|
case (issue, account, _) =>
|
||||||
|
(issue, account)
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
case (_, values) =>
|
||||||
|
(values.head._1, values.head._2, values.flatMap(_._3))
|
||||||
|
}
|
||||||
|
.toList
|
||||||
}
|
}
|
||||||
|
|
||||||
/** for api
|
/** for api
|
||||||
@@ -265,7 +291,7 @@ trait IssuesService {
|
|||||||
*/
|
*/
|
||||||
def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)(
|
def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)(
|
||||||
implicit s: Session
|
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
|
// get issues and comment count and labels
|
||||||
searchIssueQueryBase(condition, IssueSearchOption.PullRequests, offset, limit, repos)
|
searchIssueQueryBase(condition, IssueSearchOption.PullRequests, offset, limit, repos)
|
||||||
.join(PullRequests)
|
.join(PullRequests)
|
||||||
@@ -276,11 +302,30 @@ trait IssuesService {
|
|||||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t1.openedUserName }
|
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t1.openedUserName }
|
||||||
.join(Accounts)
|
.join(Accounts)
|
||||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t6.userName === t4.userName }
|
.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)
|
.joinLeft(Accounts)
|
||||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t7.userName === t1.assignedUserName }
|
.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 => i asc }
|
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => i asc }
|
||||||
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => (t1, t5, t2.commentCount, t3, t4, t6, t7) }
|
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => (t1, t5, t2.commentCount, t3, t4, t6, t8) }
|
||||||
.list
|
.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(
|
private def searchIssueQueryBase(
|
||||||
@@ -347,7 +392,7 @@ trait IssuesService {
|
|||||||
case _ => t1.closed === true || t1.closed === false
|
case _ => t1.closed === true || t1.closed === false
|
||||||
}).&&(t1.milestoneId.? isEmpty, condition.milestone == Some(None))
|
}).&&(t1.milestoneId.? isEmpty, condition.milestone == Some(None))
|
||||||
.&&(t1.priorityId.? isEmpty, condition.priority == 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) &&
|
.&&(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
|
||||||
(searchOption match {
|
(searchOption match {
|
||||||
case IssueSearchOption.Issues => t1.pullRequest === false
|
case IssueSearchOption.Issues => t1.pullRequest === false
|
||||||
@@ -371,7 +416,13 @@ trait IssuesService {
|
|||||||
condition.priority.flatten.isDefined
|
condition.priority.flatten.isDefined
|
||||||
)
|
)
|
||||||
// Assignee filter
|
// 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
|
// Label filter
|
||||||
.&&(
|
.&&(
|
||||||
IssueLabels filter { t2 =>
|
IssueLabels filter { t2 =>
|
||||||
@@ -396,7 +447,9 @@ trait IssuesService {
|
|||||||
.&&(t1.userName inSetBind condition.groups, condition.groups.nonEmpty)
|
.&&(t1.userName inSetBind condition.groups, condition.groups.nonEmpty)
|
||||||
// Mentioned filter
|
// 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 =>
|
(IssueComments filter { t2 =>
|
||||||
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind)
|
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind)
|
||||||
} exists),
|
} exists),
|
||||||
@@ -410,7 +463,6 @@ trait IssuesService {
|
|||||||
loginUser: String,
|
loginUser: String,
|
||||||
title: String,
|
title: String,
|
||||||
content: Option[String],
|
content: Option[String],
|
||||||
assignedUserName: Option[String],
|
|
||||||
milestoneId: Option[Int],
|
milestoneId: Option[Int],
|
||||||
priorityId: Option[Int],
|
priorityId: Option[Int],
|
||||||
isPullRequest: Boolean = false
|
isPullRequest: Boolean = false
|
||||||
@@ -427,7 +479,6 @@ trait IssuesService {
|
|||||||
loginUser,
|
loginUser,
|
||||||
milestoneId,
|
milestoneId,
|
||||||
priorityId,
|
priorityId,
|
||||||
assignedUserName,
|
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
false,
|
false,
|
||||||
@@ -509,7 +560,7 @@ trait IssuesService {
|
|||||||
content: String,
|
content: String,
|
||||||
action: String
|
action: String
|
||||||
)(implicit s: Session): Int = {
|
)(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(
|
IssueComments returning IssueComments.map(_.commentId) insert IssueComment(
|
||||||
userName = owner,
|
userName = owner,
|
||||||
repositoryName = repository,
|
repositoryName = repository,
|
||||||
@@ -542,35 +593,91 @@ trait IssuesService {
|
|||||||
.update(true)
|
.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,
|
owner: String,
|
||||||
repository: String,
|
repository: String,
|
||||||
issueId: Int,
|
issueId: Int,
|
||||||
assignedUserName: Option[String],
|
assigneeUserName: String,
|
||||||
insertComment: Boolean = false
|
insertComment: Boolean = false
|
||||||
)(implicit context: Context, s: Session): Int = {
|
)(
|
||||||
val oldAssigned = getIssue(owner, repository, s"${issueId}").get.assignedUserName
|
implicit context: Context,
|
||||||
val assigned = assignedUserName
|
s: Session
|
||||||
|
): Int = {
|
||||||
val assigner = context.loginAccount.map(_.userName)
|
val assigner = context.loginAccount.map(_.userName)
|
||||||
if (insertComment) {
|
if (insertComment) {
|
||||||
IssueComments insert IssueComment(
|
IssueComments insert IssueComment(
|
||||||
userName = owner,
|
userName = owner,
|
||||||
repositoryName = repository,
|
repositoryName = repository,
|
||||||
issueId = issueId,
|
issueId = issueId,
|
||||||
action = "assign",
|
action = "add_assignee",
|
||||||
commentedUserName = assigner.getOrElse("Unknown user"),
|
commentedUserName = assigner.getOrElse("Unknown user"),
|
||||||
content = s"""${oldAssigned.getOrElse("Not assigned")}:${assigned.getOrElse("Not assigned")}""",
|
content = assigneeUserName,
|
||||||
registeredDate = currentDate,
|
registeredDate = currentDate,
|
||||||
updatedDate = currentDate
|
updatedDate = currentDate
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
for (issue <- getIssue(owner, repository, issueId.toString); repo <- getRepository(owner, repository)) {
|
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
|
IssueAssignees insert IssueAssignee(owner, repository, issueId, assigneeUserName)
|
||||||
.filter(_.byPrimaryKey(owner, repository, issueId))
|
}
|
||||||
.map(t => (t.assignedUserName ?, t.updatedDate))
|
|
||||||
.update(assignedUserName, currentDate)
|
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(
|
def updateMilestoneId(
|
||||||
@@ -635,8 +742,10 @@ trait IssuesService {
|
|||||||
.update(priorityId, currentDate)
|
.update(priorityId, currentDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
def updateComment(issueId: Int, commentId: Int, content: String)(implicit s: Session): Int = {
|
def updateComment(owner: String, repository: String, issueId: Int, commentId: Int, content: String)(
|
||||||
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate)
|
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)
|
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.updatedDate)).update(content, currentDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,13 +753,13 @@ trait IssuesService {
|
|||||||
implicit context: Context,
|
implicit context: Context,
|
||||||
s: Session
|
s: Session
|
||||||
): Int = {
|
): Int = {
|
||||||
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate)
|
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(_.updatedDate).update(currentDate)
|
||||||
IssueComments.filter(_.byPrimaryKey(commentId)).firstOption match {
|
IssueComments.filter(_.byPrimaryKey(commentId)).first match {
|
||||||
case Some(c) if c.action == "reopen_comment" =>
|
case c if c.action == "reopen_comment" =>
|
||||||
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.action)).update("Reopen", "reopen")
|
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")
|
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.action)).update("Close", "close")
|
||||||
case Some(_) =>
|
case _ =>
|
||||||
IssueComments.filter(_.byPrimaryKey(commentId)).delete
|
IssueComments.filter(_.byPrimaryKey(commentId)).delete
|
||||||
IssueComments insert IssueComment(
|
IssueComments insert IssueComment(
|
||||||
userName = owner,
|
userName = owner,
|
||||||
@@ -855,39 +964,39 @@ object IssuesService {
|
|||||||
|
|
||||||
def nonEmpty: Boolean = !isEmpty
|
def nonEmpty: Boolean = !isEmpty
|
||||||
|
|
||||||
def toFilterString: String =
|
// def toFilterString: String =
|
||||||
(
|
// (
|
||||||
List(
|
// List(
|
||||||
Some(s"is:${state}"),
|
// Some(s"is:${state}"),
|
||||||
author.map(author => s"author:${author}"),
|
// author.map(author => s"author:${author}"),
|
||||||
assigned.map(assignee => s"assignee:${assignee}"),
|
// assigned.map(assignee => s"assignee:${assignee}"),
|
||||||
mentioned.map(mentioned => s"mentions:${mentioned}")
|
// mentioned.map(mentioned => s"mentions:${mentioned}")
|
||||||
).flatten ++
|
// ).flatten ++
|
||||||
labels.map(label => s"label:${label}") ++
|
// labels.map(label => s"label:${label}") ++
|
||||||
List(
|
// List(
|
||||||
milestone.map {
|
// milestone.map {
|
||||||
case Some(x) => s"milestone:${x}"
|
// case Some(x) => s"milestone:${x}"
|
||||||
case None => "no:milestone"
|
// case None => "no:milestone"
|
||||||
},
|
// },
|
||||||
priority.map {
|
// priority.map {
|
||||||
case Some(x) => s"priority:${x}"
|
// case Some(x) => s"priority:${x}"
|
||||||
case None => "no:priority"
|
// case None => "no:priority"
|
||||||
},
|
// },
|
||||||
(sort, direction) match {
|
// (sort, direction) match {
|
||||||
case ("created", "desc") => None
|
// case ("created", "desc") => None
|
||||||
case ("created", "asc") => Some("sort:created-asc")
|
// case ("created", "asc") => Some("sort:created-asc")
|
||||||
case ("comments", "desc") => Some("sort:comments-desc")
|
// case ("comments", "desc") => Some("sort:comments-desc")
|
||||||
case ("comments", "asc") => Some("sort:comments-asc")
|
// case ("comments", "asc") => Some("sort:comments-asc")
|
||||||
case ("updated", "desc") => Some("sort:updated-desc")
|
// case ("updated", "desc") => Some("sort:updated-desc")
|
||||||
case ("updated", "asc") => Some("sort:updated-asc")
|
// case ("updated", "asc") => Some("sort:updated-asc")
|
||||||
case ("priority", "desc") => Some("sort:priority-desc")
|
// case ("priority", "desc") => Some("sort:priority-desc")
|
||||||
case ("priority", "asc") => Some("sort:priority-asc")
|
// case ("priority", "asc") => Some("sort:priority-asc")
|
||||||
case x => throw new MatchError(x)
|
// case x => throw new MatchError(x)
|
||||||
},
|
// },
|
||||||
visibility.map(visibility => s"visibility:${visibility}")
|
// visibility.map(visibility => s"visibility:${visibility}")
|
||||||
).flatten ++
|
// ).flatten ++
|
||||||
groups.map(group => s"group:${group}")
|
// groups.map(group => s"group:${group}")
|
||||||
).mkString(" ")
|
// ).mkString(" ")
|
||||||
|
|
||||||
def toURL: String =
|
def toURL: String =
|
||||||
"?" + List(
|
"?" + List(
|
||||||
@@ -981,7 +1090,8 @@ object IssuesService {
|
|||||||
milestone: Option[String],
|
milestone: Option[String],
|
||||||
priority: Option[String],
|
priority: Option[String],
|
||||||
commentCount: Int,
|
commentCount: Int,
|
||||||
commitId: Option[String]
|
commitId: Option[String],
|
||||||
|
assignees: Seq[String]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package gitbucket.core.service
|
package gitbucket.core.service
|
||||||
|
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
import com.nimbusds.jose.JWSAlgorithm.Family
|
import com.nimbusds.jose.JWSAlgorithm.Family
|
||||||
import com.nimbusds.jose.proc.BadJOSEException
|
import com.nimbusds.jose.proc.BadJOSEException
|
||||||
import com.nimbusds.jose.util.DefaultResourceRetriever
|
import com.nimbusds.jose.util.DefaultResourceRetriever
|
||||||
import com.nimbusds.jose.{JOSEException, JWSAlgorithm}
|
import com.nimbusds.jose.{JOSEException, JWSAlgorithm}
|
||||||
|
import com.nimbusds.jwt.JWT
|
||||||
import com.nimbusds.oauth2.sdk._
|
import com.nimbusds.oauth2.sdk._
|
||||||
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic
|
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic
|
||||||
import com.nimbusds.oauth2.sdk.id.{ClientID, Issuer, State}
|
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.
|
* Proceed the OpenID Connect authentication.
|
||||||
*
|
*
|
||||||
@@ -60,7 +65,7 @@ trait OpenIDConnectService {
|
|||||||
* @param state State saved in the session
|
* @param state State saved in the session
|
||||||
* @param nonce Nonce saved in the session
|
* @param nonce Nonce saved in the session
|
||||||
* @param oidc OIDC settings
|
* @param oidc OIDC settings
|
||||||
* @return ID token
|
* @return (ID token, GitBucket account)
|
||||||
*/
|
*/
|
||||||
def authenticate(
|
def authenticate(
|
||||||
params: Map[String, String],
|
params: Map[String, String],
|
||||||
@@ -68,22 +73,25 @@ trait OpenIDConnectService {
|
|||||||
state: State,
|
state: State,
|
||||||
nonce: Nonce,
|
nonce: Nonce,
|
||||||
oidc: SystemSettingsService.OIDC
|
oidc: SystemSettingsService.OIDC
|
||||||
)(implicit s: Session): Option[Account] =
|
)(implicit s: Session): Option[(JWT, Account)] =
|
||||||
validateOIDCAuthenticationResponse(params, state, redirectURI) flatMap { authenticationResponse =>
|
validateOIDCAuthenticationResponse(params, state, redirectURI) flatMap { authenticationResponse =>
|
||||||
obtainOIDCToken(authenticationResponse.getAuthorizationCode, nonce, redirectURI, oidc) flatMap { claims =>
|
obtainOIDCToken(authenticationResponse.getAuthorizationCode, nonce, redirectURI, oidc) flatMap {
|
||||||
Seq("email", "preferred_username", "name").map(k => Option(claims.getStringClaim(k))) match {
|
case (jwt, claims) =>
|
||||||
case Seq(Some(email), preferredUsername, name) =>
|
Seq("email", "preferred_username", "name").map(k => Option(claims.getStringClaim(k))) match {
|
||||||
getOrCreateFederatedUser(
|
case Seq(Some(email), preferredUsername, name) =>
|
||||||
claims.getIssuer.getValue,
|
getOrCreateFederatedUser(
|
||||||
claims.getSubject.getValue,
|
claims.getIssuer.getValue,
|
||||||
email,
|
claims.getSubject.getValue,
|
||||||
preferredUsername,
|
email,
|
||||||
name
|
preferredUsername,
|
||||||
)
|
name
|
||||||
case _ =>
|
).map { account =>
|
||||||
logger.info(s"OIDC ID token must have an email claim: claims=${claims.toJSONObject}")
|
(jwt, account)
|
||||||
None
|
}
|
||||||
}
|
case _ =>
|
||||||
|
logger.info(s"OIDC ID token must have an email claim: claims=${claims.toJSONObject}")
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +144,7 @@ trait OpenIDConnectService {
|
|||||||
nonce: Nonce,
|
nonce: Nonce,
|
||||||
redirectURI: URI,
|
redirectURI: URI,
|
||||||
oidc: SystemSettingsService.OIDC
|
oidc: SystemSettingsService.OIDC
|
||||||
): Option[IDTokenClaimsSet] = {
|
): Option[(JWT, IDTokenClaimsSet)] = {
|
||||||
val metadata = OIDCProviderMetadata.resolve(oidc.issuer)
|
val metadata = OIDCProviderMetadata.resolve(oidc.issuer)
|
||||||
val tokenRequest = new TokenRequest(
|
val tokenRequest = new TokenRequest(
|
||||||
metadata.getTokenEndpointURI,
|
metadata.getTokenEndpointURI,
|
||||||
@@ -173,7 +181,7 @@ trait OpenIDConnectService {
|
|||||||
metadata: OIDCProviderMetadata,
|
metadata: OIDCProviderMetadata,
|
||||||
nonce: Nonce,
|
nonce: Nonce,
|
||||||
oidc: SystemSettingsService.OIDC
|
oidc: SystemSettingsService.OIDC
|
||||||
): Option[IDTokenClaimsSet] =
|
): Option[(JWT, IDTokenClaimsSet)] =
|
||||||
Option(response.getOIDCTokens.getIDToken) match {
|
Option(response.getOIDCTokens.getIDToken) match {
|
||||||
case Some(jwt) =>
|
case Some(jwt) =>
|
||||||
val validator = oidc.jwsAlgorithm map { jwsAlgorithm =>
|
val validator = oidc.jwsAlgorithm map { jwsAlgorithm =>
|
||||||
@@ -188,7 +196,7 @@ trait OpenIDConnectService {
|
|||||||
new IDTokenValidator(metadata.getIssuer, oidc.clientID)
|
new IDTokenValidator(metadata.getIssuer, oidc.clientID)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Some(validator.validate(jwt, nonce))
|
Some((jwt, validator.validate(jwt, nonce)))
|
||||||
} catch {
|
} catch {
|
||||||
case e @ (_: BadJOSEException | _: JOSEException) =>
|
case e @ (_: BadJOSEException | _: JOSEException) =>
|
||||||
logger.info(s"OIDC ID token has error: $e")
|
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(
|
def getRequestCompareInfo(
|
||||||
userName: String,
|
userName: String,
|
||||||
repositoryName: String,
|
repositoryName: String,
|
||||||
@@ -579,7 +613,7 @@ trait PullRequestService {
|
|||||||
case (oldGit, newGit) =>
|
case (oldGit, newGit) =>
|
||||||
if (originRepository.branchList.contains(originId)) {
|
if (originRepository.branchList.contains(originId)) {
|
||||||
val forkedId2 =
|
val forkedId2 =
|
||||||
forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.id }.getOrElse(forkedId)
|
forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.commitId }.getOrElse(forkedId)
|
||||||
|
|
||||||
val originId2 = JGitUtil.getForkedCommitId(
|
val originId2 = JGitUtil.getForkedCommitId(
|
||||||
oldGit,
|
oldGit,
|
||||||
@@ -596,9 +630,9 @@ trait PullRequestService {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
val originId2 =
|
val originId2 =
|
||||||
originRepository.tags.collectFirst { case x if x.name == originId => x.id }.getOrElse(originId)
|
originRepository.tags.collectFirst { case x if x.name == originId => x.commitId }.getOrElse(originId)
|
||||||
val forkedId2 =
|
val forkedId2 =
|
||||||
forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.id }.getOrElse(forkedId)
|
forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.commitId }.getOrElse(forkedId)
|
||||||
|
|
||||||
(Option(oldGit.getRepository.resolve(originId2)), Option(newGit.getRepository.resolve(forkedId2)))
|
(Option(oldGit.getRepository.resolve(originId2)), Option(newGit.getRepository.resolve(forkedId2)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ trait RepositoryCommitFileService {
|
|||||||
settings: SystemSettings
|
settings: SystemSettings
|
||||||
)(
|
)(
|
||||||
f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => Unit
|
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)(
|
_createFiles(repository, branch, message, loginAccount, loginAccount.fullName, loginAccount.mailAddress, settings)(
|
||||||
f
|
f
|
||||||
)._1
|
).map(_._1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,7 +58,7 @@ trait RepositoryCommitFileService {
|
|||||||
commit: String,
|
commit: String,
|
||||||
loginAccount: Account,
|
loginAccount: Account,
|
||||||
settings: SystemSettings
|
settings: SystemSettings
|
||||||
)(implicit s: Session, c: JsonFormat.Context): (ObjectId, Option[ObjectId]) = {
|
)(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, Option[ObjectId])] = {
|
||||||
commitFile(
|
commitFile(
|
||||||
repository,
|
repository,
|
||||||
branch,
|
branch,
|
||||||
@@ -92,7 +92,7 @@ trait RepositoryCommitFileService {
|
|||||||
committerName: String,
|
committerName: String,
|
||||||
committerMailAddress: String,
|
committerMailAddress: String,
|
||||||
settings: SystemSettings
|
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 =>
|
val newPath = newFileName.map { newFileName =>
|
||||||
if (path.length == 0) newFileName else s"${path}/${newFileName}"
|
if (path.length == 0) newFileName else s"${path}/${newFileName}"
|
||||||
@@ -141,7 +141,7 @@ trait RepositoryCommitFileService {
|
|||||||
settings: SystemSettings
|
settings: SystemSettings
|
||||||
)(
|
)(
|
||||||
f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => R
|
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}") {
|
LockUtil.lock(s"${repository.owner}/${repository.name}") {
|
||||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||||
@@ -177,11 +177,11 @@ trait RepositoryCommitFileService {
|
|||||||
error match {
|
error match {
|
||||||
case Some(error) =>
|
case Some(error) =>
|
||||||
// commit is rejected
|
// commit is rejected
|
||||||
// TODO Notify commit failure to edited user
|
|
||||||
val refUpdate = git.getRepository.updateRef(headName)
|
val refUpdate = git.getRepository.updateRef(headName)
|
||||||
refUpdate.setNewObjectId(headTip)
|
refUpdate.setNewObjectId(headTip)
|
||||||
refUpdate.setForceUpdate(true)
|
refUpdate.setForceUpdate(true)
|
||||||
refUpdate.update()
|
refUpdate.update()
|
||||||
|
Left(error)
|
||||||
|
|
||||||
case None =>
|
case None =>
|
||||||
// update refs
|
// update refs
|
||||||
@@ -242,8 +242,8 @@ trait RepositoryCommitFileService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Right((commitId, result))
|
||||||
}
|
}
|
||||||
(commitId, result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import gitbucket.core.util.JGitUtil.FileInfo
|
|||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import org.eclipse.jgit.lib.{Repository => _}
|
import org.eclipse.jgit.lib.{Repository => _}
|
||||||
|
|
||||||
import scala.util.Using
|
import scala.util.Using
|
||||||
|
|
||||||
trait RepositoryService {
|
trait RepositoryService {
|
||||||
@@ -253,6 +254,7 @@ trait RepositoryService {
|
|||||||
Labels.filter(_.byRepository(userName, repositoryName)).delete
|
Labels.filter(_.byRepository(userName, repositoryName)).delete
|
||||||
IssueComments.filter(_.byRepository(userName, repositoryName)).delete
|
IssueComments.filter(_.byRepository(userName, repositoryName)).delete
|
||||||
PullRequests.filter(_.byRepository(userName, repositoryName)).delete
|
PullRequests.filter(_.byRepository(userName, repositoryName)).delete
|
||||||
|
IssueAssignees.filter(_.byRepository(userName, repositoryName)).delete
|
||||||
Issues.filter(_.byRepository(userName, repositoryName)).delete
|
Issues.filter(_.byRepository(userName, repositoryName)).delete
|
||||||
Priorities.filter(_.byRepository(userName, repositoryName)).delete
|
Priorities.filter(_.byRepository(userName, repositoryName)).delete
|
||||||
IssueId.filter(_.byRepository(userName, repositoryName)).delete
|
IssueId.filter(_.byRepository(userName, repositoryName)).delete
|
||||||
@@ -341,10 +343,7 @@ trait RepositoryService {
|
|||||||
repository.originUserName.getOrElse(repository.userName),
|
repository.originUserName.getOrElse(repository.userName),
|
||||||
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
||||||
),
|
),
|
||||||
getOpenMilestones(
|
getOpenMilestones(repository.userName, repository.repositoryName),
|
||||||
repository.originUserName.getOrElse(repository.userName),
|
|
||||||
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
|
||||||
),
|
|
||||||
getRepositoryManagers(repository.userName, repository.repositoryName)
|
getRepositoryManagers(repository.userName, repository.repositoryName)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -835,12 +834,10 @@ object RepositoryService {
|
|||||||
|
|
||||||
def httpUrl(owner: String, name: String)(implicit context: Context): String =
|
def httpUrl(owner: String, name: String)(implicit context: Context): String =
|
||||||
s"${context.baseUrl}/git/${owner}/${name}.git"
|
s"${context.baseUrl}/git/${owner}/${name}.git"
|
||||||
|
|
||||||
def sshUrl(owner: String, name: String)(implicit context: Context): Option[String] =
|
def sshUrl(owner: String, name: String)(implicit context: Context): Option[String] =
|
||||||
if (context.settings.ssh.enabled) {
|
context.settings.sshUrl(owner, name)
|
||||||
context.settings.sshAddress.map { x =>
|
|
||||||
s"ssh://${x.genericUser}@${x.host}:${x.port}/${owner}/${name}.git"
|
|
||||||
}
|
|
||||||
} else None
|
|
||||||
def openRepoUrl(openUrl: String)(implicit context: Context): String =
|
def openRepoUrl(openUrl: String)(implicit context: Context): String =
|
||||||
s"github-${context.platform}://openRepo/${openUrl}"
|
s"github-${context.platform}://openRepo/${openUrl}"
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import javax.servlet.http.HttpServletRequest
|
|||||||
import com.nimbusds.jose.JWSAlgorithm
|
import com.nimbusds.jose.JWSAlgorithm
|
||||||
import com.nimbusds.oauth2.sdk.auth.Secret
|
import com.nimbusds.oauth2.sdk.auth.Secret
|
||||||
import com.nimbusds.oauth2.sdk.id.{ClientID, Issuer}
|
import com.nimbusds.oauth2.sdk.id.{ClientID, Issuer}
|
||||||
import gitbucket.core.service.SystemSettingsService._
|
import gitbucket.core.service.SystemSettingsService.{getOptionValue, _}
|
||||||
import gitbucket.core.util.ConfigUtil._
|
import gitbucket.core.util.ConfigUtil._
|
||||||
import gitbucket.core.util.Directory._
|
import gitbucket.core.util.Directory._
|
||||||
|
|
||||||
import scala.util.Using
|
import scala.util.Using
|
||||||
|
|
||||||
trait SystemSettingsService {
|
trait SystemSettingsService {
|
||||||
@@ -17,20 +18,27 @@ trait SystemSettingsService {
|
|||||||
val props = new java.util.Properties()
|
val props = new java.util.Properties()
|
||||||
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
|
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
|
||||||
settings.information.foreach(x => props.setProperty(Information, x))
|
settings.information.foreach(x => props.setProperty(Information, x))
|
||||||
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
|
props.setProperty(AllowAccountRegistration, settings.basicBehavior.allowAccountRegistration.toString)
|
||||||
props.setProperty(AllowAnonymousAccess, settings.allowAnonymousAccess.toString)
|
props.setProperty(AllowResetPassword, settings.basicBehavior.allowResetPassword.toString)
|
||||||
props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString)
|
props.setProperty(AllowAnonymousAccess, settings.basicBehavior.allowAnonymousAccess.toString)
|
||||||
props.setProperty(RepositoryOperationCreate, settings.repositoryOperation.create.toString)
|
props.setProperty(IsCreateRepoOptionPublic, settings.basicBehavior.isCreateRepoOptionPublic.toString)
|
||||||
props.setProperty(RepositoryOperationDelete, settings.repositoryOperation.delete.toString)
|
props.setProperty(RepositoryOperationCreate, settings.basicBehavior.repositoryOperation.create.toString)
|
||||||
props.setProperty(RepositoryOperationRename, settings.repositoryOperation.rename.toString)
|
props.setProperty(RepositoryOperationDelete, settings.basicBehavior.repositoryOperation.delete.toString)
|
||||||
props.setProperty(RepositoryOperationTransfer, settings.repositoryOperation.transfer.toString)
|
props.setProperty(RepositoryOperationRename, settings.basicBehavior.repositoryOperation.rename.toString)
|
||||||
props.setProperty(RepositoryOperationFork, settings.repositoryOperation.fork.toString)
|
props.setProperty(RepositoryOperationTransfer, settings.basicBehavior.repositoryOperation.transfer.toString)
|
||||||
props.setProperty(Gravatar, settings.gravatar.toString)
|
props.setProperty(RepositoryOperationFork, settings.basicBehavior.repositoryOperation.fork.toString)
|
||||||
props.setProperty(Notification, settings.notification.toString)
|
props.setProperty(Gravatar, settings.basicBehavior.gravatar.toString)
|
||||||
props.setProperty(LimitVisibleRepositories, settings.limitVisibleRepositories.toString)
|
props.setProperty(Notification, settings.basicBehavior.notification.toString)
|
||||||
|
props.setProperty(LimitVisibleRepositories, settings.basicBehavior.limitVisibleRepositories.toString)
|
||||||
props.setProperty(SshEnabled, settings.ssh.enabled.toString)
|
props.setProperty(SshEnabled, settings.ssh.enabled.toString)
|
||||||
settings.ssh.sshHost.foreach(x => props.setProperty(SshHost, x.trim))
|
settings.ssh.bindAddress.foreach { bindAddress =>
|
||||||
settings.ssh.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
|
props.setProperty(SshBindAddressHost, bindAddress.host.trim())
|
||||||
|
props.setProperty(SshBindAddressPort, bindAddress.port.toString)
|
||||||
|
}
|
||||||
|
settings.ssh.publicAddress.foreach { publicAddress =>
|
||||||
|
props.setProperty(SshPublicAddressHost, publicAddress.host.trim())
|
||||||
|
props.setProperty(SshPublicAddressPort, publicAddress.port.toString)
|
||||||
|
}
|
||||||
props.setProperty(UseSMTP, settings.useSMTP.toString)
|
props.setProperty(UseSMTP, settings.useSMTP.toString)
|
||||||
if (settings.useSMTP) {
|
if (settings.useSMTP) {
|
||||||
settings.smtp.foreach { smtp =>
|
settings.smtp.foreach { smtp =>
|
||||||
@@ -95,26 +103,44 @@ trait SystemSettingsService {
|
|||||||
props.load(in)
|
props.load(in)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
loadSystemSettings(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
def loadSystemSettings(props: java.util.Properties): SystemSettings = {
|
||||||
SystemSettings(
|
SystemSettings(
|
||||||
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
|
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
|
||||||
getOptionValue(props, Information, None),
|
getOptionValue(props, Information, None),
|
||||||
getValue(props, AllowAccountRegistration, false),
|
BasicBehavior(
|
||||||
getValue(props, AllowAnonymousAccess, true),
|
getValue(props, AllowAccountRegistration, false),
|
||||||
getValue(props, IsCreateRepoOptionPublic, true),
|
getValue(props, AllowResetPassword, false),
|
||||||
RepositoryOperation(
|
getValue(props, AllowAnonymousAccess, true),
|
||||||
create = getValue(props, RepositoryOperationCreate, true),
|
getValue(props, IsCreateRepoOptionPublic, true),
|
||||||
delete = getValue(props, RepositoryOperationDelete, true),
|
RepositoryOperation(
|
||||||
rename = getValue(props, RepositoryOperationRename, true),
|
create = getValue(props, RepositoryOperationCreate, true),
|
||||||
transfer = getValue(props, RepositoryOperationTransfer, true),
|
delete = getValue(props, RepositoryOperationDelete, true),
|
||||||
fork = getValue(props, RepositoryOperationFork, 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(
|
Ssh(
|
||||||
getValue(props, SshEnabled, false),
|
enabled = getValue(props, SshEnabled, false),
|
||||||
getOptionValue[String](props, SshHost, None).map(_.trim),
|
bindAddress = {
|
||||||
getOptionValue(props, SshPort, Some(DefaultSshPort))
|
// try the new-style configuration first
|
||||||
|
getOptionValue[String](props, SshBindAddressHost, None)
|
||||||
|
.map(h => SshAddress(h, getValue(props, SshBindAddressPort, DefaultSshPort), GenericSshUser))
|
||||||
|
.orElse(
|
||||||
|
// otherwise try to get old-style configuration
|
||||||
|
getOptionValue[String](props, SshHost, None)
|
||||||
|
.map(_.trim)
|
||||||
|
.map(h => SshAddress(h, getValue(props, SshPort, DefaultSshPort), GenericSshUser))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
publicAddress = getOptionValue[String](props, SshPublicAddressHost, None)
|
||||||
|
.map(h => SshAddress(h, getValue(props, SshPublicAddressPort, PublicSshPort), GenericSshUser))
|
||||||
),
|
),
|
||||||
getValue(
|
getValue(
|
||||||
props,
|
props,
|
||||||
@@ -182,7 +208,6 @@ trait SystemSettingsService {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object SystemSettingsService {
|
object SystemSettingsService {
|
||||||
@@ -193,13 +218,7 @@ object SystemSettingsService {
|
|||||||
case class SystemSettings(
|
case class SystemSettings(
|
||||||
baseUrl: Option[String],
|
baseUrl: Option[String],
|
||||||
information: Option[String],
|
information: Option[String],
|
||||||
allowAccountRegistration: Boolean,
|
basicBehavior: BasicBehavior,
|
||||||
allowAnonymousAccess: Boolean,
|
|
||||||
isCreateRepoOptionPublic: Boolean,
|
|
||||||
repositoryOperation: RepositoryOperation,
|
|
||||||
gravatar: Boolean,
|
|
||||||
notification: Boolean,
|
|
||||||
limitVisibleRepositories: Boolean,
|
|
||||||
ssh: Ssh,
|
ssh: Ssh,
|
||||||
useSMTP: Boolean,
|
useSMTP: Boolean,
|
||||||
smtp: Option[Smtp],
|
smtp: Option[Smtp],
|
||||||
@@ -214,7 +233,6 @@ object SystemSettingsService {
|
|||||||
upload: Upload,
|
upload: Upload,
|
||||||
repositoryViewer: RepositoryViewerSettings
|
repositoryViewer: RepositoryViewerSettings
|
||||||
) {
|
) {
|
||||||
|
|
||||||
def baseUrl(request: HttpServletRequest): String =
|
def baseUrl(request: HttpServletRequest): String =
|
||||||
baseUrl.getOrElse(parseBaseUrl(request)).stripSuffix("/")
|
baseUrl.getOrElse(parseBaseUrl(request)).stripSuffix("/")
|
||||||
|
|
||||||
@@ -231,13 +249,30 @@ object SystemSettingsService {
|
|||||||
.fold(base)(_ + base.dropWhile(_ != ':'))
|
.fold(base)(_ + base.dropWhile(_ != ':'))
|
||||||
}
|
}
|
||||||
|
|
||||||
def sshAddress: Option[SshAddress] =
|
def sshBindAddress: Option[SshAddress] =
|
||||||
ssh.sshHost.collect {
|
ssh.bindAddress
|
||||||
case host if ssh.enabled =>
|
|
||||||
SshAddress(host, ssh.sshPort.getOrElse(DefaultSshPort), "git")
|
def sshPublicAddress: Option[SshAddress] =
|
||||||
}
|
ssh.publicAddress.orElse(ssh.bindAddress)
|
||||||
|
|
||||||
|
def sshUrl: Option[String] =
|
||||||
|
ssh.getUrl
|
||||||
|
|
||||||
|
def sshUrl(owner: String, name: String): Option[String] =
|
||||||
|
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(
|
case class RepositoryOperation(
|
||||||
create: Boolean,
|
create: Boolean,
|
||||||
delete: Boolean,
|
delete: Boolean,
|
||||||
@@ -248,9 +283,35 @@ object SystemSettingsService {
|
|||||||
|
|
||||||
case class Ssh(
|
case class Ssh(
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
sshHost: Option[String],
|
bindAddress: Option[SshAddress],
|
||||||
sshPort: Option[Int]
|
publicAddress: Option[SshAddress]
|
||||||
)
|
) {
|
||||||
|
|
||||||
|
def getUrl: Option[String] =
|
||||||
|
if (enabled) {
|
||||||
|
publicAddress.map(_.getUrl).orElse(bindAddress.map(_.getUrl))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
def getUrl(owner: String, name: String): Option[String] =
|
||||||
|
if (enabled) {
|
||||||
|
publicAddress
|
||||||
|
.map(_.getUrl(owner, name))
|
||||||
|
.orElse(bindAddress.map(_.getUrl(owner, name)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Ssh {
|
||||||
|
def apply(
|
||||||
|
enabled: Boolean,
|
||||||
|
bindAddress: Option[SshAddress],
|
||||||
|
publicAddress: Option[SshAddress]
|
||||||
|
): Ssh =
|
||||||
|
new Ssh(enabled, bindAddress, publicAddress.orElse(bindAddress))
|
||||||
|
}
|
||||||
|
|
||||||
case class Ldap(
|
case class Ldap(
|
||||||
host: String,
|
host: String,
|
||||||
@@ -296,7 +357,25 @@ object SystemSettingsService {
|
|||||||
password: Option[String]
|
password: Option[String]
|
||||||
)
|
)
|
||||||
|
|
||||||
case class SshAddress(host: String, port: Int, genericUser: String)
|
case class SshAddress(host: String, port: Int, genericUser: String) {
|
||||||
|
|
||||||
|
def isDefaultPort: Boolean =
|
||||||
|
port == PublicSshPort
|
||||||
|
|
||||||
|
def getUrl: String =
|
||||||
|
if (isDefaultPort) {
|
||||||
|
s"${genericUser}@${host}"
|
||||||
|
} else {
|
||||||
|
s"${genericUser}@${host}:${port}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def getUrl(owner: String, name: String): String =
|
||||||
|
if (isDefaultPort) {
|
||||||
|
s"${genericUser}@${host}:${owner}/${name}.git"
|
||||||
|
} else {
|
||||||
|
s"ssh://${genericUser}@${host}:${port}/${owner}/${name}.git"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case class WebHook(blockPrivateAddress: Boolean, whitelist: Seq[String])
|
case class WebHook(blockPrivateAddress: Boolean, whitelist: Seq[String])
|
||||||
|
|
||||||
@@ -304,6 +383,8 @@ object SystemSettingsService {
|
|||||||
|
|
||||||
case class RepositoryViewerSettings(maxFiles: Int)
|
case class RepositoryViewerSettings(maxFiles: Int)
|
||||||
|
|
||||||
|
val GenericSshUser = "git"
|
||||||
|
val PublicSshPort = 22
|
||||||
val DefaultSshPort = 29418
|
val DefaultSshPort = 29418
|
||||||
val DefaultSmtpPort = 25
|
val DefaultSmtpPort = 25
|
||||||
val DefaultLdapPort = 389
|
val DefaultLdapPort = 389
|
||||||
@@ -311,6 +392,7 @@ object SystemSettingsService {
|
|||||||
private val BaseURL = "base_url"
|
private val BaseURL = "base_url"
|
||||||
private val Information = "information"
|
private val Information = "information"
|
||||||
private val AllowAccountRegistration = "allow_account_registration"
|
private val AllowAccountRegistration = "allow_account_registration"
|
||||||
|
private val AllowResetPassword = "allow_reset_password"
|
||||||
private val AllowAnonymousAccess = "allow_anonymous_access"
|
private val AllowAnonymousAccess = "allow_anonymous_access"
|
||||||
private val IsCreateRepoOptionPublic = "is_create_repository_option_public"
|
private val IsCreateRepoOptionPublic = "is_create_repository_option_public"
|
||||||
private val RepositoryOperationCreate = "repository_operation_create"
|
private val RepositoryOperationCreate = "repository_operation_create"
|
||||||
@@ -325,6 +407,10 @@ object SystemSettingsService {
|
|||||||
private val SshEnabled = "ssh"
|
private val SshEnabled = "ssh"
|
||||||
private val SshHost = "ssh.host"
|
private val SshHost = "ssh.host"
|
||||||
private val SshPort = "ssh.port"
|
private val SshPort = "ssh.port"
|
||||||
|
private val SshBindAddressHost = "ssh.bindAddress.host"
|
||||||
|
private val SshBindAddressPort = "ssh.bindAddress.port"
|
||||||
|
private val SshPublicAddressHost = "ssh.publicAddress.host"
|
||||||
|
private val SshPublicAddressPort = "ssh.publicAddress.port"
|
||||||
private val UseSMTP = "useSMTP"
|
private val UseSMTP = "useSMTP"
|
||||||
private val SmtpHost = "smtp.host"
|
private val SmtpHost = "smtp.host"
|
||||||
private val SmtpPort = "smtp.port"
|
private val SmtpPort = "smtp.port"
|
||||||
|
|||||||
@@ -379,8 +379,12 @@ trait WebHookPullRequestService extends WebHookService {
|
|||||||
settings: SystemSettings
|
settings: SystemSettings
|
||||||
)(implicit s: Session, context: JsonFormat.Context): Unit = {
|
)(implicit s: Session, context: JsonFormat.Context): Unit = {
|
||||||
callWebHookOf(repository.owner, repository.name, WebHook.Issues, settings) {
|
callWebHookOf(repository.owner, repository.name, WebHook.Issues, settings) {
|
||||||
|
val assigneeUsers = getIssueAssignees(repository.owner, repository.name, issue.issueId)
|
||||||
val users =
|
val users =
|
||||||
getAccountsByUserNames(Set(repository.owner, issue.openedUserName) ++ issue.assignedUserName, Set(sender))
|
getAccountsByUserNames(
|
||||||
|
Set(repository.owner, issue.openedUserName) ++ assigneeUsers.map(_.assigneeUserName),
|
||||||
|
Set(sender)
|
||||||
|
)
|
||||||
for {
|
for {
|
||||||
repoOwner <- users.get(repository.owner)
|
repoOwner <- users.get(repository.owner)
|
||||||
issueUser <- users.get(issue.openedUserName)
|
issueUser <- users.get(issue.openedUserName)
|
||||||
@@ -393,7 +397,7 @@ trait WebHookPullRequestService extends WebHookService {
|
|||||||
issue,
|
issue,
|
||||||
RepositoryName(repository),
|
RepositoryName(repository),
|
||||||
ApiUser(issueUser),
|
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)
|
getIssueLabels(repository.owner, repository.name, issue.issueId)
|
||||||
.map(ApiLabel(_, RepositoryName(repository))),
|
.map(ApiLabel(_, RepositoryName(repository))),
|
||||||
getApiMilestone(repository, issue.milestoneId getOrElse (0))
|
getApiMilestone(repository, issue.milestoneId getOrElse (0))
|
||||||
@@ -415,16 +419,15 @@ trait WebHookPullRequestService extends WebHookService {
|
|||||||
callWebHookOf(repository.owner, repository.name, WebHook.PullRequest, settings) {
|
callWebHookOf(repository.owner, repository.name, WebHook.PullRequest, settings) {
|
||||||
for {
|
for {
|
||||||
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
|
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
|
||||||
|
assignees = getIssueAssignees(repository.owner, repository.name, issueId)
|
||||||
users = getAccountsByUserNames(
|
users = getAccountsByUserNames(
|
||||||
Set(repository.owner, pullRequest.requestUserName, issue.openedUserName),
|
Set(repository.owner, pullRequest.requestUserName, issue.openedUserName) ++ assignees.map(_.assigneeUserName),
|
||||||
Set(sender)
|
Set(sender)
|
||||||
)
|
)
|
||||||
baseOwner <- users.get(repository.owner)
|
baseOwner <- users.get(repository.owner)
|
||||||
headOwner <- users.get(pullRequest.requestUserName)
|
headOwner <- users.get(pullRequest.requestUserName)
|
||||||
issueUser <- users.get(issue.openedUserName)
|
issueUser <- users.get(issue.openedUserName)
|
||||||
assignee = issue.assignedUserName.flatMap { userName =>
|
assigneeUsers = assignees.flatMap(x => users.get(x.assigneeUserName))
|
||||||
getAccountByUserName(userName, false)
|
|
||||||
}
|
|
||||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
|
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
|
||||||
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
|
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
|
||||||
.map(ApiLabel(_, RepositoryName(repository)))
|
.map(ApiLabel(_, RepositoryName(repository)))
|
||||||
@@ -433,7 +436,7 @@ trait WebHookPullRequestService extends WebHookService {
|
|||||||
action = action,
|
action = action,
|
||||||
issue = issue,
|
issue = issue,
|
||||||
issueUser = issueUser,
|
issueUser = issueUser,
|
||||||
assignee = assignee,
|
assignees = assigneeUsers,
|
||||||
pullRequest = pullRequest,
|
pullRequest = pullRequest,
|
||||||
headRepository = headRepo,
|
headRepository = headRepo,
|
||||||
headOwner = headOwner,
|
headOwner = headOwner,
|
||||||
@@ -481,8 +484,8 @@ trait WebHookPullRequestService extends WebHookService {
|
|||||||
requestRepository.name,
|
requestRepository.name,
|
||||||
requestBranch
|
requestBranch
|
||||||
)
|
)
|
||||||
assignee = issue.assignedUserName.flatMap { userName =>
|
assignees = getIssueAssignees(requestRepository.owner, requestRepository.name, issue.issueId).flatMap { x =>
|
||||||
getAccountByUserName(userName, false)
|
getAccountByUserName(x.assigneeUserName, false)
|
||||||
}
|
}
|
||||||
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName)
|
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName)
|
||||||
labels = getIssueLabels(pullRequest.userName, pullRequest.repositoryName, issue.issueId)
|
labels = getIssueLabels(pullRequest.userName, pullRequest.repositoryName, issue.issueId)
|
||||||
@@ -492,7 +495,7 @@ trait WebHookPullRequestService extends WebHookService {
|
|||||||
action = action,
|
action = action,
|
||||||
issue = issue,
|
issue = issue,
|
||||||
issueUser = issueUser,
|
issueUser = issueUser,
|
||||||
assignee = assignee,
|
assignees = assignees,
|
||||||
pullRequest = pullRequest,
|
pullRequest = pullRequest,
|
||||||
headRepository = requestRepository,
|
headRepository = requestRepository,
|
||||||
headOwner = headOwner,
|
headOwner = headOwner,
|
||||||
@@ -522,15 +525,17 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
|
|||||||
)(implicit s: Session, c: JsonFormat.Context): Unit = {
|
)(implicit s: Session, c: JsonFormat.Context): Unit = {
|
||||||
import WebHookService._
|
import WebHookService._
|
||||||
callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment, settings) {
|
callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment, settings) {
|
||||||
|
val assignees = getIssueAssignees(repository.owner, pullRequest.requestUserName, issue.issueId)
|
||||||
val users =
|
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 {
|
for {
|
||||||
baseOwner <- users.get(repository.owner)
|
baseOwner <- users.get(repository.owner)
|
||||||
headOwner <- users.get(pullRequest.requestUserName)
|
headOwner <- users.get(pullRequest.requestUserName)
|
||||||
issueUser <- users.get(issue.openedUserName)
|
issueUser <- users.get(issue.openedUserName)
|
||||||
assignee = issue.assignedUserName.flatMap { userName =>
|
assigneeUsers = assignees.flatMap(x => users.get(x.assigneeUserName))
|
||||||
getAccountByUserName(userName, false)
|
|
||||||
}
|
|
||||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
|
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
|
||||||
labels = getIssueLabels(pullRequest.userName, pullRequest.repositoryName, issue.issueId)
|
labels = getIssueLabels(pullRequest.userName, pullRequest.repositoryName, issue.issueId)
|
||||||
.map(ApiLabel(_, RepositoryName(pullRequest.userName, pullRequest.repositoryName)))
|
.map(ApiLabel(_, RepositoryName(pullRequest.userName, pullRequest.repositoryName)))
|
||||||
@@ -540,7 +545,7 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
|
|||||||
comment = comment,
|
comment = comment,
|
||||||
issue = issue,
|
issue = issue,
|
||||||
issueUser = issueUser,
|
issueUser = issueUser,
|
||||||
assignee = assignee,
|
assignees = assigneeUsers,
|
||||||
pullRequest = pullRequest,
|
pullRequest = pullRequest,
|
||||||
headRepository = headRepo,
|
headRepository = headRepo,
|
||||||
headOwner = headOwner,
|
headOwner = headOwner,
|
||||||
@@ -569,14 +574,17 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
|
|||||||
callWebHookOf(repository.owner, repository.name, WebHook.IssueComment, settings) {
|
callWebHookOf(repository.owner, repository.name, WebHook.IssueComment, settings) {
|
||||||
for {
|
for {
|
||||||
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
|
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
|
||||||
|
assignees = getIssueAssignees(repository.owner, repository.name, issue.issueId)
|
||||||
users = getAccountsByUserNames(
|
users = getAccountsByUserNames(
|
||||||
Set(issue.openedUserName, repository.owner, issueComment.commentedUserName) ++ issue.assignedUserName,
|
Set(issue.openedUserName, repository.owner, issueComment.commentedUserName) ++ assignees.map(
|
||||||
|
_.assigneeUserName
|
||||||
|
),
|
||||||
Set(sender)
|
Set(sender)
|
||||||
)
|
)
|
||||||
issueUser <- users.get(issue.openedUserName)
|
issueUser <- users.get(issue.openedUserName)
|
||||||
repoOwner <- users.get(repository.owner)
|
repoOwner <- users.get(repository.owner)
|
||||||
commenter <- users.get(issueComment.commentedUserName)
|
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)
|
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
|
||||||
milestone = getApiMilestone(repository, issue.milestoneId getOrElse (0))
|
milestone = getApiMilestone(repository, issue.milestoneId getOrElse (0))
|
||||||
} yield {
|
} yield {
|
||||||
@@ -587,7 +595,7 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
|
|||||||
commentUser = commenter,
|
commentUser = commenter,
|
||||||
repository = repository,
|
repository = repository,
|
||||||
repositoryUser = repoOwner,
|
repositoryUser = repoOwner,
|
||||||
assignedUser = assignedUser,
|
assignees = assigneeUsers,
|
||||||
sender = sender,
|
sender = sender,
|
||||||
labels = labels,
|
labels = labels,
|
||||||
milestone = milestone
|
milestone = milestone
|
||||||
@@ -710,7 +718,7 @@ object WebHookService {
|
|||||||
action: String,
|
action: String,
|
||||||
issue: Issue,
|
issue: Issue,
|
||||||
issueUser: Account,
|
issueUser: Account,
|
||||||
assignee: Option[Account],
|
assignees: List[Account],
|
||||||
pullRequest: PullRequest,
|
pullRequest: PullRequest,
|
||||||
headRepository: RepositoryInfo,
|
headRepository: RepositoryInfo,
|
||||||
headOwner: Account,
|
headOwner: Account,
|
||||||
@@ -731,7 +739,7 @@ object WebHookService {
|
|||||||
baseRepo = baseRepoPayload,
|
baseRepo = baseRepoPayload,
|
||||||
user = ApiUser(issueUser),
|
user = ApiUser(issueUser),
|
||||||
labels = labels,
|
labels = labels,
|
||||||
assignee = assignee.map(ApiUser.apply),
|
assignees = assignees.map(ApiUser.apply),
|
||||||
mergedComment = mergedComment
|
mergedComment = mergedComment
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -762,7 +770,7 @@ object WebHookService {
|
|||||||
commentUser: Account,
|
commentUser: Account,
|
||||||
repository: RepositoryInfo,
|
repository: RepositoryInfo,
|
||||||
repositoryUser: Account,
|
repositoryUser: Account,
|
||||||
assignedUser: Option[Account],
|
assignees: List[Account],
|
||||||
sender: Account,
|
sender: Account,
|
||||||
labels: List[Label],
|
labels: List[Label],
|
||||||
milestone: Option[ApiMilestone]
|
milestone: Option[ApiMilestone]
|
||||||
@@ -774,7 +782,7 @@ object WebHookService {
|
|||||||
issue,
|
issue,
|
||||||
RepositoryName(repository),
|
RepositoryName(repository),
|
||||||
ApiUser(issueUser),
|
ApiUser(issueUser),
|
||||||
assignedUser.map(ApiUser(_)),
|
assignees.map(ApiUser(_)),
|
||||||
labels.map(ApiLabel(_, RepositoryName(repository))),
|
labels.map(ApiLabel(_, RepositoryName(repository))),
|
||||||
milestone
|
milestone
|
||||||
),
|
),
|
||||||
@@ -799,7 +807,7 @@ object WebHookService {
|
|||||||
comment: CommitComment,
|
comment: CommitComment,
|
||||||
issue: Issue,
|
issue: Issue,
|
||||||
issueUser: Account,
|
issueUser: Account,
|
||||||
assignee: Option[Account],
|
assignees: List[Account],
|
||||||
pullRequest: PullRequest,
|
pullRequest: PullRequest,
|
||||||
headRepository: RepositoryInfo,
|
headRepository: RepositoryInfo,
|
||||||
headOwner: Account,
|
headOwner: Account,
|
||||||
@@ -828,7 +836,7 @@ object WebHookService {
|
|||||||
baseRepo = baseRepoPayload,
|
baseRepo = baseRepoPayload,
|
||||||
user = ApiUser(issueUser),
|
user = ApiUser(issueUser),
|
||||||
labels = labels,
|
labels = labels,
|
||||||
assignee = assignee.map(ApiUser.apply),
|
assignees = assignees.map(ApiUser.apply),
|
||||||
mergedComment = mergedComment
|
mergedComment = mergedComment
|
||||||
),
|
),
|
||||||
repository = baseRepoPayload,
|
repository = baseRepoPayload,
|
||||||
|
|||||||
@@ -98,29 +98,30 @@ class GitAuthenticationFilter extends Filter with RepositoryService with Account
|
|||||||
Database() withSession { implicit session =>
|
Database() withSession { implicit session =>
|
||||||
getRepository(repositoryOwner, repositoryName.replaceFirst("(\\.wiki)?\\.git$", "")) match {
|
getRepository(repositoryOwner, repositoryName.replaceFirst("(\\.wiki)?\\.git$", "")) match {
|
||||||
case Some(repository) => {
|
case Some(repository) => {
|
||||||
val execute = if (!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess) {
|
val execute =
|
||||||
// Authentication is not required
|
if (!isUpdating && !repository.repository.isPrivate && settings.basicBehavior.allowAnonymousAccess) {
|
||||||
true
|
// Authentication is not required
|
||||||
} else {
|
true
|
||||||
// Authentication is required
|
} else {
|
||||||
val passed = for {
|
// Authentication is required
|
||||||
authorizationHeader <- Option(request.getHeader("Authorization"))
|
val passed = for {
|
||||||
account <- authenticateByHeader(authorizationHeader, settings)
|
authorizationHeader <- Option(request.getHeader("Authorization"))
|
||||||
} yield
|
account <- authenticateByHeader(authorizationHeader, settings)
|
||||||
if (isUpdating) {
|
} yield
|
||||||
if (hasDeveloperRole(repository.owner, repository.name, Some(account))) {
|
if (isUpdating) {
|
||||||
request.setAttribute(Keys.Request.UserName, account.userName)
|
if (hasDeveloperRole(repository.owner, repository.name, Some(account))) {
|
||||||
request.setAttribute(Keys.Request.RepositoryLockKey, s"${repository.owner}/${repository.name}")
|
request.setAttribute(Keys.Request.UserName, account.userName)
|
||||||
true
|
request.setAttribute(Keys.Request.RepositoryLockKey, s"${repository.owner}/${repository.name}")
|
||||||
} else false
|
true
|
||||||
} else if (repository.repository.isPrivate) {
|
} else false
|
||||||
if (hasGuestRole(repository.owner, repository.name, Some(account))) {
|
} else if (repository.repository.isPrivate) {
|
||||||
request.setAttribute(Keys.Request.UserName, account.userName)
|
if (hasGuestRole(repository.owner, repository.name, Some(account))) {
|
||||||
true
|
request.setAttribute(Keys.Request.UserName, account.userName)
|
||||||
} else false
|
true
|
||||||
} else true
|
} else false
|
||||||
passed.getOrElse(false)
|
} else true
|
||||||
}
|
passed.getOrElse(false)
|
||||||
|
}
|
||||||
|
|
||||||
if (execute) { () =>
|
if (execute) { () =>
|
||||||
chain.doFilter(request, response)
|
chain.doFilter(request, response)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package gitbucket.core.servlet
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util
|
import java.util
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
import scala.util.Using
|
import scala.util.Using
|
||||||
import gitbucket.core.api
|
import gitbucket.core.api
|
||||||
import gitbucket.core.api.JsonFormat.Context
|
import gitbucket.core.api.JsonFormat.Context
|
||||||
@@ -53,15 +52,11 @@ import org.json4s.jackson.Serialization._
|
|||||||
*/
|
*/
|
||||||
class GitRepositoryServlet extends GitServlet with SystemSettingsService {
|
class GitRepositoryServlet extends GitServlet with SystemSettingsService {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet])
|
|
||||||
private implicit val jsonFormats: Formats = gitbucket.core.api.JsonFormat.jsonFormats
|
private implicit val jsonFormats: Formats = gitbucket.core.api.JsonFormat.jsonFormats
|
||||||
|
|
||||||
override def init(config: ServletConfig): Unit = {
|
override def init(config: ServletConfig): Unit = {
|
||||||
setReceivePackFactory(new GitBucketReceivePackFactory())
|
setReceivePackFactory(new GitBucketReceivePackFactory())
|
||||||
|
|
||||||
val root: File = new File(Directory.RepositoryHome)
|
|
||||||
setRepositoryResolver(new GitBucketRepositoryResolver)
|
setRepositoryResolver(new GitBucketRepositoryResolver)
|
||||||
|
|
||||||
super.init(config)
|
super.init(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,9 +204,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
|
|||||||
|
|
||||||
val settings = loadSystemSettings()
|
val settings = loadSystemSettings()
|
||||||
val baseUrl = settings.baseUrl(request)
|
val baseUrl = settings.baseUrl(request)
|
||||||
val sshUrl = settings.sshAddress.map { x =>
|
val sshUrl = settings.sshUrl(owner, repository)
|
||||||
s"${x.genericUser}@${x.host}:${x.port}"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!repository.endsWith(".wiki")) {
|
if (!repository.endsWith(".wiki")) {
|
||||||
val hook = new CommitLogHook(owner, repository, pusher, baseUrl, sshUrl)
|
val hook = new CommitLogHook(owner, repository, pusher, baseUrl, sshUrl)
|
||||||
@@ -423,7 +416,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
|||||||
pusherAccount,
|
pusherAccount,
|
||||||
repositoryInfo,
|
repositoryInfo,
|
||||||
ownerAccount,
|
ownerAccount,
|
||||||
ref = branchName,
|
ref = command.getRefName,
|
||||||
refType = refType
|
refType = refType
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,26 +5,31 @@ import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
|
|||||||
import gitbucket.core.service.{AccountService, DeployKeyService, RepositoryService, SystemSettingsService}
|
import gitbucket.core.service.{AccountService, DeployKeyService, RepositoryService, SystemSettingsService}
|
||||||
import gitbucket.core.servlet.{CommitLogHook, Database}
|
import gitbucket.core.servlet.{CommitLogHook, Database}
|
||||||
import gitbucket.core.util.Directory
|
import gitbucket.core.util.Directory
|
||||||
import org.apache.sshd.server.{Environment, ExitCallback, SessionAware}
|
import org.apache.sshd.server.{Environment, ExitCallback}
|
||||||
import org.apache.sshd.server.command.{Command, CommandFactory}
|
import org.apache.sshd.server.command.{Command, CommandFactory}
|
||||||
import org.apache.sshd.server.session.ServerSession
|
import org.apache.sshd.server.session.{ServerSession, ServerSessionAware}
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.{File, InputStream, OutputStream}
|
|
||||||
|
|
||||||
|
import java.io.{File, InputStream, OutputStream}
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import Directory._
|
import Directory._
|
||||||
|
import gitbucket.core.service.SystemSettingsService.SshAddress
|
||||||
import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType
|
import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType
|
||||||
|
import org.apache.sshd.server.channel.ChannelSession
|
||||||
import org.eclipse.jgit.transport.{ReceivePack, UploadPack}
|
import org.eclipse.jgit.transport.{ReceivePack, UploadPack}
|
||||||
import org.apache.sshd.server.shell.UnknownCommand
|
import org.apache.sshd.server.shell.UnknownCommand
|
||||||
import org.eclipse.jgit.errors.RepositoryNotFoundException
|
import org.eclipse.jgit.errors.RepositoryNotFoundException
|
||||||
|
|
||||||
import scala.util.Using
|
import scala.util.Using
|
||||||
|
|
||||||
object GitCommand {
|
object GitCommand {
|
||||||
val DefaultCommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-\+_.]+).git'\Z""".r
|
val DefaultCommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-\+_.]+).git'\Z""".r
|
||||||
val SimpleCommandRegex = """\Agit-(upload|receive)-pack '/(.+\.git)'\Z""".r
|
val SimpleCommandRegex = """\Agit-(upload|receive)-pack '/(.+\.git)'\Z""".r
|
||||||
|
val DefaultCommandRegexPort22 = """\Agit-(upload|receive)-pack '/?([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-\+_.]+).git'\Z""".r
|
||||||
|
val SimpleCommandRegexPort22 = """\Agit-(upload|receive)-pack '/?(.+\.git)'\Z""".r
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class GitCommand extends Command with SessionAware {
|
abstract class GitCommand extends Command with ServerSessionAware {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[GitCommand])
|
private val logger = LoggerFactory.getLogger(classOf[GitCommand])
|
||||||
|
|
||||||
@@ -57,12 +62,12 @@ abstract class GitCommand extends Command with SessionAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final override def start(env: Environment): Unit = {
|
final override def start(channel: ChannelSession, env: Environment): Unit = {
|
||||||
val thread = new Thread(newTask())
|
val thread = new Thread(newTask())
|
||||||
thread.start()
|
thread.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
override def destroy(): Unit = {}
|
override def destroy(channel: ChannelSession): Unit = {}
|
||||||
|
|
||||||
override def setExitCallback(callback: ExitCallback): Unit = {
|
override def setExitCallback(callback: ExitCallback): Unit = {
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
@@ -159,7 +164,7 @@ class DefaultGitUploadPack(owner: String, repoName: String)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, sshUrl: Option[String])
|
class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, sshAddress: SshAddress)
|
||||||
extends DefaultGitCommand(owner, repoName)
|
extends DefaultGitCommand(owner, repoName)
|
||||||
with RepositoryService
|
with RepositoryService
|
||||||
with AccountService
|
with AccountService
|
||||||
@@ -177,7 +182,8 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, ss
|
|||||||
val repository = git.getRepository
|
val repository = git.getRepository
|
||||||
val receive = new ReceivePack(repository)
|
val receive = new ReceivePack(repository)
|
||||||
if (!repoName.endsWith(".wiki")) {
|
if (!repoName.endsWith(".wiki")) {
|
||||||
val hook = new CommitLogHook(owner, repoName, userName(authType), baseUrl, sshUrl)
|
val hook =
|
||||||
|
new CommitLogHook(owner, repoName, userName(authType), baseUrl, Some(sshAddress.getUrl(owner, repoName)))
|
||||||
receive.setPreReceiveHook(hook)
|
receive.setPreReceiveHook(hook)
|
||||||
receive.setPostReceiveHook(hook)
|
receive.setPostReceiveHook(hook)
|
||||||
}
|
}
|
||||||
@@ -227,10 +233,10 @@ class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GitCommandFactory(baseUrl: String, sshUrl: Option[String]) extends CommandFactory {
|
class GitCommandFactory(baseUrl: String, sshAddress: SshAddress) extends CommandFactory {
|
||||||
private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
|
private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
|
||||||
|
|
||||||
override def createCommand(command: String): Command = {
|
override def createCommand(channel: ChannelSession, command: String): Command = {
|
||||||
import GitCommand._
|
import GitCommand._
|
||||||
logger.debug(s"command: $command")
|
logger.debug(s"command: $command")
|
||||||
|
|
||||||
@@ -238,19 +244,24 @@ class GitCommandFactory(baseUrl: String, sshUrl: Option[String]) extends Command
|
|||||||
case f if f.isDefinedAt(command) => f(command)
|
case f if f.isDefinedAt(command) => f(command)
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginCommand match {
|
pluginCommand.map(_.apply(channel)).getOrElse {
|
||||||
case Some(x) => x
|
val (simpleRegex, defaultRegex) =
|
||||||
case None =>
|
if (sshAddress.isDefaultPort) {
|
||||||
command match {
|
(SimpleCommandRegexPort22, DefaultCommandRegexPort22)
|
||||||
case SimpleCommandRegex("upload", repoName) if (pluginRepository(repoName)) =>
|
} else {
|
||||||
new PluginGitUploadPack(repoName, routing(repoName))
|
(SimpleCommandRegex, DefaultCommandRegex)
|
||||||
case SimpleCommandRegex("receive", repoName) if (pluginRepository(repoName)) =>
|
|
||||||
new PluginGitReceivePack(repoName, routing(repoName))
|
|
||||||
case DefaultCommandRegex("upload", owner, repoName) => new DefaultGitUploadPack(owner, repoName)
|
|
||||||
case DefaultCommandRegex("receive", owner, repoName) =>
|
|
||||||
new DefaultGitReceivePack(owner, repoName, baseUrl, sshUrl)
|
|
||||||
case _ => new UnknownCommand(command)
|
|
||||||
}
|
}
|
||||||
|
command match {
|
||||||
|
case simpleRegex("upload", repoName) if pluginRepository(repoName) =>
|
||||||
|
new PluginGitUploadPack(repoName, routing(repoName))
|
||||||
|
case simpleRegex("receive", repoName) if pluginRepository(repoName) =>
|
||||||
|
new PluginGitReceivePack(repoName, routing(repoName))
|
||||||
|
case defaultRegex("upload", owner, repoName) =>
|
||||||
|
new DefaultGitUploadPack(owner, repoName)
|
||||||
|
case defaultRegex("receive", owner, repoName) =>
|
||||||
|
new DefaultGitReceivePack(owner, repoName, baseUrl, sshAddress)
|
||||||
|
case _ => new UnknownCommand(command)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
package gitbucket.core.ssh
|
package gitbucket.core.ssh
|
||||||
|
|
||||||
import gitbucket.core.service.SystemSettingsService.SshAddress
|
import gitbucket.core.service.SystemSettingsService.SshAddress
|
||||||
import org.apache.sshd.common.Factory
|
import org.apache.sshd.server.channel.ChannelSession
|
||||||
import org.apache.sshd.server.{Environment, ExitCallback}
|
import org.apache.sshd.server.{Environment, ExitCallback}
|
||||||
import org.apache.sshd.server.command.Command
|
import org.apache.sshd.server.command.Command
|
||||||
import java.io.{OutputStream, InputStream}
|
import org.apache.sshd.server.shell.ShellFactory
|
||||||
|
|
||||||
|
import java.io.{InputStream, OutputStream}
|
||||||
import org.eclipse.jgit.lib.Constants
|
import org.eclipse.jgit.lib.Constants
|
||||||
|
|
||||||
class NoShell(sshAddress: SshAddress) extends Factory[Command] {
|
class NoShell(sshAddress: SshAddress) extends ShellFactory {
|
||||||
override def create(): Command = new Command() {
|
override def createShell(channel: ChannelSession): Command = new Command() {
|
||||||
private var in: InputStream = null
|
private var in: InputStream = null
|
||||||
private var out: OutputStream = null
|
private var out: OutputStream = null
|
||||||
private var err: OutputStream = null
|
private var err: OutputStream = null
|
||||||
private var callback: ExitCallback = null
|
private var callback: ExitCallback = null
|
||||||
|
|
||||||
override def start(env: Environment): Unit = {
|
override def start(channel: ChannelSession, env: Environment): Unit = {
|
||||||
|
val placeholderAddress = sshAddress.getUrl("OWNER", "REPOSITORY_NAME")
|
||||||
val message =
|
val message =
|
||||||
"""
|
"""
|
||||||
| Welcome to
|
| Welcome to
|
||||||
@@ -30,8 +33,8 @@ class NoShell(sshAddress: SshAddress) extends Factory[Command] {
|
|||||||
|
|
|
|
||||||
| Please use:
|
| Please use:
|
||||||
|
|
|
|
||||||
| git clone ssh://%s@%s:%d/OWNER/REPOSITORY_NAME.git
|
| git clone %s
|
||||||
""".stripMargin.format(sshAddress.genericUser, sshAddress.host, sshAddress.port).replace("\n", "\r\n") + "\r\n"
|
""".stripMargin.format(placeholderAddress).replace("\n", "\r\n") + "\r\n"
|
||||||
err.write(Constants.encode(message))
|
err.write(Constants.encode(message))
|
||||||
err.flush()
|
err.flush()
|
||||||
in.close()
|
in.close()
|
||||||
@@ -40,7 +43,7 @@ class NoShell(sshAddress: SshAddress) extends Factory[Command] {
|
|||||||
callback.onExit(127)
|
callback.onExit(127)
|
||||||
}
|
}
|
||||||
|
|
||||||
override def destroy(): Unit = {}
|
override def destroy(channel: ChannelSession): Unit = {}
|
||||||
|
|
||||||
override def setInputStream(in: InputStream): Unit = {
|
override def setInputStream(in: InputStream): Unit = {
|
||||||
this.in = in
|
this.in = in
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import gitbucket.core.model.Profile.profile.blockingApi._
|
|||||||
import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType
|
import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType
|
||||||
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator
|
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator
|
||||||
import org.apache.sshd.server.session.ServerSession
|
import org.apache.sshd.server.session.ServerSession
|
||||||
import org.apache.sshd.common.AttributeStore
|
import org.apache.sshd.common.AttributeRepository
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
object PublicKeyAuthenticator {
|
object PublicKeyAuthenticator {
|
||||||
|
|
||||||
// put in the ServerSession here to be read by GitCommand later
|
// put in the ServerSession here to be read by GitCommand later
|
||||||
private val authTypeSessionKey = new AttributeStore.AttributeKey[AuthType]
|
private val authTypeSessionKey = new AttributeRepository.AttributeKey[AuthType]
|
||||||
|
|
||||||
def putAuthType(serverSession: ServerSession, authType: AuthType): Unit =
|
def putAuthType(serverSession: ServerSession, authType: AuthType): Unit =
|
||||||
serverSession.setAttribute(authTypeSessionKey, authType)
|
serverSession.setAttribute(authTypeSessionKey, authType)
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package gitbucket.core.ssh
|
package gitbucket.core.ssh
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import javax.servlet.{ServletContextEvent, ServletContextListener}
|
import javax.servlet.{ServletContextEvent, ServletContextListener}
|
||||||
|
|
||||||
import gitbucket.core.service.SystemSettingsService
|
import gitbucket.core.service.SystemSettingsService
|
||||||
import gitbucket.core.service.SystemSettingsService.SshAddress
|
import gitbucket.core.service.SystemSettingsService.SshAddress
|
||||||
import gitbucket.core.util.Directory
|
import gitbucket.core.util.Directory
|
||||||
@@ -11,40 +10,48 @@ import org.slf4j.LoggerFactory
|
|||||||
|
|
||||||
object SshServer {
|
object SshServer {
|
||||||
private val logger = LoggerFactory.getLogger(SshServer.getClass)
|
private val logger = LoggerFactory.getLogger(SshServer.getClass)
|
||||||
private val server = org.apache.sshd.server.SshServer.setUpDefaultServer()
|
private val server = new AtomicReference[org.apache.sshd.server.SshServer](null)
|
||||||
private val active = new AtomicBoolean(false)
|
|
||||||
|
|
||||||
private def configure(sshAddress: SshAddress, baseUrl: String) = {
|
private def configure(
|
||||||
server.setPort(sshAddress.port)
|
bindAddress: SshAddress,
|
||||||
|
publicAddress: SshAddress,
|
||||||
|
baseUrl: String
|
||||||
|
): org.apache.sshd.server.SshServer = {
|
||||||
|
val server = org.apache.sshd.server.SshServer.setUpDefaultServer()
|
||||||
|
server.setPort(bindAddress.port)
|
||||||
val provider = new SimpleGeneratorHostKeyProvider(
|
val provider = new SimpleGeneratorHostKeyProvider(
|
||||||
java.nio.file.Paths.get(s"${Directory.GitBucketHome}/gitbucket.ser")
|
java.nio.file.Paths.get(s"${Directory.GitBucketHome}/gitbucket.ser")
|
||||||
)
|
)
|
||||||
provider.setAlgorithm("RSA")
|
provider.setAlgorithm("RSA")
|
||||||
provider.setOverwriteAllowed(false)
|
provider.setOverwriteAllowed(false)
|
||||||
server.setKeyPairProvider(provider)
|
server.setKeyPairProvider(provider)
|
||||||
server.setPublickeyAuthenticator(new PublicKeyAuthenticator(sshAddress.genericUser))
|
server.setPublickeyAuthenticator(new PublicKeyAuthenticator(bindAddress.genericUser))
|
||||||
server.setCommandFactory(
|
server.setCommandFactory(
|
||||||
new GitCommandFactory(baseUrl, Some(s"${sshAddress.genericUser}@${sshAddress.host}:${sshAddress.port}"))
|
new GitCommandFactory(baseUrl, publicAddress)
|
||||||
)
|
)
|
||||||
server.setShellFactory(new NoShell(sshAddress))
|
server.setShellFactory(new NoShell(publicAddress))
|
||||||
|
server
|
||||||
}
|
}
|
||||||
|
|
||||||
def start(sshAddress: SshAddress, baseUrl: String) = {
|
def start(bindAddress: SshAddress, publicAddress: SshAddress, baseUrl: String): Unit = {
|
||||||
if (active.compareAndSet(false, true)) {
|
this.server.synchronized {
|
||||||
configure(sshAddress, baseUrl)
|
val server = configure(bindAddress, publicAddress, baseUrl)
|
||||||
server.start()
|
if (this.server.compareAndSet(null, server)) {
|
||||||
logger.info(s"Start SSH Server Listen on ${server.getPort}")
|
server.start()
|
||||||
|
logger.info(s"Start SSH Server Listen on ${server.getPort}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def stop() = {
|
def stop(): Unit = {
|
||||||
if (active.compareAndSet(true, false)) {
|
this.server.synchronized {
|
||||||
server.stop(true)
|
val server = this.server.getAndSet(null)
|
||||||
logger.info("SSH Server is stopped.")
|
if (server != null) {
|
||||||
|
server.stop()
|
||||||
|
logger.info("SSH Server is stopped.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def isActive = active.get
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -59,13 +66,14 @@ class SshServerListener extends ServletContextListener with SystemSettingsServic
|
|||||||
|
|
||||||
override def contextInitialized(sce: ServletContextEvent): Unit = {
|
override def contextInitialized(sce: ServletContextEvent): Unit = {
|
||||||
val settings = loadSystemSettings()
|
val settings = loadSystemSettings()
|
||||||
if (settings.sshAddress.isDefined && settings.baseUrl.isEmpty) {
|
if (settings.sshBindAddress.isDefined && settings.baseUrl.isEmpty) {
|
||||||
logger.error("Could not start SshServer because the baseUrl is not configured.")
|
logger.error("Could not start SshServer because the baseUrl is not configured.")
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
sshAddress <- settings.sshAddress
|
bindAddress <- settings.sshBindAddress
|
||||||
|
publicAddress <- settings.sshPublicAddress
|
||||||
baseUrl <- settings.baseUrl
|
baseUrl <- settings.baseUrl
|
||||||
} SshServer.start(sshAddress, baseUrl)
|
} SshServer.start(bindAddress, publicAddress, baseUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
override def contextDestroyed(sce: ServletContextEvent): Unit = {
|
override def contextDestroyed(sce: ServletContextEvent): Unit = {
|
||||||
|
|||||||
@@ -38,8 +38,18 @@ object DatabaseConfig {
|
|||||||
|
|
||||||
private lazy val dbUrl = getValue("db.url", config.getString) //config.getString("db.url")
|
private lazy val dbUrl = getValue("db.url", config.getString) //config.getString("db.url")
|
||||||
|
|
||||||
def url(directory: Option[String]): String =
|
def url(directory: Option[String]): String = {
|
||||||
dbUrl.replace("${DatabaseHome}", directory.getOrElse(DatabaseHome))
|
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 url: String = url(None)
|
||||||
lazy val user: String = getValue("db.user", config.getString)
|
lazy val user: String = getValue("db.user", config.getString)
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package gitbucket.core.util
|
package gitbucket.core.util
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
|
|
||||||
import scala.jdk.CollectionConverters._
|
import scala.jdk.CollectionConverters._
|
||||||
|
|
||||||
import gitbucket.core.model.Profile._
|
import gitbucket.core.model.Profile._
|
||||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||||
import org.bouncycastle.bcpg.ArmoredInputStream
|
import org.bouncycastle.bcpg.ArmoredInputStream
|
||||||
@@ -34,29 +33,33 @@ object GpgUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def verifySign(signInfo: JGitUtil.GpgSignInfo)(implicit s: Session): Option[JGitUtil.GpgVerifyInfo] = {
|
def verifySign(signInfo: JGitUtil.GpgSignInfo)(implicit s: Session): Option[JGitUtil.GpgVerifyInfo] = {
|
||||||
new BcPGPObjectFactory(new ArmoredInputStream(new ByteArrayInputStream(signInfo.signArmored)))
|
try {
|
||||||
.iterator()
|
new BcPGPObjectFactory(new ArmoredInputStream(new ByteArrayInputStream(signInfo.signArmored)))
|
||||||
.asScala
|
.iterator()
|
||||||
.flatMap {
|
.asScala
|
||||||
case signList: PGPSignatureList =>
|
.flatMap {
|
||||||
signList
|
case signList: PGPSignatureList =>
|
||||||
.iterator()
|
signList
|
||||||
.asScala
|
.iterator()
|
||||||
.flatMap { sign =>
|
.asScala
|
||||||
getGpgKey(sign.getKeyID)
|
.flatMap { sign =>
|
||||||
.map { pubKey =>
|
getGpgKey(sign.getKeyID)
|
||||||
sign.init(new BcPGPContentVerifierBuilderProvider, pubKey)
|
.map { pubKey =>
|
||||||
sign.update(signInfo.target)
|
sign.init(new BcPGPContentVerifierBuilderProvider, pubKey)
|
||||||
(sign, pubKey)
|
sign.update(signInfo.target)
|
||||||
}
|
(sign, pubKey)
|
||||||
.collect {
|
}
|
||||||
case (sign, pubKey) if sign.verify() =>
|
.collect {
|
||||||
JGitUtil.GpgVerifyInfo(pubKey.getUserIDs.next, pubKey.getKeyID.toHexString.toUpperCase)
|
case (sign, pubKey) if sign.verify() =>
|
||||||
}
|
JGitUtil.GpgVerifyInfo(pubKey.getUserIDs.next, pubKey.getKeyID.toHexString.toUpperCase)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toList
|
||||||
|
.headOption
|
||||||
|
} catch {
|
||||||
|
case _: Throwable => None
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
.toList
|
|
||||||
.headOption
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ object Implicits {
|
|||||||
implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request)
|
implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request)
|
||||||
|
|
||||||
implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context =
|
implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context =
|
||||||
JsonFormat.Context(context.baseUrl, context.settings.sshAddress.map { x =>
|
JsonFormat.Context(context.baseUrl, context.settings.sshUrl)
|
||||||
s"${x.genericUser}@${x.host}:${x.port}"
|
|
||||||
})
|
|
||||||
|
|
||||||
implicit class RichSeq[A](private val seq: Seq[A]) extends AnyVal {
|
implicit class RichSeq[A](private val seq: Seq[A]) extends AnyVal {
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package gitbucket.core.util
|
package gitbucket.core.util
|
||||||
|
|
||||||
import java.io._
|
import java.io._
|
||||||
|
|
||||||
import gitbucket.core.service.RepositoryService
|
import gitbucket.core.service.RepositoryService
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import Directory._
|
import Directory._
|
||||||
@@ -18,10 +17,10 @@ import org.eclipse.jgit.treewalk.filter._
|
|||||||
import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
||||||
import org.eclipse.jgit.errors.{ConfigInvalidException, IncorrectObjectTypeException, MissingObjectException}
|
import org.eclipse.jgit.errors.{ConfigInvalidException, IncorrectObjectTypeException, MissingObjectException}
|
||||||
import org.eclipse.jgit.transport.RefSpec
|
import org.eclipse.jgit.transport.RefSpec
|
||||||
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import org.cache2k.{Cache, Cache2kBuilder}
|
||||||
import org.cache2k.Cache2kBuilder
|
|
||||||
import org.eclipse.jgit.api.errors._
|
import org.eclipse.jgit.api.errors._
|
||||||
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter, RawTextComparator}
|
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter, RawTextComparator}
|
||||||
import org.eclipse.jgit.dircache.DirCacheEntry
|
import org.eclipse.jgit.dircache.DirCacheEntry
|
||||||
@@ -37,9 +36,12 @@ object JGitUtil {
|
|||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(JGitUtil.getClass)
|
private val logger = LoggerFactory.getLogger(JGitUtil.getClass)
|
||||||
|
|
||||||
implicit val objectDatabaseReleasable: Releasable[ObjectDatabase] =
|
private implicit val objectDatabaseReleasable: Releasable[ObjectDatabase] =
|
||||||
_.close()
|
_.close()
|
||||||
|
|
||||||
|
private def isCacheEnabled(): Boolean =
|
||||||
|
!ConfigUtil.getConfigValue[Boolean]("gitbucket.disableCache").getOrElse(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The repository data.
|
* The repository data.
|
||||||
*
|
*
|
||||||
@@ -228,10 +230,11 @@ object JGitUtil {
|
|||||||
*
|
*
|
||||||
* @param name the tag name
|
* @param name the tag name
|
||||||
* @param time the tagged date
|
* @param time the tagged date
|
||||||
* @param id the commit id
|
* @param commitId the commit id
|
||||||
* @param message the message of the tagged commit
|
* @param message the message of the tagged commit
|
||||||
|
* @param objectId the tag object id
|
||||||
*/
|
*/
|
||||||
case class TagInfo(name: String, time: Date, id: String, message: String)
|
case class TagInfo(name: String, time: Date, commitId: String, message: String, objectId: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The submodule data
|
* The submodule data
|
||||||
@@ -283,26 +286,34 @@ object JGitUtil {
|
|||||||
revCommit
|
revCommit
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cache = new Cache2kBuilder[String, Int]() {}
|
private val cache: Cache[String, Int] = if (isCacheEnabled()) {
|
||||||
.name("commit-count")
|
Cache2kBuilder
|
||||||
.expireAfterWrite(24, TimeUnit.HOURS)
|
.of(classOf[String], classOf[Int])
|
||||||
.entryCapacity(10000)
|
.name("commit-count")
|
||||||
.build()
|
.expireAfterWrite(24, TimeUnit.HOURS)
|
||||||
|
.entryCapacity(10000)
|
||||||
|
.build()
|
||||||
|
} else null
|
||||||
|
|
||||||
private val objectCommitCache = new Cache2kBuilder[ObjectId, RevCommit]() {}
|
private val objectCommitCache: Cache[ObjectId, RevCommit] = if (isCacheEnabled()) {
|
||||||
.name("object-commit")
|
Cache2kBuilder
|
||||||
.entryCapacity(10000)
|
.of(classOf[ObjectId], classOf[RevCommit])
|
||||||
.build()
|
.name("object-commit")
|
||||||
|
.entryCapacity(10000)
|
||||||
|
.build()
|
||||||
|
} else null
|
||||||
|
|
||||||
def removeCache(git: Git): Unit = {
|
def removeCache(git: Git): Unit = {
|
||||||
val dir = git.getRepository.getDirectory
|
if (isCacheEnabled()) {
|
||||||
val keyPrefix = dir.getAbsolutePath + "@"
|
val dir = git.getRepository.getDirectory
|
||||||
|
val keyPrefix = dir.getAbsolutePath + "@"
|
||||||
|
|
||||||
cache.keys.forEach(key => {
|
cache.keys.forEach(key => {
|
||||||
if (key.startsWith(keyPrefix)) {
|
if (key.startsWith(keyPrefix)) {
|
||||||
cache.remove(key)
|
cache.remove(key)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -311,16 +322,23 @@ object JGitUtil {
|
|||||||
*/
|
*/
|
||||||
def getCommitCount(git: Git, branch: String, max: Int = 10001): Int = {
|
def getCommitCount(git: Git, branch: String, max: Int = 10001): Int = {
|
||||||
val dir = git.getRepository.getDirectory
|
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 commitId = git.getRepository.resolve(branch)
|
||||||
val commitCount = git.log.add(commitId).call.iterator.asScala.take(max).size
|
val commitCount = git.log.add(commitId).call.iterator.asScala.take(max).size
|
||||||
cache.put(key, commitCount)
|
|
||||||
commitCount
|
commitCount
|
||||||
} else {
|
|
||||||
entry.getValue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +365,8 @@ object JGitUtil {
|
|||||||
ref.getName.stripPrefix("refs/tags/"),
|
ref.getName.stripPrefix("refs/tags/"),
|
||||||
revCommit.getCommitterIdent.getWhen,
|
revCommit.getCommitterIdent.getWhen,
|
||||||
revCommit.getName,
|
revCommit.getName,
|
||||||
revCommit.getShortMessage
|
revCommit.getShortMessage,
|
||||||
|
ref.getObjectId.getName
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
@@ -442,7 +461,7 @@ object JGitUtil {
|
|||||||
(id, mode, name, path, opt, None)
|
(id, mode, name, path, opt, None)
|
||||||
} else if (commitCount < 10000) {
|
} else if (commitCount < 10000) {
|
||||||
(id, mode, name, path, opt, Some(getCommit(path)))
|
(id, mode, name, path, opt, Some(getCommit(path)))
|
||||||
} else {
|
} else if (isCacheEnabled()) {
|
||||||
// Use in-memory cache if the commit count is too big.
|
// Use in-memory cache if the commit count is too big.
|
||||||
val cached = objectCommitCache.getEntry(id)
|
val cached = objectCommitCache.getEntry(id)
|
||||||
if (cached == null) {
|
if (cached == null) {
|
||||||
@@ -452,6 +471,9 @@ object JGitUtil {
|
|||||||
} else {
|
} else {
|
||||||
(id, mode, name, path, opt, Some(cached.getValue))
|
(id, mode, name, path, opt, Some(cached.getValue))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
val commit = getCommit(path)
|
||||||
|
(id, mode, name, path, opt, Some(commit))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -688,7 +710,7 @@ object JGitUtil {
|
|||||||
|
|
||||||
val toCommit = revWalk.parseCommit(git.getRepository.resolve(to))
|
val toCommit = revWalk.parseCommit(git.getRepository.resolve(to))
|
||||||
(from match {
|
(from match {
|
||||||
case None => {
|
case None =>
|
||||||
toCommit.getParentCount match {
|
toCommit.getParentCount match {
|
||||||
case 0 =>
|
case 0 =>
|
||||||
df.scan(
|
df.scan(
|
||||||
@@ -698,11 +720,9 @@ object JGitUtil {
|
|||||||
.asScala
|
.asScala
|
||||||
case _ => df.scan(toCommit.getParent(0), toCommit.getTree).asScala
|
case _ => df.scan(toCommit.getParent(0), toCommit.getTree).asScala
|
||||||
}
|
}
|
||||||
}
|
case Some(from) =>
|
||||||
case Some(from) => {
|
|
||||||
val fromCommit = revWalk.parseCommit(git.getRepository.resolve(from))
|
val fromCommit = revWalk.parseCommit(git.getRepository.resolve(from))
|
||||||
df.scan(fromCommit.getTree, toCommit.getTree).asScala
|
df.scan(fromCommit.getTree, toCommit.getTree).asScala
|
||||||
}
|
|
||||||
}).toSeq
|
}).toSeq
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -717,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(
|
def getDiffs(
|
||||||
git: Git,
|
git: Git,
|
||||||
from: Option[String],
|
from: Option[String],
|
||||||
@@ -726,7 +769,7 @@ object JGitUtil {
|
|||||||
): List[DiffInfo] = {
|
): List[DiffInfo] = {
|
||||||
val diffs = getDiffEntries(git, from, to)
|
val diffs = getDiffEntries(git, from, to)
|
||||||
diffs.map { diff =>
|
diffs.map { diff =>
|
||||||
if (diffs.size > 100) {
|
if (diffs.size > 100) { // Don't show diff if there are more than 100 files
|
||||||
DiffInfo(
|
DiffInfo(
|
||||||
changeType = diff.getChangeType,
|
changeType = diff.getChangeType,
|
||||||
oldPath = diff.getOldPath,
|
oldPath = diff.getOldPath,
|
||||||
@@ -745,49 +788,35 @@ object JGitUtil {
|
|||||||
} else {
|
} else {
|
||||||
val oldIsImage = FileUtil.isImage(diff.getOldPath)
|
val oldIsImage = FileUtil.isImage(diff.getOldPath)
|
||||||
val newIsImage = FileUtil.isImage(diff.getNewPath)
|
val newIsImage = FileUtil.isImage(diff.getNewPath)
|
||||||
if (!fetchContent || oldIsImage || newIsImage) {
|
val patch = if (oldIsImage || newIsImage) None else Some(makePatchFromDiffEntry(git, diff)) // TODO use DiffFormatter
|
||||||
DiffInfo(
|
val tooLarge = patch.exists(_.count(_ == '\n') > 1000) // Don't show diff if the file has more than 1000 lines diff
|
||||||
changeType = diff.getChangeType,
|
val includeContent = tooLarge || !fetchContent || oldIsImage || newIsImage
|
||||||
oldPath = diff.getOldPath,
|
DiffInfo(
|
||||||
newPath = diff.getNewPath,
|
changeType = diff.getChangeType,
|
||||||
oldContent = None,
|
oldPath = diff.getOldPath,
|
||||||
newContent = None,
|
newPath = diff.getNewPath,
|
||||||
oldIsImage = oldIsImage,
|
oldContent = if (includeContent) None else getTextContent(git, diff.getOldId.toObjectId),
|
||||||
newIsImage = newIsImage,
|
newContent = if (includeContent) None else getTextContent(git, diff.getNewId.toObjectId),
|
||||||
oldObjectId = Option(diff.getOldId).map(_.name),
|
oldIsImage = oldIsImage,
|
||||||
newObjectId = Option(diff.getNewId).map(_.name),
|
newIsImage = newIsImage,
|
||||||
oldMode = diff.getOldMode.toString,
|
oldObjectId = Option(diff.getOldId).map(_.name),
|
||||||
newMode = diff.getNewMode.toString,
|
newObjectId = Option(diff.getNewId).map(_.name),
|
||||||
tooLarge = false,
|
oldMode = diff.getOldMode.toString,
|
||||||
patch = (if (makePatch) Some(makePatchFromDiffEntry(git, diff)) else None) // TODO use DiffFormatter
|
newMode = diff.getNewMode.toString,
|
||||||
)
|
tooLarge = tooLarge,
|
||||||
} else {
|
patch = if (makePatch) patch else None
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.toList
|
}.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 = {
|
private def makePatchFromDiffEntry(git: Git, diff: DiffEntry): String = {
|
||||||
val out = new ByteArrayOutputStream()
|
val out = new ByteArrayOutputStream()
|
||||||
Using.resource(new DiffFormatter(out)) { formatter =>
|
Using.resource(new DiffFormatter(out)) { formatter =>
|
||||||
|
|||||||
@@ -28,7 +28,12 @@ object Keys {
|
|||||||
/**
|
/**
|
||||||
* Session key for the OpenID Connect authentication.
|
* 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.
|
* Generate session key for the issue search condition.
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class Mailer(settings: SystemSettings) {
|
|||||||
htmlMsg: Option[String] = None,
|
htmlMsg: Option[String] = None,
|
||||||
loginAccount: Option[Account] = None
|
loginAccount: Option[Account] = None
|
||||||
): Option[HtmlEmail] = {
|
): Option[HtmlEmail] = {
|
||||||
if (settings.notification) {
|
if (settings.basicBehavior.notification) {
|
||||||
settings.smtp.map { smtp =>
|
settings.smtp.map { smtp =>
|
||||||
val email = new HtmlEmail
|
val email = new HtmlEmail
|
||||||
email.setHostName(smtp.host)
|
email.setHostName(smtp.host)
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ object StringUtil {
|
|||||||
|
|
||||||
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")
|
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 splitWords(value: String): Array[String] = value.split("[ \\t ]+")
|
||||||
|
|
||||||
def isInteger(value: String): Boolean = allCatch opt { value.toInt } map (_ => true) getOrElse (false)
|
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.handleData(content, 0, content.length)
|
||||||
detector.dataEnd()
|
detector.dataEnd()
|
||||||
detector.getDetectedCharset match {
|
detector.getDetectedCharset match {
|
||||||
case null => "UTF-8"
|
case null | "US-ASCII" => "UTF-8"
|
||||||
case e => e
|
case e => e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ trait AvatarImageProvider { self: RequestCache =>
|
|||||||
val src = if (mailAddress.isEmpty) {
|
val src = if (mailAddress.isEmpty) {
|
||||||
// by user name
|
// by user name
|
||||||
getAccountByUserNameFromCache(userName).map { account =>
|
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"""
|
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
|
||||||
} else {
|
} else {
|
||||||
s"""${context.path}/${account.userName}/_avatar?${helpers.hashDate(account.updatedDate)}"""
|
s"""${context.path}/${account.userName}/_avatar?${helpers.hashDate(account.updatedDate)}"""
|
||||||
@@ -29,13 +29,13 @@ trait AvatarImageProvider { self: RequestCache =>
|
|||||||
} else {
|
} else {
|
||||||
// by mail address
|
// by mail address
|
||||||
getAccountByMailAddressFromCache(mailAddress).map { account =>
|
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"""
|
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
|
||||||
} else {
|
} else {
|
||||||
s"""${context.path}/${account.userName}/_avatar?${helpers.hashDate(account.updatedDate)}"""
|
s"""${context.path}/${account.userName}/_avatar?${helpers.hashDate(account.updatedDate)}"""
|
||||||
}
|
}
|
||||||
} getOrElse {
|
} 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"""
|
s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
|
||||||
} else {
|
} else {
|
||||||
s"""${context.path}/_unknown/_avatar"""
|
s"""${context.path}/_unknown/_avatar"""
|
||||||
|
|||||||
@@ -212,19 +212,19 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
Html(
|
Html(
|
||||||
message
|
message
|
||||||
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]"){ m =>
|
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]"){ m =>
|
||||||
val issue = getIssueFromCache(m.group(1), m.group(2), m.group(3))
|
getIssueFromCache(m.group(1), m.group(2), m.group(3)) match {
|
||||||
if (issue.isDefined) {
|
case Some(issue) =>
|
||||||
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/issues/${m.group(3)}" title="${issue.get.title}">${m.group(1)}/${m.group(2)}#${m.group(3)}</a>"""
|
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/issues/${m.group(3)}" title="${StringUtil.escapeHtml(issue.title)}">${m.group(1)}/${m.group(2)}#${m.group(3)}</a>"""
|
||||||
} else {
|
case None =>
|
||||||
s"${m.group(1)}/${m.group(2)}#${m.group(3)}"
|
s"${m.group(1)}/${m.group(2)}#${m.group(3)}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]"){ m =>
|
.replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]"){ m =>
|
||||||
val pullreq = getIssueFromCache(m.group(1), m.group(2), m.group(3))
|
getIssueFromCache(m.group(1), m.group(2), m.group(3)) match {
|
||||||
if (pullreq.isDefined) {
|
case Some(pullreq) =>
|
||||||
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/pull/${m.group(3)}" title="${pullreq.get.title}">${m.group(1)}/${m.group(2)}#${m.group(3)}</a>"""
|
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/pull/${m.group(3)}" title="${StringUtil.escapeHtml(pullreq.title)}">${m.group(1)}/${m.group(2)}#${m.group(3)}</a>"""
|
||||||
} else {
|
case None =>
|
||||||
s"${m.group(1)}/${m.group(2)}#${m.group(3)}"
|
s"${m.group(1)}/${m.group(2)}#${m.group(3)}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]") { m =>
|
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]") { m =>
|
||||||
@@ -276,7 +276,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
/**
|
/**
|
||||||
* URL encode except '/'.
|
* 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)
|
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>
|
<label class="strong">Services</label>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label class="checkbox">
|
<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
|
Use Gravatar for profile images
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -19,22 +19,40 @@
|
|||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox" id="sshEnabled" name="ssh.enabled"@if(context.settings.ssh.enabled){ checked}/>
|
<input type="checkbox" id="sshEnabled" name="ssh.enabled"@if(context.settings.ssh.enabled){ checked}/>
|
||||||
Enable SSH access to git repository
|
Enable SSH access to git repository
|
||||||
<span class="muted normal">(Both SSH host and Base URL are required if SSH access is enabled)</span>
|
<span class="muted normal">(Both SSH bind host and Base URL are required if SSH access is enabled)</span>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="ssh">
|
<div class="ssh">
|
||||||
<div class="form-group">
|
<div class="bindAddress">
|
||||||
<label class="control-label col-md-2" for="sshHost">SSH host</label>
|
<div class="form-group">
|
||||||
<div class="col-md-10">
|
<label class="control-label col-md-2" for="sshBindHost">SSH bind host</label>
|
||||||
<input type="text" id="sshHost" name="ssh.host" class="form-control" value="@context.settings.ssh.sshHost"/>
|
<div class="col-md-10">
|
||||||
<span id="error-ssh_host" class="error"></span>
|
<input type="text" id="sshBindHost" name="ssh.bindAddress.host" class="form-control" value="@context.settings.ssh.bindAddress.map(_.host)"/>
|
||||||
|
<span id="error-ssh_bindAddress_host" class="error"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-md-2" for="sshBindPort">SSH bind port</label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input type="text" id="sshBindPort" name="ssh.bindAddress.port" class="form-control" value="@context.settings.ssh.bindAddress.map(_.port)"/>
|
||||||
|
<span id="error-ssh_bindAddress_port" class="error"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="publicAddress">
|
||||||
<label class="control-label col-md-2" for="sshPort">SSH port</label>
|
<div class="form-group">
|
||||||
<div class="col-md-10">
|
<label class="control-label col-md-2" for="sshPublicHost">SSH public host</label>
|
||||||
<input type="text" id="sshPort" name="ssh.port" class="form-control" value="@context.settings.ssh.sshPort"/>
|
<div class="col-md-10">
|
||||||
<span id="error-ssh_port" class="error"></span>
|
<input type="text" id="sshPublicHost" name="ssh.publicAddress.host" class="form-control" value="@context.settings.ssh.publicAddress.map(_.host)"/>
|
||||||
|
<span id="error-ssh_publicAddress_host" class="error"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-md-2" for="sshPublicPort">SSH public port</label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input type="text" id="sshPublicPort" name="ssh.publicAddress.port" class="form-control" value="@context.settings.ssh.publicAddress.map(_.port)"/>
|
||||||
|
<span id="error-ssh_publicAddress_port" class="error"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,7 +132,7 @@
|
|||||||
<label class="strong">Notifications</label>
|
<label class="strong">Notifications</label>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label class="checkbox" for="notification">
|
<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
|
Send notifications
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -108,15 +108,30 @@
|
|||||||
<label class="strong">Account registration</label>
|
<label class="strong">Account registration</label>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label class="radio">
|
<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>
|
<span class="strong">Allow</span> <span class="normal">- Users can create accounts by themselves.</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="radio">
|
<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>
|
<span class="strong">Deny</span> <span class="normal">- Only administrators can create accounts.</span>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</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 -->
|
<!-- Repository operations -->
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
<hr>
|
<hr>
|
||||||
@@ -126,11 +141,11 @@
|
|||||||
<label class="control-label col-md-2">Create</label>
|
<label class="control-label col-md-2">Create</label>
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<label class="radio">
|
<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>
|
<span class="strong">All users</span> <span class="normal">- All users can create repository.</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="radio">
|
<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>
|
<span class="strong">Admin only</span> <span class="normal">- Only administrators can create repository.</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,11 +154,11 @@
|
|||||||
<label class="control-label col-md-2">Delete</label>
|
<label class="control-label col-md-2">Delete</label>
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<label class="radio">
|
<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>
|
<span class="strong">All users</span> <span class="normal">- All users can delete repository.</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="radio">
|
<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>
|
<span class="strong">Admin only</span> <span class="normal">- Only administrators can delete repository.</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -152,11 +167,11 @@
|
|||||||
<label class="control-label col-md-2">Rename</label>
|
<label class="control-label col-md-2">Rename</label>
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<label class="radio">
|
<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>
|
<span class="strong">All users</span> <span class="normal">- All users can rename repository.</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="radio">
|
<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>
|
<span class="strong">Admin only</span> <span class="normal">- Only administrators can rename repository.</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,11 +180,11 @@
|
|||||||
<label class="control-label col-md-2">Transfer</label>
|
<label class="control-label col-md-2">Transfer</label>
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<label class="radio">
|
<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>
|
<span class="strong">All users</span> <span class="normal">- All users can transfer repository.</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="radio">
|
<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>
|
<span class="strong">Admin only</span> <span class="normal">- Only administrators can transfer repository.</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -178,11 +193,11 @@
|
|||||||
<label class="control-label col-md-2">Fork</label>
|
<label class="control-label col-md-2">Fork</label>
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<label class="radio">
|
<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>
|
<span class="strong">All users</span> <span class="normal">- All users can fork repository.</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="radio">
|
<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>
|
<span class="strong">Admin only</span> <span class="normal">- Only administrators can fork repository.</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,11 +207,11 @@
|
|||||||
<label class="strong">Default visibility when creating a new repository</label>
|
<label class="strong">Default visibility when creating a new repository</label>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label class="radio">
|
<label class="radio">
|
||||||
<input type="radio" name="isCreateRepoOptionPublic" value="true"@if(context.settings.isCreateRepoOptionPublic){ checked}>
|
<input type="radio" name="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>
|
<span class="strong">Public</span> <span class="normal">- All users and guests can read the repository.</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="radio">
|
<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>
|
<span class="strong">Private</span> <span class="normal">- Only collaborators can read the repository.</span>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -207,11 +222,11 @@
|
|||||||
<label class="strong">Anonymous access</label>
|
<label class="strong">Anonymous access</label>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label class="radio">
|
<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>
|
<span class="strong">Allow</span> <span class="normal">- Anyone can view public repositories and user/group profiles.</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="radio">
|
<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>
|
<span class="strong">Deny</span> <span class="normal">- Users must authenticate before viewing any information.</span>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -272,11 +287,11 @@
|
|||||||
<label><span class="strong">Show repositories in sidebar</span></label>
|
<label><span class="strong">Show repositories in sidebar</span></label>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label class="radio">
|
<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>
|
<span class="strong">All</span> <span class="normal">- Show all visible repositories in sidebar.</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="radio">
|
<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>
|
<span class="strong">Limited</span> <span class="normal">- Show only owned repositories in sidebar.</span>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -5,10 +5,11 @@
|
|||||||
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
|
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
|
||||||
filter: String,
|
filter: String,
|
||||||
groups: List[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.html.main("Issues"){
|
||||||
@gitbucket.core.dashboard.html.sidebar(recentRepositories){
|
@gitbucket.core.dashboard.html.sidebar(recentRepositories){
|
||||||
@gitbucket.core.dashboard.html.tab("issues")
|
@gitbucket.core.dashboard.html.tab(enableNewsFeed, "issues")
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@gitbucket.core.dashboard.html.issuesnavi("issues", filter, openCount, closedCount, condition)
|
@gitbucket.core.dashboard.html.issuesnavi("issues", filter, openCount, closedCount, condition)
|
||||||
@gitbucket.core.dashboard.html.issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
@gitbucket.core.dashboard.html.issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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>
|
<tr>
|
||||||
<td style="padding-top: 12px; padding-bottom: 12px;">
|
<td style="padding-top: 12px; padding-bottom: 12px;">
|
||||||
<a href="@context.path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a> ・
|
<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="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">
|
<span class="pull-right muted">
|
||||||
@issue.assignedUserName.map { userName =>
|
@assignedUserNames.map { userName =>
|
||||||
@helpers.avatar(userName, 20, tooltip = true)
|
@helpers.avatar(userName, 20, tooltip = true)
|
||||||
}
|
}
|
||||||
@if(commentCount > 0){
|
@if(commentCount > 0){
|
||||||
|
|||||||
@@ -5,10 +5,11 @@
|
|||||||
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
|
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
|
||||||
filter: String,
|
filter: String,
|
||||||
groups: List[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.html.main("Pull requests"){
|
||||||
@gitbucket.core.dashboard.html.sidebar(recentRepositories){
|
@gitbucket.core.dashboard.html.sidebar(recentRepositories){
|
||||||
@gitbucket.core.dashboard.html.tab("pulls")
|
@gitbucket.core.dashboard.html.tab(enableNewsFeed, "pulls")
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@gitbucket.core.dashboard.html.issuesnavi("pulls", filter, openCount, closedCount, condition)
|
@gitbucket.core.dashboard.html.issuesnavi("pulls", filter, openCount, closedCount, condition)
|
||||||
@gitbucket.core.dashboard.html.issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
@gitbucket.core.dashboard.html.issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
@(groups: List[String],
|
@(groups: List[String],
|
||||||
visibleRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
|
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
|
@import gitbucket.core.view.helpers
|
||||||
@gitbucket.core.html.main("Repositories"){
|
@gitbucket.core.html.main("Repositories"){
|
||||||
@gitbucket.core.dashboard.html.sidebar(recentRepositories){
|
@gitbucket.core.dashboard.html.sidebar(recentRepositories){
|
||||||
@gitbucket.core.dashboard.html.tab("repos")
|
@gitbucket.core.dashboard.html.tab(enableNewsFeed, "repos")
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="btn-group" id="owner-dropdown">
|
<div class="btn-group" id="owner-dropdown">
|
||||||
<button class="dropdown-toggle btn btn-default" data-toggle="dropdown" aria-expanded="false">
|
<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)
|
@(enableNewsFeed: Boolean, active: String = "")(implicit context: gitbucket.core.controller.Context)
|
||||||
<ul class="nav nav-tabs" style="margin-bottom: 20px;">
|
@if(enableNewsFeed || context.loginAccount.isDefined) {
|
||||||
<li @if(active == ""){ class="active"}><a href="@context.path/">News feed</a></li>
|
<ul class="nav nav-tabs" style="margin-bottom: 20px;">
|
||||||
@if(context.loginAccount.isDefined){
|
@if(enableNewsFeed) {
|
||||||
<li @if(active == "repos" ){ class="active"}><a href="@context.path/dashboard/repos">Repositories</a></li>
|
<li @if(active == "") {class="active"}><a href="@context.path/">News feed</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>
|
@if(context.loginAccount.isDefined) {
|
||||||
@gitbucket.core.plugin.PluginRegistry().getDashboardTabs.map { tab =>
|
<li @if(active == "repos") {class="active"}><a href="@context.path/dashboard/repos">Repositories</a></li>
|
||||||
@tab(context).map { link =>
|
<li @if(active == "pulls") {class="active"}><a href="@context.path/dashboard/pulls">Pull requests</a></li>
|
||||||
<li @if(active == link.id){ class="active"}><a href="@context.path/@link.path">@link.label</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>
|
}
|
||||||
|
|||||||
@@ -24,57 +24,15 @@
|
|||||||
case "create_tag" => simpleActivity(activity)
|
case "create_tag" => simpleActivity(activity)
|
||||||
case "delete_tag" => simpleActivity(activity)
|
case "delete_tag" => simpleActivity(activity)
|
||||||
case "fork" => simpleActivity(activity)
|
case "fork" => simpleActivity(activity)
|
||||||
case "push" => customActivity(activity){
|
case "push" => pushActivity(activity)
|
||||||
<div class="small activity-message">
|
case "create_wiki" => createWikiActivity(activity)
|
||||||
{activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
|
case "edit_wiki" => editWikiActivity(activity)
|
||||||
if(i == 3){
|
case "delete_wiki" => simpleActivity(activity)
|
||||||
<div>...</div>
|
|
||||||
} else {
|
|
||||||
if(commit.nonEmpty){
|
|
||||||
<div>
|
|
||||||
<a href={s"${context.path}/${activity.userName}/${activity.repositoryName}/commit/${commit. substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a>
|
|
||||||
<span>{commit.substring(41)}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
case "create_wiki" => customActivity(activity){
|
|
||||||
<div class="small activity-message">
|
|
||||||
Created <a href={s"${context.path}/${activity.userName}/${activity.repositoryName}/wiki/${activity.additionalInfo.get}"}>{activity.additionalInfo.get}</a>.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
case "edit_wiki" => customActivity(activity){
|
|
||||||
activity.additionalInfo.get.split(":") match {
|
|
||||||
case Array(pageName, commitId) =>
|
|
||||||
<div class="small activity-message">
|
|
||||||
Edited <a href={s"${context.path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>.
|
|
||||||
<a href={s"${context.path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}/_compare/${commitId.substring(0, 7)}^...${commitId.substring(0, 7)}"}>View the diff »</a>
|
|
||||||
</div>
|
|
||||||
case Array(pageName) =>
|
|
||||||
<div class="small activity-message">
|
|
||||||
Edited <a href={s"${context.path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "delete_wiki" => simpleActivity(activity)
|
|
||||||
})
|
})
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@customActivity(activity: gitbucket.core.model.Activity)(additionalInfo: Any) = {
|
|
||||||
<div>
|
|
||||||
<div class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</div>
|
|
||||||
<div class="strong">
|
|
||||||
@helpers.avatarLink(activity.activityUserName, 16)
|
|
||||||
@helpers.activityMessage(activity.message)
|
|
||||||
</div>
|
|
||||||
@additionalInfo
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@simpleActivity(activity: gitbucket.core.model.Activity) = {
|
@simpleActivity(activity: gitbucket.core.model.Activity) = {
|
||||||
<div>
|
<div>
|
||||||
<span class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</span>
|
<span class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</span>
|
||||||
@@ -85,3 +43,66 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@pushActivity(activity: gitbucket.core.model.Activity) = {
|
||||||
|
<div>
|
||||||
|
<div class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</div>
|
||||||
|
<div class="strong">
|
||||||
|
@helpers.avatarLink(activity.activityUserName, 16)
|
||||||
|
@helpers.activityMessage(activity.message)
|
||||||
|
</div>
|
||||||
|
<div class="small activity-message">
|
||||||
|
@activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map { case (commit, i) =>
|
||||||
|
@if(i == 3){
|
||||||
|
<div>...</div>
|
||||||
|
} else {
|
||||||
|
@if(commit.nonEmpty){
|
||||||
|
<div>
|
||||||
|
<a href="@{context.path}/@{activity.userName}/@{activity.repositoryName}/commit/@{commit. substring(0, 40)}" class="monospace">@{commit.substring(0, 7)}</a>
|
||||||
|
<span>@{commit.substring(41)}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@createWikiActivity(activity: gitbucket.core.model.Activity) = {
|
||||||
|
<div>
|
||||||
|
<div class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</div>
|
||||||
|
<div class="strong">
|
||||||
|
@helpers.avatarLink(activity.activityUserName, 16)
|
||||||
|
@helpers.activityMessage(activity.message)
|
||||||
|
</div>
|
||||||
|
<div class="small activity-message">
|
||||||
|
Created <a href="@{context.path}/@{activity.userName}/@{activity.repositoryName}/wiki/@{activity.additionalInfo}">@{activity.additionalInfo}</a>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@editWikiActivity(activity: gitbucket.core.model.Activity) = {
|
||||||
|
<div>
|
||||||
|
<div class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</div>
|
||||||
|
<div class="strong">
|
||||||
|
@helpers.avatarLink(activity.activityUserName, 16)
|
||||||
|
@helpers.activityMessage(activity.message)
|
||||||
|
</div>
|
||||||
|
@defining(activity.additionalInfo.get.split(":")){ additionalInfo =>
|
||||||
|
@if(additionalInfo.length == 2) {
|
||||||
|
@defining((additionalInfo(0), additionalInfo(1))) { case (pageName, commitId) =>
|
||||||
|
<div class="small activity-message">
|
||||||
|
Edited <a href="@{context.path}/@{activity.userName}/@{activity.repositoryName}/wiki/@pageName">@pageName</a>.
|
||||||
|
<a href="@{context.path}/@{activity.userName}/@{activity.repositoryName}/wiki/@{pageName}/_compare/@{commitId.substring(0, 7)}^...@{commitId.substring(0, 7)}">View the diff »</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if(additionalInfo.length == 1) {
|
||||||
|
@defining(additionalInfo(0)) { pageName =>
|
||||||
|
<div class="small activity-message">
|
||||||
|
Edited <a href="@{context.path}/@{activity.userName}/@{activity.repositoryName}/wiki/@{pageName}">@pageName</a>.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user