mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-08 23:27:43 +02:00
Compare commits
264 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baf560d532 | ||
|
|
448b3ac144 | ||
|
|
82be26dbae | ||
|
|
102c9e8245 | ||
|
|
2587f2bd19 | ||
|
|
9c4c2dfc81 | ||
|
|
6ff90aae1a | ||
|
|
79a9cfb3bb | ||
|
|
c218cf70b6 | ||
|
|
4b4810438e | ||
|
|
371913a9d5 | ||
|
|
3d13ae705a | ||
|
|
abd94807fe | ||
|
|
eea43c30d2 | ||
|
|
32bf51ff1a | ||
|
|
e32c68c7d7 | ||
|
|
c6691e1766 | ||
|
|
860813174a | ||
|
|
d10736387b | ||
|
|
90b9c17d6e | ||
|
|
f9e4500bbb | ||
|
|
df60395ea2 | ||
|
|
3111ab3234 | ||
|
|
74a9aaa31d | ||
|
|
175232cac1 | ||
|
|
cce9c62591 | ||
|
|
35d670843f | ||
|
|
40555c8464 | ||
|
|
28dadef118 | ||
|
|
b697fdf189 | ||
|
|
f72617769b | ||
|
|
b355599a96 | ||
|
|
af6e99d832 | ||
|
|
312927efda | ||
|
|
366074ec55 | ||
|
|
7d679781a8 | ||
|
|
8f2e0f8505 | ||
|
|
73ccfd0788 | ||
|
|
c25145b793 | ||
|
|
1b7fbcb59d | ||
|
|
03760f126b | ||
|
|
3826b690cf | ||
|
|
3f3b111afc | ||
|
|
379c86ba9d | ||
|
|
6dce8672ab | ||
|
|
85d8432755 | ||
|
|
edbec530f3 | ||
|
|
952bfcc37b | ||
|
|
0f48c3c926 | ||
|
|
ed0a6ef4ff | ||
|
|
0bdc765198 | ||
|
|
9a613bb678 | ||
|
|
06ea83579d | ||
|
|
a84e501099 | ||
|
|
17b3587263 | ||
|
|
6cce445dcb | ||
|
|
22c09c2cb1 | ||
|
|
241d46847b | ||
|
|
6b145ea760 | ||
|
|
5fc3dd74a8 | ||
|
|
6ee03843c5 | ||
|
|
516f67f812 | ||
|
|
2b3b26d542 | ||
|
|
858de58cd0 | ||
|
|
3248bef98d | ||
|
|
0c9863f956 | ||
|
|
afc5c2d3d0 | ||
|
|
544a291dac | ||
|
|
9e8d5b89c7 | ||
|
|
86f069c980 | ||
|
|
69bd70b381 | ||
|
|
f1d0b0c080 | ||
|
|
45ae0cfd62 | ||
|
|
d2233e78e4 | ||
|
|
8ab6b386a3 | ||
|
|
2dd2c4f568 | ||
|
|
36f7011ebf | ||
|
|
fbcf962630 | ||
|
|
3b9b261878 | ||
|
|
b334809e3e | ||
|
|
9a1324a870 | ||
|
|
b7c39e90d2 | ||
|
|
63b4f25687 | ||
|
|
b582af4469 | ||
|
|
ecd3f5b4eb | ||
|
|
7be433d331 | ||
|
|
154be0f425 | ||
|
|
9ddd2065b7 | ||
|
|
6917cbf224 | ||
|
|
637b033782 | ||
|
|
7aee451a55 | ||
|
|
e1e369d653 | ||
|
|
c2ad66438c | ||
|
|
c88e5adac2 | ||
|
|
703fb4a650 | ||
|
|
465282580f | ||
|
|
e041f8ffa0 | ||
|
|
094386bb65 | ||
|
|
b9e830f06e | ||
|
|
bdbfbc62a9 | ||
|
|
5787a02daa | ||
|
|
0d6e5af8d7 | ||
|
|
88a8973685 | ||
|
|
e640cec323 | ||
|
|
5a4226db1d | ||
|
|
fb48c2a874 | ||
|
|
28da703052 | ||
|
|
2568171083 | ||
|
|
2dff7f1b9e | ||
|
|
8a79de8b50 | ||
|
|
1b763c753c | ||
|
|
3bb1b1269c | ||
|
|
dbbaf3cc9c | ||
|
|
4465a62b5f | ||
|
|
fb48e75ecf | ||
|
|
a79142074f | ||
|
|
9f58c6dce7 | ||
|
|
6fe903afab | ||
|
|
0d94865633 | ||
|
|
2fbdead64b | ||
|
|
72a354931e | ||
|
|
cb8affcd0d | ||
|
|
a5a997eb40 | ||
|
|
b4220aab68 | ||
|
|
c8a4798f86 | ||
|
|
6b8e9e8892 | ||
|
|
9ab9363d0b | ||
|
|
1edb18a147 | ||
|
|
d8b4bf3033 | ||
|
|
6597c4490b | ||
|
|
daaf1696ad | ||
|
|
53a1ca7874 | ||
|
|
f045fa4c6a | ||
|
|
53a7c5adf8 | ||
|
|
b142eca9a5 | ||
|
|
ba753a373b | ||
|
|
95ceca75a4 | ||
|
|
f16cc117a9 | ||
|
|
af66f8f746 | ||
|
|
d9cc57e8e0 | ||
|
|
72b6dad3a2 | ||
|
|
18f396b4a2 | ||
|
|
9f3fde8de2 | ||
|
|
dfd6f80b63 | ||
|
|
119d91210c | ||
|
|
75ef30ee03 | ||
|
|
d1cf9dd600 | ||
|
|
9c9fea908c | ||
|
|
1145c4d0f6 | ||
|
|
d847fc6e0f | ||
|
|
bb9585f7a6 | ||
|
|
e91411fa45 | ||
|
|
adbc065a6f | ||
|
|
862283b729 | ||
|
|
217df7012c | ||
|
|
e672d41e77 | ||
|
|
d975700bd4 | ||
|
|
f65e41561a | ||
|
|
dab4f33ed9 | ||
|
|
5ff45ef5ae | ||
|
|
af7c622647 | ||
|
|
f7027e57df | ||
|
|
6a4719469d | ||
|
|
6ebc865ba5 | ||
|
|
0c1e8b932b | ||
|
|
fda67a32e2 | ||
|
|
14d7e9ee90 | ||
|
|
b7b7322cce | ||
|
|
5eb44398d0 | ||
|
|
be7bb255c3 | ||
|
|
cb9522d416 | ||
|
|
d80afb473b | ||
|
|
607d85c661 | ||
|
|
a5fab3bc96 | ||
|
|
eb403ada58 | ||
|
|
911c102f39 | ||
|
|
bf23e854f8 | ||
|
|
52427c0a1e | ||
|
|
d8e5ac585c | ||
|
|
2fbeef73b0 | ||
|
|
15e39572dd | ||
|
|
9eac4f42c5 | ||
|
|
01d18bb5c3 | ||
|
|
00258e9125 | ||
|
|
046b337337 | ||
|
|
46cc7b6fd3 | ||
|
|
59344b4f05 | ||
|
|
c4d8af02b2 | ||
|
|
a10bc3687a | ||
|
|
b0d21dee42 | ||
|
|
13ea0e7507 | ||
|
|
d66fdaede5 | ||
|
|
d6217d89eb | ||
|
|
c99ff1cf0f | ||
|
|
001b9ae2ae | ||
|
|
f63493f1c0 | ||
|
|
c9095722f8 | ||
|
|
b9d2efa582 | ||
|
|
9c2e09020a | ||
|
|
1ffcf8c1e9 | ||
|
|
0124091840 | ||
|
|
2a28a7b35b | ||
|
|
2a68ffc8dc | ||
|
|
6b47c49cdd | ||
|
|
b634967776 | ||
|
|
172bed760d | ||
|
|
1b7eb69083 | ||
|
|
185c01db99 | ||
|
|
ab548d8c25 | ||
|
|
983975620b | ||
|
|
b512b08256 | ||
|
|
a54fb4960f | ||
|
|
93de53d717 | ||
|
|
f262c0a9eb | ||
|
|
e5d15569df | ||
|
|
369b08eae3 | ||
|
|
8e6fcb022b | ||
|
|
456b1f6571 | ||
|
|
6e459ad225 | ||
|
|
cece4c1c7d | ||
|
|
42e7a9fa9f | ||
|
|
f9510aba8e | ||
|
|
8a7d719025 | ||
|
|
1293a21450 | ||
|
|
65dd597ab7 | ||
|
|
e145b5151e | ||
|
|
b9684c277b | ||
|
|
4accb77533 | ||
|
|
9eef961025 | ||
|
|
546b40cdd1 | ||
|
|
274a08c14c | ||
|
|
eed4b51189 | ||
|
|
5c2f84367b | ||
|
|
d5b625e43f | ||
|
|
27f9e3dec9 | ||
|
|
00ef4db9a7 | ||
|
|
bf4f814389 | ||
|
|
23e45afd7f | ||
|
|
bfb02eef62 | ||
|
|
c129aae73a | ||
|
|
a955856cef | ||
|
|
a43a3fa55c | ||
|
|
a6254ab955 | ||
|
|
b505c3dc12 | ||
|
|
9d69b9e980 | ||
|
|
44b2320644 | ||
|
|
8fb9643ea5 | ||
|
|
10ea988298 | ||
|
|
7896945519 | ||
|
|
210342d2bc | ||
|
|
fdacea858b | ||
|
|
6825028d37 | ||
|
|
2089882d41 | ||
|
|
8e1d938155 | ||
|
|
39eb4cef04 | ||
|
|
d4e3adafa6 | ||
|
|
a7b8326499 | ||
|
|
249f8738d3 | ||
|
|
fefe6ef74f | ||
|
|
9ca6cd1d90 | ||
|
|
bff7b7c460 | ||
|
|
cfff79758b | ||
|
|
ded8ceb2c6 | ||
|
|
c502ebfc16 |
@@ -3,3 +3,6 @@
|
||||
|
||||
# Scala Steward: Reformat with scalafmt 3.8.2
|
||||
f1360f44c61f8e12666965c10e79f11cd75d6d30
|
||||
|
||||
# Scala Steward: Reformat with scalafmt 3.9.7
|
||||
a54fb4960ff0762738f4895cdc29bf2715a57f87
|
||||
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -3,3 +3,4 @@
|
||||
build.sbt @xuwei-k
|
||||
project/* @xuwei-k
|
||||
.github/workflows/* @xuwei-k
|
||||
.scalafmt.conf @xuwei-k
|
||||
|
||||
22
.github/ISSUE_TEMPLATE.md
vendored
22
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,22 +0,0 @@
|
||||
### Before submitting an issue to GitBucket I have first:
|
||||
|
||||
- [ ] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
|
||||
- [ ] searched for similar already existing issue
|
||||
- [ ] read the documentation and [wiki](https://github.com/gitbucket/gitbucket/wiki)
|
||||
|
||||
<!--
|
||||
|
||||
*(if you have performed all the above, remove the paragraph and continue describing the issue with template below)*
|
||||
|
||||
-->
|
||||
|
||||
## Issue
|
||||
**Impacted version**: xxxx
|
||||
|
||||
**Deployment mode**: *explain here how you use GitBucket : standalone app, under webcontainer (which one), with an http frontend (nginx, httpd, ...)*
|
||||
|
||||
**Problem description**:
|
||||
- *be as explicit as you can*
|
||||
- *describe the problem and its symptoms*
|
||||
- *explain how to reproduce*
|
||||
- *attach whatever information that can help understanding the context (screen capture, log files)*
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
41
.github/ISSUE_TEMPLATE/issue_template.yml
vendored
Normal file
41
.github/ISSUE_TEMPLATE/issue_template.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Report issue
|
||||
description: Report a problem or feature request with GitBucket
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Before submitting an issue to GitBucket, please ensure you have:
|
||||
- Read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
|
||||
- Searched for similar existing issues
|
||||
- Read the documentation and [wiki](https://github.com/gitbucket/gitbucket/wiki)
|
||||
- You can use [Gitter chat room](https://gitter.im/gitbucket/gitbucket) instead of GitHub Issues for casual discussion or inquiry
|
||||
|
||||
- type: checkboxes
|
||||
id: prerequisites
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
options:
|
||||
- label: I have read the contribution guidelines
|
||||
- label: I have searched for similar issues
|
||||
- label: I have read the documentation and wiki
|
||||
|
||||
- type: input
|
||||
id: impacted_version
|
||||
attributes:
|
||||
label: Impacted version
|
||||
description: Which version of GitBucket is affected?
|
||||
placeholder: e.g. 4.37.0
|
||||
|
||||
- type: input
|
||||
id: deployment_mode
|
||||
attributes:
|
||||
label: Deployment mode
|
||||
description: How do you use GitBucket? (standalone app, under webcontainer, with an HTTP frontend, etc.)
|
||||
placeholder: e.g. Standalone app, Tomcat, nginx
|
||||
|
||||
- type: textarea
|
||||
id: problem_description
|
||||
attributes:
|
||||
label: Problem description
|
||||
description: Be as explicit as you can. Describe the problem, its symptoms, how to reproduce, and attach any relevant information (screenshots, logs, etc.)
|
||||
placeholder: Describe the problem and how to reproduce it
|
||||
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -9,11 +9,11 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java: [17, 21]
|
||||
java: [17, 25]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
env:
|
||||
cache-name: cache-sbt-libs
|
||||
with:
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
~/.cache/coursier/v1
|
||||
key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }}
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: ${{ matrix.java }}
|
||||
distribution: adopt
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
- name: Build executable
|
||||
run: sbt executable
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gitbucket-java${{ matrix.java }}-${{ github.sha }}
|
||||
path: ./target/executable/gitbucket.*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
version = "3.8.5"
|
||||
version = "3.11.0"
|
||||
project.git = true
|
||||
|
||||
maxColumn = 120
|
||||
|
||||
49
CHANGELOG.md
49
CHANGELOG.md
@@ -1,6 +1,53 @@
|
||||
# Changelog
|
||||
All changes to the project will be documented in this file.
|
||||
|
||||
## 4.46.1 - 18 Apr 2026
|
||||
- Fix NullPointerException that could happen in the commits page
|
||||
- Add options to improve pull request compare performance
|
||||
|
||||
## 4.46.0 - 7 Mar 2026
|
||||
- Add support for reverting pull request
|
||||
- Add markdown toolbar
|
||||
- Enable text completion in Ace editor
|
||||
- Apply Ace editor for Wiki editing
|
||||
- Webhook security: SHA-256 support
|
||||
- Preserve UTF-8 BOM when editing files in browser
|
||||
|
||||
## 4.45.0 - 10 Jan 2026
|
||||
- Add new option to show full username on UI
|
||||
- Support render plugin in issues, pull requests, wiki and commit comments
|
||||
- Support link to other pages from Wiki page using Wiki link syntax
|
||||
|
||||
## 4.44.0 - 23 Sep 2025
|
||||
- Enhanced branch protection which supports the following settings:
|
||||
- Prevent pushes from non-allowed users
|
||||
- Whether to apply restrictions to administrator users as well
|
||||
- Improve logging for initialization errors
|
||||
|
||||
## 4.43.0 - 29 Jun 2025
|
||||
- Upgrade H2 database from 1.x to 2.x
|
||||
|
||||
Note that upgrading from h2 1.x to 2.x requires data file migration: https://www.h2database.com/html/migration-to-v2.html
|
||||
|
||||
It can't be done automatically using GitBucket's auto migration mechanism because it relies on database itself. So, users who use h2 will have to dump and recreate their database manually with the following steps:
|
||||
```bash
|
||||
# Export database using the current version of H2
|
||||
$ curl -O https://repo1.maven.org/maven2/com/h2database/h2/1.4.199/h2-1.4.199.jar
|
||||
$ java -cp h2-1.4.199.jar org.h2.tools.Script -url "jdbc:h2:~/.gitbucket/data" -user sa -password sa -script dump.sql
|
||||
|
||||
# Recreate database using the new version of H2
|
||||
$ curl -O https://repo1.maven.org/maven2/com/h2database/h2/2.3.232/h2-2.3.232.jar
|
||||
$ java -cp h2-2.3.232.jar org.h2.tools.RunScript -url "jdbc:h2:~/.gitbucket/data" -user sa -password sa -script dump.sql
|
||||
```
|
||||
|
||||
In addition, if `~/.gitbucket/database.conf` has the following configuration, remove `;MVCC=true` from `url`.
|
||||
```
|
||||
db {
|
||||
url = "jdbc:h2:${DatabaseHome};MVCC=true" // => "jdbc:h2:${DatabaseHome}"
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## 4.42.1 - 20 Jan 2025
|
||||
- Fix LDAP issue with SSL
|
||||
|
||||
@@ -47,7 +94,7 @@ All changes to the project will be documented in this file.
|
||||
## 4.38.1 - 10 Sep 2022
|
||||
- Fix comment diff in Chrome 105
|
||||
- Fix Markdown table CSS
|
||||
- Fix HTML rendering of multiple asignees
|
||||
- Fix HTML rendering of multiple assignees
|
||||
|
||||
## 4.38.0 - 3 Sep 2022
|
||||
- Support multiple assignees for Issues and Pull requests
|
||||
|
||||
47
README.md
47
README.md
@@ -44,7 +44,7 @@ GitBucket has a plug-in system that allows extra functionality. Officially the f
|
||||
- [gitbucket-pages-plugin](https://github.com/gitbucket/gitbucket-pages-plugin)
|
||||
- [gitbucket-notifications-plugin](https://github.com/gitbucket/gitbucket-notifications-plugin)
|
||||
|
||||
You can find more plugins made by the community at [GitBucket community plugins](https://gitbucket-plugins.github.io/).
|
||||
You can find more plugins made by the community at [GitBucket community plugins](https://github.com/gitbucket/gitbucket/wiki/Community-Plugins).
|
||||
|
||||
Building and Development
|
||||
-----------
|
||||
@@ -56,19 +56,42 @@ Support
|
||||
--------
|
||||
|
||||
- If you have any questions about GitBucket, see [Wiki](https://github.com/gitbucket/gitbucket/wiki) and check issues whether there is a same question or request in the past.
|
||||
- If you can't find same question and report, send it to our [Gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
|
||||
- If you can't find same question and report, send it to our [Gitter chat room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
|
||||
- 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.42.x
|
||||
What's New in 4.46.x
|
||||
-------------
|
||||
## 4.42.1 - 20 Jan 2025
|
||||
- Fix LDAP issue with SSL
|
||||
## 4.46.1 - 18 Apr 2026
|
||||
- Fix NullPointerException that could happen in the commits page
|
||||
- Add options to improve pull request compare performance
|
||||
|
||||
## 4.42.0 - 30 Dec 2024
|
||||
- Increase max branch name length 100 -> 255
|
||||
- Fix some GitHub incompatible Web APIs
|
||||
- Apply user-defined CSS after all plugins
|
||||
- Improve performance of listing commit logs
|
||||
- Drop Java 11 support. Java 17 is now required
|
||||
## 4.46.0 - 7 Mar 2026
|
||||
- Add support for reverting pull request
|
||||
- Add markdown toolbar
|
||||
- Enable text completion in Ace editor
|
||||
- Apply Ace editor for Wiki editing
|
||||
- Webhook security: SHA-256 support
|
||||
- Preserve UTF-8 BOM when editing files in browser
|
||||
|
||||
See the [change log](CHANGELOG.md) for all of the updates.
|
||||
Note that you have to migrate h2 database file if you will upgrade GitBucket from 4.42 or before to 4.43 or later and you are using the default h2 database because h2 1.x and h2.x don't have compatibility: https://www.h2database.com/html/migration-to-v2.html
|
||||
|
||||
It can't be done automatically using GitBucket's auto migration mechanism because it relies on database itself. So, users who use h2 will have to dump and recreate their database manually with the following steps:
|
||||
```bash
|
||||
# Export database using the current version of H2
|
||||
$ curl -O https://repo1.maven.org/maven2/com/h2database/h2/1.4.199/h2-1.4.199.jar
|
||||
$ java -cp h2-1.4.199.jar org.h2.tools.Script -url "jdbc:h2:~/.gitbucket/data" -user sa -password sa -script dump.sql
|
||||
|
||||
# Recreate database using the new version of H2
|
||||
$ curl -O https://repo1.maven.org/maven2/com/h2database/h2/2.3.232/h2-2.3.232.jar
|
||||
$ java -cp h2-2.3.232.jar org.h2.tools.RunScript -url "jdbc:h2:~/.gitbucket/data" -user sa -password sa -script dump.sql
|
||||
```
|
||||
|
||||
In addition, if `~/.gitbucket/database.conf` has the following configuration, remove `;MVCC=true` from `url`.
|
||||
```
|
||||
db {
|
||||
url = "jdbc:h2:${DatabaseHome};MVCC=true" // => "jdbc:h2:${DatabaseHome}"
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
See the [change log](CHANGELOG.md) for all the past updates.
|
||||
|
||||
89
build.sbt
89
build.sbt
@@ -2,10 +2,10 @@ import com.jsuereth.sbtpgp.PgpKeys._
|
||||
|
||||
val Organization = "io.github.gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val GitBucketVersion = "4.42.1"
|
||||
val ScalatraVersion = "3.1.1"
|
||||
val JettyVersion = "10.0.24"
|
||||
val JgitVersion = "6.10.0.202406032230-r"
|
||||
val GitBucketVersion = "4.46.1"
|
||||
val ScalatraVersion = "3.1.2"
|
||||
val JettyVersion = "10.0.26"
|
||||
val JgitVersion = "6.10.1.202505221210-r"
|
||||
|
||||
lazy val root = (project in file("."))
|
||||
.enablePlugins(SbtTwirl, ContainerPlugin)
|
||||
@@ -14,9 +14,9 @@ sourcesInBase := false
|
||||
organization := Organization
|
||||
name := Name
|
||||
version := GitBucketVersion
|
||||
scalaVersion := "2.13.16"
|
||||
scalaVersion := "2.13.18"
|
||||
|
||||
crossScalaVersions += "3.6.3"
|
||||
crossScalaVersions += "3.8.3"
|
||||
|
||||
// scalafmtOnCompile := true
|
||||
|
||||
@@ -28,46 +28,47 @@ libraryDependencies ++= Seq(
|
||||
"org.scalatra" %% "scalatra-javax" % ScalatraVersion,
|
||||
"org.scalatra" %% "scalatra-json-javax" % ScalatraVersion,
|
||||
"org.scalatra" %% "scalatra-forms-javax" % ScalatraVersion,
|
||||
"org.json4s" %% "json4s-jackson" % "4.1.0-M8",
|
||||
"commons-io" % "commons-io" % "2.18.0",
|
||||
"io.github.json4s" %% "json4s-jackson" % "4.1.0",
|
||||
"commons-io" % "commons-io" % "2.21.0",
|
||||
"io.github.gitbucket" % "solidbase" % "1.1.0",
|
||||
"io.github.gitbucket" % "markedj" % "1.0.20",
|
||||
"org.tukaani" % "xz" % "1.10",
|
||||
"org.apache.commons" % "commons-compress" % "1.27.1",
|
||||
"org.tukaani" % "xz" % "1.12",
|
||||
"org.apache.commons" % "commons-compress" % "1.28.0",
|
||||
"org.apache.commons" % "commons-email" % "1.6.0",
|
||||
"commons-net" % "commons-net" % "3.11.1",
|
||||
"commons-net" % "commons-net" % "3.13.0",
|
||||
"org.apache.httpcomponents" % "httpclient" % "4.5.14",
|
||||
"org.apache.sshd" % "apache-sshd" % "2.14.0" exclude ("org.slf4j", "slf4j-jdk14") exclude (
|
||||
"org.apache.sshd" % "apache-sshd" % "2.17.1" exclude ("org.slf4j", "slf4j-jdk14") exclude (
|
||||
"org.apache.sshd",
|
||||
"sshd-mina"
|
||||
) exclude ("org.apache.sshd", "sshd-netty"),
|
||||
"org.apache.tika" % "tika-core" % "3.0.0",
|
||||
"com.github.takezoe" %% "blocking-slick" % "0.0.14",
|
||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||
"com.h2database" % "h2" % "1.4.199",
|
||||
"org.mariadb.jdbc" % "mariadb-java-client" % "2.7.12",
|
||||
"org.postgresql" % "postgresql" % "42.7.5",
|
||||
"ch.qos.logback" % "logback-classic" % "1.5.16",
|
||||
"com.zaxxer" % "HikariCP" % "6.2.1" exclude ("org.slf4j", "slf4j-api"),
|
||||
"com.typesafe" % "config" % "1.4.3",
|
||||
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0",
|
||||
"io.github.java-diff-utils" % "java-diff-utils" % "4.15",
|
||||
"org.cache2k" % "cache2k-all" % "1.6.0.Final",
|
||||
"net.coobird" % "thumbnailator" % "0.4.20",
|
||||
"com.github.zafarkhaja" % "java-semver" % "0.10.2",
|
||||
"com.nimbusds" % "oauth2-oidc-sdk" % "11.21",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
|
||||
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
|
||||
"junit" % "junit" % "4.13.2" % "test",
|
||||
"org.scalatra" %% "scalatra-scalatest-javax" % ScalatraVersion % "test",
|
||||
"org.mockito" % "mockito-core" % "5.15.2" % "test",
|
||||
"com.dimafeng" %% "testcontainers-scala" % "0.41.5" % "test",
|
||||
"org.testcontainers" % "mysql" % "1.20.4" % "test",
|
||||
"org.testcontainers" % "postgresql" % "1.20.4" % "test",
|
||||
"net.i2p.crypto" % "eddsa" % "0.3.0",
|
||||
"is.tagomor.woothee" % "woothee-java" % "1.11.0",
|
||||
"org.ec4j.core" % "ec4j-core" % "1.1.0",
|
||||
"org.kohsuke" % "github-api" % "1.326" % "test"
|
||||
) exclude ("org.apache.sshd", "sshd-netty")
|
||||
exclude ("org.apache.sshd", "sshd-spring-sftp"),
|
||||
"org.apache.tika" % "tika-core" % "3.3.0",
|
||||
"com.github.takezoe" %% "blocking-slick" % "0.0.14",
|
||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||
"com.h2database" % "h2" % "2.4.240",
|
||||
"org.mariadb.jdbc" % "mariadb-java-client" % "2.7.13",
|
||||
"org.postgresql" % "postgresql" % "42.7.10",
|
||||
"ch.qos.logback" % "logback-classic" % "1.5.32",
|
||||
"com.zaxxer" % "HikariCP" % "7.0.2" exclude ("org.slf4j", "slf4j-api"),
|
||||
"com.typesafe" % "config" % "1.4.6",
|
||||
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0",
|
||||
"io.github.java-diff-utils" % "java-diff-utils" % "4.16",
|
||||
"org.cache2k" % "cache2k-api" % "2.6.1.Final",
|
||||
"org.cache2k" % "cache2k-core" % "2.6.1.Final",
|
||||
"net.coobird" % "thumbnailator" % "0.4.21",
|
||||
"com.github.zafarkhaja" % "java-semver" % "0.10.2",
|
||||
"com.nimbusds" % "oauth2-oidc-sdk" % "11.37",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
|
||||
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
|
||||
"junit" % "junit" % "4.13.2" % "test",
|
||||
"org.scalatra" %% "scalatra-scalatest-javax" % ScalatraVersion % "test",
|
||||
"org.mockito" % "mockito-core" % "5.23.0" % "test",
|
||||
"org.testcontainers" % "testcontainers-mysql" % "2.0.4" % "test",
|
||||
"org.testcontainers" % "testcontainers-postgresql" % "2.0.4" % "test",
|
||||
"net.i2p.crypto" % "eddsa" % "0.3.0",
|
||||
"is.tagomor.woothee" % "woothee-java" % "1.11.0",
|
||||
"org.ec4j.core" % "ec4j-core" % "1.2.0",
|
||||
"org.kohsuke" % "github-api" % "1.330" % "test"
|
||||
)
|
||||
|
||||
// Compiler settings
|
||||
@@ -191,7 +192,7 @@ executableKey := {
|
||||
|
||||
// zip it up
|
||||
IO delete (temp / "META-INF" / "MANIFEST.MF")
|
||||
val contentMappings = (temp.allPaths --- PathFinder(temp)).get pair { file =>
|
||||
val contentMappings = (temp.allPaths --- PathFinder(temp)).get() pair { file =>
|
||||
IO.relativizeFile(temp, file)
|
||||
}
|
||||
val manifest = new JarManifest
|
||||
@@ -215,9 +216,9 @@ executableKey := {
|
||||
outputFile
|
||||
}
|
||||
publishTo := {
|
||||
val nexus = "https://oss.sonatype.org/"
|
||||
if (version.value.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots")
|
||||
else Some("releases" at nexus + "service/local/staging/deploy/maven2")
|
||||
val centralSnapshots = "https://central.sonatype.com/repository/maven-snapshots/"
|
||||
if (isSnapshot.value) Some("central-snapshots" at centralSnapshots)
|
||||
else localStaging.value
|
||||
}
|
||||
publishMavenStyle := true
|
||||
pomIncludeRepository := { _ =>
|
||||
|
||||
@@ -12,7 +12,7 @@ javaOptions in Jetty ++= Seq(
|
||||
Run GitBucket:
|
||||
|
||||
```shell
|
||||
$ sbt ~jetty:start
|
||||
$ sbt ~container:start
|
||||
```
|
||||
|
||||
In IntelliJ, create remote debug configuration as follows. Make sure port number is same as above configuration.
|
||||
|
||||
@@ -38,15 +38,26 @@ Generate release files
|
||||
|
||||
For plug-in development, we have to publish the GitBucket jar file to the Maven central repository before release GitBucket itself.
|
||||
|
||||
First, hit following command to publish artifacts to the sonatype OSS repository:
|
||||
First, stage artifacts on your machine:
|
||||
|
||||
```bash
|
||||
$ sbt publishSigned
|
||||
```
|
||||
|
||||
Then logged-in to https://oss.sonatype.org/, close and release the repository.
|
||||
Next, upload artifacts to Sonatype's Central Portal with the following command:
|
||||
|
||||
You need to wait up to a day until [gitbucket-notification-plugin](https://plugins.gitbucket-community.org/) which is default bundled plugin is built for new version of GitBucket.
|
||||
```bash
|
||||
$ sbt sonaUpload
|
||||
```
|
||||
|
||||
Then logged-in to https://central.sonatype.com/ and publish the deployment.
|
||||
|
||||
You need to wait up to a day until default bundled plugins:
|
||||
|
||||
- https://github.com/gitbucket/gitbucket-notifications-plugin
|
||||
- https://github.com/gitbucket/gitbucket-gist-plugin
|
||||
- https://github.com/gitbucket/gitbucket-pages-plugin
|
||||
- https://github.com/gitbucket/gitbucket-emoji-plugin
|
||||
|
||||
### Make release war file
|
||||
|
||||
@@ -55,5 +66,4 @@ Run `sbt executable`. The release war file and fingerprint are generated into `t
|
||||
```bash
|
||||
$ sbt executable
|
||||
```
|
||||
|
||||
Create new release from the corresponded tag on GitHub, then upload generated jar file and fingerprints to the release.
|
||||
|
||||
@@ -1 +1 @@
|
||||
sbt.version=1.10.7
|
||||
sbt.version=1.12.9
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
|
||||
|
||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4")
|
||||
addSbtPlugin("org.playframework.twirl" % "sbt-twirl" % "2.0.7")
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.0")
|
||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.0")
|
||||
addSbtPlugin("org.playframework.twirl" % "sbt-twirl" % "2.0.9")
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.1")
|
||||
addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.4")
|
||||
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1")
|
||||
addSbtPlugin("com.github.sbt" % "sbt-license-report" % "1.7.0")
|
||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.0")
|
||||
addSbtPlugin("com.github.sbt" % "sbt-license-report" % "1.9.0")
|
||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.4.4")
|
||||
|
||||
addDependencyTreePlugin
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
notifications:1.11.0
|
||||
gist:4.23.0
|
||||
gist:4.24.0
|
||||
emoji:4.6.0
|
||||
pages:1.10.0
|
||||
|
||||
@@ -237,7 +237,7 @@
|
||||
<addForeignKeyConstraint constraintName="IDX_ISSUE_ID_FK1" baseTableName="ISSUE_ID" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
|
||||
|
||||
<!--================================================================================================-->
|
||||
<!-- ISSUE_ID -->
|
||||
<!-- ISSUE_LABEL -->
|
||||
<!--================================================================================================-->
|
||||
<createTable tableName="ISSUE_LABEL">
|
||||
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||
|
||||
32
src/main/resources/update/gitbucket-core_4.44.xml
Normal file
32
src/main/resources/update/gitbucket-core_4.44.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<changeSet>
|
||||
<!--================================================================================================-->
|
||||
<!-- PROTECTED_BRANCH -->
|
||||
<!--================================================================================================-->
|
||||
<addColumn tableName="PROTECTED_BRANCH">
|
||||
<column name="REQUIRED_STATUS_CHECK" type="boolean" nullable="false" defaultValue="false"/>
|
||||
<column name="RESTRICTIONS" type="boolean" nullable="false" defaultValue="false"/>
|
||||
</addColumn>
|
||||
|
||||
<sql>
|
||||
UPDATE PROTECTED_BRANCH SET REQUIRED_STATUS_CHECK = TRUE
|
||||
WHERE EXISTS (SELECT * FROM PROTECTED_BRANCH_REQUIRE_CONTEXT
|
||||
WHERE PROTECTED_BRANCH.USER_NAME = PROTECTED_BRANCH_REQUIRE_CONTEXT.USER_NAME
|
||||
AND PROTECTED_BRANCH.REPOSITORY_NAME = PROTECTED_BRANCH_REQUIRE_CONTEXT.REPOSITORY_NAME
|
||||
AND PROTECTED_BRANCH.BRANCH = PROTECTED_BRANCH_REQUIRE_CONTEXT.BRANCH)
|
||||
</sql>
|
||||
|
||||
<!--================================================================================================-->
|
||||
<!-- PROTECTED_BRANCH_RESTRICTIONS_USER -->
|
||||
<!--================================================================================================-->
|
||||
<createTable tableName="PROTECTED_BRANCH_RESTRICTION">
|
||||
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||
<column name="BRANCH" type="varchar(100)" nullable="false"/>
|
||||
<column name="ALLOWED_USER" type="varchar(255)" nullable="false"/>
|
||||
</createTable>
|
||||
|
||||
<addPrimaryKey constraintName="IDX_PROTECTED_BRANCH_RESTRICTION_PK" tableName="PROTECTED_BRANCH_RESTRICTION" columnNames="USER_NAME, REPOSITORY_NAME, BRANCH, ALLOWED_USER"/>
|
||||
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_RESTRICTION_FK0" baseTableName="PROTECTED_BRANCH_RESTRICTION" baseColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" referencedTableName="PROTECTED_BRANCH" referencedColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" onDelete="CASCADE" onUpdate="CASCADE"/>
|
||||
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_RESTRICTION_FK1" baseTableName="PROTECTED_BRANCH_RESTRICTION" baseColumnNames="ALLOWED_USER" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
|
||||
</changeSet>
|
||||
10
src/main/resources/update/gitbucket-core_4.46.xml
Normal file
10
src/main/resources/update/gitbucket-core_4.46.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<changeSet>
|
||||
<!--================================================================================================-->
|
||||
<!-- PULL_REQUEST -->
|
||||
<!--================================================================================================-->
|
||||
<addColumn tableName="PULL_REQUEST">
|
||||
<column name="MERGED_COMMIT_IDS" type="text" nullable="true"/>
|
||||
</addColumn>
|
||||
|
||||
</changeSet>
|
||||
@@ -119,7 +119,12 @@ object GitBucketCoreModule
|
||||
new Version("4.40.0"),
|
||||
new Version("4.41.0"),
|
||||
new Version("4.42.0", new LiquibaseMigration("update/gitbucket-core_4.42.xml")),
|
||||
new Version("4.42.1")
|
||||
new Version("4.42.1"),
|
||||
new Version("4.43.0"),
|
||||
new Version("4.44.0", new LiquibaseMigration("update/gitbucket-core_4.44.xml")),
|
||||
new Version("4.45.0"),
|
||||
new Version("4.46.0", new LiquibaseMigration("update/gitbucket-core_4.46.xml")),
|
||||
new Version("4.46.1")
|
||||
) {
|
||||
java.util.logging.Logger.getLogger("liquibase").setLevel(Level.SEVERE)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import gitbucket.core.util.RepositoryName
|
||||
* https://developer.github.com/v3/repos/#get-branch
|
||||
* https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection
|
||||
*/
|
||||
case class ApiBranch(name: String, commit: ApiBranchCommit, protection: ApiBranchProtection)(
|
||||
case class ApiBranch(name: String, commit: ApiBranchCommit, protection: ApiBranchProtectionResponse)(
|
||||
repositoryName: RepositoryName
|
||||
) extends FieldSerializable {
|
||||
val _links =
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
|
||||
case class ApiBranchProtectionRequest(
|
||||
enabled: Boolean,
|
||||
required_status_checks: Option[ApiBranchProtectionRequest.Status],
|
||||
restrictions: Option[ApiBranchProtectionRequest.Restrictions],
|
||||
enforce_admins: Option[Boolean]
|
||||
)
|
||||
|
||||
object ApiBranchProtectionRequest {
|
||||
|
||||
/** form for enabling-and-disabling-branch-protection */
|
||||
case class EnablingAndDisabling(protection: ApiBranchProtectionRequest)
|
||||
|
||||
case class Status(
|
||||
contexts: Seq[String]
|
||||
)
|
||||
|
||||
case class Restrictions(users: Seq[String])
|
||||
}
|
||||
@@ -4,55 +4,68 @@ import gitbucket.core.service.ProtectedBranchService
|
||||
import org.json4s._
|
||||
|
||||
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
|
||||
case class ApiBranchProtection(
|
||||
case class ApiBranchProtectionResponse(
|
||||
url: Option[ApiPath], // for output
|
||||
enabled: Boolean,
|
||||
required_status_checks: Option[ApiBranchProtection.Status]
|
||||
required_status_checks: Option[ApiBranchProtectionResponse.Status],
|
||||
restrictions: Option[ApiBranchProtectionResponse.Restrictions],
|
||||
enforce_admins: Option[ApiBranchProtectionResponse.EnforceAdmins]
|
||||
) {
|
||||
def status: ApiBranchProtection.Status = required_status_checks.getOrElse(ApiBranchProtection.statusNone)
|
||||
def status: ApiBranchProtectionResponse.Status =
|
||||
required_status_checks.getOrElse(ApiBranchProtectionResponse.statusNone)
|
||||
}
|
||||
|
||||
object ApiBranchProtection {
|
||||
object ApiBranchProtectionResponse {
|
||||
|
||||
/** form for enabling-and-disabling-branch-protection */
|
||||
case class EnablingAndDisabling(protection: ApiBranchProtection)
|
||||
case class EnforceAdmins(enabled: Boolean)
|
||||
|
||||
def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtection =
|
||||
ApiBranchProtection(
|
||||
// /** form for enabling-and-disabling-branch-protection */
|
||||
// case class EnablingAndDisabling(protection: ApiBranchProtectionResponse)
|
||||
|
||||
def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtectionResponse =
|
||||
ApiBranchProtectionResponse(
|
||||
url = Some(
|
||||
ApiPath(
|
||||
s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection"
|
||||
)
|
||||
),
|
||||
enabled = info.enabled,
|
||||
required_status_checks = Some(
|
||||
required_status_checks = info.contexts.map { contexts =>
|
||||
Status(
|
||||
Some(
|
||||
ApiPath(
|
||||
s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection/required_status_checks"
|
||||
)
|
||||
),
|
||||
EnforcementLevel(info.enabled && info.contexts.nonEmpty, info.includeAdministrators),
|
||||
info.contexts,
|
||||
EnforcementLevel(info.enabled && info.contexts.nonEmpty, info.enforceAdmins),
|
||||
contexts,
|
||||
Some(
|
||||
ApiPath(
|
||||
s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection/required_status_checks/contexts"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
restrictions = info.restrictionsUsers.map { restrictionsUsers =>
|
||||
Restrictions(restrictionsUsers)
|
||||
},
|
||||
enforce_admins = if (info.enabled) Some(EnforceAdmins(info.enforceAdmins)) else None
|
||||
)
|
||||
val statusNone = Status(None, Off, Seq.empty, None)
|
||||
|
||||
val statusNone: Status = Status(None, Off, Seq.empty, None)
|
||||
|
||||
case class Status(
|
||||
url: Option[ApiPath], // for output
|
||||
enforcement_level: EnforcementLevel,
|
||||
contexts: Seq[String],
|
||||
contexts_url: Option[ApiPath] // for output
|
||||
)
|
||||
|
||||
sealed class EnforcementLevel(val name: String)
|
||||
case object Off extends EnforcementLevel("off")
|
||||
case object NonAdmins extends EnforcementLevel("non_admins")
|
||||
case object Everyone extends EnforcementLevel("everyone")
|
||||
|
||||
object EnforcementLevel {
|
||||
def apply(enabled: Boolean, includeAdministrators: Boolean): EnforcementLevel =
|
||||
if (enabled) {
|
||||
@@ -66,6 +79,8 @@ object ApiBranchProtection {
|
||||
}
|
||||
}
|
||||
|
||||
case class Restrictions(users: Seq[String])
|
||||
|
||||
implicit val enforcementLevelSerializer: CustomSerializer[EnforcementLevel] =
|
||||
new CustomSerializer[EnforcementLevel](format =>
|
||||
(
|
||||
@@ -44,7 +44,7 @@ object JsonFormat {
|
||||
FieldSerializer[ApiCommits.File]() +
|
||||
FieldSerializer[ApiRelease]() +
|
||||
FieldSerializer[ApiReleaseAsset]() +
|
||||
ApiBranchProtection.enforcementLevelSerializer
|
||||
ApiBranchProtectionResponse.enforcementLevelSerializer
|
||||
|
||||
def apiPathSerializer(c: Context) =
|
||||
new CustomSerializer[ApiPath](_ =>
|
||||
|
||||
@@ -351,7 +351,7 @@ case class Context(
|
||||
val path: String = settings.baseUrl.getOrElse(request.getContextPath)
|
||||
val currentPath: String = request.getRequestURI.substring(request.getContextPath.length)
|
||||
val baseUrl: String = settings.baseUrl(request)
|
||||
val host: String = new java.net.URL(baseUrl).getHost
|
||||
val host: String = new java.net.URI(baseUrl).toURL.getHost
|
||||
val platform: String = request.getHeader("User-Agent") match {
|
||||
case null => null
|
||||
case agent if agent.contains("Mac") => "mac"
|
||||
|
||||
@@ -13,8 +13,8 @@ import gitbucket.core.util.*
|
||||
import gitbucket.core.view.helpers.*
|
||||
import org.scalatra.Ok
|
||||
import org.scalatra.forms.*
|
||||
|
||||
import gitbucket.core.service.ActivityService.*
|
||||
import gitbucket.core.view.helpers
|
||||
|
||||
class IndexController
|
||||
extends IndexControllerBase
|
||||
@@ -92,10 +92,18 @@ trait IndexControllerBase extends ControllerBase {
|
||||
}
|
||||
}
|
||||
|
||||
get("/_is_renderable") {
|
||||
helpers.isRenderable(params("filename"))
|
||||
}
|
||||
|
||||
get("/signin") {
|
||||
val redirect = params.get("redirect")
|
||||
if (redirect.isDefined && redirect.get.startsWith("/")) {
|
||||
flash.update(Keys.Flash.Redirect, redirect.get)
|
||||
if (context.loginAccount.nonEmpty) {
|
||||
redirect("/")
|
||||
}
|
||||
params.get("redirect").foreach { redirect =>
|
||||
if (redirect.startsWith("/")) {
|
||||
flash.update(Keys.Flash.Redirect, redirect)
|
||||
}
|
||||
}
|
||||
gitbucket.core.html.signin(flash.get("userName"), flash.get("password"), flash.get("error"))
|
||||
}
|
||||
@@ -243,13 +251,22 @@ trait IndexControllerBase extends ControllerBase {
|
||||
}
|
||||
}
|
||||
.map { t =>
|
||||
Map(
|
||||
"label" -> s"${avatar(t.userName, 16)}<b>@${StringUtil.escapeHtml(
|
||||
StringUtil.cutTail(t.userName, 25, "...")
|
||||
)}</b> ${StringUtil
|
||||
.escapeHtml(StringUtil.cutTail(t.fullName, 25, "..."))}",
|
||||
"value" -> t.userName
|
||||
)
|
||||
if (t.isGroupAccount) {
|
||||
Map(
|
||||
"label" -> s"${avatar(t.userName, 16)} <b>@${StringUtil.escapeHtml(
|
||||
StringUtil.cutTail(t.userName, 25, "...")
|
||||
)}</b>",
|
||||
"value" -> t.userName
|
||||
)
|
||||
} else {
|
||||
Map(
|
||||
"label" -> s"${avatar(t.userName, 16)} <b>@${StringUtil.escapeHtml(
|
||||
StringUtil.cutTail(t.userName, 25, "...")
|
||||
)}</b> (${StringUtil
|
||||
.escapeHtml(StringUtil.cutTail(t.fullName, 25, "..."))})",
|
||||
"value" -> t.userName
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -257,11 +274,22 @@ trait IndexControllerBase extends ControllerBase {
|
||||
|
||||
/**
|
||||
* JSON API for checking user or group existence.
|
||||
*
|
||||
* Returns a single string which is any of "group", "user" or "".
|
||||
* Additionally, check whether the user is writable to the repository
|
||||
* if "owner" and "repository" are given,
|
||||
*/
|
||||
post("/_user/existence")(usersOnly {
|
||||
getAccountByUserNameIgnoreCase(params("userName")).map { account =>
|
||||
if (account.isGroupAccount) "group" else "user"
|
||||
if (!account.isGroupAccount && params.get("repository").isDefined && params.get("owner").isDefined) {
|
||||
getRepository(params("owner"), params("repository"))
|
||||
.collect {
|
||||
case repository if isWritable(repository.repository, Some(account)) => "user"
|
||||
}
|
||||
.getOrElse("")
|
||||
} else {
|
||||
if (account.isGroupAccount) "group" else "user"
|
||||
}
|
||||
} getOrElse ""
|
||||
})
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ import gitbucket.core.service.IssuesService.*
|
||||
import gitbucket.core.service.*
|
||||
import gitbucket.core.util.Implicits.*
|
||||
import gitbucket.core.util.*
|
||||
import gitbucket.core.view
|
||||
import gitbucket.core.view.Markdown
|
||||
import gitbucket.core.view.helpers
|
||||
import org.scalatra.forms.*
|
||||
import org.scalatra.{BadRequest, Ok}
|
||||
|
||||
@@ -271,20 +270,24 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
case t if t == "html" => html.editissue(x.content, x.issueId, repository)
|
||||
} getOrElse {
|
||||
contentType = formats("json")
|
||||
val content = helpers
|
||||
.renderMarkup(
|
||||
filePath = List("temporary.md"),
|
||||
fileContent = x.content getOrElse "No description given.",
|
||||
branch = repository.repository.defaultBranch,
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableAnchor = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = true
|
||||
)
|
||||
.toString()
|
||||
org.json4s.jackson.Serialization.write(
|
||||
Map(
|
||||
"title" -> x.title,
|
||||
"content" -> Markdown.toHtml(
|
||||
markdown = x.content getOrElse "No description given.",
|
||||
repository = repository,
|
||||
branch = repository.repository.defaultBranch,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableAnchor = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = true
|
||||
)
|
||||
"content" -> content
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -301,19 +304,23 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
case t if t == "html" => html.editcomment(x.content, x.commentId, repository)
|
||||
} getOrElse {
|
||||
contentType = formats("json")
|
||||
val content = helpers
|
||||
.renderMarkup(
|
||||
filePath = List("temporary.md"),
|
||||
fileContent = x.content,
|
||||
branch = repository.repository.defaultBranch,
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableAnchor = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = true
|
||||
)
|
||||
.toString()
|
||||
org.json4s.jackson.Serialization.write(
|
||||
Map(
|
||||
"content" -> view.Markdown.toHtml(
|
||||
markdown = x.content,
|
||||
repository = repository,
|
||||
branch = repository.repository.defaultBranch,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableAnchor = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = true
|
||||
)
|
||||
"content" -> content
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ trait PreProcessControllerBase extends ControllerBase {
|
||||
if (
|
||||
!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
|
||||
!context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs") &&
|
||||
!context.currentPath.startsWith("/plugin-assets") &&
|
||||
!context.currentPath.startsWith("/plugin-assets") && !context.currentPath.equals("/user.css") &&
|
||||
!PluginRegistry().getAnonymousAccessiblePaths().exists { path =>
|
||||
context.currentPath.startsWith(path)
|
||||
}
|
||||
|
||||
@@ -247,41 +247,43 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
get("/:owner/:repository/pull/:id/delete_branch")(readableUsersOnly { baseRepository =>
|
||||
(for {
|
||||
issueId <- params("id").toIntOpt
|
||||
loginAccount <- context.loginAccount
|
||||
case (issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
|
||||
owner = pullreq.requestUserName
|
||||
name = pullreq.requestRepositoryName
|
||||
if hasDeveloperRole(owner, name, context.loginAccount)
|
||||
} yield {
|
||||
val repository = getRepository(owner, name).get
|
||||
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch)
|
||||
if (branchProtection.enabled) {
|
||||
flash.update("error", s"branch ${pullreq.requestBranch} is protected.")
|
||||
} else {
|
||||
if (repository.repository.defaultBranch != pullreq.requestBranch) {
|
||||
val userName = context.loginAccount.get.userName
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
git.branchDelete().setForce(true).setBranchNames(pullreq.requestBranch).call()
|
||||
val deleteBranchInfo = DeleteBranchInfo(repository.owner, repository.name, userName, pullreq.requestBranch)
|
||||
recordActivity(deleteBranchInfo)
|
||||
}
|
||||
createComment(
|
||||
baseRepository.owner,
|
||||
baseRepository.name,
|
||||
userName,
|
||||
issueId,
|
||||
pullreq.requestBranch,
|
||||
"delete_branch"
|
||||
)
|
||||
context.withLoginAccount { _ =>
|
||||
(for {
|
||||
issueId <- params("id").toIntOpt
|
||||
case (issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
|
||||
owner = pullreq.requestUserName
|
||||
name = pullreq.requestRepositoryName
|
||||
if hasDeveloperRole(owner, name, context.loginAccount)
|
||||
} yield {
|
||||
val repository = getRepository(owner, name).get
|
||||
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch)
|
||||
if (branchProtection.enabled) {
|
||||
flash.update("error", s"branch ${pullreq.requestBranch} is protected.")
|
||||
} else {
|
||||
flash.update("error", s"""Can't delete the default branch "${pullreq.requestBranch}".""")
|
||||
if (repository.repository.defaultBranch != pullreq.requestBranch) {
|
||||
val userName = context.loginAccount.get.userName
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
git.branchDelete().setForce(true).setBranchNames(pullreq.requestBranch).call()
|
||||
val deleteBranchInfo =
|
||||
DeleteBranchInfo(repository.owner, repository.name, userName, pullreq.requestBranch)
|
||||
recordActivity(deleteBranchInfo)
|
||||
}
|
||||
createComment(
|
||||
baseRepository.owner,
|
||||
baseRepository.name,
|
||||
userName,
|
||||
issueId,
|
||||
pullreq.requestBranch,
|
||||
"delete_branch"
|
||||
)
|
||||
} else {
|
||||
flash.update("error", s"""Can't delete the default branch "${pullreq.requestBranch}".""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
redirect(s"/${baseRepository.owner}/${baseRepository.name}/pull/${issueId}")
|
||||
}) getOrElse NotFound()
|
||||
redirect(s"/${baseRepository.owner}/${baseRepository.name}/pull/${issueId}")
|
||||
}) getOrElse NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
post("/:owner/:repository/pull/:id/update_branch")(readableUsersOnly { baseRepository =>
|
||||
@@ -361,8 +363,11 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
form.isDraft,
|
||||
context.settings
|
||||
) match {
|
||||
case Right(objectId) => redirect(s"/${repository.owner}/${repository.name}/pull/$issueId")
|
||||
case Left(message) => Some(BadRequest(message))
|
||||
case Right(result) =>
|
||||
updateMergedCommitIds(repository.owner, repository.name, issueId, result.mergedCommitId)
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/$issueId")
|
||||
case Left(message) =>
|
||||
Some(BadRequest(message))
|
||||
}
|
||||
} getOrElse NotFound()
|
||||
}
|
||||
@@ -370,6 +375,11 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
|
||||
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
|
||||
val headBranch = params.get("head")
|
||||
val quickLoad = params
|
||||
.get("quick")
|
||||
.map(_.equalsIgnoreCase("true"))
|
||||
.getOrElse(context.settings.basicBehavior.compareNoCheckByDefault)
|
||||
val quickQuery = if (quickLoad) "?quick=true" else ""
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(originUserName), Some(originRepositoryName)) =>
|
||||
getRepository(originUserName, originRepositoryName).map { originRepository =>
|
||||
@@ -383,7 +393,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
.getOrElse(JGitUtil.getDefaultBranch(oldGit, originRepository).get._2)
|
||||
|
||||
redirect(
|
||||
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/$originUserName:$oldBranch...$newBranch"
|
||||
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}${quickQuery}"
|
||||
)
|
||||
}
|
||||
} getOrElse NotFound()
|
||||
@@ -391,7 +401,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
Using.resource(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))) { git =>
|
||||
JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
|
||||
redirect(
|
||||
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/$defaultBranch...${headBranch.getOrElse(defaultBranch)}"
|
||||
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${headBranch.getOrElse(defaultBranch)}${quickQuery}"
|
||||
)
|
||||
} getOrElse {
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
|
||||
@@ -431,76 +441,112 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
val Seq(origin, forked) = multiParams("splat")
|
||||
val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner)
|
||||
val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner)
|
||||
val requestedCheck = params.get("check").contains("true")
|
||||
val quickLoad = params
|
||||
.get("quick")
|
||||
.map(_.equalsIgnoreCase("true"))
|
||||
.getOrElse(!requestedCheck && context.settings.basicBehavior.compareNoCheckByDefault)
|
||||
|
||||
(for (
|
||||
originRepositoryName <- getOriginRepositoryName(originOwner, forkedOwner, forkedRepository);
|
||||
originRepository <- getRepository(originOwner, originRepositoryName)
|
||||
) yield {
|
||||
val (oldId, newId) =
|
||||
getPullRequestCommitFromTo(originRepository, forkedRepository, originId, forkedId)
|
||||
val members =
|
||||
((forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(userName), Some(repositoryName)) =>
|
||||
getRepository(userName, repositoryName) match {
|
||||
case Some(x) => x.repository :: getForkedRepositories(userName, repositoryName)
|
||||
case None => getForkedRepositories(userName, repositoryName)
|
||||
}
|
||||
case _ =>
|
||||
forkedRepository.repository :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
||||
}).map { repository =>
|
||||
(repository.userName, repository.repositoryName, repository.defaultBranch)
|
||||
}
|
||||
|
||||
(oldId, newId) match {
|
||||
case (Some(oldId), Some(newId)) =>
|
||||
val (commits, diffs) = getRequestCompareInfo(
|
||||
originRepository.owner,
|
||||
originRepository.name,
|
||||
oldId.getName,
|
||||
forkedRepository.owner,
|
||||
forkedRepository.name,
|
||||
newId.getName,
|
||||
context.settings
|
||||
)
|
||||
val text = forkedId.replaceAll("[\\-_]", " ")
|
||||
val fallbackTitle = text.substring(0, 1).toUpperCase + text.substring(1)
|
||||
|
||||
val title = if (commits.flatten.length == 1) {
|
||||
commits.flatten.head.shortMessage
|
||||
} else {
|
||||
val text = forkedId.replaceAll("[\\-_]", " ")
|
||||
text.substring(0, 1).toUpperCase + text.substring(1)
|
||||
}
|
||||
if (quickLoad) {
|
||||
html.compare(
|
||||
fallbackTitle,
|
||||
Seq.empty,
|
||||
Seq.empty,
|
||||
members,
|
||||
List.empty,
|
||||
originId,
|
||||
forkedId,
|
||||
"",
|
||||
"",
|
||||
getContentTemplate(originRepository, "PULL_REQUEST_TEMPLATE"),
|
||||
forkedRepository,
|
||||
originRepository,
|
||||
forkedRepository,
|
||||
hasDeveloperRole(originRepository.owner, originRepository.name, context.loginAccount),
|
||||
getAssignableUserNames(originRepository.owner, originRepository.name),
|
||||
getMilestones(originRepository.owner, originRepository.name),
|
||||
getPriorities(originRepository.owner, originRepository.name),
|
||||
getDefaultPriority(originRepository.owner, originRepository.name),
|
||||
getLabels(originRepository.owner, originRepository.name),
|
||||
getCustomFields(originRepository.owner, originRepository.name).filter(_.enableForPullRequests),
|
||||
quickLoad
|
||||
)
|
||||
} else {
|
||||
val (oldId, newId) =
|
||||
getPullRequestCommitFromTo(originRepository, forkedRepository, originId, forkedId)
|
||||
|
||||
html.compare(
|
||||
title,
|
||||
commits,
|
||||
diffs,
|
||||
((forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(userName), Some(repositoryName)) =>
|
||||
getRepository(userName, repositoryName) match {
|
||||
case Some(x) => x.repository :: getForkedRepositories(userName, repositoryName)
|
||||
case None => getForkedRepositories(userName, repositoryName)
|
||||
}
|
||||
case _ =>
|
||||
forkedRepository.repository :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
||||
}).map { repository =>
|
||||
(repository.userName, repository.repositoryName, repository.defaultBranch)
|
||||
},
|
||||
commits.flatten
|
||||
.flatMap(commit =>
|
||||
getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, includePullRequest = false)
|
||||
)
|
||||
.toList,
|
||||
originId,
|
||||
forkedId,
|
||||
oldId.getName,
|
||||
newId.getName,
|
||||
getContentTemplate(originRepository, "PULL_REQUEST_TEMPLATE"),
|
||||
forkedRepository,
|
||||
originRepository,
|
||||
forkedRepository,
|
||||
hasDeveloperRole(originRepository.owner, originRepository.name, context.loginAccount),
|
||||
getAssignableUserNames(originRepository.owner, originRepository.name),
|
||||
getMilestones(originRepository.owner, originRepository.name),
|
||||
getPriorities(originRepository.owner, originRepository.name),
|
||||
getDefaultPriority(originRepository.owner, originRepository.name),
|
||||
getLabels(originRepository.owner, originRepository.name),
|
||||
getCustomFields(originRepository.owner, originRepository.name).filter(_.enableForPullRequests)
|
||||
)
|
||||
case (oldId, newId) =>
|
||||
redirect(
|
||||
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/" +
|
||||
s"$originOwner:${oldId.map(_ => originId).getOrElse(originRepository.repository.defaultBranch)}..." +
|
||||
s"$forkedOwner:${newId.map(_ => forkedId).getOrElse(forkedRepository.repository.defaultBranch)}"
|
||||
)
|
||||
(oldId, newId) match {
|
||||
case (Some(oldId), Some(newId)) =>
|
||||
val (commits, diffs) = getRequestCompareInfo(
|
||||
originRepository.owner,
|
||||
originRepository.name,
|
||||
oldId.getName,
|
||||
forkedRepository.owner,
|
||||
forkedRepository.name,
|
||||
newId.getName,
|
||||
context.settings
|
||||
)
|
||||
|
||||
val title = if (commits.flatten.length == 1) {
|
||||
commits.flatten.head.shortMessage
|
||||
} else {
|
||||
fallbackTitle
|
||||
}
|
||||
|
||||
val commitComments = commits.flatten
|
||||
.flatMap(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false))
|
||||
.toList
|
||||
|
||||
html.compare(
|
||||
title,
|
||||
commits,
|
||||
diffs,
|
||||
members,
|
||||
commitComments,
|
||||
originId,
|
||||
forkedId,
|
||||
oldId.getName,
|
||||
newId.getName,
|
||||
getContentTemplate(originRepository, "PULL_REQUEST_TEMPLATE"),
|
||||
forkedRepository,
|
||||
originRepository,
|
||||
forkedRepository,
|
||||
hasDeveloperRole(originRepository.owner, originRepository.name, context.loginAccount),
|
||||
getAssignableUserNames(originRepository.owner, originRepository.name),
|
||||
getMilestones(originRepository.owner, originRepository.name),
|
||||
getPriorities(originRepository.owner, originRepository.name),
|
||||
getDefaultPriority(originRepository.owner, originRepository.name),
|
||||
getLabels(originRepository.owner, originRepository.name),
|
||||
getCustomFields(originRepository.owner, originRepository.name).filter(_.enableForPullRequests),
|
||||
quickLoad
|
||||
)
|
||||
case (oldId, newId) =>
|
||||
redirect(
|
||||
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/" +
|
||||
s"${originOwner}:${oldId.map(_ => originId).getOrElse(originRepository.repository.defaultBranch)}..." +
|
||||
s"${forkedOwner}:${newId.map(_ => forkedId).getOrElse(forkedRepository.repository.defaultBranch)}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
@@ -722,15 +768,84 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
)
|
||||
}
|
||||
|
||||
post("/:owner/:repository/pull/:id/revert")(writableUsersOnly { repository =>
|
||||
context.withLoginAccount { loginAccount =>
|
||||
(for {
|
||||
issueId <- params.get("id").map(_.toInt)
|
||||
(issue, pullreq) <- getPullRequest(repository.owner, repository.name, issueId) if issue.closed
|
||||
} yield {
|
||||
val baseBranch = pullreq.branch
|
||||
val revertBranch = s"revert-pr-$issueId-${System.currentTimeMillis()}"
|
||||
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
try {
|
||||
// Create a new branch from base
|
||||
JGitUtil.createBranch(git, baseBranch, revertBranch)
|
||||
|
||||
val revertCommitId = pullreq.mergedCommitIds match {
|
||||
case Some(mergedCommitIds) =>
|
||||
createRevertCommit(
|
||||
git,
|
||||
revertBranch,
|
||||
mergedCommitIds.split(",").toSeq,
|
||||
loginAccount.fullName,
|
||||
loginAccount.mailAddress,
|
||||
s"Revert #$issueId"
|
||||
)
|
||||
case None =>
|
||||
Left("No merged commit IDs found for this pull request")
|
||||
}
|
||||
|
||||
revertCommitId match {
|
||||
case Right(revertCommitObjectId) =>
|
||||
val newIssueId = insertIssue(
|
||||
owner = repository.owner,
|
||||
repository = repository.name,
|
||||
loginUser = loginAccount.userName,
|
||||
title = s"Revert #${issueId}",
|
||||
content = Some(s"Revert #${issueId}"),
|
||||
milestoneId = None,
|
||||
priorityId = None,
|
||||
isPullRequest = true
|
||||
)
|
||||
createPullRequest(
|
||||
originRepository = repository,
|
||||
issueId = newIssueId,
|
||||
originBranch = baseBranch,
|
||||
requestUserName = repository.owner,
|
||||
requestRepositoryName = repository.name,
|
||||
requestBranch = revertBranch,
|
||||
commitIdFrom = git.getRepository.resolve(s"refs/heads/$baseBranch").getName,
|
||||
commitIdTo = revertCommitObjectId.name(),
|
||||
isDraft = false,
|
||||
loginAccount = loginAccount,
|
||||
settings = context.settings
|
||||
)
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/$newIssueId")
|
||||
|
||||
case Left(errorMessage) =>
|
||||
// Clean up the branch we created
|
||||
git.branchDelete().setForce(true).setBranchNames(revertBranch).call()
|
||||
BadRequest(s"Failed to create revert commit: $errorMessage")
|
||||
}
|
||||
} catch {
|
||||
case ex: Exception =>
|
||||
BadRequest(s"Revert failed: ${ex.getMessage}")
|
||||
}
|
||||
}
|
||||
}) getOrElse NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests whether an logged-in user can manage pull requests.
|
||||
* Tests whether the logged-in user can manage pull requests.
|
||||
*/
|
||||
private def isManageable(repository: RepositoryInfo)(implicit context: Context): Boolean = {
|
||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether an logged-in user can post pull requests.
|
||||
* Tests whether the logged-in user can post pull requests.
|
||||
*/
|
||||
private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = {
|
||||
repository.repository.options.issuesOption match {
|
||||
@@ -740,5 +855,4 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
case "DISABLE" => false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
if (!repository.branchList.contains(branch)) {
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/branches")
|
||||
} else {
|
||||
val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch))
|
||||
val protection = ApiBranchProtectionResponse(getProtectedBranchInfo(repository.owner, repository.name, branch))
|
||||
val lastWeeks = getRecentStatusContexts(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
@@ -628,7 +628,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
new Constraint() {
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
getAccountByUserName(value) match {
|
||||
case None => Some("User does not exist.")
|
||||
case None => Some("User does not exist.")
|
||||
case Some(x) =>
|
||||
if (x.userName == params("owner")) {
|
||||
Some("This is current repository owner.")
|
||||
|
||||
@@ -23,8 +23,7 @@ import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOut
|
||||
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream
|
||||
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream
|
||||
import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream
|
||||
import org.apache.commons.compress.utils.IOUtils
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.{FileUtils, IOUtils}
|
||||
import org.scalatra.forms.*
|
||||
import org.eclipse.jgit.api.{ArchiveCommand, Git}
|
||||
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
|
||||
@@ -35,7 +34,7 @@ import org.eclipse.jgit.treewalk.TreeWalk.OperationType
|
||||
import org.eclipse.jgit.treewalk.filter.PathFilter
|
||||
import org.eclipse.jgit.util.io.EolStreamTypeUtil
|
||||
import org.json4s.jackson.Serialization
|
||||
import org.scalatra._
|
||||
import org.scalatra.*
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
class RepositoryViewerController
|
||||
@@ -89,6 +88,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
message: Option[String],
|
||||
charset: String,
|
||||
lineSeparator: String,
|
||||
hasBom: Boolean,
|
||||
newFileName: String,
|
||||
oldFileName: Option[String],
|
||||
commit: String,
|
||||
@@ -135,6 +135,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
"message" -> trim(label("Message", optional(text()))),
|
||||
"charset" -> trim(label("Charset", text(required))),
|
||||
"lineSeparator" -> trim(label("Line Separator", text(required))),
|
||||
"hasBom" -> trim(label("Has BOM", boolean())),
|
||||
"newFileName" -> trim(label("Filename", text(required))),
|
||||
"oldFileName" -> trim(label("Old filename", optional(text()))),
|
||||
"commit" -> trim(label("Commit", text(required, conflict))),
|
||||
@@ -170,31 +171,19 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
*/
|
||||
post("/:owner/:repository/_preview")(referrersOnly { repository =>
|
||||
contentType = "text/html"
|
||||
val filename = params.get("filename")
|
||||
filename match {
|
||||
case Some(f) =>
|
||||
helpers.renderMarkup(
|
||||
filePath = List(f),
|
||||
fileContent = params("content"),
|
||||
branch = repository.repository.defaultBranch,
|
||||
repository = repository,
|
||||
enableWikiLink = params("enableWikiLink").toBoolean,
|
||||
enableRefsLink = params("enableRefsLink").toBoolean,
|
||||
enableAnchor = false
|
||||
)
|
||||
case None =>
|
||||
helpers.markdown(
|
||||
markdown = params("content"),
|
||||
repository = repository,
|
||||
branch = repository.repository.defaultBranch,
|
||||
enableWikiLink = params("enableWikiLink").toBoolean,
|
||||
enableRefsLink = params("enableRefsLink").toBoolean,
|
||||
enableLineBreaks = params("enableLineBreaks").toBoolean,
|
||||
enableTaskList = params("enableTaskList").toBoolean,
|
||||
enableAnchor = false,
|
||||
hasWritePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||
)
|
||||
}
|
||||
val filename = params.get("filename").getOrElse("temporary.md")
|
||||
helpers.renderMarkup(
|
||||
filePath = filename.split("/").toList,
|
||||
fileContent = params("content"),
|
||||
branch = repository.repository.defaultBranch,
|
||||
repository = repository,
|
||||
enableWikiLink = params("enableWikiLink").toBoolean,
|
||||
enableRefsLink = params("enableRefsLink").toBoolean,
|
||||
enableLineBreaks = params("enableLineBreaks").toBoolean,
|
||||
enableTaskList = params("enableTaskList").toBoolean,
|
||||
enableAnchor = false,
|
||||
hasWritePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -452,7 +441,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
message = form.message.getOrElse(s"Create ${form.newFileName}"),
|
||||
commit = form.commit,
|
||||
loginAccount = loginAccount,
|
||||
settings = context.settings
|
||||
settings = context.settings,
|
||||
hasBom = form.hasBom
|
||||
).map(_._1)
|
||||
}
|
||||
|
||||
@@ -479,11 +469,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
case Right(_) =>
|
||||
if (form.path.isEmpty) {
|
||||
redirect(
|
||||
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${urlEncode(form.newFileName)}"
|
||||
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${encodeRefName(form.newFileName)}"
|
||||
)
|
||||
} else {
|
||||
redirect(
|
||||
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${encodeRefName(form.path)}/${urlEncode(form.newFileName)}"
|
||||
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${encodeRefName(form.path)}/${encodeRefName(form.newFileName)}"
|
||||
)
|
||||
}
|
||||
case Left(error) => Forbidden(gitbucket.core.html.error(error))
|
||||
@@ -509,7 +499,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
},
|
||||
commit = form.commit,
|
||||
loginAccount = loginAccount,
|
||||
settings = context.settings
|
||||
settings = context.settings,
|
||||
hasBom = form.hasBom
|
||||
).map(_._1)
|
||||
}
|
||||
|
||||
@@ -536,11 +527,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
case Right(_) =>
|
||||
if (form.path.isEmpty) {
|
||||
redirect(
|
||||
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${urlEncode(form.newFileName)}"
|
||||
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${encodeRefName(form.newFileName)}"
|
||||
)
|
||||
} else {
|
||||
redirect(
|
||||
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${encodeRefName(form.path)}/${urlEncode(form.newFileName)}"
|
||||
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${encodeRefName(form.path)}/${encodeRefName(form.newFileName)}"
|
||||
)
|
||||
}
|
||||
case Left(error) => Forbidden(gitbucket.core.html.error(error))
|
||||
@@ -897,19 +888,23 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
case t if t == "html" => html.editcomment(x.content, x.commentId, repository)
|
||||
} getOrElse {
|
||||
contentType = formats("json")
|
||||
val content = helpers
|
||||
.renderMarkup(
|
||||
filePath = List("temporary.md"),
|
||||
fileContent = x.content,
|
||||
branch = repository.repository.defaultBranch,
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableAnchor = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = true
|
||||
)
|
||||
.toString()
|
||||
org.json4s.jackson.Serialization.write(
|
||||
Map(
|
||||
"content" -> view.Markdown.toHtml(
|
||||
markdown = x.content,
|
||||
repository = repository,
|
||||
branch = repository.repository.defaultBranch,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableAnchor = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = true
|
||||
)
|
||||
"content" -> content
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1076,14 +1071,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
redirect(s"${repository.owner}/${repository.name}/releases")
|
||||
})
|
||||
|
||||
get("/:owner/:repository/archive/:name")(referrersOnly { repository =>
|
||||
val name = params("name")
|
||||
archiveRepository(name, repository, "")
|
||||
})
|
||||
|
||||
get("/:owner/:repository/archive/*/:name")(referrersOnly { repository =>
|
||||
val name = params("name")
|
||||
val path = multiParams("splat").head
|
||||
get("/:owner/:repository/archive/*")(referrersOnly { repository =>
|
||||
val name = multiParams("splat").mkString("/")
|
||||
val path = params.get("path").getOrElse("")
|
||||
archiveRepository(name, repository, path)
|
||||
})
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
"gravatar" -> trim(label("Gravatar", boolean())),
|
||||
"notification" -> trim(label("Notification", boolean())),
|
||||
"limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())),
|
||||
"compareNoCheckByDefault" -> trim(label("Default compare mode", boolean())),
|
||||
)(BasicBehavior.apply),
|
||||
"ssh" -> mapping(
|
||||
"enabled" -> trim(label("SSH access", boolean())),
|
||||
@@ -125,7 +126,8 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
"maxDiffFiles" -> trim(label("Max diff files", number(required))),
|
||||
"maxDiffLines" -> trim(label("Max diff lines", number(required)))
|
||||
)(RepositoryViewerSettings.apply),
|
||||
"defaultBranch" -> trim(label("Default branch", text(required)))
|
||||
"defaultBranch" -> trim(label("Default branch", text(required))),
|
||||
"showFullName" -> trim(label("Show full name", boolean()))
|
||||
)(SystemSettings.apply).verifying { settings =>
|
||||
Vector(
|
||||
if (settings.ssh.enabled && settings.baseUrl.isEmpty) {
|
||||
|
||||
@@ -67,7 +67,8 @@ trait WikiControllerBase extends ControllerBase {
|
||||
repository,
|
||||
isEditable(repository),
|
||||
getWikiPage(repository.owner, repository.name, "_Sidebar", branch),
|
||||
getWikiPage(repository.owner, repository.name, "_Footer", branch)
|
||||
getWikiPage(repository.owner, repository.name, "_Footer", branch),
|
||||
branch
|
||||
)
|
||||
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
|
||||
})
|
||||
@@ -84,7 +85,8 @@ trait WikiControllerBase extends ControllerBase {
|
||||
repository,
|
||||
isEditable(repository),
|
||||
getWikiPage(repository.owner, repository.name, "_Sidebar", branch),
|
||||
getWikiPage(repository.owner, repository.name, "_Footer", branch)
|
||||
getWikiPage(repository.owner, repository.name, "_Footer", branch),
|
||||
branch
|
||||
)
|
||||
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
|
||||
})
|
||||
@@ -101,12 +103,6 @@ trait WikiControllerBase extends ControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
private def getWikiBranch(owner: String, repository: String): String = {
|
||||
Using.resource(Git.open(Directory.getWikiRepositoryDir(owner, repository))) { git =>
|
||||
git.getRepository.getBranch
|
||||
}
|
||||
}
|
||||
|
||||
get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository =>
|
||||
val pageName = StringUtil.urlDecode(params("page"))
|
||||
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||
@@ -306,7 +302,8 @@ trait WikiControllerBase extends ControllerBase {
|
||||
|
||||
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
|
||||
Using.resource(Git.open(getWikiRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
JGitUtil.getCommitLog(git, "master") match {
|
||||
val branch = getWikiBranch(repository.owner, repository.name)
|
||||
JGitUtil.getCommitLog(git, branch) match {
|
||||
case Right((logs, hasNext)) => html.history(None, logs, repository, isEditable(repository))
|
||||
case Left(_) => NotFound()
|
||||
}
|
||||
@@ -316,7 +313,8 @@ trait WikiControllerBase extends ControllerBase {
|
||||
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
|
||||
val path = multiParams("splat").head
|
||||
Using.resource(Git.open(getWikiRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve("master"))
|
||||
val branch = getWikiBranch(repository.owner, repository.name)
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
|
||||
|
||||
getPathObjectId(git, path, revCommit).map { objectId =>
|
||||
responseRawFile(git, objectId, path, repository)
|
||||
|
||||
@@ -138,7 +138,7 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
|
||||
|
||||
val name = RepositoryName(repository)
|
||||
val result = JsonFormat(revstr match {
|
||||
case "tags" => repository.tags.map(ApiRef.fromTag(name, _))
|
||||
case "tags" => repository.tags.map(ApiRef.fromTag(name, _))
|
||||
case x if x.startsWith("tags/") =>
|
||||
val tagName = x.substring("tags/".length)
|
||||
repository.tags.find(_.name == tagName) match {
|
||||
|
||||
@@ -29,7 +29,7 @@ trait ApiIssueMilestoneControllerBase extends ControllerBase {
|
||||
)
|
||||
}).reverse
|
||||
state match {
|
||||
case "all" => JsonFormat(apiMilestones)
|
||||
case "all" => JsonFormat(apiMilestones)
|
||||
case "open" | "closed" =>
|
||||
JsonFormat(
|
||||
apiMilestones.filter(p => p.state == state)
|
||||
|
||||
@@ -43,7 +43,9 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
|
||||
} yield {
|
||||
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
|
||||
JsonFormat(
|
||||
ApiBranch(branch, ApiBranchCommit(br.commitId), ApiBranchProtection(protection))(RepositoryName(repository))
|
||||
ApiBranch(branch, ApiBranchCommit(br.commitId), ApiBranchProtectionResponse(protection))(
|
||||
RepositoryName(repository)
|
||||
)
|
||||
)
|
||||
}) getOrElse NotFound()
|
||||
}
|
||||
@@ -58,7 +60,7 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
|
||||
if (repository.branchList.contains(branch)) {
|
||||
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
|
||||
JsonFormat(
|
||||
ApiBranchProtection(protection)
|
||||
ApiBranchProtectionResponse(protection)
|
||||
)
|
||||
} else { NotFound() }
|
||||
})
|
||||
@@ -138,7 +140,7 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
|
||||
if (repository.branchList.contains(branch)) {
|
||||
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
|
||||
JsonFormat(
|
||||
ApiBranchProtection(protection).required_status_checks
|
||||
ApiBranchProtectionResponse(protection).required_status_checks
|
||||
)
|
||||
} else { NotFound() }
|
||||
})
|
||||
@@ -262,7 +264,7 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
(for {
|
||||
branch <- params.get("splat") if repository.branchList.contains(branch)
|
||||
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
|
||||
protection <- extractFromJsonBody[ApiBranchProtectionRequest.EnablingAndDisabling].map(_.protection)
|
||||
br <- getBranchesNoMergeInfo(git).find(_.name == branch)
|
||||
} yield {
|
||||
if (protection.enabled) {
|
||||
@@ -270,13 +272,17 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
|
||||
repository.owner,
|
||||
repository.name,
|
||||
branch,
|
||||
protection.status.enforcement_level == ApiBranchProtection.Everyone,
|
||||
protection.status.contexts
|
||||
protection.enforce_admins.getOrElse(false),
|
||||
protection.required_status_checks.isDefined,
|
||||
protection.required_status_checks.map(_.contexts).getOrElse(Nil),
|
||||
protection.restrictions.isDefined,
|
||||
protection.restrictions.map(_.users).getOrElse(Nil)
|
||||
)
|
||||
} else {
|
||||
disableBranchProtection(repository.owner, repository.name, branch)
|
||||
}
|
||||
JsonFormat(ApiBranch(branch, ApiBranchCommit(br.commitId), protection)(RepositoryName(repository)))
|
||||
val response = ApiBranchProtectionResponse(getProtectedBranchInfo(repository.owner, repository.name, branch))
|
||||
JsonFormat(ApiBranch(branch, ApiBranchCommit(br.commitId), response)(RepositoryName(repository)))
|
||||
}) getOrElse NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -90,7 +90,17 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
|
||||
path,
|
||||
"\" id=\"file\">",
|
||||
"<article>",
|
||||
renderMarkup(path.split("/").toList, new String(c), refStr, repository, false, false, true).body,
|
||||
renderMarkup(
|
||||
filePath = path.split("/").toList,
|
||||
fileContent = new String(c),
|
||||
branch = refStr,
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = false,
|
||||
enableAnchor = false,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true
|
||||
).body,
|
||||
"</article>",
|
||||
"</div>"
|
||||
).mkString
|
||||
@@ -142,10 +152,11 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
|
||||
revCommit.name
|
||||
}
|
||||
val paths = multiParams("splat").head.split("/")
|
||||
val fullPath = multiParams("splat").head
|
||||
val paths = fullPath.split("/")
|
||||
val path = paths.take(paths.size - 1).toList.mkString("/")
|
||||
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
|
||||
val fileInfo = getFileInfo(git, commit, path, false)
|
||||
val fileInfo = getFileInfo(git, commit, fullPath, ignoreCase = false)
|
||||
|
||||
fileInfo match {
|
||||
case Some(f) if !data.sha.contains(f.id.getName) =>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
trait ProtectedBranchComponent extends TemplateComponent { self: Profile =>
|
||||
import profile.api._
|
||||
import self._
|
||||
import profile.api.*
|
||||
|
||||
lazy val ProtectedBranches = TableQuery[ProtectedBranches]
|
||||
class ProtectedBranches(tag: Tag) extends Table[ProtectedBranch](tag, "PROTECTED_BRANCH") with BranchTemplate {
|
||||
val statusCheckAdmin = column[Boolean]("STATUS_CHECK_ADMIN")
|
||||
def * = (userName, repositoryName, branch, statusCheckAdmin).mapTo[ProtectedBranch]
|
||||
def byPrimaryKey(userName: String, repositoryName: String, branch: String) =
|
||||
val statusCheckAdmin = column[Boolean]("STATUS_CHECK_ADMIN") // enforceAdmins
|
||||
val requiredStatusCheck = column[Boolean]("REQUIRED_STATUS_CHECK")
|
||||
val restrictions = column[Boolean]("RESTRICTIONS")
|
||||
def * =
|
||||
(userName, repositoryName, branch, statusCheckAdmin, requiredStatusCheck, restrictions).mapTo[ProtectedBranch]
|
||||
def byPrimaryKey(userName: String, repositoryName: String, branch: String): Rep[Boolean] =
|
||||
byBranch(userName, repositoryName, branch)
|
||||
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], branch: Rep[String]) =
|
||||
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], branch: Rep[String]): Rep[Boolean] =
|
||||
byBranch(userName, repositoryName, branch)
|
||||
}
|
||||
|
||||
@@ -22,8 +24,27 @@ trait ProtectedBranchComponent extends TemplateComponent { self: Profile =>
|
||||
def * =
|
||||
(userName, repositoryName, branch, context).mapTo[ProtectedBranchContext]
|
||||
}
|
||||
|
||||
lazy val ProtectedBranchRestrictions = TableQuery[ProtectedBranchRestrictions]
|
||||
class ProtectedBranchRestrictions(tag: Tag)
|
||||
extends Table[ProtectedBranchRestriction](tag, "PROTECTED_BRANCH_RESTRICTION")
|
||||
with BranchTemplate {
|
||||
val allowedUser = column[String]("ALLOWED_USER")
|
||||
def * = (userName, repositoryName, branch, allowedUser).mapTo[ProtectedBranchRestriction]
|
||||
def byPrimaryKey(userName: String, repositoryName: String, branch: String, allowedUser: String): Rep[Boolean] =
|
||||
this.userName === userName.bind && this.repositoryName === repositoryName.bind && this.branch === branch.bind && this.allowedUser === allowedUser.bind
|
||||
}
|
||||
}
|
||||
|
||||
case class ProtectedBranch(userName: String, repositoryName: String, branch: String, statusCheckAdmin: Boolean)
|
||||
case class ProtectedBranch(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
branch: String,
|
||||
enforceAdmins: Boolean,
|
||||
requiredStatusCheck: Boolean,
|
||||
restrictions: Boolean
|
||||
)
|
||||
|
||||
case class ProtectedBranchContext(userName: String, repositoryName: String, branch: String, context: String)
|
||||
|
||||
case class ProtectedBranchRestriction(userName: String, repositoryName: String, branch: String, allowedUser: String)
|
||||
|
||||
@@ -13,6 +13,7 @@ trait PullRequestComponent extends TemplateComponent { self: Profile =>
|
||||
val commitIdFrom = column[String]("COMMIT_ID_FROM")
|
||||
val commitIdTo = column[String]("COMMIT_ID_TO")
|
||||
val isDraft = column[Boolean]("IS_DRAFT")
|
||||
val mergedCommitIds = column[String]("MERGED_COMMIT_IDS")
|
||||
def * =
|
||||
(
|
||||
userName,
|
||||
@@ -24,12 +25,13 @@ trait PullRequestComponent extends TemplateComponent { self: Profile =>
|
||||
requestBranch,
|
||||
commitIdFrom,
|
||||
commitIdTo,
|
||||
isDraft
|
||||
isDraft,
|
||||
mergedCommitIds.?
|
||||
).mapTo[PullRequest]
|
||||
|
||||
def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) =
|
||||
def byPrimaryKey(userName: String, repositoryName: String, issueId: Int): Rep[Boolean] =
|
||||
byIssue(userName, repositoryName, issueId)
|
||||
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], issueId: Rep[Int]) =
|
||||
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], issueId: Rep[Int]): Rep[Boolean] =
|
||||
byIssue(userName, repositoryName, issueId)
|
||||
}
|
||||
}
|
||||
@@ -44,5 +46,6 @@ case class PullRequest(
|
||||
requestBranch: String,
|
||||
commitIdFrom: String,
|
||||
commitIdTo: String,
|
||||
isDraft: Boolean
|
||||
isDraft: Boolean,
|
||||
mergedCommitIds: Option[String]
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.service.SystemSettingsService
|
||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||
import gitbucket.core.util.{ConfigUtil, DatabaseConfig}
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.Directory.*
|
||||
import io.github.gitbucket.solidbase.Solidbase
|
||||
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
|
||||
import io.github.gitbucket.solidbase.model.Module
|
||||
@@ -25,7 +25,7 @@ import org.apache.sshd.server.command.Command
|
||||
import org.slf4j.LoggerFactory
|
||||
import play.twirl.api.Html
|
||||
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
class PluginRegistry {
|
||||
|
||||
@@ -233,29 +233,29 @@ object PluginRegistry {
|
||||
override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
|
||||
})
|
||||
.toSeq
|
||||
.sortBy(x => Version.valueOf(getPluginVersion(x.getName)))
|
||||
.sortBy(x => Version.parse(getPluginVersion(x.getName)))
|
||||
.reverse
|
||||
}
|
||||
|
||||
lazy val extraPluginDir: Option[String] = ConfigUtil.getConfigValue[String]("gitbucket.pluginDir")
|
||||
private lazy val extraPluginDir: Option[String] = ConfigUtil.getConfigValue[String]("gitbucket.pluginDir")
|
||||
|
||||
def getGitBucketVersion(pluginJarFileName: String): Option[String] = {
|
||||
val regex = ".+-gitbucket\\_(\\d+\\.\\d+\\.\\d+(-SNAPSHOT)?)-.+".r
|
||||
private def getGitBucketVersion(pluginJarFileName: String): Option[String] = {
|
||||
val regex = ".+-gitbucket_(\\d+\\.\\d+\\.\\d+(-SNAPSHOT)?)-.+".r
|
||||
pluginJarFileName match {
|
||||
case regex(all, _) => Some(all)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
def getPluginVersion(pluginJarFileName: String): String = {
|
||||
private def getPluginVersion(pluginJarFileName: String): String = {
|
||||
val regex = ".+-((\\d+)\\.(\\d+)(\\.(\\d+))?(-SNAPSHOT)?)\\.jar$".r
|
||||
pluginJarFileName match {
|
||||
case regex(all, major, minor, _, patch, modifier) => {
|
||||
if (patch != null) all
|
||||
else {
|
||||
case regex(all, major, minor, _, patch, modifier) =>
|
||||
if (patch != null) {
|
||||
all
|
||||
} else {
|
||||
s"${major}.${minor}.0" + (if (modifier == null) "" else modifier)
|
||||
}
|
||||
}
|
||||
case _ => "0.0.0"
|
||||
}
|
||||
}
|
||||
@@ -295,11 +295,10 @@ object PluginRegistry {
|
||||
|
||||
// Check duplication
|
||||
instance.getPlugins().find(_.pluginId == pluginId) match {
|
||||
case Some(x) => {
|
||||
case Some(x) =>
|
||||
logger.warn(s"Plugin ${pluginId} is duplicated. ${x.pluginJar.getName} is available.")
|
||||
classLoader.close()
|
||||
}
|
||||
case None => {
|
||||
case None =>
|
||||
// Migration
|
||||
val solidbase = new Solidbase()
|
||||
solidbase
|
||||
@@ -334,7 +333,6 @@ object PluginRegistry {
|
||||
classLoader = classLoader
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
case e: Throwable =>
|
||||
@@ -369,9 +367,8 @@ object PluginRegistry {
|
||||
extraWatcher = null
|
||||
}
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
case e: Exception =>
|
||||
logger.error(s"Error during plugin shutdown: ${plugin.pluginJar.getName}", e)
|
||||
}
|
||||
} finally {
|
||||
plugin.classLoader.close()
|
||||
}
|
||||
@@ -437,7 +434,7 @@ class PluginWatchThread(context: ServletContext, dir: String) extends Thread wit
|
||||
logger.info("Start PluginWatchThread: " + path)
|
||||
|
||||
try {
|
||||
while (watchKey.isValid()) {
|
||||
while (watchKey.isValid) {
|
||||
val detectedWatchKey = watcher.take()
|
||||
val events = detectedWatchKey.pollEvents.asScala.filter { e =>
|
||||
e.context.toString != ".installed" && !e.context.toString.endsWith(".bak")
|
||||
|
||||
@@ -20,7 +20,7 @@ trait Renderer {
|
||||
|
||||
object MarkdownRenderer extends Renderer {
|
||||
override def render(request: RenderRequest): Html = {
|
||||
import request._
|
||||
import request.*
|
||||
Html(
|
||||
Markdown.toHtml(
|
||||
markdown = fileContent,
|
||||
@@ -29,9 +29,9 @@ object MarkdownRenderer extends Renderer {
|
||||
enableWikiLink = enableWikiLink,
|
||||
enableRefsLink = enableRefsLink,
|
||||
enableAnchor = enableAnchor,
|
||||
enableLineBreaks = false,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = false
|
||||
enableLineBreaks = enableLineBreaks,
|
||||
enableTaskList = enableTaskList,
|
||||
hasWritePermission = hasWritePermission
|
||||
)(context)
|
||||
)
|
||||
}
|
||||
@@ -51,5 +51,8 @@ case class RenderRequest(
|
||||
enableWikiLink: Boolean,
|
||||
enableRefsLink: Boolean,
|
||||
enableAnchor: Boolean,
|
||||
enableLineBreaks: Boolean,
|
||||
enableTaskList: Boolean,
|
||||
hasWritePermission: Boolean,
|
||||
context: Context
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ trait AccountFederationService {
|
||||
.orElse(extractSafeStringForUserName(mailAddress)) match {
|
||||
case Some(safeUserName) =>
|
||||
getAccountByUserName(safeUserName, includeRemoved = true) match {
|
||||
case None => Some(safeUserName)
|
||||
case None => Some(safeUserName)
|
||||
case Some(_) =>
|
||||
logger.info(
|
||||
s"User ($safeUserName) already exists. preferredUserName=$preferredUserName, mailAddress=$mailAddress"
|
||||
|
||||
@@ -46,7 +46,7 @@ trait AccountService {
|
||||
case account if !account.isGroupAccount =>
|
||||
account.password match {
|
||||
case pbkdf2re(iter, salt, hash) if (pbkdf2_sha256(iter.toInt, salt, password) == hash) => Some(account)
|
||||
case p if p == sha1(password) =>
|
||||
case p if p == sha1(password) =>
|
||||
updateAccount(account.copy(password = pbkdf2_sha256(password)))
|
||||
Some(account)
|
||||
case _ => None
|
||||
|
||||
@@ -62,7 +62,7 @@ trait HandleCommentService {
|
||||
.getOrElse(None -> None)
|
||||
|
||||
val commentId = (content, action) match {
|
||||
case (None, None) => None
|
||||
case (None, None) => None
|
||||
case (None, Some(action)) =>
|
||||
Some(createComment(owner, name, userName, issue.issueId, action.capitalize, action))
|
||||
case (Some(content), _) =>
|
||||
|
||||
@@ -7,7 +7,7 @@ import gitbucket.core.plugin.{PluginRegistry, ReceiveHook}
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.{JGitUtil, LockUtil}
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.model.Profile.profile.blockingApi.*
|
||||
import gitbucket.core.model.activity.{CloseIssueInfo, MergeInfo, PushInfo}
|
||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||
import gitbucket.core.service.WebHookService.WebHookPushPayload
|
||||
@@ -19,14 +19,14 @@ import org.eclipse.jgit.errors.NoMergeBaseException
|
||||
import org.eclipse.jgit.lib.{CommitBuilder, ObjectId, PersonIdent, Repository}
|
||||
import org.eclipse.jgit.revwalk.{RevCommit, RevWalk}
|
||||
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.jdk.CollectionConverters.*
|
||||
import scala.util.Using
|
||||
|
||||
trait MergeService {
|
||||
self: AccountService & ActivityService & IssuesService & RepositoryService & PullRequestService &
|
||||
WebHookPullRequestService & WebHookService =>
|
||||
|
||||
import MergeService._
|
||||
import MergeService.*
|
||||
|
||||
/**
|
||||
* Checks whether conflict will be caused in merging within pull request.
|
||||
@@ -61,15 +61,16 @@ trait MergeService {
|
||||
repository: RepositoryInfo,
|
||||
branch: String,
|
||||
issueId: Int,
|
||||
commits: Seq[RevCommit],
|
||||
message: String,
|
||||
loginAccount: Account,
|
||||
settings: SystemSettings
|
||||
)(implicit s: Session, c: JsonFormat.Context): ObjectId = {
|
||||
)(implicit s: Session, c: JsonFormat.Context): MergeResult = {
|
||||
val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}")
|
||||
val afterCommitId = new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks())
|
||||
.merge(message, new PersonIdent(loginAccount.fullName, loginAccount.mailAddress), loginAccount.userName)
|
||||
callWebHook(git, repository, branch, beforeCommitId, afterCommitId, loginAccount, settings)
|
||||
afterCommitId
|
||||
val mergeResult = new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks())
|
||||
.merge(message, new PersonIdent(loginAccount.fullName, loginAccount.mailAddress), loginAccount.userName, commits)
|
||||
callWebHook(git, repository, branch, beforeCommitId, mergeResult.newCommitId, loginAccount, settings)
|
||||
mergeResult
|
||||
}
|
||||
|
||||
/** rebase to the head of the pull request branch */
|
||||
@@ -81,13 +82,13 @@ trait MergeService {
|
||||
commits: Seq[RevCommit],
|
||||
loginAccount: Account,
|
||||
settings: SystemSettings
|
||||
)(implicit s: Session, c: JsonFormat.Context): ObjectId = {
|
||||
)(implicit s: Session, c: JsonFormat.Context): MergeResult = {
|
||||
val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}")
|
||||
val afterCommitId =
|
||||
val mergeResult =
|
||||
new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks())
|
||||
.rebase(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress), loginAccount.userName, commits)
|
||||
callWebHook(git, repository, branch, beforeCommitId, afterCommitId, loginAccount, settings)
|
||||
afterCommitId
|
||||
callWebHook(git, repository, branch, beforeCommitId, mergeResult.newCommitId, loginAccount, settings)
|
||||
mergeResult
|
||||
}
|
||||
|
||||
/** squash commits in the pull request and append it */
|
||||
@@ -99,13 +100,13 @@ trait MergeService {
|
||||
message: String,
|
||||
loginAccount: Account,
|
||||
settings: SystemSettings
|
||||
)(implicit s: Session, c: JsonFormat.Context): ObjectId = {
|
||||
)(implicit s: Session, c: JsonFormat.Context): MergeResult = {
|
||||
val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}")
|
||||
val afterCommitId =
|
||||
val mergeResult =
|
||||
new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks())
|
||||
.squash(message, new PersonIdent(loginAccount.fullName, loginAccount.mailAddress), loginAccount.userName)
|
||||
callWebHook(git, repository, branch, beforeCommitId, afterCommitId, loginAccount, settings)
|
||||
afterCommitId
|
||||
callWebHook(git, repository, branch, beforeCommitId, mergeResult.newCommitId, loginAccount, settings)
|
||||
mergeResult
|
||||
}
|
||||
|
||||
private def callWebHook(
|
||||
@@ -337,7 +338,7 @@ trait MergeService {
|
||||
strategy: String,
|
||||
isDraft: Boolean,
|
||||
settings: SystemSettings
|
||||
)(implicit s: Session, c: JsonFormat.Context, context: Context): Either[String, ObjectId] = {
|
||||
)(implicit s: Session, c: JsonFormat.Context, context: Context): Either[String, MergeResult] = {
|
||||
if (!isDraft) {
|
||||
if (repository.repository.options.mergeOptions.split(",").contains(strategy)) {
|
||||
LockUtil.lock(s"${repository.owner}/${repository.name}") {
|
||||
@@ -493,7 +494,7 @@ trait MergeService {
|
||||
commits: Seq[Seq[CommitInfo]],
|
||||
receiveHooks: Seq[ReceiveHook],
|
||||
settings: SystemSettings
|
||||
)(implicit s: Session, c: JsonFormat.Context): Option[ObjectId] = {
|
||||
)(implicit s: Session, c: JsonFormat.Context): Option[MergeResult] = {
|
||||
val revCommits = Using
|
||||
.resource(new RevWalk(git.getRepository)) { revWalk =>
|
||||
commits.flatten.map { commit =>
|
||||
@@ -510,6 +511,7 @@ trait MergeService {
|
||||
repository,
|
||||
pullRequest.branch,
|
||||
issue.issueId,
|
||||
revCommits,
|
||||
s"Merge pull request #${issue.issueId} from ${pullRequest.requestUserName}/${pullRequest.requestBranch}\n\n" + message,
|
||||
loginAccount,
|
||||
settings
|
||||
@@ -600,13 +602,13 @@ object MergeService {
|
||||
private val mergedBranchName = s"refs/pull/${issueId}/merge"
|
||||
private val conflictedBranchName = s"refs/pull/${issueId}/conflict"
|
||||
|
||||
lazy val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${branch}")
|
||||
lazy val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
|
||||
lazy val mergeBaseTip: ObjectId = git.getRepository.resolve(s"refs/heads/${branch}")
|
||||
lazy val mergeTip: ObjectId = git.getRepository.resolve(s"refs/pull/${issueId}/head")
|
||||
|
||||
def checkConflictCache(): Option[Option[String]] = {
|
||||
Option(git.getRepository.resolve(mergedBranchName))
|
||||
.flatMap { merged =>
|
||||
if (parseCommit(merged).getParents().toSet == Set(mergeBaseTip, mergeTip)) {
|
||||
if (parseCommit(merged).getParents.toSet == Set(mergeBaseTip, mergeTip)) {
|
||||
// merged branch exists
|
||||
Some(None)
|
||||
} else {
|
||||
@@ -615,7 +617,7 @@ object MergeService {
|
||||
}
|
||||
.orElse(Option(git.getRepository.resolve(conflictedBranchName)).flatMap { conflicted =>
|
||||
val commit = parseCommit(conflicted)
|
||||
if (commit.getParents().toSet == Set(mergeBaseTip, mergeTip)) {
|
||||
if (commit.getParents.toSet == Set(mergeBaseTip, mergeTip)) {
|
||||
// conflict branch exists
|
||||
Some(Some(commit.getFullMessage))
|
||||
} else {
|
||||
@@ -651,14 +653,16 @@ object MergeService {
|
||||
None
|
||||
} else {
|
||||
val message = createConflictMessage(mergeTip, mergeBaseTip, merger)
|
||||
_updateBranch(mergeTipCommit.getTree().getId(), message, conflictedBranchName)
|
||||
_updateBranch(mergeTipCommit.getTree.getId, message, conflictedBranchName)
|
||||
git.branchDelete().setForce(true).setBranchNames(mergedBranchName).call()
|
||||
Some(message)
|
||||
}
|
||||
}
|
||||
|
||||
// update branch from cache
|
||||
def merge(message: String, committer: PersonIdent, pusher: String)(implicit s: Session): ObjectId = {
|
||||
def merge(message: String, committer: PersonIdent, pusher: String, commits: Seq[RevCommit])(implicit
|
||||
s: Session
|
||||
): MergeResult = {
|
||||
if (checkConflict().isDefined) {
|
||||
throw new RuntimeException("This pull request can't merge automatically.")
|
||||
}
|
||||
@@ -666,7 +670,7 @@ object MergeService {
|
||||
throw new RuntimeException(s"Not found branch ${mergedBranchName}")
|
||||
})
|
||||
// creates merge commit
|
||||
val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message)
|
||||
val mergeCommitId = createMergeCommit(mergeResultCommit.getTree.getId, committer, message)
|
||||
|
||||
val refName = s"refs/heads/${branch}"
|
||||
val currentObjectId = git.getRepository.resolve(refName)
|
||||
@@ -690,10 +694,10 @@ object MergeService {
|
||||
hook.postReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true)
|
||||
}
|
||||
|
||||
objectId
|
||||
MergeResult(objectId, commits.map(_.name()))
|
||||
}
|
||||
|
||||
def rebase(committer: PersonIdent, pusher: String, commits: Seq[RevCommit])(implicit s: Session): ObjectId = {
|
||||
def rebase(committer: PersonIdent, pusher: String, commits: Seq[RevCommit])(implicit s: Session): MergeResult = {
|
||||
if (checkConflict().isDefined) {
|
||||
throw new RuntimeException("This pull request can't merge automatically.")
|
||||
}
|
||||
@@ -713,11 +717,13 @@ object MergeService {
|
||||
|
||||
val mergeBaseTipCommit = Using.resource(new RevWalk(git.getRepository))(_.parseCommit(mergeBaseTip))
|
||||
var previousId = mergeBaseTipCommit.getId
|
||||
val mergedCommitIds = Seq.newBuilder[String]
|
||||
|
||||
Using.resource(git.getRepository.newObjectInserter) { inserter =>
|
||||
commits.foreach { commit =>
|
||||
val nextCommit = _cloneCommit(commit, previousId, mergeBaseTipCommit.getId)
|
||||
previousId = inserter.insert(nextCommit)
|
||||
mergedCommitIds += previousId.name()
|
||||
}
|
||||
inserter.flush()
|
||||
}
|
||||
@@ -745,10 +751,10 @@ object MergeService {
|
||||
hook.postReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true)
|
||||
}
|
||||
|
||||
objectId
|
||||
MergeResult(objectId, mergedCommitIds.result())
|
||||
}
|
||||
|
||||
def squash(message: String, committer: PersonIdent, pusher: String)(implicit s: Session): ObjectId = {
|
||||
def squash(message: String, committer: PersonIdent, pusher: String)(implicit s: Session): MergeResult = {
|
||||
if (checkConflict().isDefined) {
|
||||
throw new RuntimeException("This pull request can't merge automatically.")
|
||||
}
|
||||
@@ -804,7 +810,7 @@ object MergeService {
|
||||
hook.postReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true)
|
||||
}
|
||||
|
||||
objectId
|
||||
MergeResult(objectId, Seq(newCommitId.name()))
|
||||
}
|
||||
|
||||
// return treeId
|
||||
@@ -823,4 +829,5 @@ object MergeService {
|
||||
mergeResults.asScala.map { case (key, _) => "- `" + key + "`\n" }.mkString
|
||||
}
|
||||
|
||||
case class MergeResult(newCommitId: ObjectId, mergedCommitId: Seq[String])
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.model.{Session => _, _}
|
||||
import gitbucket.core.plugin.ReceiveHook
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.model.Profile.*
|
||||
import gitbucket.core.model.Profile.profile.blockingApi.*
|
||||
import gitbucket.core.model.{CommitState, ProtectedBranch, ProtectedBranchContext, ProtectedBranchRestriction, Role}
|
||||
import gitbucket.core.util.SyntaxSugars.*
|
||||
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
|
||||
|
||||
trait ProtectedBranchService {
|
||||
@@ -13,17 +14,27 @@ trait ProtectedBranchService {
|
||||
): Option[ProtectedBranchInfo] =
|
||||
ProtectedBranches
|
||||
.joinLeft(ProtectedBranchContexts)
|
||||
.on { case (pb, c) => pb.byBranch(c.userName, c.repositoryName, c.branch) }
|
||||
.map { case (pb, c) => pb -> c.map(_.context) }
|
||||
.on { case pb ~ c => pb.byBranch(c.userName, c.repositoryName, c.branch) }
|
||||
.joinLeft(ProtectedBranchRestrictions)
|
||||
.on { case pb ~ c ~ r => pb.byBranch(r.userName, r.repositoryName, r.branch) }
|
||||
.map { case pb ~ c ~ r => pb -> (c.map(_.context), r.map(_.allowedUser)) }
|
||||
.filter(_._1.byPrimaryKey(owner, repository, branch))
|
||||
.list
|
||||
.groupBy(_._1)
|
||||
.headOption
|
||||
.map { p =>
|
||||
p._1 -> p._2.flatMap(_._2)
|
||||
.map { (p: (ProtectedBranch, List[(ProtectedBranch, (Option[String], Option[String]))])) =>
|
||||
p._1 -> (p._2.flatMap(_._2._1), p._2.flatMap(_._2._2))
|
||||
}
|
||||
.map { case (t1, contexts) =>
|
||||
new ProtectedBranchInfo(t1.userName, t1.repositoryName, t1.branch, true, contexts, t1.statusCheckAdmin)
|
||||
.map { case (t1, (contexts, users)) =>
|
||||
new ProtectedBranchInfo(
|
||||
t1.userName,
|
||||
t1.repositoryName,
|
||||
t1.branch,
|
||||
true,
|
||||
if (t1.requiredStatusCheck) Some(contexts) else None,
|
||||
t1.enforceAdmins,
|
||||
if (t1.restrictions) Some(users) else None
|
||||
)
|
||||
}
|
||||
|
||||
def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit
|
||||
@@ -40,19 +51,32 @@ trait ProtectedBranchService {
|
||||
owner: String,
|
||||
repository: String,
|
||||
branch: String,
|
||||
includeAdministrators: Boolean,
|
||||
contexts: Seq[String]
|
||||
enforceAdmins: Boolean,
|
||||
requiredStatusCheck: Boolean,
|
||||
contexts: Seq[String],
|
||||
restrictions: Boolean,
|
||||
restrictionsUsers: Seq[String]
|
||||
)(implicit session: Session): Unit = {
|
||||
disableBranchProtection(owner, repository, branch)
|
||||
ProtectedBranches.insert(new ProtectedBranch(owner, repository, branch, includeAdministrators && contexts.nonEmpty))
|
||||
contexts.map { context =>
|
||||
ProtectedBranchContexts.insert(new ProtectedBranchContext(owner, repository, branch, context))
|
||||
ProtectedBranches.insert(
|
||||
ProtectedBranch(owner, repository, branch, enforceAdmins, requiredStatusCheck, restrictions)
|
||||
)
|
||||
|
||||
if (restrictions) {
|
||||
restrictionsUsers.foreach { user =>
|
||||
ProtectedBranchRestrictions.insert(ProtectedBranchRestriction(owner, repository, branch, user))
|
||||
}
|
||||
}
|
||||
|
||||
if (requiredStatusCheck) {
|
||||
contexts.foreach { context =>
|
||||
ProtectedBranchContexts.insert(ProtectedBranchContext(owner, repository, branch, context))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def disableBranchProtection(owner: String, repository: String, branch: String)(implicit session: Session): Unit =
|
||||
ProtectedBranches.filter(_.byPrimaryKey(owner, repository, branch)).delete
|
||||
|
||||
}
|
||||
|
||||
object ProtectedBranchService {
|
||||
@@ -101,6 +125,7 @@ object ProtectedBranchService {
|
||||
)
|
||||
}
|
||||
} else {
|
||||
println("-> else")
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -117,12 +142,16 @@ object ProtectedBranchService {
|
||||
* When enabled, commits must first be pushed to another branch,
|
||||
* then merged or pushed directly to test after status checks have passed.
|
||||
*/
|
||||
contexts: Seq[String],
|
||||
contexts: Option[Seq[String]],
|
||||
/**
|
||||
* Include administrators
|
||||
* Enforce required status checks for repository administrators.
|
||||
*/
|
||||
includeAdministrators: Boolean
|
||||
enforceAdmins: Boolean,
|
||||
/**
|
||||
* Users who can push to the branch.
|
||||
*/
|
||||
restrictionsUsers: Option[Seq[String]]
|
||||
) extends AccountService
|
||||
with RepositoryService
|
||||
with CommitStatusService {
|
||||
@@ -148,42 +177,66 @@ object ProtectedBranchService {
|
||||
session: Session
|
||||
): Option[String] = {
|
||||
if (enabled) {
|
||||
command.getType() match {
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.UPDATE_NONFASTFORWARD if isAllowNonFastForwards =>
|
||||
Some("Cannot force-push to a protected branch")
|
||||
case ReceiveCommand.Type.UPDATE | ReceiveCommand.Type.UPDATE_NONFASTFORWARD if !isPushAllowed(pusher) =>
|
||||
Some("You do not have permission to push to this branch")
|
||||
case ReceiveCommand.Type.UPDATE | ReceiveCommand.Type.UPDATE_NONFASTFORWARD if needStatusCheck(pusher) =>
|
||||
unSuccessedContexts(command.getNewId.name) match {
|
||||
case s if s.sizeIs == 1 => Some(s"""Required status check "${s.toSeq(0)}" is expected""")
|
||||
case s if s.sizeIs >= 1 => Some(s"${s.size} of ${contexts.size} required status checks are expected")
|
||||
case _ => None
|
||||
case s if s.sizeIs == 1 => Some(s"""Required status check "${s.head}" is expected""")
|
||||
case s if s.sizeIs >= 1 =>
|
||||
Some(s"${s.size} of ${contexts.map(_.size).getOrElse(0)} required status checks are expected")
|
||||
case _ => None
|
||||
}
|
||||
case ReceiveCommand.Type.DELETE =>
|
||||
Some("Cannot delete a protected branch")
|
||||
Some("You do not have permission to push to this branch")
|
||||
case _ => None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
def unSuccessedContexts(sha1: String)(implicit session: Session): Set[String] =
|
||||
if (contexts.isEmpty) {
|
||||
Set.empty
|
||||
} else {
|
||||
contexts.toSet -- getCommitStatuses(owner, repository, sha1)
|
||||
.filter(_.state == CommitState.SUCCESS)
|
||||
.map(_.context)
|
||||
.toSet
|
||||
|
||||
def unSuccessedContexts(sha1: String)(implicit session: Session): Set[String] = {
|
||||
contexts match {
|
||||
case None => Set.empty
|
||||
case Some(x) if x.isEmpty => Set.empty
|
||||
case Some(x) =>
|
||||
x.toSet -- getCommitStatuses(owner, repository, sha1)
|
||||
.filter(_.state == CommitState.SUCCESS)
|
||||
.map(_.context)
|
||||
.toSet
|
||||
}
|
||||
}
|
||||
|
||||
def needStatusCheck(pusher: String)(implicit session: Session): Boolean = pusher match {
|
||||
case _ if !enabled => false
|
||||
case _ if contexts.isEmpty => false
|
||||
case _ if includeAdministrators => true
|
||||
case p if isAdministrator(p) => false
|
||||
case _ => true
|
||||
case _ if !enabled => false
|
||||
case _ if contexts.isEmpty => false
|
||||
case _ if enforceAdmins => true
|
||||
case p if isAdministrator(p) => false
|
||||
case _ => true
|
||||
}
|
||||
|
||||
def isPushAllowed(pusher: String)(implicit session: Session): Boolean = pusher match {
|
||||
case _ if !enabled || restrictionsUsers.isEmpty => true
|
||||
case _ if restrictionsUsers.get.contains(pusher) => true
|
||||
case p if isAdministrator(p) && enforceAdmins => false
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
object ProtectedBranchInfo {
|
||||
def disabled(owner: String, repository: String, branch: String): ProtectedBranchInfo =
|
||||
ProtectedBranchInfo(owner, repository, branch, false, Nil, false)
|
||||
def disabled(owner: String, repository: String, branch: String): ProtectedBranchInfo = {
|
||||
ProtectedBranchInfo(
|
||||
owner,
|
||||
repository,
|
||||
branch,
|
||||
enabled = false,
|
||||
contexts = None,
|
||||
enforceAdmins = false,
|
||||
restrictionsUsers = None
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,10 @@ import gitbucket.core.util.StringUtil.*
|
||||
import gitbucket.core.view
|
||||
import gitbucket.core.view.helpers
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.ObjectId
|
||||
import org.eclipse.jgit.dircache.{DirCache, DirCacheEntry}
|
||||
import org.eclipse.jgit.lib.{CommitBuilder, FileMode, ObjectId, ObjectInserter, PersonIdent, Repository}
|
||||
import org.eclipse.jgit.revwalk.{RevTree, RevWalk}
|
||||
import org.eclipse.jgit.treewalk.{EmptyTreeIterator, TreeWalk}
|
||||
|
||||
import scala.jdk.CollectionConverters.*
|
||||
import scala.util.Using
|
||||
@@ -63,6 +66,15 @@ trait PullRequestService {
|
||||
.update((baseBranch, commitIdTo))
|
||||
}
|
||||
|
||||
def updateMergedCommitIds(owner: String, repository: String, issueId: Int, mergedCommitIds: Seq[String])(implicit
|
||||
s: Session
|
||||
): Unit = {
|
||||
PullRequests
|
||||
.filter(_.byPrimaryKey(owner, repository, issueId))
|
||||
.map(pr => pr.mergedCommitIds)
|
||||
.update(mergedCommitIds.mkString(","))
|
||||
}
|
||||
|
||||
def getPullRequestCountGroupByUser(closed: Boolean, owner: Option[String], repository: Option[String])(implicit
|
||||
s: Session
|
||||
): List[PullRequestCount] =
|
||||
@@ -126,7 +138,8 @@ trait PullRequestService {
|
||||
requestBranch,
|
||||
commitIdFrom,
|
||||
commitIdTo,
|
||||
isDraft
|
||||
isDraft,
|
||||
None
|
||||
)
|
||||
|
||||
// fetch requested branch
|
||||
@@ -408,11 +421,10 @@ trait PullRequestService {
|
||||
.find(x => x.oldPath == file)
|
||||
.map { diff =>
|
||||
(diff.oldContent, diff.newContent) match {
|
||||
case (Some(oldContent), Some(newContent)) => {
|
||||
case (Some(oldContent), Some(newContent)) =>
|
||||
val oldLines = convertLineSeparator(oldContent, "LF").split("\n")
|
||||
val newLines = convertLineSeparator(newContent, "LF").split("\n")
|
||||
file -> Option(DiffUtils.diff(oldLines.toList.asJava, newLines.toList.asJava))
|
||||
}
|
||||
case _ =>
|
||||
file -> None
|
||||
}
|
||||
@@ -427,7 +439,7 @@ trait PullRequestService {
|
||||
case Some(patch) =>
|
||||
file -> comments.foreach { case (commentId, lineNumber) =>
|
||||
lineNumber match {
|
||||
case Left(oldLine) => updateCommitCommentPosition(commentId, newCommitId, Some(oldLine), None)
|
||||
case Left(oldLine) => updateCommitCommentPosition(commentId, newCommitId, Some(oldLine), None)
|
||||
case Right(newLine) =>
|
||||
var counter = newLine
|
||||
patch.getDeltas.asScala.filter(_.getSource.getPosition < newLine).foreach { delta =>
|
||||
@@ -524,7 +536,6 @@ trait PullRequestService {
|
||||
helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
|
||||
}
|
||||
|
||||
// TODO Isolate to an another method?
|
||||
val diffs = JGitUtil.getDiffs(
|
||||
git = newGit,
|
||||
from = Some(oldId.getName),
|
||||
@@ -634,6 +645,157 @@ trait PullRequestService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a revert commit directly on the bare repository without cloning.
|
||||
* This works by creating a reverse diff of the merged commits and applying it to the base branch.
|
||||
*/
|
||||
def createRevertCommit(
|
||||
git: Git,
|
||||
targetBranch: String,
|
||||
mergedCommitIds: Seq[String],
|
||||
committerName: String,
|
||||
committerEmail: String,
|
||||
commitMessage: String
|
||||
): Either[String, ObjectId] = {
|
||||
try {
|
||||
val repository = git.getRepository
|
||||
val inserter = repository.newObjectInserter()
|
||||
|
||||
Using.resource(new RevWalk(repository)) { revWalk =>
|
||||
// Get the target branch head
|
||||
val targetHeadId = repository.resolve(s"refs/heads/$targetBranch")
|
||||
if (targetHeadId == null) {
|
||||
return Left(s"Branch $targetBranch not found")
|
||||
}
|
||||
val targetHead = revWalk.parseCommit(targetHeadId)
|
||||
|
||||
// Parse the commits to revert (in reverse order for proper reverting)
|
||||
val commitsToRevert = mergedCommitIds.reverse.map { commitId =>
|
||||
val objectId = repository.resolve(commitId)
|
||||
if (objectId == null) {
|
||||
throw new IllegalArgumentException(s"Commit $commitId not found")
|
||||
}
|
||||
revWalk.parseCommit(objectId)
|
||||
}
|
||||
|
||||
// Start with the current tree of the target branch
|
||||
var currentTreeId = targetHead.getTree.getId
|
||||
|
||||
// Apply reverse changes for each commit
|
||||
for (commit <- commitsToRevert) {
|
||||
val parentCommit = if (commit.getParentCount > 0) {
|
||||
revWalk.parseCommit(commit.getParent(0))
|
||||
} else {
|
||||
// This is an initial commit, revert by creating empty tree
|
||||
null
|
||||
}
|
||||
|
||||
// Create new tree by applying reverse diff
|
||||
currentTreeId = createTreeWithReverseDiff(
|
||||
repository,
|
||||
inserter,
|
||||
currentTreeId,
|
||||
if (parentCommit != null) parentCommit.getTree else null,
|
||||
commit.getTree
|
||||
)
|
||||
}
|
||||
|
||||
// Create the revert commit
|
||||
val commitBuilder = new CommitBuilder()
|
||||
commitBuilder.setTreeId(currentTreeId)
|
||||
commitBuilder.setParentId(targetHeadId)
|
||||
commitBuilder.setAuthor(new PersonIdent(committerName, committerEmail))
|
||||
commitBuilder.setCommitter(new PersonIdent(committerName, committerEmail))
|
||||
commitBuilder.setMessage(commitMessage)
|
||||
|
||||
val revertCommitId = inserter.insert(commitBuilder)
|
||||
inserter.flush()
|
||||
|
||||
// Update the branch to point to the new commit
|
||||
val refUpdate = repository.updateRef(s"refs/heads/$targetBranch")
|
||||
refUpdate.setNewObjectId(revertCommitId)
|
||||
refUpdate.update()
|
||||
|
||||
Right(revertCommitId)
|
||||
}
|
||||
} catch {
|
||||
case ex: Exception =>
|
||||
Left(ex.getMessage)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new tree by applying the reverse of changes between fromTree and toTree to baseTree.
|
||||
*/
|
||||
private def createTreeWithReverseDiff(
|
||||
repository: Repository,
|
||||
inserter: ObjectInserter,
|
||||
baseTreeId: ObjectId,
|
||||
fromTree: RevTree,
|
||||
toTree: RevTree
|
||||
): ObjectId = {
|
||||
val dirCache = DirCache.newInCore()
|
||||
val builder = dirCache.builder()
|
||||
|
||||
val entries = scala.collection.mutable.Map[String, DirCacheEntry]()
|
||||
|
||||
// Start with all files from the base tree
|
||||
if (baseTreeId != null) {
|
||||
Using.resource(new TreeWalk(repository)) { walk =>
|
||||
walk.addTree(baseTreeId)
|
||||
walk.setRecursive(true)
|
||||
|
||||
while (walk.next()) {
|
||||
val entry = new DirCacheEntry(walk.getPathString)
|
||||
entry.setFileMode(walk.getFileMode(0))
|
||||
entry.setObjectId(walk.getObjectId(0))
|
||||
entries(walk.getPathString) = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply reverse changes: if a file was added in the original change, remove it
|
||||
// if a file was deleted, restore it; if modified, restore original content
|
||||
Using.resource(new TreeWalk(repository)) { walk =>
|
||||
if (fromTree != null) walk.addTree(fromTree) else walk.addTree(new EmptyTreeIterator())
|
||||
walk.addTree(toTree)
|
||||
walk.setRecursive(true)
|
||||
|
||||
while (walk.next()) {
|
||||
val path = walk.getPathString
|
||||
val fromMode = if (walk.getTreeCount > 1) walk.getFileMode(0) else FileMode.MISSING
|
||||
val toMode = walk.getFileMode(walk.getTreeCount - 1)
|
||||
|
||||
if (fromMode == FileMode.MISSING && toMode != FileMode.MISSING) {
|
||||
// File was added in the original change, so remove it in the revert
|
||||
entries.remove(path)
|
||||
} else if (fromMode != FileMode.MISSING && toMode == FileMode.MISSING) {
|
||||
// File was deleted in the original change, so restore it in the revert
|
||||
val entry = new DirCacheEntry(path)
|
||||
entry.setFileMode(fromMode)
|
||||
entry.setObjectId(walk.getObjectId(0))
|
||||
entries(path) = entry
|
||||
} else if (fromMode != FileMode.MISSING && toMode != FileMode.MISSING) {
|
||||
val fromObjectId = walk.getObjectId(0)
|
||||
val toObjectId = walk.getObjectId(walk.getTreeCount - 1)
|
||||
|
||||
if (!fromObjectId.equals(toObjectId)) {
|
||||
// File was modified in the original change, restore original content
|
||||
val entry = new DirCacheEntry(path)
|
||||
entry.setFileMode(fromMode)
|
||||
entry.setObjectId(fromObjectId)
|
||||
entries(path) = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the final tree
|
||||
entries.values.toSeq.sortBy(_.getPathString).foreach(builder.add)
|
||||
builder.finish()
|
||||
dirCache.writeTree(inserter)
|
||||
}
|
||||
}
|
||||
|
||||
object PullRequestService {
|
||||
@@ -653,18 +815,18 @@ object PullRequestService {
|
||||
commitIdTo: String
|
||||
) {
|
||||
|
||||
val hasConflict = conflictMessage.isDefined
|
||||
val hasConflict: Boolean = conflictMessage.isDefined
|
||||
val statuses: List[CommitStatus] =
|
||||
commitStatuses ++ (branchProtection.contexts.toSet -- commitStatuses.map(_.context).toSet)
|
||||
commitStatuses ++ (branchProtection.contexts.getOrElse(Nil).toSet -- commitStatuses.map(_.context).toSet)
|
||||
.map(CommitStatus.pending(branchProtection.owner, branchProtection.repository, _))
|
||||
val hasRequiredStatusProblem = needStatusCheck && branchProtection.contexts.exists(context =>
|
||||
statuses.find(_.context == context).map(_.state) != Some(CommitState.SUCCESS)
|
||||
)
|
||||
val hasProblem = hasRequiredStatusProblem || hasConflict || (statuses.nonEmpty && CommitState.combine(
|
||||
val hasRequiredStatusProblem: Boolean = needStatusCheck && branchProtection.contexts
|
||||
.getOrElse(Nil)
|
||||
.exists(context => !statuses.find(_.context == context).map(_.state).contains(CommitState.SUCCESS))
|
||||
val hasProblem: Boolean = hasRequiredStatusProblem || hasConflict || (statuses.nonEmpty && CommitState.combine(
|
||||
statuses.map(_.state).toSet
|
||||
) != CommitState.SUCCESS)
|
||||
val canUpdate = branchIsOutOfDate && !hasConflict
|
||||
val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
|
||||
val canUpdate: Boolean = branchIsOutOfDate && !hasConflict
|
||||
val canMerge: Boolean = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
|
||||
lazy val commitStateSummary: (CommitState, String) = {
|
||||
val stateMap = statuses.groupBy(_.state)
|
||||
val state = CommitState.combine(stateMap.keySet)
|
||||
@@ -672,8 +834,8 @@ object PullRequestService {
|
||||
state -> summary
|
||||
}
|
||||
lazy val statusesAndRequired: List[(CommitStatus, Boolean)] = statuses.map { s =>
|
||||
s -> branchProtection.contexts.contains(s.context)
|
||||
s -> branchProtection.contexts.getOrElse(Nil).contains(s.context)
|
||||
}
|
||||
lazy val isAllSuccess = commitStateSummary._1 == CommitState.SUCCESS
|
||||
lazy val isAllSuccess: Boolean = commitStateSummary._1 == CommitState.SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
package gitbucket.core.service
|
||||
import gitbucket.core.api.JsonFormat
|
||||
import gitbucket.core.model.{Account, WebHook}
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.model.Profile.profile.blockingApi.*
|
||||
import gitbucket.core.model.activity.{CloseIssueInfo, PushInfo}
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||
import gitbucket.core.service.WebHookService.WebHookPushPayload
|
||||
import gitbucket.core.util.Directory.getRepositoryDir
|
||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||
import gitbucket.core.util.{JGitUtil, LockUtil}
|
||||
import gitbucket.core.util.{JGitUtil, LockUtil, StringUtil}
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder}
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.eclipse.jgit.lib.*
|
||||
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
|
||||
|
||||
import scala.util.Using
|
||||
|
||||
trait RepositoryCommitFileService {
|
||||
self: AccountService & ActivityService & IssuesService & PullRequestService & WebHookPullRequestService &
|
||||
RepositoryService =>
|
||||
RepositoryService & ProtectedBranchService =>
|
||||
|
||||
/**
|
||||
* Create multiple files by callback function.
|
||||
@@ -53,16 +53,22 @@ trait RepositoryCommitFileService {
|
||||
message: String,
|
||||
commit: String,
|
||||
loginAccount: Account,
|
||||
settings: SystemSettings
|
||||
settings: SystemSettings,
|
||||
hasBom: Boolean = false
|
||||
)(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, Option[ObjectId])] = {
|
||||
val contentBytes = if (content.nonEmpty) {
|
||||
val bytes = content.getBytes(charset)
|
||||
if (hasBom) StringUtil.Utf8Bom ++ bytes else bytes
|
||||
} else {
|
||||
Array.emptyByteArray
|
||||
}
|
||||
commitFile(
|
||||
repository,
|
||||
branch,
|
||||
path,
|
||||
newFileName,
|
||||
oldFileName,
|
||||
if (content.nonEmpty) { content.getBytes(charset) }
|
||||
else { Array.emptyByteArray },
|
||||
contentBytes,
|
||||
message,
|
||||
commit,
|
||||
loginAccount,
|
||||
@@ -92,10 +98,10 @@ trait RepositoryCommitFileService {
|
||||
)(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, Option[ObjectId])] = {
|
||||
|
||||
val newPath = newFileName.map { newFileName =>
|
||||
if (path.length == 0) newFileName else s"${path}/${newFileName}"
|
||||
if (path.isEmpty) newFileName else s"${path}/${newFileName}"
|
||||
}
|
||||
val oldPath = oldFileName.map { oldFileName =>
|
||||
if (path.length == 0) oldFileName else s"${path}/${oldFileName}"
|
||||
if (path.isEmpty) oldFileName else s"${path}/${oldFileName}"
|
||||
}
|
||||
|
||||
_createFiles(repository, branch, message, pusherAccount, committerName, committerMailAddress, settings) {
|
||||
@@ -139,7 +145,6 @@ trait RepositoryCommitFileService {
|
||||
)(
|
||||
f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => R
|
||||
)(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, R)] = {
|
||||
|
||||
LockUtil.lock(s"${repository.owner}/${repository.name}") {
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
val builder = DirCache.newInCore.builder()
|
||||
@@ -168,7 +173,14 @@ trait RepositoryCommitFileService {
|
||||
|
||||
// call pre-commit hook
|
||||
val error = PluginRegistry().getReceiveHooks.flatMap { hook =>
|
||||
hook.preReceive(repository.owner, repository.name, receivePack, receiveCommand, pusherAccount.userName, false)
|
||||
hook.preReceive(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
receivePack,
|
||||
receiveCommand,
|
||||
pusherAccount.userName,
|
||||
mergePullRequest = false
|
||||
)
|
||||
}.headOption
|
||||
|
||||
error match {
|
||||
@@ -194,7 +206,8 @@ trait RepositoryCommitFileService {
|
||||
// record activity
|
||||
updateLastActivityDate(repository.owner, repository.name)
|
||||
val commitInfo = new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
|
||||
val pushInfo = PushInfo(repository.owner, repository.name, pusherAccount.userName, branch, List(commitInfo))
|
||||
val pushInfo =
|
||||
PushInfo(repository.owner, repository.name, pusherAccount.userName, branch, List(commitInfo))
|
||||
recordActivity(pushInfo)
|
||||
|
||||
// create issue comment by commit message
|
||||
@@ -221,7 +234,14 @@ trait RepositoryCommitFileService {
|
||||
|
||||
// call post-commit hook
|
||||
PluginRegistry().getReceiveHooks.foreach { hook =>
|
||||
hook.postReceive(repository.owner, repository.name, receivePack, receiveCommand, committerName, false)
|
||||
hook.postReceive(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
receivePack,
|
||||
receiveCommand,
|
||||
committerName,
|
||||
mergePullRequest = false
|
||||
)
|
||||
}
|
||||
|
||||
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
|
||||
|
||||
@@ -654,11 +654,11 @@ trait RepositoryService {
|
||||
|
||||
def hasOwnerRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
|
||||
loginAccount match {
|
||||
case Some(a) if (a.isAdmin) => true
|
||||
case Some(a) if (a.userName == owner) => true
|
||||
case Some(a) if (getGroupMembers(owner).exists(_.userName == a.userName)) => true
|
||||
case Some(a) if (getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN)).contains(a.userName)) => true
|
||||
case _ => false
|
||||
case Some(a) if a.isAdmin => true
|
||||
case Some(a) if a.userName == owner => true
|
||||
case Some(a) if getGroupMembers(owner).exists(_.userName == a.userName) => true
|
||||
case Some(a) if getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN)).contains(a.userName) => true
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,11 +666,11 @@ trait RepositoryService {
|
||||
s: Session
|
||||
): Boolean = {
|
||||
loginAccount match {
|
||||
case Some(a) if (a.isAdmin) => true
|
||||
case Some(a) if (a.userName == owner) => true
|
||||
case Some(a) if (getGroupMembers(owner).exists(_.userName == a.userName)) => true
|
||||
case Some(a) if a.isAdmin => true
|
||||
case Some(a) if a.userName == owner => true
|
||||
case Some(a) if getGroupMembers(owner).exists(_.userName == a.userName) => true
|
||||
case Some(a)
|
||||
if (getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER)).contains(a.userName)) =>
|
||||
if getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER)).contains(a.userName) =>
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
@@ -678,12 +678,12 @@ trait RepositoryService {
|
||||
|
||||
def hasGuestRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
|
||||
loginAccount match {
|
||||
case Some(a) if (a.isAdmin) => true
|
||||
case Some(a) if (a.userName == owner) => true
|
||||
case Some(a) if (getGroupMembers(owner).exists(_.userName == a.userName)) => true
|
||||
case Some(a) if a.isAdmin => true
|
||||
case Some(a) if a.userName == owner => true
|
||||
case Some(a) if getGroupMembers(owner).exists(_.userName == a.userName) => true
|
||||
case Some(a)
|
||||
if (getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER, Role.GUEST))
|
||||
.contains(a.userName)) =>
|
||||
if getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER, Role.GUEST))
|
||||
.contains(a.userName) =>
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
@@ -694,17 +694,29 @@ trait RepositoryService {
|
||||
true
|
||||
} else {
|
||||
loginAccount match {
|
||||
case Some(x) if (x.isAdmin) => true
|
||||
case Some(x) if (repository.userName == x.userName) => true
|
||||
case Some(x) if (getGroupMembers(repository.userName).exists(_.userName == x.userName)) => true
|
||||
case Some(x)
|
||||
if (getCollaboratorUserNames(repository.userName, repository.repositoryName).contains(x.userName)) =>
|
||||
case Some(x) if x.isAdmin => true
|
||||
case Some(x) if repository.userName == x.userName => true
|
||||
case Some(x) if getGroupMembers(repository.userName).exists(_.userName == x.userName) => true
|
||||
case Some(x) if getCollaboratorUserNames(repository.userName, repository.repositoryName).contains(x.userName) =>
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def isWritable(repository: Repository, loginAccount: Option[Account])(implicit s: Session): Boolean = {
|
||||
loginAccount match {
|
||||
case Some(x) if x.isAdmin => true
|
||||
case Some(x) if repository.userName == x.userName => true
|
||||
case Some(x) if getGroupMembers(repository.userName).exists(_.userName == x.userName) => true
|
||||
case Some(x)
|
||||
if getCollaboratorUserNames(repository.userName, repository.repositoryName, Seq(Role.ADMIN, Role.DEVELOPER))
|
||||
.contains(x.userName) =>
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int =
|
||||
Query(Repositories.filter { t =>
|
||||
(t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)
|
||||
|
||||
@@ -30,6 +30,7 @@ trait SystemSettingsService {
|
||||
props.setProperty(Gravatar, settings.basicBehavior.gravatar.toString)
|
||||
props.setProperty(Notification, settings.basicBehavior.notification.toString)
|
||||
props.setProperty(LimitVisibleRepositories, settings.basicBehavior.limitVisibleRepositories.toString)
|
||||
props.setProperty(CompareNoCheckByDefault, settings.basicBehavior.compareNoCheckByDefault.toString)
|
||||
props.setProperty(SshEnabled, settings.ssh.enabled.toString)
|
||||
settings.ssh.bindAddress.foreach { bindAddress =>
|
||||
props.setProperty(SshBindAddressHost, bindAddress.host.trim())
|
||||
@@ -93,6 +94,7 @@ trait SystemSettingsService {
|
||||
props.setProperty(RepositoryViewerMaxDiffFiles, settings.repositoryViewer.maxDiffFiles.toString)
|
||||
props.setProperty(RepositoryViewerMaxDiffLines, settings.repositoryViewer.maxDiffLines.toString)
|
||||
props.setProperty(DefaultBranch, settings.defaultBranch)
|
||||
props.setProperty(ShowFullName, settings.showFullName.toString)
|
||||
|
||||
Using.resource(new java.io.FileOutputStream(GitBucketConf)) { out =>
|
||||
props.store(out, null)
|
||||
@@ -127,7 +129,8 @@ trait SystemSettingsService {
|
||||
),
|
||||
getValue(props, Gravatar, false),
|
||||
getValue(props, Notification, false),
|
||||
getValue(props, LimitVisibleRepositories, false)
|
||||
getValue(props, LimitVisibleRepositories, false),
|
||||
getValue(props, CompareNoCheckByDefault, false)
|
||||
),
|
||||
Ssh(
|
||||
enabled = getValue(props, SshEnabled, false),
|
||||
@@ -211,7 +214,8 @@ trait SystemSettingsService {
|
||||
getValue(props, RepositoryViewerMaxDiffFiles, 100),
|
||||
getValue(props, RepositoryViewerMaxDiffLines, 1000)
|
||||
),
|
||||
getValue(props, DefaultBranch, "main")
|
||||
getValue(props, DefaultBranch, "main"),
|
||||
getValue(props, ShowFullName, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -238,7 +242,8 @@ object SystemSettingsService {
|
||||
webHook: WebHook,
|
||||
upload: Upload,
|
||||
repositoryViewer: RepositoryViewerSettings,
|
||||
defaultBranch: String
|
||||
defaultBranch: String,
|
||||
showFullName: Boolean
|
||||
) {
|
||||
def baseUrl(request: HttpServletRequest): String =
|
||||
baseUrl.getOrElse(parseBaseUrl(request)).stripSuffix("/")
|
||||
@@ -278,6 +283,7 @@ object SystemSettingsService {
|
||||
gravatar: Boolean,
|
||||
notification: Boolean,
|
||||
limitVisibleRepositories: Boolean,
|
||||
compareNoCheckByDefault: Boolean,
|
||||
)
|
||||
|
||||
case class RepositoryOperation(
|
||||
@@ -410,6 +416,7 @@ object SystemSettingsService {
|
||||
private val Gravatar = "gravatar"
|
||||
private val Notification = "notification"
|
||||
private val LimitVisibleRepositories = "limitVisibleRepositories"
|
||||
private val CompareNoCheckByDefault = "compare_no_check_by_default"
|
||||
private val SshEnabled = "ssh"
|
||||
private val SshHost = "ssh.host"
|
||||
private val SshPort = "ssh.port"
|
||||
@@ -457,6 +464,7 @@ object SystemSettingsService {
|
||||
private val RepositoryViewerMaxDiffFiles = "repository_viewer_max_diff_files"
|
||||
private val RepositoryViewerMaxDiffLines = "repository_viewer_max_diff_lines"
|
||||
private val DefaultBranch = "default_branch"
|
||||
private val ShowFullName = "show_full_name"
|
||||
|
||||
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = {
|
||||
getConfigValue(key).getOrElse {
|
||||
|
||||
@@ -265,7 +265,7 @@ trait WebHookService {
|
||||
}
|
||||
|
||||
private def validateTargetAddress(settings: SystemSettings, url: String): Boolean = {
|
||||
val host = new java.net.URL(url).getHost
|
||||
val host = new java.net.URI(url).toURL.getHost
|
||||
|
||||
!settings.webHook.blockPrivateAddress ||
|
||||
!HttpClientUtil.isPrivateAddress(host) ||
|
||||
@@ -302,42 +302,45 @@ trait WebHookService {
|
||||
httpPost.addHeader("Content-Type", webHook.ctype.ctype)
|
||||
httpPost.addHeader("X-Github-Event", event.name)
|
||||
httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString)
|
||||
|
||||
def addXHubSignature(content: Array[Byte]): Unit = {
|
||||
webHook.token
|
||||
.filter(_.trim.nonEmpty)
|
||||
.foreach { token =>
|
||||
// https://developer.github.com/webhooks/securing/#validating-payloads-from-github
|
||||
// SHA1 is required for backward compatibility, but SHA256 is recommended.
|
||||
httpPost.addHeader(
|
||||
"X-Hub-Signature",
|
||||
XHub.generateHeaderXHubToken(
|
||||
XHubConverter.HEXA_LOWERCASE,
|
||||
XHubDigest.SHA1,
|
||||
token,
|
||||
content
|
||||
)
|
||||
)
|
||||
httpPost.addHeader(
|
||||
"X-Hub-Signature-256",
|
||||
XHub.generateHeaderXHubToken(
|
||||
XHubConverter.HEXA_LOWERCASE,
|
||||
XHubDigest.SHA256,
|
||||
token,
|
||||
content
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
webHook.ctype match {
|
||||
case WebHookContentType.FORM => {
|
||||
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
|
||||
params.add(new BasicNameValuePair("payload", json))
|
||||
def postContent = new UrlEncodedFormEntity(params, "UTF-8")
|
||||
httpPost.setEntity(postContent)
|
||||
if (webHook.token.exists(_.trim.nonEmpty)) {
|
||||
// TODO find a better way and see how to extract content from postContent
|
||||
val contentAsBytes = URLEncodedUtils.format(params, "UTF-8").getBytes("UTF-8")
|
||||
httpPost.addHeader(
|
||||
"X-Hub-Signature",
|
||||
XHub.generateHeaderXHubToken(
|
||||
XHubConverter.HEXA_LOWERCASE,
|
||||
XHubDigest.SHA1,
|
||||
webHook.token.get,
|
||||
contentAsBytes
|
||||
)
|
||||
)
|
||||
}
|
||||
addXHubSignature(URLEncodedUtils.format(params, "UTF-8").getBytes("UTF-8"))
|
||||
}
|
||||
case WebHookContentType.JSON => {
|
||||
httpPost.setEntity(
|
||||
EntityBuilder.create().setContentType(ContentType.APPLICATION_JSON).setText(json).build()
|
||||
)
|
||||
if (webHook.token.exists(_.trim.nonEmpty)) {
|
||||
httpPost.addHeader(
|
||||
"X-Hub-Signature",
|
||||
XHub.generateHeaderXHubToken(
|
||||
XHubConverter.HEXA_LOWERCASE,
|
||||
XHubDigest.SHA1,
|
||||
webHook.token.orNull,
|
||||
json.getBytes("UTF-8")
|
||||
)
|
||||
)
|
||||
}
|
||||
addXHubSignature(json.getBytes("UTF-8"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -341,4 +341,18 @@ trait WikiService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the branch name from the HEAD of the Wiki repository.
|
||||
*
|
||||
* from gitbucket.core.controller.WikiController
|
||||
*
|
||||
* @param owner Wiki owner
|
||||
* @param repository Wiki repository
|
||||
* @return Branch name
|
||||
*/
|
||||
def getWikiBranch(owner: String, repository: String): String = {
|
||||
Using.resource(Git.open(Directory.getWikiRepositoryDir(owner, repository))) { git =>
|
||||
git.getRepository.getBranch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ class ApiAuthenticationFilter extends Filter with AccessTokenService with Accoun
|
||||
val response = res.asInstanceOf[HttpServletResponse]
|
||||
Option(request.getHeader("Authorization"))
|
||||
.map {
|
||||
case auth if auth.toLowerCase().startsWith("token ") =>
|
||||
AccessTokenService.getAccountByAccessToken(auth.substring(6).trim).toRight(())
|
||||
case auth if auth.toLowerCase().startsWith("token ") || auth.toLowerCase().startsWith("bearer ") =>
|
||||
AccessTokenService.getAccountByAccessToken(auth.substring(auth.indexOf(" ") + 1).trim).toRight(())
|
||||
case auth if auth.startsWith("Basic ") => doBasicAuth(auth, loadSystemSettings(), request).toRight(())
|
||||
case _ => Left(())
|
||||
}
|
||||
@@ -42,7 +42,7 @@ class ApiAuthenticationFilter extends Filter with AccessTokenService with Accoun
|
||||
updateLastLoginDate(account.userName)
|
||||
}
|
||||
chain.doFilter(req, res)
|
||||
case None => chain.doFilter(req, res)
|
||||
case None => chain.doFilter(req, res)
|
||||
case Some(Left(_)) => {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED)
|
||||
response.setContentType("application/json; charset=utf-8")
|
||||
|
||||
@@ -9,11 +9,11 @@ import gitbucket.core.api.JsonFormat.Context
|
||||
import gitbucket.core.model.WebHook
|
||||
import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
|
||||
import gitbucket.core.service.IssuesService.IssueSearchCondition
|
||||
import gitbucket.core.service.WebHookService._
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.service.WebHookService.*
|
||||
import gitbucket.core.service.*
|
||||
import gitbucket.core.util.Implicits.*
|
||||
import gitbucket.core.util.*
|
||||
import gitbucket.core.model.Profile.profile.blockingApi.*
|
||||
import gitbucket.core.model.activity.{
|
||||
BaseActivityInfo,
|
||||
CloseIssueInfo,
|
||||
@@ -33,9 +33,9 @@ import gitbucket.core.servlet.Database
|
||||
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.http.server.GitServlet
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.eclipse.jgit.transport._
|
||||
import org.eclipse.jgit.transport.resolver._
|
||||
import org.eclipse.jgit.lib.*
|
||||
import org.eclipse.jgit.transport.*
|
||||
import org.eclipse.jgit.transport.resolver.*
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.servlet.ServletConfig
|
||||
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
|
||||
@@ -43,7 +43,7 @@ import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
||||
import org.eclipse.jgit.internal.storage.file.FileRepository
|
||||
import org.json4s.Formats
|
||||
import org.json4s.convertToJsonInput
|
||||
import org.json4s.jackson.Serialization._
|
||||
import org.json4s.jackson.Serialization.*
|
||||
|
||||
/**
|
||||
* Provides Git repository via HTTP.
|
||||
@@ -117,7 +117,7 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
|
||||
GitLfs.BatchResponseObject(
|
||||
requestObject.oid,
|
||||
requestObject.size,
|
||||
true,
|
||||
authenticated = true,
|
||||
GitLfs.Actions(
|
||||
upload = Some(
|
||||
GitLfs.Action(
|
||||
@@ -138,7 +138,7 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
|
||||
GitLfs.BatchResponseObject(
|
||||
requestObject.oid,
|
||||
requestObject.size,
|
||||
true,
|
||||
authenticated = true,
|
||||
GitLfs.Actions(
|
||||
download = Some(
|
||||
GitLfs.Action(
|
||||
@@ -223,7 +223,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
|
||||
}
|
||||
}
|
||||
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String, sshUrl: Option[String])
|
||||
extends PostReceiveHook
|
||||
@@ -242,6 +242,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
with WebHookPullRequestReviewCommentService
|
||||
with CommitsService
|
||||
with SystemSettingsService
|
||||
with ProtectedBranchService
|
||||
with RequestCache {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
|
||||
@@ -253,7 +254,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
commands.asScala.foreach { command =>
|
||||
// call pre-commit hook
|
||||
PluginRegistry().getReceiveHooks
|
||||
.flatMap(_.preReceive(owner, repository, receivePack, command, pusher, false))
|
||||
.flatMap(_.preReceive(owner, repository, receivePack, command, pusher, mergePullRequest = false))
|
||||
.headOption
|
||||
.foreach { error =>
|
||||
command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, error)
|
||||
@@ -428,8 +429,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
repositoryInfo,
|
||||
newCommits,
|
||||
ownerAccount,
|
||||
newId = command.getNewId(),
|
||||
oldId = command.getOldId()
|
||||
newId = command.getNewId,
|
||||
oldId = command.getOldId
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -453,7 +454,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
|
||||
// call post-commit hook
|
||||
PluginRegistry().getReceiveHooks
|
||||
.foreach(_.postReceive(owner, repository, receivePack, command, pusher, false))
|
||||
.foreach(_.postReceive(owner, repository, receivePack, command, pusher, mergePullRequest = false))
|
||||
}
|
||||
}
|
||||
// update repository last modified time.
|
||||
@@ -543,7 +544,7 @@ class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
case ChangeType.ADD | ChangeType.RENAME => "created"
|
||||
case ChangeType.MODIFY => "edited"
|
||||
case ChangeType.DELETE => "deleted"
|
||||
case other =>
|
||||
case other =>
|
||||
logger.error(s"Unsupported Wiki action: $other")
|
||||
"unsupported action"
|
||||
}
|
||||
@@ -560,7 +561,7 @@ class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
case "created" => Some(CreateWikiPageInfo(owner, repo, commit.committerName, pageName))
|
||||
case "edited" => Some(EditWikiPageInfo(owner, repo, commit.committerName, pageName, commit.id))
|
||||
case "deleted" => Some(DeleteWikiInfo(owner, repo, commit.committerName, pageName))
|
||||
case other =>
|
||||
case other =>
|
||||
logger.info(s"Attempted to build wiki record for unsupported action: $other")
|
||||
None
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import gitbucket.core.GitBucketCoreModule
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.service.SystemSettingsService
|
||||
import gitbucket.core.util.DatabaseConfig
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.JDBCUtil._
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.util.Directory.*
|
||||
import gitbucket.core.util.JDBCUtil.*
|
||||
import gitbucket.core.model.Profile.profile.blockingApi.*
|
||||
// Imported names have higher precedence than names, defined in other files.
|
||||
// If Database is not bound by explicit import, then "Database" refers to the Database introduced by the wildcard import above.
|
||||
import gitbucket.core.servlet.Database
|
||||
@@ -20,7 +20,7 @@ import javax.servlet.{ServletContextEvent, ServletContextListener}
|
||||
import org.apache.commons.io.{FileUtils, IOUtils}
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.jdk.CollectionConverters.*
|
||||
import scala.util.Using
|
||||
|
||||
/**
|
||||
@@ -50,51 +50,57 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
|
||||
// )
|
||||
|
||||
override def contextInitialized(event: ServletContextEvent): Unit = {
|
||||
val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
|
||||
if (dataDir != null) {
|
||||
System.setProperty("gitbucket.home", dataDir)
|
||||
}
|
||||
org.h2.Driver.load()
|
||||
try {
|
||||
val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
|
||||
if (dataDir != null) {
|
||||
System.setProperty("gitbucket.home", dataDir)
|
||||
}
|
||||
org.h2.Driver.load()
|
||||
|
||||
Database() withTransaction { session =>
|
||||
val conn = session.conn
|
||||
val manager = new JDBCVersionManager(conn)
|
||||
Database() withTransaction { session =>
|
||||
val conn = session.conn
|
||||
val manager = new JDBCVersionManager(conn)
|
||||
|
||||
// Check version
|
||||
checkVersion(manager, conn)
|
||||
// Check version
|
||||
checkVersion(manager, conn)
|
||||
|
||||
// Run normal migration
|
||||
logger.info("Start schema update")
|
||||
new Solidbase()
|
||||
.migrate(conn, Thread.currentThread.getContextClassLoader, DatabaseConfig.liquiDriver, GitBucketCoreModule)
|
||||
// Run normal migration
|
||||
logger.info("Start schema update")
|
||||
new Solidbase()
|
||||
.migrate(conn, Thread.currentThread.getContextClassLoader, DatabaseConfig.liquiDriver, GitBucketCoreModule)
|
||||
|
||||
// Rescue code for users who updated from 3.14 to 4.0.0
|
||||
// https://github.com/gitbucket/gitbucket/issues/1227
|
||||
val currentVersion = manager.getCurrentVersion(GitBucketCoreModule.getModuleId)
|
||||
val databaseVersion = if (currentVersion == "4.0") {
|
||||
manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0.0")
|
||||
"4.0.0"
|
||||
} else currentVersion
|
||||
// Rescue code for users who updated from 3.14 to 4.0.0
|
||||
// https://github.com/gitbucket/gitbucket/issues/1227
|
||||
val currentVersion = manager.getCurrentVersion(GitBucketCoreModule.getModuleId)
|
||||
val databaseVersion = if (currentVersion == "4.0") {
|
||||
manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0.0")
|
||||
"4.0.0"
|
||||
} else currentVersion
|
||||
|
||||
val gitbucketVersion = GitBucketCoreModule.getVersions.asScala.last.getVersion
|
||||
if (databaseVersion != gitbucketVersion) {
|
||||
throw new IllegalStateException(
|
||||
s"Initialization failed. GitBucket version is ${gitbucketVersion}, but database version is ${databaseVersion}."
|
||||
)
|
||||
val gitbucketVersion = GitBucketCoreModule.getVersions.asScala.last.getVersion
|
||||
if (databaseVersion != gitbucketVersion) {
|
||||
throw new IllegalStateException(
|
||||
s"Initialization failed. GitBucket version is ${gitbucketVersion}, but database version is ${databaseVersion}."
|
||||
)
|
||||
}
|
||||
|
||||
// Install bundled plugins
|
||||
extractBundledPlugins()
|
||||
|
||||
// Load plugins
|
||||
logger.info("Initialize plugins")
|
||||
PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn)
|
||||
}
|
||||
|
||||
// Install bundled plugins
|
||||
extractBundledPlugins()
|
||||
|
||||
// Load plugins
|
||||
logger.info("Initialize plugins")
|
||||
PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn)
|
||||
// // Start Quartz scheduler
|
||||
// val scheduler = QuartzSchedulerExtension(system)
|
||||
//
|
||||
// scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity")
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
logger.error(e.getMessage, e)
|
||||
throw e
|
||||
}
|
||||
|
||||
// // Start Quartz scheduler
|
||||
// val scheduler = QuartzSchedulerExtension(system)
|
||||
//
|
||||
// scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity")
|
||||
}
|
||||
|
||||
private def checkVersion(manager: JDBCVersionManager, conn: java.sql.Connection): Unit = {
|
||||
@@ -141,7 +147,7 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
|
||||
Using.resource(cl.getResourceAsStream("bundle-plugins.txt")) { pluginsFile =>
|
||||
if (pluginsFile != null) {
|
||||
val plugins = IOUtils.readLines(pluginsFile, "UTF-8")
|
||||
val gitbucketVersion = GitBucketCoreModule.getVersions.asScala.last.getVersion
|
||||
// val gitbucketVersion = GitBucketCoreModule.getVersions.asScala.last.getVersion
|
||||
|
||||
plugins.asScala.foreach { plugin =>
|
||||
plugin.trim.split(":") match {
|
||||
|
||||
@@ -15,9 +15,9 @@ trait OneselfAuthenticator { self: ControllerBase =>
|
||||
|
||||
private def authenticate(action: => Any) = {
|
||||
context.loginAccount match {
|
||||
case Some(x) if (x.isAdmin) => action
|
||||
case Some(x) if (request.paths(0) == x.userName) => action
|
||||
case _ => Unauthorized()
|
||||
case Some(x) if x.isAdmin => action
|
||||
case Some(x) if request.paths(0) == x.userName => action
|
||||
case _ => Unauthorized()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ trait OneselfAuthenticator { self: ControllerBase =>
|
||||
* Allows only the repository owner and administrators.
|
||||
*/
|
||||
trait OwnerAuthenticator { self: ControllerBase & RepositoryService & AccountService =>
|
||||
protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
|
||||
protected def ownerOnly(action: RepositoryInfo => Any) = { authenticate(action) }
|
||||
protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
|
||||
|
||||
private def authenticate(action: (RepositoryInfo) => Any) = {
|
||||
@@ -34,14 +34,14 @@ trait OwnerAuthenticator { self: ControllerBase & RepositoryService & AccountSer
|
||||
val repoName = params("repository")
|
||||
getRepository(userName, repoName).map { repository =>
|
||||
context.loginAccount match {
|
||||
case Some(x) if (x.isAdmin) => action(repository)
|
||||
case Some(x) if (repository.owner == x.userName) => action(repository)
|
||||
case Some(x) if x.isAdmin => action(repository)
|
||||
case Some(x) if repository.owner == x.userName => action(repository)
|
||||
// TODO Repository management is allowed for only group managers?
|
||||
case Some(x) if (getGroupMembers(repository.owner).exists { m =>
|
||||
case Some(x) if getGroupMembers(repository.owner).exists { m =>
|
||||
m.userName == x.userName && m.isManager
|
||||
}) =>
|
||||
} =>
|
||||
action(repository)
|
||||
case Some(x) if (getCollaboratorUserNames(userName, repoName, Seq(Role.ADMIN)).contains(x.userName)) =>
|
||||
case Some(x) if getCollaboratorUserNames(userName, repoName, Seq(Role.ADMIN)).contains(x.userName) =>
|
||||
action(repository)
|
||||
case _ => Unauthorized()
|
||||
}
|
||||
@@ -83,10 +83,10 @@ trait AdminAuthenticator { self: ControllerBase =>
|
||||
* Allows only guests and signed in users who can access the repository.
|
||||
*/
|
||||
trait ReferrerAuthenticator { self: ControllerBase & RepositoryService & AccountService =>
|
||||
protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
|
||||
protected def referrersOnly(action: RepositoryInfo => Any) = { authenticate(action) }
|
||||
protected def referrersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
|
||||
|
||||
private def authenticate(action: (RepositoryInfo) => Any) = {
|
||||
private def authenticate(action: RepositoryInfo => Any) = {
|
||||
val userName = params("owner")
|
||||
val repoName = params("repository")
|
||||
getRepository(userName, repoName).map { repository =>
|
||||
@@ -103,12 +103,12 @@ trait ReferrerAuthenticator { self: ControllerBase & RepositoryService & Account
|
||||
* Allows only signed in users who have read permission for the repository.
|
||||
*/
|
||||
trait ReadableUsersAuthenticator { self: ControllerBase & RepositoryService & AccountService =>
|
||||
protected def readableUsersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
|
||||
protected def readableUsersOnly(action: RepositoryInfo => Any) = { authenticate(action) }
|
||||
protected def readableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => {
|
||||
authenticate(action(form, _))
|
||||
}
|
||||
|
||||
private def authenticate(action: (RepositoryInfo) => Any) = {
|
||||
private def authenticate(action: RepositoryInfo => Any) = {
|
||||
val userName = params("owner")
|
||||
val repoName = params("repository")
|
||||
getRepository(userName, repoName).map { repository =>
|
||||
@@ -125,24 +125,19 @@ trait ReadableUsersAuthenticator { self: ControllerBase & RepositoryService & Ac
|
||||
* Allows only signed in users who have write permission for the repository.
|
||||
*/
|
||||
trait WritableUsersAuthenticator { self: ControllerBase & RepositoryService & AccountService =>
|
||||
protected def writableUsersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
|
||||
protected def writableUsersOnly(action: RepositoryInfo => Any) = { authenticate(action) }
|
||||
protected def writableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => {
|
||||
authenticate(action(form, _))
|
||||
}
|
||||
|
||||
private def authenticate(action: (RepositoryInfo) => Any) = {
|
||||
private def authenticate(action: RepositoryInfo => Any) = {
|
||||
val userName = params("owner")
|
||||
val repoName = params("repository")
|
||||
getRepository(userName, repoName).map { repository =>
|
||||
context.loginAccount match {
|
||||
case Some(x) if (x.isAdmin) => action(repository)
|
||||
case Some(x) if (userName == x.userName) => action(repository)
|
||||
case Some(x) if (getGroupMembers(repository.owner).exists(_.userName == x.userName)) => action(repository)
|
||||
case Some(x)
|
||||
if (getCollaboratorUserNames(userName, repoName, Seq(Role.ADMIN, Role.DEVELOPER))
|
||||
.contains(x.userName)) =>
|
||||
action(repository)
|
||||
case _ => Unauthorized()
|
||||
if (isWritable(repository.repository, context.loginAccount)) {
|
||||
action(repository)
|
||||
} else {
|
||||
Unauthorized()
|
||||
}
|
||||
} getOrElse NotFound()
|
||||
}
|
||||
@@ -159,9 +154,9 @@ trait GroupManagerAuthenticator { self: ControllerBase & AccountService =>
|
||||
context.loginAccount match {
|
||||
case Some(x) if x.isAdmin => action
|
||||
case Some(x) if x.userName == request.paths(0) => action
|
||||
case Some(x) if (getGroupMembers(request.paths(0)).exists { member =>
|
||||
case Some(x) if getGroupMembers(request.paths(0)).exists { member =>
|
||||
member.userName == x.userName && member.isManager
|
||||
}) =>
|
||||
} =>
|
||||
action
|
||||
case _ => Unauthorized()
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ object DatabaseConfig {
|
||||
FileUtils.write(
|
||||
file,
|
||||
"""db {
|
||||
| url = "jdbc:h2:${DatabaseHome};MVCC=true"
|
||||
| url = "jdbc:h2:${DatabaseHome}"
|
||||
| user = "sa"
|
||||
| password = "sa"
|
||||
|# connectionTimeout = 30000
|
||||
|
||||
@@ -10,7 +10,7 @@ object Directory {
|
||||
val GitBucketHome = (System.getProperty("gitbucket.home") match {
|
||||
// -Dgitbucket.home=<path>
|
||||
case path if (path != null) => new File(path)
|
||||
case _ =>
|
||||
case _ =>
|
||||
scala.util.Properties.envOrNone("GITBUCKET_HOME") match {
|
||||
// environment variable GITBUCKET_HOME
|
||||
case Some(env) => new File(env)
|
||||
|
||||
@@ -94,6 +94,8 @@ object JGitUtil {
|
||||
case class GpgVerifyInfo(signedUser: String, signedKeyId: String)
|
||||
|
||||
private def getSignTarget(rev: RevCommit): Array[Byte] = {
|
||||
logger.debug(s"getSignTarget(${rev})")
|
||||
|
||||
val ascii = "ASCII"
|
||||
val os = new ByteArrayOutputStream()
|
||||
val w = new OutputStreamWriter(os, rev.getEncoding)
|
||||
@@ -214,8 +216,15 @@ object JGitUtil {
|
||||
* @param size total size of object in bytes
|
||||
* @param content the string content
|
||||
* @param charset the character encoding
|
||||
* @param hasBom true if the content has UTF-8 BOM
|
||||
*/
|
||||
case class ContentInfo(viewType: String, size: Option[Long], content: Option[String], charset: Option[String]) {
|
||||
case class ContentInfo(
|
||||
viewType: String,
|
||||
size: Option[Long],
|
||||
content: Option[String],
|
||||
charset: Option[String],
|
||||
hasBom: Boolean = false
|
||||
) {
|
||||
|
||||
/**
|
||||
* the line separator of this content ("LF" or "CRLF")
|
||||
@@ -283,6 +292,8 @@ object JGitUtil {
|
||||
* @return the RevCommit for the specified commit or tag
|
||||
*/
|
||||
def getRevCommitFromId(git: Git, objectId: ObjectId): RevCommit = {
|
||||
logger.debug(s"getRevCommitFromId(${git}, ${objectId})")
|
||||
|
||||
val revWalk = new RevWalk(git.getRepository)
|
||||
val revCommit = revWalk.parseAny(objectId) match {
|
||||
case r: RevTag => revWalk.parseCommit(r.getObject)
|
||||
@@ -310,6 +321,8 @@ object JGitUtil {
|
||||
} else null
|
||||
|
||||
def removeCache(git: Git): Unit = {
|
||||
logger.debug(s"removeCache(${git})")
|
||||
|
||||
if (isCacheEnabled) {
|
||||
val dir = git.getRepository.getDirectory
|
||||
val keyPrefix = dir.getAbsolutePath + "@"
|
||||
@@ -327,6 +340,8 @@ object JGitUtil {
|
||||
* If the specified branch has over 10000 commits, this method returns 100001.
|
||||
*/
|
||||
def getCommitCount(git: Git, branch: String, max: Int = 10001): Int = {
|
||||
logger.debug(s"getCommitCount(${git}, ${branch}, ${max})")
|
||||
|
||||
val dir = git.getRepository.getDirectory
|
||||
|
||||
if (isCacheEnabled) {
|
||||
@@ -352,6 +367,8 @@ object JGitUtil {
|
||||
* Returns the repository information. It contains branch names and tag names.
|
||||
*/
|
||||
def getRepositoryInfo(owner: String, repository: String): RepositoryInfo = {
|
||||
logger.debug(s"getRepositoryInfo(${owner}, ${repository})")
|
||||
|
||||
Using.resource(Git.open(getRepositoryDir(owner, repository))) { git =>
|
||||
try {
|
||||
RepositoryInfo(
|
||||
@@ -409,6 +426,10 @@ object JGitUtil {
|
||||
commitCount: Int = 0,
|
||||
maxFiles: Int = 5
|
||||
): List[FileInfo] = {
|
||||
logger.debug(
|
||||
s"getFileList(${git}, ${revision}, ${path}, ${baseUrl}, ${commitCount}, ${maxFiles})"
|
||||
)
|
||||
|
||||
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
|
||||
val objectId = git.getRepository.resolve(revision)
|
||||
if (objectId == null) return Nil
|
||||
@@ -484,6 +505,8 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
def getCommit(path: String): RevCommit = {
|
||||
logger.debug(s"getCommit(${path})")
|
||||
|
||||
git
|
||||
.log()
|
||||
.addPath(path)
|
||||
@@ -543,6 +566,8 @@ object JGitUtil {
|
||||
* Returns the first line of the commit message.
|
||||
*/
|
||||
private def getSummaryMessage(fullMessage: String, shortMessage: String): String = {
|
||||
logger.debug(s"getSummaryMessage(${fullMessage}, ${shortMessage})")
|
||||
|
||||
val i = fullMessage.trim.indexOf('\n')
|
||||
val firstLine = if (i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage
|
||||
if (firstLine.length > shortMessage.length) shortMessage else firstLine
|
||||
@@ -552,6 +577,8 @@ object JGitUtil {
|
||||
* get all file list by revision. only file.
|
||||
*/
|
||||
def getTreeId(git: Git, revision: String): Option[String] = {
|
||||
logger.debug(s"getTreeId(${git}, ${revision})")
|
||||
|
||||
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
|
||||
val objectId = git.getRepository.resolve(revision)
|
||||
if (objectId == null) {
|
||||
@@ -567,6 +594,8 @@ object JGitUtil {
|
||||
* get all file list by tree object id.
|
||||
*/
|
||||
def getAllFileListByTreeId(git: Git, treeId: String): List[String] = {
|
||||
logger.debug(s"getAllFileListByTreeId(${git}, ${treeId})")
|
||||
|
||||
val objectId = git.getRepository.resolve(treeId + "^{tree}")
|
||||
if (objectId == null) return Nil
|
||||
Using.resource(new TreeWalk(git.getRepository)) { treeWalk =>
|
||||
@@ -599,6 +628,8 @@ object JGitUtil {
|
||||
limit: Int = 0,
|
||||
path: String = ""
|
||||
): Either[String, (List[CommitInfo], Boolean)] = {
|
||||
logger.debug(s"getCommitLog(${git}, ${revision}, ${page}, ${limit}, ${path})")
|
||||
|
||||
val fixedPage = if (page <= 0) 1 else page
|
||||
|
||||
@scala.annotation.tailrec
|
||||
@@ -607,7 +638,7 @@ object JGitUtil {
|
||||
count: Int,
|
||||
logs: List[CommitInfo]
|
||||
): (List[CommitInfo], Boolean) =
|
||||
if (i.hasNext) {
|
||||
if (i.hasNext && (limit <= 0 || logs.size < limit)) {
|
||||
val commit = i.next
|
||||
getCommitLog(
|
||||
i,
|
||||
@@ -635,6 +666,8 @@ object JGitUtil {
|
||||
def getCommitLogs(git: Git, begin: String, includesLastCommit: Boolean = false)(
|
||||
endCondition: RevCommit => Boolean
|
||||
): List[CommitInfo] = {
|
||||
logger.debug(s"getCommitLogs(${git}, ${begin}, ${includesLastCommit})")
|
||||
|
||||
@scala.annotation.tailrec
|
||||
def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[CommitInfo]): List[CommitInfo] =
|
||||
if (i.hasNext) {
|
||||
@@ -665,11 +698,13 @@ object JGitUtil {
|
||||
* @return The commits before 'to', that are not already present in the tree of 'from'.
|
||||
*/
|
||||
def getCommitLog(git: Git, from: String, to: String): List[CommitInfo] = {
|
||||
logger.debug(s"getCommitLog(${git}, ${from}, ${to})")
|
||||
|
||||
def resolveString(name: String): ObjectId = {
|
||||
val objectId = git.getRepository.resolve(name)
|
||||
git.getRepository.open(objectId).getType match {
|
||||
case Constants.OBJ_COMMIT => objectId
|
||||
case Constants.OBJ_TAG =>
|
||||
case Constants.OBJ_TAG =>
|
||||
val ref = git.getRepository.getRefDatabase.findRef(name)
|
||||
git.getRepository.getRefDatabase.peel(ref).getPeeledObjectId
|
||||
case _ => ObjectId.zeroId()
|
||||
@@ -731,6 +766,8 @@ object JGitUtil {
|
||||
* @return the list of latest commit
|
||||
*/
|
||||
def getLatestCommitFromPaths(git: Git, paths: List[String], revision: String): Map[String, RevCommit] = {
|
||||
logger.debug(s"getLatestCommitFromPaths(${git}, ${paths}, ${revision})")
|
||||
|
||||
val start = getRevCommitFromId(git, git.getRepository.resolve(revision))
|
||||
paths.flatMap { path =>
|
||||
val commit = git.log.add(start).addPath(path).setMaxCount(1).call.iterator.next
|
||||
@@ -743,6 +780,8 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
def getPatch(git: Git, from: Option[String], to: String): String = {
|
||||
logger.debug(s"getPatch(${git}, ${from}, ${to})")
|
||||
|
||||
val out = new ByteArrayOutputStream()
|
||||
val df = new DiffFormatter(out)
|
||||
df.setRepository(git.getRepository)
|
||||
@@ -756,6 +795,8 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
private def getDiffEntries(git: Git, from: Option[String], to: String): Seq[DiffEntry] = {
|
||||
logger.debug(s"getDiffEntries(${git}, ${from}, ${to})")
|
||||
|
||||
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
|
||||
val df = new DiffFormatter(DisabledOutputStream.INSTANCE)
|
||||
df.setRepository(git.getRepository)
|
||||
@@ -779,6 +820,8 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
def getParentCommitId(git: Git, id: String): Option[String] = {
|
||||
logger.debug(s"getParentCommitId(${git}, ${id})")
|
||||
|
||||
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
|
||||
val commit = revWalk.parseCommit(git.getRepository.resolve(id))
|
||||
commit.getParentCount match {
|
||||
@@ -789,6 +832,8 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
def getDiff(git: Git, from: Option[String], to: String, path: String): Option[DiffInfo] = {
|
||||
logger.debug(s"getDiff(${git}, ${from}, ${to}, ${path})")
|
||||
|
||||
getDiffEntries(git, from, to).find(_.getNewPath == path).map { diff =>
|
||||
val oldIsImage = FileUtil.isImage(diff.getOldPath)
|
||||
val newIsImage = FileUtil.isImage(diff.getNewPath)
|
||||
@@ -820,6 +865,10 @@ object JGitUtil {
|
||||
maxFiles: Int = 100,
|
||||
maxLines: Int = 1000
|
||||
): List[DiffInfo] = {
|
||||
logger.debug(
|
||||
s"getDiffs(${git}, ${from.getOrElse("")}, ${to}, ${fetchContent}, ${makePatch}, ${maxFiles}, ${maxLines})"
|
||||
)
|
||||
|
||||
val diffs = getDiffEntries(git, from, to)
|
||||
diffs.map { diff =>
|
||||
if (maxFiles > 0 && diffs.size > maxFiles) { // Don't show diff if there are more than maxFiles
|
||||
@@ -866,6 +915,8 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
private def getTextContent(git: Git, objectId: ObjectId): Option[String] = {
|
||||
logger.debug(s"getTextContent(${git}, ${objectId})")
|
||||
|
||||
JGitUtil
|
||||
.getContentFromId(git, objectId, fetchLargeFile = false)
|
||||
.filter(FileUtil.isText)
|
||||
@@ -873,6 +924,8 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
private def makePatchFromDiffEntry(git: Git, diff: DiffEntry): String = {
|
||||
logger.debug(s"makePatchFromDiffEntry(${git}, ${diff})")
|
||||
|
||||
val out = new ByteArrayOutputStream()
|
||||
Using.resource(new DiffFormatter(out)) { formatter =>
|
||||
formatter.setRepository(git.getRepository)
|
||||
@@ -905,6 +958,8 @@ object JGitUtil {
|
||||
* Returns the list of tags which pointed on the specified commit.
|
||||
*/
|
||||
def getTagsOnCommit(git: Git, commitId: String): List[String] = {
|
||||
logger.debug(s"getTagsOnCommit(${git}, ${commitId})")
|
||||
|
||||
git.getRepository.getAllRefsByPeeledObjectId.asScala
|
||||
.get(git.getRepository.resolve(commitId + "^0"))
|
||||
.map {
|
||||
@@ -956,6 +1011,8 @@ object JGitUtil {
|
||||
def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null
|
||||
|
||||
private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit = {
|
||||
logger.debug(s"setReceivePack(${repository})")
|
||||
|
||||
val config = repository.getConfig
|
||||
config.setBoolean("http", null, "receivepack", true)
|
||||
config.save()
|
||||
@@ -966,6 +1023,8 @@ object JGitUtil {
|
||||
repository: RepositoryService.RepositoryInfo,
|
||||
revstr: String = ""
|
||||
): Option[(ObjectId, String)] = {
|
||||
logger.debug(s"getDefaultBranch(${git}, ${repository})")
|
||||
|
||||
Seq(
|
||||
Some(if (revstr.isEmpty) repository.repository.defaultBranch else revstr),
|
||||
repository.branchList.headOption
|
||||
@@ -976,6 +1035,8 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
def createTag(git: Git, name: String, message: Option[String], commitId: String): Either[String, String] = {
|
||||
logger.debug(s"createTag(${git}, ${message}, ${commitId})")
|
||||
|
||||
try {
|
||||
val objectId: ObjectId = git.getRepository.resolve(commitId)
|
||||
Using.resource(new RevWalk(git.getRepository)) { walk =>
|
||||
@@ -995,6 +1056,8 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
def createBranch(git: Git, fromBranch: String, newBranch: String): Either[String, String] = {
|
||||
logger.debug(s"createBranch(${git}, ${fromBranch}, ${newBranch})")
|
||||
|
||||
try {
|
||||
git.branchCreate().setStartPoint(fromBranch).setName(newBranch).call()
|
||||
Right("Branch created.")
|
||||
@@ -1006,6 +1069,8 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = {
|
||||
logger.debug(s"createDirCacheEntry(${path}, ${mode}, ${objectId})")
|
||||
|
||||
val entry = new DirCacheEntry(path)
|
||||
entry.setFileMode(mode)
|
||||
entry.setObjectId(objectId)
|
||||
@@ -1022,6 +1087,10 @@ object JGitUtil {
|
||||
mailAddress: String,
|
||||
message: String
|
||||
): ObjectId = {
|
||||
logger.debug(
|
||||
s"createNewCommit(${git}, ${inserter}, ${headId}, ${treeId}, ${ref}, ${fullName}, ${mailAddress}, ${message})"
|
||||
)
|
||||
|
||||
val newCommit = new CommitBuilder()
|
||||
newCommit.setCommitter(new PersonIdent(fullName, mailAddress))
|
||||
newCommit.setAuthor(new PersonIdent(fullName, mailAddress))
|
||||
@@ -1048,6 +1117,8 @@ object JGitUtil {
|
||||
* Read submodule information from .gitmodules
|
||||
*/
|
||||
def getSubmodules(git: Git, tree: RevTree, baseUrl: Option[String]): List[SubmoduleInfo] = {
|
||||
logger.debug(s"getSubmodules(${git}, ${tree}, ${baseUrl}")
|
||||
|
||||
val repository = git.getRepository
|
||||
getContentFromPath(git, tree, ".gitmodules", fetchLargeFile = true).map { bytes =>
|
||||
(try {
|
||||
@@ -1075,6 +1146,8 @@ object JGitUtil {
|
||||
* @return the byte array of content or None if object does not exist
|
||||
*/
|
||||
def getContentFromPath(git: Git, revTree: RevTree, path: String, fetchLargeFile: Boolean): Option[Array[Byte]] = {
|
||||
logger.debug(s"getContentFromPath(${git}, ${revTree}, ${path}, ${fetchLargeFile})")
|
||||
|
||||
@scala.annotation.tailrec
|
||||
def getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] =
|
||||
walk.next match {
|
||||
@@ -1108,6 +1181,8 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
def getContentSize(loader: ObjectLoader): Long = {
|
||||
logger.debug(s"getContentSize(${loader})")
|
||||
|
||||
if (loader.isLarge) {
|
||||
loader.getSize
|
||||
} else {
|
||||
@@ -1123,10 +1198,14 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
def isLfsPointer(loader: ObjectLoader): Boolean = {
|
||||
logger.debug(s"isLfsPointer(${loader})")
|
||||
|
||||
!loader.isLarge && new String(loader.getBytes(), "UTF-8").startsWith("version https://git-lfs.github.com/spec/v1")
|
||||
}
|
||||
|
||||
def getContentInfo(git: Git, path: String, objectId: ObjectId, safeMode: Boolean): ContentInfo = {
|
||||
logger.debug(s"getContentInfo(${git}, ${path}, ${objectId}, ${safeMode})")
|
||||
|
||||
// Viewer
|
||||
Using.resource(git.getRepository.getObjectDatabase) { db =>
|
||||
val loader = db.open(objectId)
|
||||
@@ -1143,7 +1222,8 @@ object JGitUtil {
|
||||
"text",
|
||||
size,
|
||||
Some(StringUtil.convertFromByteArray(bytes.get)),
|
||||
Some(StringUtil.detectEncoding(bytes.get))
|
||||
Some(StringUtil.detectEncoding(bytes.get)),
|
||||
StringUtil.hasUtf8Bom(bytes.get)
|
||||
)
|
||||
} else {
|
||||
// binary
|
||||
@@ -1211,6 +1291,8 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
def processTree[T](git: Git, id: ObjectId)(f: (String, CanonicalTreeParser) => T): Seq[T] = {
|
||||
logger.debug(s"processTree(${git}, ${id})")
|
||||
|
||||
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
|
||||
Using.resource(new TreeWalk(git.getRepository)) { treeWalk =>
|
||||
val index = treeWalk.addTree(revWalk.parseTree(id))
|
||||
@@ -1237,6 +1319,10 @@ object JGitUtil {
|
||||
requestRepositoryName: String,
|
||||
requestBranch: String
|
||||
): String = {
|
||||
logger.debug(
|
||||
s"getForkedCommitId(${oldGit}, ${newGit}, ${userName}, ${repositoryName}, ${branch}, ${requestUserName}, ${requestRepositoryName}, ${requestBranch})"
|
||||
)
|
||||
|
||||
val existIds = getAllCommitIds(oldGit)
|
||||
getCommitLogs(newGit, requestBranch, includesLastCommit = true) { commit =>
|
||||
existIds.contains(commit.name) && getBranchesOfCommit(oldGit, commit.getName).contains(branch)
|
||||
@@ -1288,10 +1374,14 @@ object JGitUtil {
|
||||
* @return the last modified commit of specified path
|
||||
*/
|
||||
def getLastModifiedCommit(git: Git, startCommit: RevCommit, path: String): RevCommit = {
|
||||
logger.debug(s"getLastModifiedCommit(${git}, ${startCommit}, ${path})")
|
||||
|
||||
git.log.add(startCommit).addPath(path).setMaxCount(1).call.iterator.next
|
||||
}
|
||||
|
||||
def getBranchesNoMergeInfo(git: Git): Seq[BranchInfoSimple] = {
|
||||
logger.debug(s"getBranchesNoMergeInfo(${git})")
|
||||
|
||||
val repo = git.getRepository
|
||||
|
||||
git.branchList.call.asScala.map { ref =>
|
||||
@@ -1310,6 +1400,8 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
def getBranches(git: Git, defaultBranch: String, origin: Boolean): Seq[BranchInfo] = {
|
||||
logger.debug(s"getBranches(${git}, ${defaultBranch}, ${origin})")
|
||||
|
||||
val repo = git.getRepository
|
||||
val defaultObject = repo.resolve(defaultBranch)
|
||||
|
||||
@@ -1348,6 +1440,8 @@ object JGitUtil {
|
||||
}
|
||||
|
||||
def getBlame(git: Git, id: String, path: String): Iterable[BlameInfo] = {
|
||||
logger.debug(s"getBlame(${git}, ${id}, ${path})")
|
||||
|
||||
Option(git.getRepository.resolve(id))
|
||||
.map { commitId =>
|
||||
val blamer = new org.eclipse.jgit.api.BlameCommand(git.getRepository)
|
||||
@@ -1392,6 +1486,8 @@ object JGitUtil {
|
||||
* @return sha1
|
||||
*/
|
||||
def getShaByRef(owner: String, name: String, revstr: String): Option[String] = {
|
||||
logger.debug(s"getShaByRef(${owner}, ${name}, ${revstr})")
|
||||
|
||||
Using.resource(Git.open(getRepositoryDir(owner, name))) { git =>
|
||||
Option(git.getRepository.resolve(revstr)).map(ObjectId.toString)
|
||||
}
|
||||
@@ -1400,6 +1496,8 @@ object JGitUtil {
|
||||
private def openFile[T](git: Git, repository: RepositoryService.RepositoryInfo, treeWalk: TreeWalk)(
|
||||
f: InputStream => T
|
||||
): T = {
|
||||
logger.debug(s"openFile(${git}, ${repository}, ${treeWalk})")
|
||||
|
||||
val attrs = treeWalk.getAttributes
|
||||
val loader = git.getRepository.open(treeWalk.getObjectId(0))
|
||||
if (attrs.containsKey("filter") && attrs.get("filter").getValue == "lfs") {
|
||||
@@ -1423,12 +1521,16 @@ object JGitUtil {
|
||||
def openFile[T](git: Git, repository: RepositoryService.RepositoryInfo, tree: RevTree, path: String)(
|
||||
f: InputStream => T
|
||||
): T = {
|
||||
logger.debug(s"openFile(${git}, ${repository}, ${tree}, ${path})")
|
||||
|
||||
Using.resource(TreeWalk.forPath(git.getRepository, path, tree)) { treeWalk =>
|
||||
openFile(git, repository, treeWalk)(f)
|
||||
}
|
||||
}
|
||||
|
||||
private def getLfsAttributes(loader: ObjectLoader): Map[String, String] = {
|
||||
logger.debug(s"getLfsAttributes(${loader})")
|
||||
|
||||
val bytes = loader.getCachedBytes
|
||||
val text = new String(bytes, "UTF-8")
|
||||
|
||||
|
||||
@@ -91,7 +91,10 @@ object StringUtil {
|
||||
* And if given bytes contains UTF-8 BOM, it's removed from returned string.
|
||||
*/
|
||||
def convertFromByteArray(content: Array[Byte]): String =
|
||||
IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content))
|
||||
IOUtils.toString(
|
||||
BOMInputStream.builder().setInputStream(new java.io.ByteArrayInputStream(content)).setInclude(true).get(),
|
||||
detectEncoding(content)
|
||||
)
|
||||
|
||||
def detectEncoding(content: Array[Byte]): String = {
|
||||
val detector = new UniversalDetector(null)
|
||||
@@ -103,6 +106,19 @@ object StringUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if the given byte array starts with UTF-8 BOM (Byte Order Mark).
|
||||
* UTF-8 BOM is the byte sequence: 0xEF 0xBB 0xBF
|
||||
*/
|
||||
def hasUtf8Bom(content: Array[Byte]): Boolean =
|
||||
content.length >= 3 &&
|
||||
(content(0) & 0xff) == 0xef &&
|
||||
(content(1) & 0xff) == 0xbb &&
|
||||
(content(2) & 0xff) == 0xbf
|
||||
|
||||
/** UTF-8 BOM byte sequence */
|
||||
val Utf8Bom: Array[Byte] = Array(0xef.toByte, 0xbb.toByte, 0xbf.toByte)
|
||||
|
||||
/**
|
||||
* Converts line separator in the given content.
|
||||
*
|
||||
|
||||
@@ -47,18 +47,28 @@ trait AvatarImageProvider { self: RequestCache =>
|
||||
}
|
||||
}
|
||||
|
||||
val displayName = if (!context.settings.showFullName) {
|
||||
s"@$userName"
|
||||
} else {
|
||||
if (mailAddress.isEmpty) {
|
||||
getAccountByUserNameFromCache(userName).map(_.fullName).getOrElse(s"@$userName")
|
||||
} else {
|
||||
getAccountByMailAddressFromCache(mailAddress).map(_.fullName).getOrElse(s"@$userName")
|
||||
}
|
||||
}
|
||||
|
||||
if (tooltip) {
|
||||
Html(
|
||||
s"""<img src="${src}" class="${if (size > 20) { "avatar" }
|
||||
else { "avatar-mini" }}" style="width: ${size}px; height: ${size}px;"
|
||||
| alt="@${StringUtil.escapeHtml(userName)}"
|
||||
| data-toggle="tooltip" title="${StringUtil.escapeHtml(userName)}" />""".stripMargin
|
||||
| alt="${StringUtil.escapeHtml(displayName)}"
|
||||
| data-toggle="tooltip" title="${StringUtil.escapeHtml(displayName)}" />""".stripMargin
|
||||
)
|
||||
} else {
|
||||
Html(
|
||||
s"""<img src="${src}" class="${if (size > 20) { "avatar" }
|
||||
else { "avatar-mini" }}" style="width: ${size}px; height: ${size}px;"
|
||||
| alt="@${StringUtil.escapeHtml(userName)}" />""".stripMargin
|
||||
| alt="${StringUtil.escapeHtml(displayName)}" />""".stripMargin
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import gitbucket.core.service.{RepositoryService, RequestCache}
|
||||
import gitbucket.core.util.StringUtil
|
||||
import io.github.gitbucket.markedj._
|
||||
import io.github.gitbucket.markedj.Utils._
|
||||
import gitbucket.core.service.WikiService
|
||||
|
||||
object Markdown {
|
||||
|
||||
@@ -75,7 +76,8 @@ object Markdown {
|
||||
)(implicit val context: Context)
|
||||
extends Renderer(options)
|
||||
with LinkConverter
|
||||
with RequestCache {
|
||||
with RequestCache
|
||||
with WikiService {
|
||||
|
||||
override def heading(text: String, level: Int, raw: String): String = {
|
||||
val id = generateAnchorName(text)
|
||||
@@ -192,7 +194,16 @@ object Markdown {
|
||||
.stripSuffix(".git") + "/blob/" + branch + "/" + urlWithRawParam
|
||||
}
|
||||
} else {
|
||||
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url
|
||||
// URL is being modified to link to the image file on the repository, but users may want to link to the page if the page name is a link.
|
||||
// If the wiki page cannot be retrieved from the url, the blob address is returned; otherwise, the page address is returned.
|
||||
val pathElems = context.currentPath.split("/")
|
||||
val owner = pathElems(1)
|
||||
val repos = pathElems(2)
|
||||
if (getWikiPage(owner, repos, url, getWikiBranch(owner, repos)) == None) {
|
||||
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url
|
||||
} else {
|
||||
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,8 +53,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
def datetimeAgoRecentOnly(date: Date): String = {
|
||||
val duration = new Date().getTime - date.getTime
|
||||
timeUnits.find(tuple => duration / tuple._1 > 0) match {
|
||||
case Some((_, "month")) => s"on ${new SimpleDateFormat("d MMM", Locale.ENGLISH).format(date)}"
|
||||
case Some((_, "year")) => s"on ${new SimpleDateFormat("d MMM yyyy", Locale.ENGLISH).format(date)}"
|
||||
case Some((_, "month")) => s"on ${new SimpleDateFormat("d MMM", Locale.ENGLISH).format(date)}"
|
||||
case Some((_, "year")) => s"on ${new SimpleDateFormat("d MMM yyyy", Locale.ENGLISH).format(date)}"
|
||||
case Some((unitValue, unitString)) =>
|
||||
val value = duration / unitValue
|
||||
s"${value} ${unitString}${if (value > 1) "s" else ""} ago"
|
||||
@@ -101,6 +101,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
/**
|
||||
* Converts Markdown of Wiki pages to HTML.
|
||||
*/
|
||||
@deprecated("This doesn't apply render plugins. Should use renderMarkup() instead.", "4.45.0")
|
||||
def markdown(
|
||||
markdown: String,
|
||||
repository: RepositoryService.RepositoryInfo,
|
||||
@@ -139,14 +140,29 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
repository: RepositoryService.RepositoryInfo,
|
||||
enableWikiLink: Boolean,
|
||||
enableRefsLink: Boolean,
|
||||
enableAnchor: Boolean
|
||||
enableAnchor: Boolean,
|
||||
enableLineBreaks: Boolean,
|
||||
enableTaskList: Boolean,
|
||||
hasWritePermission: Boolean = false
|
||||
)(implicit context: Context): Html = {
|
||||
|
||||
val fileName = filePath.last.toLowerCase
|
||||
val extension = FileUtil.getExtension(fileName)
|
||||
val renderer = PluginRegistry().getRenderer(extension)
|
||||
renderer.render(
|
||||
RenderRequest(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, enableAnchor, context)
|
||||
RenderRequest(
|
||||
filePath,
|
||||
fileContent,
|
||||
branch,
|
||||
repository,
|
||||
enableWikiLink,
|
||||
enableRefsLink,
|
||||
enableAnchor,
|
||||
enableLineBreaks,
|
||||
enableTaskList,
|
||||
hasWritePermission,
|
||||
context
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -316,12 +332,30 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
*/
|
||||
def assets(path: String)(implicit context: Context): String = s"${context.path}/assets${path}?${hashQuery}"
|
||||
|
||||
def displayUserName(userName: String, mailAddress: String = "")(implicit context: Context): String = {
|
||||
val displayName = if (!context.settings.showFullName) {
|
||||
userName
|
||||
} else {
|
||||
if (mailAddress.isEmpty) {
|
||||
getAccountByUserNameFromCache(userName).map(_.fullName).getOrElse(userName)
|
||||
} else {
|
||||
getAccountByMailAddressFromCache(mailAddress).map(_.fullName).getOrElse(userName)
|
||||
}
|
||||
}
|
||||
if (userName == displayName) {
|
||||
userName
|
||||
} else {
|
||||
s"$userName ($displayName)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates the text link to the account page.
|
||||
* If user does not exist or disabled, this method returns user name as text without link.
|
||||
*/
|
||||
def user(userName: String, mailAddress: String = "", styleClass: String = "")(implicit context: Context): Html =
|
||||
userWithContent(userName, mailAddress, styleClass)(Html(StringUtil.escapeHtml(userName)))
|
||||
userWithContent(userName, mailAddress, styleClass)(Html(StringUtil.escapeHtml(displayUserName(userName, mailAddress))))
|
||||
|
||||
/**
|
||||
* Generates the avatar link to the account page.
|
||||
@@ -511,6 +545,18 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
Html(sb.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to enable checkboxes
|
||||
*/
|
||||
def enableCheckbox(html: Html, enable: Boolean): Html = {
|
||||
if (enable) {
|
||||
val re = "(<input\\s+[^<>]*type=\"checkbox\"\\s+[^<>]*)\\s+disabled[^<>]*>".r
|
||||
Html(re.replaceAllIn(html.toString(), "$1>"))
|
||||
} else {
|
||||
html
|
||||
}
|
||||
}
|
||||
|
||||
case class CommentDiffLine(newLine: Option[String], oldLine: Option[String], `type`: String, text: String)
|
||||
|
||||
def appendQueryString(baseUrl: String, queryString: String): String = {
|
||||
|
||||
@@ -56,16 +56,16 @@ $(function(){
|
||||
|
||||
$('#addMember').click(function(){
|
||||
$('#error-members').text('');
|
||||
var userName = $('#memberName').val();
|
||||
const userName = $('#memberName').val();
|
||||
|
||||
// check empty
|
||||
if($.trim(userName) == ''){
|
||||
if($.trim(userName) === ''){
|
||||
return false;
|
||||
}
|
||||
|
||||
// check duplication
|
||||
var exists = $('#member-list li').filter(function(){
|
||||
return $(this).data('name') == userName;
|
||||
const exists = $('#member-list li').filter(function(){
|
||||
return $(this).data('name') === userName;
|
||||
}).length > 0;
|
||||
if(exists){
|
||||
$('#error-members').text('User has been already added.');
|
||||
@@ -75,7 +75,7 @@ $(function(){
|
||||
// check existence
|
||||
$.post('@context.path/_user/existence', { 'userName': userName },
|
||||
function(data, status){
|
||||
if(data == 'user'){
|
||||
if(data === 'user'){
|
||||
addMemberHTML(userName, false);
|
||||
$('#memberName').val('');
|
||||
} else {
|
||||
@@ -90,7 +90,7 @@ $(function(){
|
||||
|
||||
// Don't submit form by ENTER key
|
||||
$('#memberName').keypress(function(e){
|
||||
return !(e.keyCode == 13);
|
||||
return !(e.keyCode === 13);
|
||||
});
|
||||
|
||||
$('#delete').click(function(){
|
||||
@@ -102,11 +102,11 @@ $(function(){
|
||||
}
|
||||
|
||||
function addMemberHTML(userName, isManager){
|
||||
var memberButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="false" name="' + userName + '">Member</label>');
|
||||
const memberButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="false" name="' + userName + '">Member</label>');
|
||||
if(!isManager){
|
||||
memberButton.addClass('active');
|
||||
}
|
||||
var managerButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="true" name="' + userName + '">Manager</label>');
|
||||
const managerButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="true" name="' + userName + '">Manager</label>');
|
||||
if(isManager){
|
||||
managerButton.addClass('active');
|
||||
}
|
||||
@@ -123,8 +123,8 @@ $(function(){
|
||||
}
|
||||
|
||||
function updateMembers(){
|
||||
var members = $('#member-list li').map(function(i, e){
|
||||
var userName = $(e).data('name');
|
||||
const members = $('#member-list li').map(function(i, e){
|
||||
const userName = $(e).data('name');
|
||||
return userName + ':' + $(e).find('label.active input[type=radio]').attr('value');
|
||||
}).get().join(',');
|
||||
$('#members').val(members);
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</li>
|
||||
@gitbucket.core.plugin.PluginRegistry().getSystemSettingMenus.map { menu =>
|
||||
@menu(context).map { link =>
|
||||
<li@if(active==link.id){ class="active"}>
|
||||
<li class="menu-item-hover @if(active==link.id){active}">
|
||||
<a href="@context.path/@link.path">
|
||||
<i class="menu-icon octicon octicon-@link.icon.getOrElse("plug")"></i>
|
||||
<span>@link.label</span>
|
||||
|
||||
@@ -240,6 +240,21 @@
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Compare default mode -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">Pull request compare default mode</label>
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="basicBehavior.compareNoCheckByDefault" value="true"@if(context.settings.basicBehavior.compareNoCheckByDefault){ checked}>
|
||||
<span class="strong">No check (fast)</span> <span class="normal">- Open compare quickly and run checks only when clicking Check mergeability.</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="basicBehavior.compareNoCheckByDefault" value="false"@if(!context.settings.basicBehavior.compareNoCheckByDefault){ checked}>
|
||||
<span class="strong">Check by default</span> <span class="normal">- Load full compare and mergeability behavior by default.</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Show mail address -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
@@ -253,6 +268,21 @@
|
||||
<input type="radio" name="showMailAddress" value="false"@if(!context.settings.showMailAddress){ checked}>
|
||||
<span class="strong">Hide</span> <span class="normal">- Hide mail address in user's profile page.</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Show full name -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">Username display on UI</label>
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="showFullName" value="false"@if(!context.settings.showFullName){ checked}>
|
||||
<span class="strong">User name</span> <span class="normal">- Username is primarily displayed on UI.</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="showFullName" value="true"@if(context.settings.showFullName){ checked}>
|
||||
<span class="strong">Full name</span> <span class="normal">- Fullname is primarily displayed instead of username on UI.</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- File upload -->
|
||||
|
||||
@@ -25,20 +25,23 @@
|
||||
<a href="@context.path/admin/users/@account.userName/_edituser">Edit</a>
|
||||
}
|
||||
</div>
|
||||
<div class="strong">
|
||||
@helpers.avatarLink(account.userName, 20)
|
||||
@if(account.isRemoved){
|
||||
@account.userName
|
||||
} else {
|
||||
<a href="@helpers.url(account.userName)">@account.userName</a>
|
||||
}
|
||||
@if(account.isGroupAccount){
|
||||
(Group)
|
||||
} else {
|
||||
@if(account.isAdmin){
|
||||
(Administrator)
|
||||
<div>
|
||||
<span class="strong">
|
||||
@helpers.avatarLink(account.userName, 20)
|
||||
@if(account.isRemoved){
|
||||
@account.userName
|
||||
} else {
|
||||
(Normal)
|
||||
<a href="@helpers.url(account.userName)">@account.userName</a>
|
||||
}
|
||||
</span>
|
||||
@if(account.isGroupAccount){
|
||||
- Group
|
||||
} else {
|
||||
(@account.fullName)
|
||||
@if(account.isAdmin){
|
||||
- Administrator
|
||||
} else {
|
||||
- Normal
|
||||
}
|
||||
}
|
||||
@if(account.isGroupAccount){
|
||||
@@ -50,10 +53,10 @@
|
||||
<div>
|
||||
<hr>
|
||||
@if(!account.isGroupAccount){
|
||||
<i class="octicon octicon-mail"></i> @account.mailAddress
|
||||
<i class="octicon octicon-mail"></i>@account.mailAddress
|
||||
}
|
||||
@account.url.map { url =>
|
||||
<i class="octicon octicon-home"></i> @url
|
||||
<i class="octicon octicon-home"></i>@url
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
|
||||
93
src/main/twirl/gitbucket/core/helper/acenavbar.scala.html
Normal file
93
src/main/twirl/gitbucket/core/helper/acenavbar.scala.html
Normal file
@@ -0,0 +1,93 @@
|
||||
@()(implicit context: gitbucket.core.controller.Context)
|
||||
<nav class="navbar navbar-default" style="margin-bottom: 0px; min-height: auto; border-radius: 0px;">
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li>
|
||||
<div class="btn-group" data-toggle="buttons-radio">
|
||||
<input type="button" id="btn-code" class="btn btn-default btn-sm active navbar-btn" value="Code">
|
||||
<input type="button" id="btn-preview" class="btn btn-default btn-sm navbar-btn" value="Preview">
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav" style="margin-left: 8px;">
|
||||
<li>
|
||||
<div class="btn-group border" id="markdown-toolbar" style="display: none;">
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-bold" title="Bold"><i class="fa fa-bold"></i></button>
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-italic" title="Italic"><i class="fa fa-italic"></i></button>
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-code" title="Code"><i class="fa fa-code"></i></button>
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-link" title="Link"><i class="fa fa-link"></i></button>
|
||||
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-heading" title="Heading" style="margin-left: 4px;"><i class="fa fa-header"></i></button>
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-quote" title="Quote"><i class="fa fa-quote-right"></i></button>
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-list-ul" title="Unordered List"><i class="fa fa-list-ul"></i></button>
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-list-ol" title="Ordered List"><i class="fa fa-list-ol"></i></button>
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-task" title="Task List"><i class="fa fa-check-square-o"></i></button>
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-code-block" title="Code Block"><i class="fa fa-file-code-o"></i></button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li>
|
||||
<select id="aceKeyboardSelect" class="form-control navbar-form input-sm" aria-label="Keyboard" style="margin-right: 4px;">
|
||||
<optgroup label="Keyboard">
|
||||
<option value="">Default</option>
|
||||
<option value="ace/keyboard/emacs">Emacs</option>
|
||||
<option value="ace/keyboard/sublime">Sublime</option>
|
||||
<option value="ace/keyboard/vim">Vim</option>
|
||||
<option value="ace/keyboard/vscode">VSCode</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</li>
|
||||
<li>
|
||||
<select id="theme" class="form-control navbar-form input-sm" aria-label="Theme" style="margin-right: 4px;">
|
||||
<optgroup label="Editor Theme">
|
||||
<option value="ambiance">Ambiance</option>
|
||||
<option value="chaos">Chaos</option>
|
||||
<option value="chrome">Chrome</option>
|
||||
<option value="clouds">Clouds</option>
|
||||
<option value="clouds_midnight">Clouds Midnight</option>
|
||||
<option value="cobalt">Cobalt</option>
|
||||
<option value="crimson_editor">Crimson</option>
|
||||
<option value="dawn">Dawn</option>
|
||||
<option value="dracula">Dracula</option>
|
||||
<option value="dreamweaver">Dreamweaver</option>
|
||||
<option value="eclipse">Eclipse</option>
|
||||
<option value="github">GitHub</option>
|
||||
<option value="gob">Gob</option>
|
||||
<option value="gruvbox">Gruvbox</option>
|
||||
<option value="idle_fingers">Idle Fingers</option>
|
||||
<option value="iplastic">Iplastic</option>
|
||||
<option value="katzenmilch">Katzenmilch</option>
|
||||
<option value="kr_theme">Kr</option>
|
||||
<option value="kuroir">Kuroir</option>
|
||||
<option value="merbivore">Merbivore</option>
|
||||
<option value="mono_industrial">Mono Industrial</option>
|
||||
<option selected value="monokai">Monokai</option>
|
||||
<option value="nord_dark">Nord Dark</option>
|
||||
<option value="pastel_on_dark">Pastel on Dark</option>
|
||||
<option value="solarized_dark">Solarized Dark</option>
|
||||
<option value="solarized_light">Solarized Light</option>
|
||||
<option value="sqlserver">Sqlserver</option>
|
||||
<option value="terminal">Terminal</option>
|
||||
<option value="textmate">Textmate</option>
|
||||
<option value="tomorrow">Tomorrow</option>
|
||||
<option value="tomorrow_night">Tomorrow Night</option>
|
||||
<option value="tomorrow_night_bright">Tomorrow Night Bright</option>
|
||||
<option value="tomorrow_night_eighties">Tomorrow Night Eighties</option>
|
||||
<option value="twilight">Twilight</option>
|
||||
<option value="vibrant_ink">Vibrant Ink</option>
|
||||
<option value="xcode">Xcode</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</li>
|
||||
<li>
|
||||
<select id="wrap" class="form-control navbar-form input-sm" aria-label="Wrap" style="margin-right: 15px;">
|
||||
<optgroup label="Line Wrap Mode">
|
||||
<option value="false">No wrap</option>
|
||||
<option value="true">Soft wrap</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -67,7 +67,7 @@
|
||||
});
|
||||
|
||||
function updateBranchControlList(active) {
|
||||
if (active == 'branches') {
|
||||
if (active === 'branches') {
|
||||
$('li#branch-control-tab-branches').addClass('active');
|
||||
$('li#branch-control-tab-tags').removeClass('active');
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
} else {
|
||||
$('#branch-control-input').attr('placeholder', 'Find branch ...');
|
||||
}
|
||||
} else if (active == 'tags') {
|
||||
} else if (active === 'tags') {
|
||||
$('li#branch-control-tab-branches').removeClass('active');
|
||||
$('li#branch-control-tab-tags').addClass('active');
|
||||
|
||||
|
||||
@@ -21,16 +21,18 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="commit-commentContent-@comment.commentId">
|
||||
@helpers.markdown(
|
||||
markdown = comment.content,
|
||||
repository = repository,
|
||||
branch = repository.repository.defaultBranch,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = hasWritePermission
|
||||
)
|
||||
@helpers.renderMarkup(
|
||||
filePath = List("temporary.md"),
|
||||
fileContent = comment.content,
|
||||
branch = repository.repository.defaultBranch,
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableAnchor = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = hasWritePermission
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
57
src/main/twirl/gitbucket/core/helper/completion.scala.html
Normal file
57
src/main/twirl/gitbucket/core/helper/completion.scala.html
Normal file
@@ -0,0 +1,57 @@
|
||||
@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
completionContext: String, generateScript: Boolean = true)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
<style>
|
||||
@gitbucket.core.plugin.PluginRegistry().getSuggestionProviders.map { provider =>
|
||||
@if(provider.context.contains(completionContext)){
|
||||
@if(provider.id == "emoji") {
|
||||
@provider.options(repository).map { case (value, label) =>
|
||||
@Html(s"""
|
||||
.ace_${label.replaceAll("[^a-z0-9\\-_]", "_")}::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
background-image: url("${context.path}/plugin-assets/emoji/${label}.png");
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-size: contain;
|
||||
vertical-align: middle;
|
||||
}"""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var mdCompleters = [];
|
||||
|
||||
@gitbucket.core.plugin.PluginRegistry().getSuggestionProviders.map { provider =>
|
||||
@if(provider.context.contains(completionContext)){
|
||||
var @provider.id = @Html(helpers.json(provider.options(repository).map { case (value, label) =>
|
||||
Map("value" -> (s"${provider.prefix}${value}${provider.suffix}"), "label" -> label)
|
||||
}));
|
||||
|
||||
var @{provider.id}Completer = {
|
||||
triggerCharacters: [ `@provider.prefix.charAt(0)` ],
|
||||
getCompletions: function (editor, session, pos, prefix, callback) {
|
||||
if (session.$mode && session.$mode.$id === 'ace/mode/markdown') {
|
||||
callback(null, @{provider.id}.map(function (table) {
|
||||
var token = session.getTokenAt(pos.row, pos.column).value.slice(-1);
|
||||
return {
|
||||
caption: table.description,
|
||||
value: token == "@provider.prefix.charAt(0)" ? table.value.replace(token, "") : table.value,
|
||||
className: "@{provider.id}" == "emoji" ? table.label.replace(/[^a-z0-9\-_]/g, '_') : "@{provider.id}",
|
||||
meta: "@{provider.id}"
|
||||
};
|
||||
}));
|
||||
}
|
||||
},
|
||||
id: "@{provider.id}Completer"
|
||||
};
|
||||
|
||||
mdCompleters.push(@{provider.id}Completer);
|
||||
|
||||
@Html(provider.additionalScript(repository))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -16,9 +16,24 @@
|
||||
uid: Long = new java.util.Date().getTime())(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
<div class="tabbable">
|
||||
<ul class="nav nav-tabs fill-width" style="margin-bottom: 10px;">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active"><a href="#tab@uid" data-toggle="tab" id="write@uid">Write</a></li>
|
||||
<li><a href="#tab@(uid + 1)" data-toggle="tab" id="preview@uid">Preview</a></li>
|
||||
<li class="nav navbar-nav navbar-default" id="markdown-toolbar@uid" style="background-color: inherit;">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-bold@uid" title="Bold" style="margin-top: 8px; margin-bottom: 8px;"><i class="fa fa-bold"></i></button>
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-italic@uid" title="Italic" style="margin-top: 8px; margin-bottom: 8px;"><i class="fa fa-italic"></i></button>
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-code@uid" title="Code" style="margin-top: 8px; margin-bottom: 8px;"><i class="fa fa-code"></i></button>
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-link@uid" title="Link" style="margin-top: 8px; margin-bottom: 8px;"><i class="fa fa-link"></i></button>
|
||||
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-heading@uid" title="Heading" style="margin-left: 4px; margin-top: 8px; margin-bottom: 8px;"><i class="fa fa-header"></i></button>
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-quote@uid" title="Quote" style="margin-top: 8px; margin-bottom: 8px;"><i class="fa fa-quote-right"></i></button>
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-list-ul@uid" title="Unordered List" style="margin-top: 8px; margin-bottom: 8px;"><i class="fa fa-list-ul"></i></button>
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-list-ol@uid" title="Ordered List" style="margin-top: 8px; margin-bottom: 8px;"><i class="fa fa-list-ol"></i></button>
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-task@uid" title="Task List" style="margin-top: 8px; margin-bottom: 8px;"><i class="fa fa-check-square-o"></i></button>
|
||||
<button class="btn btn-default navbar-btn btn-sm" id="markdown-code-block@uid" title="Code Block" style="margin-top: 8px; margin-bottom: 8px;"><i class="fa fa-file-code-o"></i></button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" style="margin-top: 4px;" id="tab@uid">
|
||||
@@ -42,6 +57,78 @@
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
// load textarea.js
|
||||
var script = document.getElementById("textareajs");
|
||||
if (script == null) {
|
||||
script = document.createElement("script");
|
||||
script.id = "textareajs";
|
||||
script.src = '@helpers.assets("/common/js/textarea.js")';
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
|
||||
// target textarea
|
||||
const textarea = document.getElementById("content@uid");
|
||||
|
||||
// for capture markdown bold button clicks
|
||||
const markedownBold = document.getElementById("markdown-bold@uid");
|
||||
markedownBold.addEventListener('click', (e) => {
|
||||
surroundSelection(textarea, "**", "**", false);
|
||||
}, true);
|
||||
|
||||
// for capture markdown italic button clicks
|
||||
const markdownItalic = document.getElementById("markdown-italic@uid");
|
||||
markdownItalic.addEventListener('click', (e) => {
|
||||
surroundSelection(textarea, "*", "*", false);
|
||||
}, true);
|
||||
|
||||
// for capture markdown code button clicks
|
||||
const markdownCode = document.getElementById("markdown-code@uid");
|
||||
markdownCode.addEventListener('click', (e) => {
|
||||
surroundSelection(textarea, "`", "`", false);
|
||||
}, true);
|
||||
|
||||
// for capture markdown link button clicks
|
||||
const markdownLink = document.getElementById("markdown-link@uid");
|
||||
markdownLink.addEventListener('click', (e) => {
|
||||
surroundSelection(textarea, "[", "]()", true);
|
||||
}, true);
|
||||
|
||||
// for capture markdown heading button clicks
|
||||
const markdownHeading = document.getElementById("markdown-heading@uid");
|
||||
markdownHeading.addEventListener('click', (e) => {
|
||||
setHeadding(textarea);
|
||||
}, true);
|
||||
|
||||
// for capture markdown quote button clicks
|
||||
const markdownQuote =document.getElementById("markdown-quote@uid");
|
||||
markdownQuote.addEventListener('click', (e) => {
|
||||
setQuote(textarea);
|
||||
}, true);
|
||||
|
||||
// for capture markdown list ul button clicks
|
||||
const markdownListUl =document.getElementById("markdown-list-ul@uid");
|
||||
markdownListUl.addEventListener('click', (e) => {
|
||||
setUnorderedList(textarea);
|
||||
}, true);
|
||||
|
||||
// for capture markdown list ul button clicks
|
||||
const markdownListOl =document.getElementById("markdown-list-ol@uid");
|
||||
markdownListOl.addEventListener('click', (e) => {
|
||||
setOrderedList(textarea);
|
||||
}, true);
|
||||
|
||||
// for capture markdown list ul button clicks
|
||||
const markdownTask =document.getElementById("markdown-task@uid");
|
||||
markdownTask.addEventListener('click', (e) => {
|
||||
setTaskList(textarea);
|
||||
}, true);
|
||||
|
||||
// for capture markdown list ul button clicks
|
||||
const markdownCodeBlock =document.getElementById("markdown-code-block@uid");
|
||||
markdownCodeBlock.addEventListener('click', (e) => {
|
||||
setCodeBlock(textarea);
|
||||
}, true);
|
||||
|
||||
@if(elastic){
|
||||
$('#content@uid').elastic();
|
||||
$('#content@uid').trigger('blur');
|
||||
@@ -52,11 +139,13 @@ $(function(){
|
||||
|
||||
$('#write@uid').on('shown.bs.tab', function(){
|
||||
$('#content@uid').trigger('focus');
|
||||
$('#markdown-toolbar@uid').show()
|
||||
});
|
||||
|
||||
$('#preview@uid').click(function(){
|
||||
$('#preview-area@uid').html('<img src="@helpers.assets("/common/images/indicator.gif")"> Previewing...');
|
||||
$.post('@helpers.url(repository)/_preview', {
|
||||
filename : "temporary.md",
|
||||
content : $('#content@uid').val(),
|
||||
enableWikiLink : @enableWikiLink,
|
||||
enableRefsLink : @enableRefsLink,
|
||||
@@ -67,6 +156,7 @@ $(function(){
|
||||
$('#preview-area@uid input').prop('disabled', true);
|
||||
prettyPrint();
|
||||
});
|
||||
$('#markdown-toolbar@uid').hide();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</a>
|
||||
</span>
|
||||
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer"
|
||||
&& (isManageable || context.loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
|
||||
&& (isManageable || context.loginAccount.exists(_.userName == comment.commentedUserName))){
|
||||
<span class="pull-right">
|
||||
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-pencil" aria-label="Edit"></i></a>
|
||||
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-x" aria-label="Remove"></i></a>
|
||||
@@ -32,12 +32,14 @@
|
||||
}
|
||||
</div>
|
||||
<div class="panel-body markdown-body" id="commentContent-@comment.commentId">
|
||||
@helpers.markdown(
|
||||
markdown = comment.content,
|
||||
repository = repository,
|
||||
@helpers.renderMarkup(
|
||||
filePath = List("temporary.md"),
|
||||
fileContent = comment.content,
|
||||
branch = repository.repository.defaultBranch,
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableAnchor = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = isManageable
|
||||
@@ -52,18 +54,20 @@
|
||||
@helpers.user(issue.get.openedUserName, styleClass="username strong")
|
||||
<span class="muted">commented @gitbucket.core.helper.html.datetimeago(issue.get.registeredDate)</span>
|
||||
<span class="pull-right">
|
||||
@if(isManageable || context.loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){
|
||||
@if(isManageable || context.loginAccount.exists(_.userName == issue.get.openedUserName)){
|
||||
<a href="#" data-issue-id="@issue.get.issueId"><i class="octicon octicon-pencil" aria-label="Edit"></i></a>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="panel-body markdown-body" id="issueContent">
|
||||
@helpers.markdown(
|
||||
markdown = issue.get.content getOrElse "No description provided.",
|
||||
repository = repository,
|
||||
@helpers.renderMarkup(
|
||||
filePath = List("temporary.md"),
|
||||
fileContent = issue.get.content getOrElse "No description provided.",
|
||||
branch = repository.repository.defaultBranch,
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableAnchor = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = isManageable
|
||||
@@ -331,12 +335,14 @@
|
||||
}
|
||||
</div>
|
||||
<div class="panel-body markdown-body commit-commentContent-@comment.commentId">
|
||||
@helpers.markdown(
|
||||
markdown = comment.content,
|
||||
repository = repository,
|
||||
@helpers.renderMarkup(
|
||||
filePath = List("temporary.md"),
|
||||
fileContent = comment.content,
|
||||
branch = repository.repository.defaultBranch,
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableAnchor = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = isManageable
|
||||
@@ -400,7 +406,7 @@ $(function(){
|
||||
// diff view
|
||||
const tr = comment.closest('.not-diff');
|
||||
if(tr.length > 0){
|
||||
if(tr.prev('.not-diff').length == 0){
|
||||
if(tr.prev('.not-diff').length === 0){
|
||||
tr.next('.not-diff:has(.reply-comment)').remove();
|
||||
}
|
||||
tr.remove();
|
||||
@@ -410,7 +416,7 @@ $(function(){
|
||||
const panel = comment.closest('div.panel:has(.commit-comment-box)');
|
||||
if(panel.length > 0){
|
||||
comment.parent('.commit-comment-box').remove();
|
||||
if(panel.has('.commit-comment-box').length == 0){
|
||||
if(panel.has('.commit-comment-box').length === 0){
|
||||
panel.remove();
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
@gitbucket.core.html.menu("issues", repository){
|
||||
<div>
|
||||
<div class="show-title pull-right">
|
||||
@if(isManageable || context.loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
|
||||
@if(isManageable || context.loginAccount.exists(_.userName == issue.openedUserName)){
|
||||
<a class="btn btn-default" href="#" id="edit">Edit</a>
|
||||
}
|
||||
@if(isEditable){
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
@collaborators.map { collaborator =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator">
|
||||
@gitbucket.core.helper.html.checkicon(issueAssignees.exists(_.assigneeUserName == collaborator))@helpers.avatar(collaborator, 20) @collaborator
|
||||
@gitbucket.core.helper.html.checkicon(issueAssignees.exists(_.assigneeUserName == collaborator))@helpers.avatar(collaborator, 20) @helpers.displayUserName(collaborator)
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<a class="btn btn-success" href="@helpers.url(repository)/issues/new">New issue</a>
|
||||
}
|
||||
@if(target == "pulls"){
|
||||
<a class="btn btn-success" href="@helpers.url(repository)/compare">New pull request</a>
|
||||
<a class="btn btn-success" href="@helpers.url(repository)/compare@if(context.settings.basicBehavior.compareNoCheckByDefault){?quick=true}">New pull request</a>
|
||||
}
|
||||
}
|
||||
</form>
|
||||
|
||||
@@ -197,7 +197,7 @@
|
||||
@if(target == "issues"){
|
||||
<a href="@helpers.url(repository.get)/issues/new">Create a new issue.</a>
|
||||
} else {
|
||||
<a href="@helpers.url(repository.get)/compare">Create a new pull request.</a>
|
||||
<a href="@helpers.url(repository.get)/compare@if(context.settings.basicBehavior.compareNoCheckByDefault){?quick=true}">Create a new pull request.</a>
|
||||
}
|
||||
}
|
||||
@*
|
||||
|
||||
@@ -78,13 +78,16 @@
|
||||
</div>
|
||||
@milestone.description.map { description =>
|
||||
<div class="milestone-description markdown-body">
|
||||
@helpers.markdown(
|
||||
markdown = description,
|
||||
repository = repository,
|
||||
@helpers.renderMarkup(
|
||||
filePath = List("temporary.md"),
|
||||
fileContent = description,
|
||||
branch = repository.repository.defaultBranch,
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = false,
|
||||
enableLineBreaks = true
|
||||
enableAnchor = false,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = false
|
||||
)
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -64,13 +64,16 @@
|
||||
</div>
|
||||
@milestone.description.map { description =>
|
||||
<div class="milestone-description markdown-body">
|
||||
@helpers.markdown(
|
||||
markdown = description,
|
||||
repository = repository,
|
||||
@helpers.renderMarkup(
|
||||
filePath = List("temporary.md"),
|
||||
fileContent = description,
|
||||
branch = repository.repository.defaultBranch,
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = false,
|
||||
enableLineBreaks = true
|
||||
enableAnchor = false,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = false
|
||||
)
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
priorities: List[gitbucket.core.model.Priority],
|
||||
defaultPriority: Option[gitbucket.core.model.Priority],
|
||||
labels: List[gitbucket.core.model.Label],
|
||||
customFields: List[gitbucket.core.model.CustomField])(implicit context: gitbucket.core.controller.Context)
|
||||
customFields: List[gitbucket.core.model.CustomField],
|
||||
quickLoad: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main(s"Pull requests - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@gitbucket.core.html.menu("pulls", repository){
|
||||
<form method="POST" action="@context.path/@originRepository.owner/@originRepository.name/pulls/new" validate="true">
|
||||
<div class="pullreq-info">
|
||||
<div id="compare-edit">
|
||||
@gitbucket.core.helper.html.dropdown(originRepository.owner + "/" + originRepository.name, "base fork", filter=("origin_repo", "Find Repository...")) {
|
||||
@@ -54,14 +54,25 @@
|
||||
}
|
||||
<span class="error" id="error-requestBranch"></span>
|
||||
</div>
|
||||
@if(originRepository.branchList.contains(originId) && forkedRepository.branchList.contains(forkedId)){
|
||||
<div class="check-conflict" style="display: none;">
|
||||
<img src="@helpers.assets("/common/images/indicator.gif")"/> Checking...
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if(commits.nonEmpty && context.loginAccount.isDefined && originRepository.branchList.contains(originId) && forkedRepository.branchList.contains(forkedId)){
|
||||
<div id="pull-request-form" style="margin-bottom: 20px;">
|
||||
@if(originRepository.branchList.contains(originId) && forkedRepository.branchList.contains(forkedId)){
|
||||
<div style="margin-top: 8px;">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="check-conflict-button">Check mergeability</button>
|
||||
</div>
|
||||
<div class="check-conflict" style="display: none;">
|
||||
<img src="@helpers.assets("/common/images/indicator.gif")"/> Checking...
|
||||
</div>
|
||||
}
|
||||
<div id="compare-state"
|
||||
data-quick-load="@quickLoad"
|
||||
data-has-valid-branches="@(originRepository.branchList.contains(originId) && forkedRepository.branchList.contains(forkedId))"
|
||||
data-default-no-check="@context.settings.basicBehavior.compareNoCheckByDefault"
|
||||
data-can-auto-mergecheck="@(context.loginAccount.isDefined && originRepository.branchList.contains(originId) && forkedRepository.branchList.contains(forkedId))"
|
||||
style="display: none;"></div>
|
||||
</div>
|
||||
@if(commits.nonEmpty && context.loginAccount.isDefined && originRepository.branchList.contains(originId) && forkedRepository.branchList.contains(forkedId)){
|
||||
<div id="pull-request-form" style="margin-bottom: 20px;">
|
||||
<form method="POST" action="@context.path/@originRepository.owner/@originRepository.name/pulls/new" validate="true">
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<span class="error" id="error-title"></span>
|
||||
@@ -115,10 +126,15 @@
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
@if(commits.isEmpty){
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
@if(quickLoad){
|
||||
<div class="panel panel-default" style="padding: 20px; background-color: #eee; text-align: center;">
|
||||
<h4>Comparison is ready to load.</h4>
|
||||
<span class="muted">Click <span class="strong">Check mergeability</span> to compute changes and show merge status.</span>
|
||||
</div>
|
||||
} else if(commits.isEmpty){
|
||||
<div class="panel panel-default" style="padding: 20px; background-color: #eee; text-align: center;">
|
||||
<h4>There isn't anything to compare.</h4>
|
||||
<span class="strong">@originRepository.owner:@originId</span> and <span class="strong">@forkedRepository.owner:@forkedId</span> are identical.
|
||||
@@ -194,6 +210,17 @@ $(function(){
|
||||
|
||||
|
||||
$(function(){
|
||||
var compareState = $('#compare-state');
|
||||
var isQuickLoad = compareState.data('quick-load') === true || compareState.data('quick-load') === 'true';
|
||||
var defaultNoCheck = compareState.data('default-no-check') === true || compareState.data('default-no-check') === 'true';
|
||||
var autoMergecheck =
|
||||
/(?:\?|&)check=true(?:&|$)/.test(window.location.search) || (!isQuickLoad && !defaultNoCheck);
|
||||
var hasValidBranches =
|
||||
compareState.data('has-valid-branches') === true || compareState.data('has-valid-branches') === 'true';
|
||||
var canAutoMergecheck =
|
||||
compareState.data('can-auto-mergecheck') === true || compareState.data('can-auto-mergecheck') === 'true';
|
||||
var compareQuery = (defaultNoCheck || isQuickLoad) ? '?quick=true' : '';
|
||||
|
||||
function updateSelector(e){
|
||||
e.parents('ul').find('i').attr('class', 'octicon');
|
||||
e.find('i').addClass('octicon-check');
|
||||
@@ -209,7 +236,7 @@ $(function(){
|
||||
$.trim($('i.octicon-check').parents('a.origin-owner' ).data('owner')) + ':' +
|
||||
$.trim($('i.octicon-check').parents('a.origin-owner' ).data('default-branch')) + '...' +
|
||||
$.trim($('i.octicon-check').parents('a.forked-owner' ).data('owner')) + ':' +
|
||||
$.trim($('i.octicon-check').parents('a.forked-branch').data('branch'));
|
||||
$.trim($('i.octicon-check').parents('a.forked-branch').data('branch')) + compareQuery;
|
||||
});
|
||||
|
||||
$('a.forked-owner').click(function(){
|
||||
@@ -221,7 +248,7 @@ $(function(){
|
||||
$.trim($('i.octicon-check').parents('a.origin-owner' ).data('owner')) + ':' +
|
||||
$.trim($('i.octicon-check').parents('a.origin-branch').data('branch')) + '...' +
|
||||
$.trim($('i.octicon-check').parents('a.forked-owner' ).data('owner')) + ':' +
|
||||
$.trim($('i.octicon-check').parents('a.forked-owner' ).data('default-branch'));
|
||||
$.trim($('i.octicon-check').parents('a.forked-owner' ).data('default-branch')) + compareQuery;
|
||||
});
|
||||
|
||||
$('a.origin-branch, a.forked-branch').click(function(){
|
||||
@@ -233,22 +260,44 @@ $(function(){
|
||||
$.trim($('i.octicon-check').parents('a.origin-owner' ).data('owner')) + ':' +
|
||||
$.trim($('i.octicon-check').parents('a.origin-branch').data('branch')) + '...' +
|
||||
$.trim($('i.octicon-check').parents('a.forked-owner' ).data('owner')) + ':' +
|
||||
$.trim($('i.octicon-check').parents('a.forked-branch').data('branch'));
|
||||
$.trim($('i.octicon-check').parents('a.forked-branch').data('branch')) + compareQuery;
|
||||
});
|
||||
|
||||
@if(context.loginAccount.isDefined && originRepository.branchList.contains(originId) && forkedRepository.branchList.contains(forkedId)){
|
||||
if (isQuickLoad && hasValidBranches) {
|
||||
$('#check-conflict-button').click(function(){
|
||||
location.href = '@helpers.url(forkedRepository)/compare/' +
|
||||
$.trim($('i.octicon-check').parents('a.origin-owner' ).data('owner')) + ':' +
|
||||
$.trim($('i.octicon-check').parents('a.origin-branch').data('branch')) + '...' +
|
||||
$.trim($('i.octicon-check').parents('a.forked-owner' ).data('owner')) + ':' +
|
||||
$.trim($('i.octicon-check').parents('a.forked-branch').data('branch')) + '?check=true';
|
||||
});
|
||||
} else if (canAutoMergecheck) {
|
||||
function checkConflict(from, to){
|
||||
var button = $('#check-conflict-button');
|
||||
button.attr('disabled', 'disabled');
|
||||
$('.check-conflict').show();
|
||||
$('.check-conflict').html('<img src="' + '@helpers.assets("/common/images/indicator.gif")' + '"/> Checking...');
|
||||
$.get('@helpers.url(forkedRepository)/compare/' + from + '...' + to + '/mergecheck',
|
||||
function(data){ $('.check-conflict').html(data); });
|
||||
function(data){ $('.check-conflict').html(data); }
|
||||
).fail(function(){
|
||||
$('.check-conflict').html('<span class="strong" style="color: #bd2c00;">Failed to check mergeability.</span>');
|
||||
}).always(function(){
|
||||
button.removeAttr('disabled');
|
||||
});
|
||||
}
|
||||
|
||||
checkConflict(
|
||||
$.trim($('i.octicon-check').parents('a.origin-owner' ).data('owner')) + ":" +
|
||||
$.trim($('i.octicon-check').parents('a.origin-branch').data('branch')),
|
||||
$.trim($('i.octicon-check').parents('a.forked-owner' ).data('owner')) + ":" +
|
||||
$.trim($('i.octicon-check').parents('a.forked-branch').data('branch'))
|
||||
);
|
||||
$('#check-conflict-button').click(function(){
|
||||
checkConflict(
|
||||
$.trim($('i.octicon-check').parents('a.origin-owner' ).data('owner')) + ":" +
|
||||
$.trim($('i.octicon-check').parents('a.origin-branch').data('branch')),
|
||||
$.trim($('i.octicon-check').parents('a.forked-owner' ).data('owner')) + ":" +
|
||||
$.trim($('i.octicon-check').parents('a.forked-branch').data('branch'))
|
||||
);
|
||||
});
|
||||
|
||||
if (autoMergecheck) {
|
||||
$('#check-conflict-button').click();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -36,17 +36,37 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if(isManageableForkedRepository && issue.closed && merged &&
|
||||
forkedRepository.map(r => (r.branchList.contains(pullreq.requestBranch) && r.repository.defaultBranch != pullreq.requestBranch)).getOrElse(false)){
|
||||
<div class="issue-comment-box" style="background-color: #d0eeff;">
|
||||
<div class="box-content" style="border: 1px solid #87a8c9; padding: 10px;">
|
||||
<a href="@helpers.url(repository)/pull/@issue.issueId/delete_branch" class="btn btn-info pull-right delete-branch" data-name="@pullreq.requestBranch">Delete branch</a>
|
||||
<div>
|
||||
<span class="strong">Pull request successfully merged and closed</span>
|
||||
@defining(
|
||||
isManageableForkedRepository && issue.closed && merged &&
|
||||
forkedRepository.exists(r => r.branchList.contains(pullreq.requestBranch) && r.repository.defaultBranch != pullreq.requestBranch),
|
||||
issue.closed && pullreq.mergedCommitIds.isDefined
|
||||
) { case (isBranchDeletable, isRevertible) =>
|
||||
@if(isBranchDeletable || isRevertible) {
|
||||
<div class="issue-comment-box" style="margin-bottom: 20px;">
|
||||
<div class="box-content" style="border: 1px solid #ddd; padding: 10px;">
|
||||
@if(isRevertible) {
|
||||
<form method="post" action="@helpers.url(repository)/pull/@issue.issueId/revert" style="display:inline;">
|
||||
<button type="submit" class="btn btn-default pull-right" onclick="return confirm('Would you create a revert pull request?');" style="margin-left: 4px;">Revert</button>
|
||||
</form>
|
||||
}
|
||||
@if(isBranchDeletable) {
|
||||
<a href="@helpers.url(repository)/pull/@issue.issueId/delete_branch" class="btn btn-default pull-right delete-branch" data-name="@pullreq.requestBranch">Delete branch</a>
|
||||
}
|
||||
<div>
|
||||
<span class="strong">Pull request successfully merged and closed</span>
|
||||
</div>
|
||||
<span class="small muted">
|
||||
@if(isBranchDeletable) {
|
||||
The <code>@pullreq.requestBranch</code> branch can now be safely deleted.
|
||||
} else {
|
||||
You can create another pull request to revert this merge.
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<span class="small muted">You're all set. The <span class="label label-info monospace">@pullreq.requestBranch</span> branch can now be safely deleted.</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if(isBranchDeletable) {
|
||||
}
|
||||
}
|
||||
@gitbucket.core.issues.html.commentform(issue, !merged, isEditable, isManageable, repository)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<a class="btn btn-default" href="#" id="edit">Edit</a>
|
||||
}
|
||||
@if(context.loginAccount.isDefined) {
|
||||
<a class="btn btn-success" href="@helpers.url(repository)/compare">New pull request</a>
|
||||
<a class="btn btn-success" href="@helpers.url(repository)/compare@if(context.settings.basicBehavior.compareNoCheckByDefault){?quick=true}">New pull request</a>
|
||||
}
|
||||
</div>
|
||||
<div class="edit-title pull-right" style="display: none;">
|
||||
|
||||
@@ -43,13 +43,16 @@
|
||||
</div>
|
||||
<div>
|
||||
<hr>
|
||||
@status.conflictMessage.map { message => @helpers.markdown(
|
||||
markdown = message,
|
||||
repository = originRepository,
|
||||
@status.conflictMessage.map { message => @helpers.renderMarkup(
|
||||
filePath = List("temporary.md"),
|
||||
fileContent = message,
|
||||
branch = originRepository.repository.defaultBranch,
|
||||
repository = originRepository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableLineBreaks = false
|
||||
enableAnchor = false,
|
||||
enableLineBreaks = false,
|
||||
enableTaskList = false
|
||||
) }
|
||||
</div>
|
||||
} else {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="box-content" style="line-height: 20pt; margin-bottom: 6px; padding: 10px 6px 10px 10px; background-color: #fff9ea">
|
||||
<strong><i class="menu-icon octicon octicon-git-branch"></i><span class="muted">@branch</span></strong>
|
||||
<a class="pull-right btn btn-success" style="position: relative; top: -4px;"
|
||||
href="@helpers.url(repository)/compare/@{parent.owner}:@{helpers.encodeRefName(parent.repository.defaultBranch)}...@{repository.owner}:@{helpers.encodeRefName(branch)}">Compare & pull request</a>
|
||||
href="@helpers.url(repository)/compare/@{parent.owner}:@{helpers.encodeRefName(parent.repository.defaultBranch)}...@{repository.owner}:@{helpers.encodeRefName(branch)}@if(context.settings.basicBehavior.compareNoCheckByDefault){?quick=true}">Compare & pull request</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,12 +26,14 @@
|
||||
<p class="muted">
|
||||
@helpers.avatarLink(release.author, 20) @helpers.user(release.author, styleClass="username") released this @gitbucket.core.helper.html.datetimeago(release.registeredDate)
|
||||
</p>
|
||||
@helpers.markdown(
|
||||
markdown = release.content getOrElse "No description provided.",
|
||||
repository = repository,
|
||||
@helpers.renderMarkup(
|
||||
filePath = List("temporary.md"),
|
||||
fileContent = release.content getOrElse "No description provided.",
|
||||
branch = repository.repository.defaultBranch,
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableAnchor = false,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = hasWritePermission
|
||||
|
||||
@@ -31,12 +31,14 @@
|
||||
<p class="muted">
|
||||
@helpers.avatarLink(release.author, 20) @helpers.user(release.author, styleClass="username") released this @gitbucket.core.helper.html.datetimeago(release.registeredDate)
|
||||
</p>
|
||||
@helpers.markdown(
|
||||
markdown = release.content getOrElse "No description provided.",
|
||||
repository = repository,
|
||||
@helpers.renderMarkup(
|
||||
filePath = List("temporary.md"),
|
||||
fileContent = release.content getOrElse "No description provided.",
|
||||
branch = repository.repository.defaultBranch,
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableAnchor = false,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = hasWritePermission
|
||||
|
||||
@@ -40,7 +40,10 @@
|
||||
<div id="branchCtrlWrapper" style="display:inline;">
|
||||
@gitbucket.core.helper.html.branchcontrol(branch, repository, hasWritePermission){
|
||||
@repository.branchList.map { x =>
|
||||
<li><a href="@helpers.url(repository)/blob/@helpers.encodeRefName((x :: pathList).mkString("/"))">@gitbucket.core.helper.html.checkicon(x == branch) @x</a></li>
|
||||
<li class="branch-control-item-branch"><a href="@helpers.url(repository)/blob/@helpers.encodeRefName((x :: pathList).mkString("/"))">@gitbucket.core.helper.html.checkicon(x == branch) @x</a></li>
|
||||
}
|
||||
@repository.tags.map { x =>
|
||||
<li class="branch-control-item-tag"><a href="@helpers.url(repository)/blob/@helpers.encodeRefName((x.name :: pathList).mkString("/"))">@gitbucket.core.helper.html.checkicon(x.name == branch) @x.name</a></li>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -80,7 +83,18 @@
|
||||
@defining(helpers.isRenderable(pathList.last)){ isRenderable =>
|
||||
@if(!isBlame && isRenderable) {
|
||||
<div class="box-content-bottom @if(content.viewType == "text"){ markdown-body } " style="padding-left: 20px; padding-right: 20px;">
|
||||
@helpers.renderMarkup(pathList, content.content.getOrElse(""), branch, repository, false, false, true)
|
||||
@helpers.renderMarkup(
|
||||
filePath = pathList,
|
||||
fileContent = content.content.getOrElse(""),
|
||||
branch = branch,
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = false,
|
||||
enableAnchor = true,
|
||||
enableLineBreaks = false,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = hasWritePermission
|
||||
)
|
||||
</div>
|
||||
}else{
|
||||
@if(content.viewType == "text"){
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
helpers.urlEncode(parent) + ":" + helpers.encodeRefName(repository.repository.defaultBranch)
|
||||
}.getOrElse {
|
||||
helpers.encodeRefName(repository.repository.defaultBranch)
|
||||
}}...@{helpers.encodeRefName(branch.name)}" class="btn btn-default btn-sm">@if(context.loginAccount.isDefined){Create pull request} else {Compare}</a>
|
||||
}}...@{helpers.encodeRefName(branch.name)}@if(context.settings.basicBehavior.compareNoCheckByDefault){?quick=true}" class="btn btn-default btn-sm">@if(context.loginAccount.isDefined){Create pull request} else {Compare}</a>
|
||||
}
|
||||
@if(hasWritePermission){
|
||||
<span style="margin-left: 8px;">
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
@if(pathList.isEmpty){
|
||||
@gitbucket.core.helper.html.branchcontrol(branch, repository, hasWritePermission){
|
||||
@repository.branchList.map { x =>
|
||||
<li><a href="@helpers.url(repository)/commits/@helpers.encodeRefName(x)">@gitbucket.core.helper.html.checkicon(x == branch) @x</a></li>
|
||||
<li class="branch-control-item-branch"><a href="@helpers.url(repository)/commits/@helpers.encodeRefName(x)">@gitbucket.core.helper.html.checkicon(x == branch) @x</a></li>
|
||||
}
|
||||
@repository.tags.map { x =>
|
||||
<li class="branch-control-item-tag"><a href="@helpers.url(repository)/commits/@helpers.encodeRefName(x.name)">@gitbucket.core.helper.html.checkicon(x.name == branch) @x.name</a></li>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,72 +21,15 @@
|
||||
<div class="head">
|
||||
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName(branch)">@repository.name</a> /
|
||||
@pathList.zipWithIndex.map { case (section, i) =>
|
||||
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName((branch :: pathList.take(i + 1)).mkString("/"))">@section</a> /
|
||||
<a href='@helpers.url(repository)/tree/@helpers.encodeRefName((branch :: pathList.take(i + 1)).mkString("/"))'>@section</a> /
|
||||
}
|
||||
<input type="text" name="newFileName" id="newFileName" class="form-control" placeholder="Name your file..." value="@fileName" style="display: inline; width: 300px;" aria-label="New file name"/>
|
||||
<input type="hidden" name="oldFileName" id="oldFileName" value="@fileName"/>
|
||||
<input type="hidden" name="branch" id="branch" value="@branch"/>
|
||||
<input type="hidden" name="path" id="path" value="@pathList.mkString("/")"/>
|
||||
<input type="hidden" name="path" id="path" value='@pathList.mkString("/")'/>
|
||||
</div>
|
||||
@gitbucket.core.helper.html.acenavbar()
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>
|
||||
<div class="pull-right">
|
||||
<select id="wrap" class="form-control" style="margin-bottom: 0px; padding: 0px;" aria-label="Wrap">
|
||||
<optgroup label="Line Wrap Mode">
|
||||
<option value="false">No wrap</option>
|
||||
<option value="true">Soft wrap</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<select id="theme" class="form-control" style="margin-bottom: 0px; padding: 0px;" aria-label="Theme">
|
||||
<optgroup label="Editor Theme">
|
||||
<option value="ambiance">Ambiance</option>
|
||||
<option value="chaos">Chaos</option>
|
||||
<option value="chrome">Chrome</option>
|
||||
<option value="clouds">Clouds</option>
|
||||
<option value="clouds_midnight">Clouds Midnight</option>
|
||||
<option value="cobalt">Cobalt</option>
|
||||
<option value="crimson_editor">Crimson</option>
|
||||
<option value="dawn">Dawn</option>
|
||||
<option value="dracula">Dracula</option>
|
||||
<option value="dreamweaver">Dreamweaver</option>
|
||||
<option value="eclipse">Eclipse</option>
|
||||
<option value="github">GitHub</option>
|
||||
<option value="gob">Gob</option>
|
||||
<option value="gruvbox">Gruvbox</option>
|
||||
<option value="idle_fingers">Idle Fingers</option>
|
||||
<option value="iplastic">Iplastic</option>
|
||||
<option value="katzenmilch">Katzenmilch</option>
|
||||
<option value="kr_theme">Kr</option>
|
||||
<option value="kuroir">Kuroir</option>
|
||||
<option value="merbivore">Merbivore</option>
|
||||
<option value="mono_industrial">Mono Industrial</option>
|
||||
<option selected value="monokai">Monokai</option>
|
||||
<option value="nord_dark">Nord Dark</option>
|
||||
<option value="pastel_on_dark">Pastel on Dark</option>
|
||||
<option value="solarized_dark">Solarized Dark</option>
|
||||
<option value="solarized_light">Solarized Light</option>
|
||||
<option value="sqlserver">Sqlserver</option>
|
||||
<option value="terminal">Terminal</option>
|
||||
<option value="textmate">Textmate</option>
|
||||
<option value="tomorrow">Tomorrow</option>
|
||||
<option value="tomorrow_night">Tomorrow Night</option>
|
||||
<option value="tomorrow_night_bright">Tomorrow Night Bright</option>
|
||||
<option value="tomorrow_night_eighties">Tomorrow Night Eighties</option>
|
||||
<option value="twilight">Twilight</option>
|
||||
<option value="vibrant_ink">Vibrant Ink</option>
|
||||
<option value="xcode">Xcode</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div class="btn-group" data-toggle="buttons-radio">
|
||||
<input type="button" id="btn-code" class="btn btn-default btn-small active" value="Code">
|
||||
<input type="button" id="btn-preview" class="btn btn-default btn-small" value="Preview">
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div id="editor" style="width: 100%; height: 600px;"></div>
|
||||
@@ -114,13 +57,14 @@
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
@if(fileName.isEmpty){
|
||||
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName((branch :: pathList).mkString("/"))" class="btn btn-default">Cancel</a>
|
||||
<a href='@helpers.url(repository)/tree/@helpers.encodeRefName((branch :: pathList).mkString("/"))' class="btn btn-default">Cancel</a>
|
||||
} else {
|
||||
<a href="@helpers.url(repository)/blob/@helpers.encodeRefName((branch :: pathList ++ Seq(fileName.get)).mkString("/"))" class="btn btn-default">Cancel</a>
|
||||
<a href='@helpers.url(repository)/blob/@helpers.encodeRefName((branch :: pathList ++ Seq(fileName.get)).mkString("/"))' class="btn btn-default">Cancel</a>
|
||||
}
|
||||
<input type="submit" id="commitButton" class="btn btn-success" value="Commit changes" disabled="true"/>
|
||||
<input type="hidden" id="charset" name="charset" value="@content.charset"/>
|
||||
<input type="hidden" id="lineSeparator" name="lineSeparator" value="@content.lineSeparator"/>
|
||||
<input type="hidden" id="hasBom" name="hasBom" value="@content.hasBom"/>
|
||||
<input type="hidden" id="content" name="content" value=""/>
|
||||
<input type="hidden" id="initial" value="@content.content"/>
|
||||
<input type="hidden" id="commit" name="commit" value="@commit"/>
|
||||
@@ -128,109 +72,39 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<script src='@helpers.assets("/vendors/ace/ace.js")' type="text/javascript" charset="utf-8"></script>
|
||||
<script src='@helpers.assets("/vendors/ace/ext-modelist.js")' type="text/javascript" charset="utf-8"></script>
|
||||
<script src='@helpers.assets("/vendors/ace/ext-language_tools.js")' type="text/javascript" charset="utf-8"></script>
|
||||
<script type="text/javascript" src='@helpers.assets("/vendors/jsdifflib/difflib.js")'></script>
|
||||
<link href='@helpers.assets("/vendors/jsdifflib/diffview.css")' type="text/css" rel="stylesheet" />
|
||||
@gitbucket.core.helper.html.completion(
|
||||
repository = repository,
|
||||
completionContext = "wiki",
|
||||
generateScript = false
|
||||
)
|
||||
<script>
|
||||
const gitbucket = {
|
||||
editor: null,
|
||||
enableWikiLink: false,
|
||||
pluginAssets: "@context.path/plugin-assets/",
|
||||
protected: @protectedBranch,
|
||||
previewTemplate: '<img src="@helpers.assets("/common/images/indicator.gif")"> Previewing...',
|
||||
previewUrl: '@helpers.url(repository)/_preview',
|
||||
uploadUrl: '@context.path/upload/wiki/@repository.owner/@repository.name',
|
||||
maxFilesize: @{ context.settings.upload.maxFileSize / 1024 / 1024 },
|
||||
timeout: @{ context.settings.upload.timeout },
|
||||
isRenderableUrl: "@context.baseUrl/_is_renderable?filename=",
|
||||
tabSize: @tabSize,
|
||||
newLineMode: "@newLineMode",
|
||||
useSoftTabs: @useSoftTabs,
|
||||
getFileName: function () {
|
||||
return $('#newFileName').val();
|
||||
},
|
||||
getFilePath: function () {
|
||||
return $('#path').val() + '/' + $('#newFileName').val();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<script src="@helpers.assets("/common/js/editor.js")" type="text/javascript" charset="utf-8"></script>
|
||||
}
|
||||
}
|
||||
<script src="@helpers.assets("/vendors/ace/ace.js")" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="@helpers.assets("/vendors/ace/ext-modelist.js")" type="text/javascript" charset="utf-8"></script>
|
||||
<script type="text/javascript" src="@helpers.assets("/vendors/jsdifflib/difflib.js")"></script>
|
||||
<link href="@helpers.assets("/vendors/jsdifflib/diffview.css")" type="text/css" rel="stylesheet" />
|
||||
<script>
|
||||
$(function(){
|
||||
$('#editor').text($('#initial').val());
|
||||
var editor = ace.edit("editor");
|
||||
|
||||
if(typeof localStorage.getItem('gitbucket:editor:theme') == "string"){
|
||||
$('#theme').val(localStorage.getItem('gitbucket:editor:theme'));
|
||||
}
|
||||
|
||||
editor.setTheme("ace/theme/" + $('#theme').val());
|
||||
|
||||
$('#theme').change(function(){
|
||||
editor.setTheme("ace/theme/" + $('#theme').val());
|
||||
localStorage.setItem('gitbucket:editor:theme', $('#theme').val());
|
||||
});
|
||||
|
||||
if(localStorage.getItem('gitbucket:editor:wrap') == 'true'){
|
||||
editor.getSession().setUseWrapMode(true);
|
||||
$('#wrap').val('true');
|
||||
}
|
||||
|
||||
@if(fileName.isDefined){
|
||||
var modelist = ace.require("ace/ext/modelist");
|
||||
var mode = modelist.getModeForPath("@fileName.get");
|
||||
editor.getSession().setMode(mode.mode);
|
||||
}
|
||||
@if(protectedBranch){
|
||||
editor.setReadOnly(true);
|
||||
}
|
||||
editor.getSession().setOption("tabSize", @tabSize);
|
||||
editor.getSession().setOption("newLineMode", "@newLineMode");
|
||||
editor.getSession().setOption("useSoftTabs", @useSoftTabs);
|
||||
|
||||
editor.on('change', function(){
|
||||
updateCommitButtonStatus();
|
||||
});
|
||||
|
||||
function updateCommitButtonStatus(){
|
||||
if(editor.getValue() == $('#initial').val() && $('#newFileName').val() == $('#oldFileName').val()){
|
||||
$('#commitButton').attr('disabled', true);
|
||||
} else {
|
||||
$('#commitButton').attr('disabled', false);
|
||||
}
|
||||
}
|
||||
|
||||
$('#wrap').change(function(){
|
||||
if($('#wrap option:selected').val() == 'true'){
|
||||
editor.getSession().setUseWrapMode(true);
|
||||
localStorage.setItem('gitbucket:editor:wrap', 'true');
|
||||
} else {
|
||||
editor.getSession().setUseWrapMode(false);
|
||||
localStorage.setItem('gitbucket:editor:wrap', 'false');
|
||||
}
|
||||
});
|
||||
|
||||
$('#newFileName').watch(function(){
|
||||
updateCommitButtonStatus();
|
||||
});
|
||||
|
||||
$('#commitButton').click(function(){
|
||||
$('#content').val(editor.getValue());
|
||||
});
|
||||
|
||||
$('#btn-code').click(function(){
|
||||
$('#editor').show();
|
||||
$('#preview').hide();
|
||||
$('#btn-preview').removeClass('active');
|
||||
});
|
||||
|
||||
$('#btn-preview').click(function(){
|
||||
$('#editor').hide();
|
||||
$('#preview').show();
|
||||
$('#btn-code').removeClass('active');
|
||||
|
||||
@if(fileName.map(helpers.isRenderable _).getOrElse(false)) {
|
||||
// update preview
|
||||
$('#preview').html('<img src="@helpers.assets("/common/images/indicator.gif")"> Previewing...');
|
||||
$.post('@helpers.url(repository)/_preview', {
|
||||
content : editor.getValue(),
|
||||
enableWikiLink : false,
|
||||
filename : $('#newFileName').val(),
|
||||
enableRefsLink : false,
|
||||
enableLineBreaks : false,
|
||||
enableTaskList : false
|
||||
}, function(data){
|
||||
$('#preview').empty().append(
|
||||
$('<div class="markdown-body" style="padding-left: 20px; padding-right: 20px;">').html(data));
|
||||
prettyPrint();
|
||||
});
|
||||
} else {
|
||||
// Show diff
|
||||
$('#preview').empty()
|
||||
.append($('<div id="diffText">'))
|
||||
.append($('<textarea id="newText" style="display: none;">').data('file-name',$("#newFileName").val()).data('val', editor.getValue()))
|
||||
.append($('<textarea id="oldText" style="display: none;">').data('file-name',$("#oldFileName").val()).data('val', $('#initial').val()));
|
||||
diffUsingJS('oldText', 'newText', 'diffText', 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<div class="head" style="height: 24px;">
|
||||
<div class="pull-right">
|
||||
<div class="btn-group">
|
||||
<a href="@{helpers.url(repository)}/archive@if(pathList.length > 0){/@pathList.map(helpers.urlEncode).mkString("/")}/@{helpers.urlEncode(branch)}.zip" class="btn btn-sm btn-default pc"><i class="octicon octicon-cloud-download"></i> Download ZIP</a>
|
||||
<a href="@{helpers.url(repository)}/archive/@{helpers.urlEncode(branch)}.zip@if(pathList.nonEmpty){?path=@helpers.urlEncode(pathList.mkString("/"))}" class="btn btn-sm btn-default pc"><i class="octicon octicon-cloud-download"></i> Download ZIP</a>
|
||||
<a href="@helpers.url(repository)/find/@helpers.encodeRefName(branch)" class="btn btn-sm btn-default" data-hotkey="t" title="Search files"><i class="octicon octicon-search" aria-label="Search files"></i></a>
|
||||
<a href="@helpers.url(repository)/commits/@helpers.encodeRefName((branch :: pathList).mkString("/"))" class="btn btn-sm btn-default"><i class="octicon octicon-history"></i> @if(commitCount > 10000){10000+} else {@commitCount} @helpers.plural(commitCount, "commit")</a>
|
||||
</div>
|
||||
@@ -220,7 +220,7 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-bottom markdown-body" style="padding-left: 20px; padding-right: 20px;">@helpers.renderMarkup(filePath, content, branch, repository, false, false, true)</div>
|
||||
<div class="box-content-bottom markdown-body" style="padding-left: 20px; padding-right: 20px;">@helpers.renderMarkup(filePath, content, branch, repository, false, false, false, true, true)</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
branch: String,
|
||||
protection: gitbucket.core.api.ApiBranchProtection,
|
||||
protection: gitbucket.core.api.ApiBranchProtectionResponse,
|
||||
knownContexts: Seq[String],
|
||||
info: Option[Any])(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@@ -22,9 +22,44 @@
|
||||
</label>
|
||||
<p class="help-block">Disables force-pushes to this branch and prevents it from being deleted.</p>
|
||||
</div>
|
||||
|
||||
<!--====================================================================-->
|
||||
<!-- Enforce administrators -->
|
||||
<!--====================================================================-->
|
||||
<div class="checkbox js-enabled" style="display:none">
|
||||
<label>
|
||||
<input type="checkbox" name="has_required_statuses" onclick="update()" @check(protection.status.enforcement_level.name!="off") @if(knownContexts.isEmpty){disabled }>
|
||||
<input type="checkbox" name="enforce_for_admins" onclick="update()" @check(protection.enforce_admins.exists(_.enabled))>
|
||||
<span class="strong">Include administrators</span>
|
||||
</label>
|
||||
<p class="help-block">Enforce restrictions even for repository administrators.</p>
|
||||
</div>
|
||||
|
||||
<!--====================================================================-->
|
||||
<!-- Push restrictions -->
|
||||
<!--====================================================================-->
|
||||
<div class="checkbox js-enabled" style="display:none">
|
||||
<label>
|
||||
<input type="checkbox" name="restrictions" onclick="update()" @check(protection.restrictions.isDefined)>
|
||||
<span class="strong">Restrict users for push</span>
|
||||
</label>
|
||||
<p class="help-block">Restrict users who can push to this branch</p>
|
||||
<div class="js-restrictions_enabled" style="display: none;">
|
||||
<ul id="restrictions-user-list">
|
||||
</ul>
|
||||
@gitbucket.core.helper.html.account("userName-restrictions-user", 200, true, false)
|
||||
<input type="button" class="btn btn-default add-restrictions-user" value="Add"/>
|
||||
<div>
|
||||
<span class="error" id="error-restrictions-user"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--====================================================================-->
|
||||
<!-- Status check -->
|
||||
<!--====================================================================-->
|
||||
<div class="checkbox js-enabled" style="display:none">
|
||||
<label>
|
||||
<input type="checkbox" name="has_required_statuses" onclick="update()" @check(protection.required_status_checks.isDefined) @if(knownContexts.isEmpty){disabled }>
|
||||
<span class="strong">Require status checks to pass before merging</span>
|
||||
</label>
|
||||
<p class="help-block">When enabled, commits must first be pushed to another branch, then merged or pushed directly to <b>@branch</b> after status checks have passed.</p>
|
||||
@@ -48,14 +83,6 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="enforce_for_admins" onclick="update()" @check(protection.status.enforcement_level.name=="everyone")>
|
||||
<span class="strong">Include administrators</span>
|
||||
</label>
|
||||
<p class="help-block">Enforce required status checks for repository administrators.</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -68,59 +95,114 @@
|
||||
}
|
||||
<script>
|
||||
function getValue(){
|
||||
var v = {}, contexts=[];
|
||||
const v = {}, contexts = [];
|
||||
let restrictions = undefined;
|
||||
$("input[type=checkbox]:checked").each(function(){
|
||||
if(this.name === 'contexts'){
|
||||
if(this.name === 'contexts') {
|
||||
contexts.push(this.value);
|
||||
} else if (this.name === 'restrictions') {
|
||||
restrictions = $('#restrictions-user-list li').map(function(i, e){
|
||||
return $(e).data('name');
|
||||
}).get();
|
||||
} else {
|
||||
v[this.name] = true;
|
||||
}
|
||||
});
|
||||
|
||||
if(v.enabled){
|
||||
return {
|
||||
enabled: true,
|
||||
required_status_checks: {
|
||||
enforcement_level: v.has_required_statuses ? ((v.enforce_for_admins ? 'everyone' : 'non_admins')) : 'off',
|
||||
contexts: v.has_required_statuses ? contexts : []
|
||||
}
|
||||
};
|
||||
enforce_admins: v.enforce_for_admins,
|
||||
required_status_checks: v.has_required_statuses ? { contexts: contexts } : undefined,
|
||||
restrictions: restrictions ? { users: restrictions } : undefined
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
enabled: false,
|
||||
required_status_checks: {
|
||||
enforcement_level: "off",
|
||||
contexts: []
|
||||
}
|
||||
enabled: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function updateView(protection){
|
||||
$('.js-enabled').toggle(protection.enabled);
|
||||
$('.js-has_required_statuses').toggle(protection.required_status_checks.enforcement_level != 'off');
|
||||
$('.js-submit-btn').attr('disabled',protection.required_status_checks.enforcement_level != 'off' && protection.required_status_checks.contexts.length == 0);
|
||||
$('.js-restrictions_enabled').toggle(protection.restrictions !== undefined);
|
||||
$('.js-has_required_statuses').toggle(protection.required_status_checks !== undefined);
|
||||
}
|
||||
|
||||
function update(){
|
||||
var protection = getValue();
|
||||
const protection = getValue();
|
||||
updateView(protection);
|
||||
}
|
||||
$(update);
|
||||
|
||||
function submitForm(e){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
var protection = getValue();
|
||||
const protection = getValue();
|
||||
$.ajax({
|
||||
method:'PATCH',
|
||||
url:'@context.path/api/v3/repos/@repository.owner/@repository.name/branches/@helpers.urlEncode(branch)',
|
||||
method: 'PATCH',
|
||||
url: '@context.path/api/v3/repos/@repository.owner/@repository.name/branches/@helpers.urlEncode(branch)',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
data:JSON.stringify({protection:protection}),
|
||||
success:function(r){
|
||||
data: JSON.stringify({protection: protection}),
|
||||
success: function(r){
|
||||
$('#saved-info').show();
|
||||
},
|
||||
error:function(err){
|
||||
error: function(err){
|
||||
console.log(err);
|
||||
alert('update error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addUserToListHTML(userName, id){
|
||||
$(id).append($('<li>').data('name', userName)
|
||||
.append(' ')
|
||||
.append(userName)
|
||||
.append($('<a href="#" onclick="$(this).parent().remove();" class="remove">(remove)</a>')));
|
||||
}
|
||||
|
||||
$(function() {
|
||||
// Initialize
|
||||
update();
|
||||
|
||||
@protection.restrictions.map(_.users).map { users =>
|
||||
@users.map { user =>
|
||||
addUserToListHTML('@user', '#restrictions-user-list');
|
||||
}
|
||||
}
|
||||
|
||||
$('.add-restrictions-user').click(function(){
|
||||
$('#error-restrictions-user').text('');
|
||||
const userName = $('#userName-restrictions-user').val();
|
||||
|
||||
// check empty
|
||||
if($.trim(userName) === ''){
|
||||
return false;
|
||||
}
|
||||
|
||||
// check duplication
|
||||
const exists = $('#restrictions-user-list li').filter(function(){
|
||||
return $(this).data('name') === userName;
|
||||
}).length > 0;
|
||||
if(exists){
|
||||
$('#error-restrictions-user').text('User has been already added.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// check existence
|
||||
$.post('@context.path/_user/existence', {
|
||||
'userName': userName,
|
||||
'owner': '@repository.owner',
|
||||
'repository': '@repository.name'
|
||||
},
|
||||
function(data, status){
|
||||
if(data !== ''){
|
||||
addUserToListHTML(userName, '#restrictions-user-list');
|
||||
$('#userName-restrictions-user').val('');
|
||||
} else {
|
||||
$('#error-restrictions-user').text("User does not exist or isn't writable to this repository.");
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -48,25 +48,25 @@ $(function(){
|
||||
});
|
||||
|
||||
$('.add').click(function(){
|
||||
var id = $(this).attr('id') == 'addCollaborator' ? 'collaborator' : 'group';
|
||||
const id = $(this).attr('id') === 'addCollaborator' ? 'collaborator' : 'group';
|
||||
|
||||
$('#error-' + id).text('');
|
||||
var userName = $('#userName-' + id).val();
|
||||
const userName = $('#userName-' + id).val();
|
||||
|
||||
// check empty
|
||||
if($.trim(userName) == ''){
|
||||
if($.trim(userName) === ''){
|
||||
return false;
|
||||
}
|
||||
|
||||
// check owner
|
||||
var owner = '@repository.owner' == userName
|
||||
const owner = '@repository.owner' === userName
|
||||
if(owner){
|
||||
$('#error-' + id).text('User is owner of this repository.');
|
||||
return false;
|
||||
}
|
||||
// check duplication
|
||||
var exists = $('#' + id + '-list li').filter(function(){
|
||||
return $(this).data('name') == userName;
|
||||
const exists = $('#' + id + '-list li').filter(function(){
|
||||
return $(this).data('name') === userName;
|
||||
}).length > 0;
|
||||
if(exists){
|
||||
$('#error-' + id).text('User has been already added.');
|
||||
@@ -76,7 +76,7 @@ $(function(){
|
||||
// check existence
|
||||
$.post('@context.path/_user/existence', { 'userName': userName },
|
||||
function(data, status){
|
||||
if(data != ''){
|
||||
if(data !== ''){
|
||||
addListHTML(userName, '@Role.ADMIN.name', '#' + id + '-list');
|
||||
$('#userName-' + id).val('');
|
||||
} else {
|
||||
@@ -91,7 +91,7 @@ $(function(){
|
||||
|
||||
// Don't submit form by ENTER key
|
||||
$('#userName-collaborator, #userName-group').keypress(function(e){
|
||||
return !(e.keyCode == 13);
|
||||
return !(e.keyCode === 13);
|
||||
});
|
||||
|
||||
@collaborators.map { case (collaborator, isGroup) =>
|
||||
@@ -99,16 +99,16 @@ $(function(){
|
||||
}
|
||||
|
||||
function addListHTML(userName, role, id){
|
||||
var adminButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="@Role.ADMIN.name" name="' + userName + '">Admin</label>');
|
||||
if(role == '@Role.ADMIN.name'){
|
||||
const adminButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="@Role.ADMIN.name" name="' + userName + '">Admin</label>');
|
||||
if(role === '@Role.ADMIN.name'){
|
||||
adminButton.addClass('active');
|
||||
}
|
||||
var writeButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="@Role.DEVELOPER.name" name="' + userName + '">Developer</label>');
|
||||
if(role == '@Role.DEVELOPER.name'){
|
||||
const writeButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="@Role.DEVELOPER.name" name="' + userName + '">Developer</label>');
|
||||
if(role === '@Role.DEVELOPER.name'){
|
||||
writeButton.addClass('active');
|
||||
}
|
||||
var readButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="@Role.GUEST.name" name="' + userName + '">Guest</label>');
|
||||
if(role == '@Role.GUEST.name'){
|
||||
const readButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="@Role.GUEST.name" name="' + userName + '">Guest</label>');
|
||||
if(role === '@Role.GUEST.name'){
|
||||
readButton.addClass('active');
|
||||
}
|
||||
|
||||
@@ -124,13 +124,13 @@ $(function(){
|
||||
}
|
||||
|
||||
function updateValues(){
|
||||
var collaborators = $('#collaborator-list li').map(function(i, e){
|
||||
var userName = $(e).data('name');
|
||||
const collaborators = $('#collaborator-list li').map(function(i, e){
|
||||
const userName = $(e).data('name');
|
||||
return userName + ':' + $(e).find('label.active input[type=radio]').attr('value');
|
||||
}).get().join(',');
|
||||
|
||||
var groups = $('#group-list li').map(function(i, e){
|
||||
var userName = $(e).data('name');
|
||||
const groups = $('#group-list li').map(function(i, e){
|
||||
const userName = $(e).data('name');
|
||||
return userName + ':' + $(e).find('label.active input[type=radio]').attr('value');
|
||||
}).get().join(',');
|
||||
|
||||
|
||||
@@ -15,22 +15,11 @@
|
||||
<form action="@helpers.url(repository)/wiki/@if(page.isEmpty){_new} else {_edit}" method="POST" validate="true" autocomplete="off">
|
||||
<span id="error-pageName" class="error"></span>
|
||||
<input type="text" name="pageName" value="@pageName" class="form-control" style="font-weight: bold; margin-bottom: 10px;" placeholder="Input a page name." aria-label="Page name"/>
|
||||
@gitbucket.core.helper.html.acenavbar()
|
||||
<div class="muted attachable">
|
||||
@gitbucket.core.helper.html.preview(
|
||||
repository = repository,
|
||||
content = page.map(_.content).getOrElse(""),
|
||||
enableWikiLink = true,
|
||||
enableRefsLink = false,
|
||||
enableLineBreaks = false,
|
||||
enableTaskList = false,
|
||||
hasWritePermission = false,
|
||||
completionContext = "wiki",
|
||||
style = "height: 400px;",
|
||||
styleClass = "monospace",
|
||||
placeholder = "",
|
||||
ariaLabel = "Page content",
|
||||
uid = 1
|
||||
)
|
||||
<div id="editor" style="width: 100%; height: 600px;"></div>
|
||||
<div class="clickable">Attach images or documents by dragging & dropping, or selecting them.</div>
|
||||
<div id="preview" style="width: 100%; display: none;"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="message">Edit Message</label>
|
||||
@@ -39,45 +28,44 @@
|
||||
<div class="form-group pull-right">
|
||||
<input type="hidden" name="currentPageName" value="@pageName"/>
|
||||
<input type="hidden" name="id" value="@page.map(_.id)"/>
|
||||
<input type="submit" value="Save" class="btn btn-success">
|
||||
<input type="submit" id="commitButton" value="Save" class="btn btn-success">
|
||||
<input type="hidden" id="content" name="content" value=""/>
|
||||
<input type="hidden" id="initial" value='@page.map(_.content)'/>
|
||||
</div>
|
||||
</form>
|
||||
<script src='@helpers.assets("/vendors/ace/ace.js")' type="text/javascript" charset="utf-8"></script>
|
||||
<script src='@helpers.assets("/vendors/ace/ext-modelist.js")' type="text/javascript" charset="utf-8"></script>
|
||||
<script src='@helpers.assets("/vendors/ace/ext-language_tools.js")' type="text/javascript" charset="utf-8"></script>
|
||||
<script type="text/javascript" src='@helpers.assets("/vendors/jsdifflib/difflib.js")'></script>
|
||||
<link href='@helpers.assets("/vendors/jsdifflib/diffview.css")' type="text/css" rel="stylesheet" />
|
||||
@gitbucket.core.helper.html.completion(
|
||||
repository = repository,
|
||||
completionContext = "wiki",
|
||||
generateScript = false
|
||||
)
|
||||
<script>
|
||||
const gitbucket = {
|
||||
editor: null,
|
||||
enableWikiLink: true,
|
||||
pluginAssets: "@context.path/plugin-assets/",
|
||||
protected: false,
|
||||
previewTemplate: '<img src="@helpers.assets("/common/images/indicator.gif")"> Previewing...',
|
||||
previewUrl: '@helpers.url(repository)/_preview',
|
||||
uploadUrl: '@context.path/upload/wiki/@repository.owner/@repository.name',
|
||||
maxFilesize: @{ context.settings.upload.maxFileSize / 1024 / 1024 },
|
||||
timeout: @{ context.settings.upload.timeout },
|
||||
isRenderableUrl: "@context.baseUrl/_is_renderable?filename=",
|
||||
tabSize: 8,
|
||||
newLineMode: "auto",
|
||||
useSoftTabs: false,
|
||||
getFileName: function () {
|
||||
return '@page.map(_.name).getOrElse("temporary.md")';
|
||||
},
|
||||
getFilePath: function () {
|
||||
return '@page.map(_.name).getOrElse("temporary.md")';
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<script src='@helpers.assets("/common/js/editor.js")' type="text/javascript" charset="utf-8"></script>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
try {
|
||||
$('#content1').dropzone({
|
||||
url: '@context.path/upload/wiki/@repository.owner/@repository.name',
|
||||
maxFilesize: @{context.settings.upload.maxFileSize / 1024 / 1024},
|
||||
timeout: @{context.settings.upload.timeout},
|
||||
clickable: false,
|
||||
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
|
||||
success: function(file, id) {
|
||||
var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) + '](' + file.name + ')';
|
||||
$('#content1').val($('#content1').val() + attachFile);
|
||||
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
|
||||
}
|
||||
});
|
||||
$('.clickable').dropzone({
|
||||
url: '@context.path/upload/wiki/@repository.owner/@repository.name',
|
||||
maxFilesize: @{context.settings.upload.maxFileSize / 1024 / 1024},
|
||||
timeout: @{context.settings.upload.timeout},
|
||||
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
|
||||
success: function(file, id) {
|
||||
var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) + '](' + file.name + ')';
|
||||
$('#content1').val($('#content1').val() + attachFile);
|
||||
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
|
||||
}
|
||||
});
|
||||
} catch(e) {
|
||||
if (e.message !== "Dropzone already attached.") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
$('#delete').click(function(){
|
||||
return confirm('Are you sure you want to delete this page?');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
isEditable: Boolean,
|
||||
sidebar: Option[gitbucket.core.service.WikiService.WikiPageInfo],
|
||||
footer: Option[gitbucket.core.service.WikiService.WikiPageInfo])(implicit context: gitbucket.core.controller.Context)
|
||||
footer: Option[gitbucket.core.service.WikiService.WikiPageInfo],
|
||||
branch: String)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@import gitbucket.core.service.WikiService
|
||||
@gitbucket.core.html.main(s"${pageName} - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@@ -59,7 +60,17 @@
|
||||
@if(isEditable){
|
||||
<a href="@helpers.url(repository)/wiki/_Sidebar/_edit" style="text-decoration: none;"><span class="octicon octicon-pencil pull-right"></span></a>
|
||||
}
|
||||
@helpers.markdown(sidebarPage.content, repository, "master", true, false, false, false, pages)
|
||||
@helpers.renderMarkup(
|
||||
filePath = sidebarPage.name.split("/").toList,
|
||||
fileContent = sidebarPage.content,
|
||||
branch = branch,
|
||||
repository = repository,
|
||||
enableWikiLink = true,
|
||||
enableRefsLink = false,
|
||||
enableAnchor = false,
|
||||
enableLineBreaks = false,
|
||||
enableTaskList = false
|
||||
)
|
||||
</div>
|
||||
}.getOrElse{
|
||||
@if(isEditable){
|
||||
@@ -82,16 +93,16 @@
|
||||
</div>
|
||||
<div class="wiki-main">
|
||||
<div class="markdown-body">
|
||||
@helpers.markdown(
|
||||
markdown = page.content,
|
||||
@helpers.renderMarkup(
|
||||
filePath = page.name.split("/").toList,
|
||||
fileContent = page.content,
|
||||
branch = branch,
|
||||
repository = repository,
|
||||
branch = "master",
|
||||
enableWikiLink = true,
|
||||
enableRefsLink = false,
|
||||
enableAnchor = true,
|
||||
enableLineBreaks = false,
|
||||
enableTaskList = false,
|
||||
hasWritePermission = false,
|
||||
pages = pages
|
||||
enableTaskList = false
|
||||
)
|
||||
</div>
|
||||
@footer.map { footerPage =>
|
||||
@@ -99,15 +110,16 @@
|
||||
@if(isEditable){
|
||||
<a href="@helpers.url(repository)/wiki/_Footer/_edit" style="text-decoration: none;"><span class="octicon octicon-pencil pull-right"></span></a>
|
||||
}
|
||||
@helpers.markdown(
|
||||
markdown = footerPage.content,
|
||||
repository = repository,
|
||||
branch = "master",
|
||||
enableWikiLink = true,
|
||||
enableRefsLink = false,
|
||||
@helpers.renderMarkup(
|
||||
filePath = footerPage.name.split("/").toList,
|
||||
fileContent = footerPage.content,
|
||||
branch = branch,
|
||||
repository = repository,
|
||||
enableWikiLink = true,
|
||||
enableRefsLink = false,
|
||||
enableAnchor = false,
|
||||
enableLineBreaks = false,
|
||||
enableAnchor = false,
|
||||
pages = pages
|
||||
enableTaskList = false
|
||||
)
|
||||
</div>
|
||||
}.getOrElse{
|
||||
|
||||
@@ -928,30 +928,38 @@ pre.reset.discussion-item-content-text{
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.discussion-item-icon .octicon-bookmark,
|
||||
.discussion-item-icon .octicon-person,
|
||||
.discussion-item-icon .octicon-git-branch{
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.discussion-item-merge .discussion-item-icon {
|
||||
background-color: #6e5494;
|
||||
color: white;
|
||||
padding-top: 1px;
|
||||
padding-left: 3px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.discussion-item-close .discussion-item-icon {
|
||||
background-color: #bd2c00;
|
||||
color: white;
|
||||
padding-left: 2px;
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.discussion-item-delete_branch .discussion-item-icon {
|
||||
padding-left: 2px;
|
||||
padding-top: 1px;
|
||||
background-color: #767676;
|
||||
color: white;
|
||||
padding-left: 2px;
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.discussion-item-reopen .discussion-item-icon {
|
||||
background-color: #6cc644;
|
||||
padding-top: 1px;
|
||||
color: white;
|
||||
padding-left: 7px;
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.discussion-item-delete-comment .discussion-item-icon {
|
||||
@@ -1536,7 +1544,6 @@ div.markdown-body table th,
|
||||
div.markdown-body table td {
|
||||
padding: 8px;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
border-top: 1px solid #dddddd;
|
||||
}
|
||||
|
||||
500
src/main/webapp/assets/common/js/editor.js
Normal file
500
src/main/webapp/assets/common/js/editor.js
Normal file
@@ -0,0 +1,500 @@
|
||||
(function () {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
/**
|
||||
* element to attach images or documents by dragging & dropping, or selecting them.
|
||||
* @type {Element}
|
||||
*/
|
||||
const clickable = document.querySelector(".clickable");
|
||||
|
||||
/**
|
||||
* element of markdown formatting toolbar buttons
|
||||
* @type {Element}
|
||||
*/
|
||||
// inline format
|
||||
const markdownToolBar = document.getElementById("markdown-toolbar");
|
||||
const markdownBold = document.getElementById("markdown-bold");
|
||||
const markdownItalic = document.getElementById("markdown-italic");
|
||||
const markdownCode = document.getElementById("markdown-code");
|
||||
const markdownLink = document.getElementById("markdown-link");
|
||||
// block level format
|
||||
const markdownHeading = document.getElementById("markdown-heading");
|
||||
const markdownQuote = document.getElementById("markdown-quote");
|
||||
const markdownListUl = document.getElementById("markdown-list-ul");
|
||||
const markdownListOl = document.getElementById("markdown-list-ol");
|
||||
const markdownTask = document.getElementById("markdown-task");
|
||||
const markdownCodeBlock = document.getElementById("markdown-code-block");
|
||||
|
||||
/**
|
||||
* constants for line type
|
||||
*/
|
||||
const headingPattern = /^#{1,6} /;
|
||||
const quotePattern = /^((>+ )+|>+ )/;
|
||||
const nestedQuotePattern = /^(> ?){2,}/;
|
||||
const taskPattern = /^ *- \[ \] /;
|
||||
const doneTaskPattern = /^ *- \[x\] /;
|
||||
const ulPattern = /^ *- /;
|
||||
const olPattern = /^ *[0-9]. /;
|
||||
|
||||
const TYPE_NONE = 0;
|
||||
const TYPE_HEADING = 1;
|
||||
const TYPE_QUOTE = 2;
|
||||
const TYPE_TASK = 3;
|
||||
const TYPE_DONE_TASK = 4;
|
||||
const TYPE_UL = 5;
|
||||
const TYPE_OL = 6;
|
||||
|
||||
/**
|
||||
* Determine the type of line
|
||||
*
|
||||
* @param {*} line line of editor contents
|
||||
* @returns [TYPE, match]
|
||||
*/
|
||||
const getLineType = function (line) {
|
||||
if (m = line.match(headingPattern)) {
|
||||
return [TYPE_HEADING, m];
|
||||
} else if (m = line.match(quotePattern)) {
|
||||
return [TYPE_QUOTE, m];
|
||||
} else if (m = line.match(taskPattern)) {
|
||||
return [TYPE_TASK, m];
|
||||
} else if (m = line.match(doneTaskPattern)) {
|
||||
return [TYPE_DONE_TASK, m];
|
||||
} else if (m = line.match(ulPattern)) {
|
||||
return [TYPE_UL, m];
|
||||
} else if (m = line.match(olPattern)) {
|
||||
return [TYPE_OL, m];
|
||||
} else {
|
||||
return [TYPE_NONE, null];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Event Handler to capture markdown bold button clicks
|
||||
*/
|
||||
markdownBold.addEventListener('click', (e) => {
|
||||
var selectedText = gitbucket.editor.getSelectedText();
|
||||
if (selectedText.length > 0) {
|
||||
var boldText = "**" + selectedText + "**";
|
||||
gitbucket.editor.insert(boldText);
|
||||
}
|
||||
markdownBold.blur();
|
||||
gitbucket.editor.focus();
|
||||
});
|
||||
|
||||
/**
|
||||
* Event Handler to capture markdown italic button clicks
|
||||
*/
|
||||
markdownItalic.addEventListener('click', (e) => {
|
||||
var selectedText = gitbucket.editor.getSelectedText();
|
||||
if (selectedText.length > 0) {
|
||||
var italicText = "*" + selectedText + "*";
|
||||
gitbucket.editor.insert(italicText);
|
||||
}
|
||||
markdownItalic.blur();
|
||||
gitbucket.editor.focus();
|
||||
});
|
||||
|
||||
/**
|
||||
* Event Handler to capture markdown code button clicks
|
||||
*/
|
||||
markdownCode.addEventListener('click', (e) => {
|
||||
var selectedText = gitbucket.editor.getSelectedText();
|
||||
if (selectedText.length > 0) {
|
||||
var codeText = "`" + selectedText + "`";
|
||||
gitbucket.editor.insert(codeText);
|
||||
}
|
||||
markdownCode.blur();
|
||||
gitbucket.editor.focus();
|
||||
});
|
||||
|
||||
/**
|
||||
* Event Handler to capture markdown link button clicks
|
||||
*/
|
||||
markdownLink.addEventListener('click', (e) => {
|
||||
var linkText = "[" + gitbucket.editor.getSelectedText() + "]()";
|
||||
gitbucket.editor.insert(linkText);
|
||||
markdownLink.blur();
|
||||
gitbucket.editor.focus();
|
||||
});
|
||||
|
||||
/**
|
||||
* Event Handler to capture markdown heading button clicks
|
||||
*/
|
||||
markdownHeading.addEventListener('click', (e) => {
|
||||
var range = gitbucket.editor.getSelectionRange();
|
||||
for (row = range.start.row; row <= range.end.row; row++) {
|
||||
gitbucket.editor.clearSelection();
|
||||
gitbucket.editor.selection.moveCursorTo(row, 0);
|
||||
gitbucket.editor.selection.selectLineEnd();
|
||||
var selectedText = gitbucket.editor.getSelectedText();
|
||||
var lineType = getLineType(selectedText);
|
||||
switch(lineType[0]) {
|
||||
case TYPE_HEADING:
|
||||
var level = selectedText.indexOf(' ');
|
||||
if (level < 6) {
|
||||
var headingText = "#" + selectedText;
|
||||
gitbucket.editor.insert(headingText);
|
||||
} else {
|
||||
var headingText = selectedText.replace("###### ","");
|
||||
gitbucket.editor.insert(headingText);
|
||||
}
|
||||
break;
|
||||
case TYPE_NONE:
|
||||
var headingText = "# " + selectedText;
|
||||
gitbucket.editor.insert(headingText);
|
||||
break;
|
||||
default:
|
||||
var headingText = selectedText.replace(lineType[1][0], "# ");
|
||||
gitbucket.editor.insert(headingText);
|
||||
}
|
||||
}
|
||||
markdownHeading.blur();
|
||||
gitbucket.editor.focus();
|
||||
});
|
||||
|
||||
/**
|
||||
* Event Handler to capture markdown quote button clicks
|
||||
*/
|
||||
markdownQuote.addEventListener('click', (e) => {
|
||||
var range = gitbucket.editor.getSelectionRange();
|
||||
for (row = range.start.row; row <= range.end.row; row++) {
|
||||
gitbucket.editor.clearSelection();
|
||||
gitbucket.editor.selection.moveCursorTo(row, 0);
|
||||
gitbucket.editor.selection.selectLineEnd();
|
||||
var selectedText = gitbucket.editor.getSelectedText();
|
||||
var lineType = getLineType(selectedText);
|
||||
switch(lineType[0]) {
|
||||
case TYPE_QUOTE:
|
||||
if (m = selectedText.match(nestedQuotePattern)) {
|
||||
var unquoteText = selectedText.replace(m[0], "");
|
||||
gitbucket.editor.insert(unquoteText);
|
||||
} else {
|
||||
var quoteText = "> " + selectedText;
|
||||
gitbucket.editor.insert(quoteText);
|
||||
}
|
||||
break;
|
||||
case TYPE_NONE:
|
||||
var quoteText = "> " + selectedText;
|
||||
gitbucket.editor.insert(quoteText);
|
||||
break;
|
||||
default:
|
||||
var quoteText = selectedText.replace(lineType[1][0], "> ");
|
||||
gitbucket.editor.insert(quoteText);
|
||||
}
|
||||
}
|
||||
markdownQuote.blur();
|
||||
gitbucket.editor.focus();
|
||||
});
|
||||
|
||||
/**
|
||||
* Event Handler to capture markdown unordered list button clicks
|
||||
*/
|
||||
markdownListUl.addEventListener('click', (e) => {
|
||||
var range = gitbucket.editor.getSelectionRange();
|
||||
for (row = range.start.row; row <= range.end.row; row++) {
|
||||
gitbucket.editor.clearSelection();
|
||||
gitbucket.editor.selection.moveCursorTo(row, 0);
|
||||
gitbucket.editor.selection.selectLineEnd();
|
||||
var selectedText = gitbucket.editor.getSelectedText();
|
||||
var lineType = getLineType(selectedText);
|
||||
switch(lineType[0]) {
|
||||
case TYPE_UL:
|
||||
var unorderedList = selectedText.replace(lineType[1][0], "");
|
||||
gitbucket.editor.insert(unorderedList);
|
||||
break;
|
||||
case TYPE_NONE:
|
||||
var unorderedList = "- " + selectedText;
|
||||
gitbucket.editor.insert(unorderedList);
|
||||
break;
|
||||
default:
|
||||
var unorderedList = selectedText.replace(lineType[1][0], "- ");
|
||||
gitbucket.editor.insert(unorderedList);
|
||||
}
|
||||
}
|
||||
markdownListUl.blur();
|
||||
gitbucket.editor.focus();
|
||||
});
|
||||
|
||||
/**
|
||||
* Event Handler to capture markdown ordered list button clicks
|
||||
*/
|
||||
markdownListOl.addEventListener('click', (e) => {
|
||||
var range = gitbucket.editor.getSelectionRange();
|
||||
for (row = range.start.row; row <= range.end.row; row++) {
|
||||
gitbucket.editor.clearSelection();
|
||||
gitbucket.editor.selection.moveCursorTo(row, 0);
|
||||
gitbucket.editor.selection.selectLineEnd();
|
||||
var selectedText = gitbucket.editor.getSelectedText();
|
||||
var lineType = getLineType(selectedText);
|
||||
switch(lineType[0]) {
|
||||
case TYPE_OL:
|
||||
var orderedList = selectedText.replace(lineType[1][0], "");
|
||||
gitbucket.editor.insert(orderedList);
|
||||
break;
|
||||
case TYPE_NONE:
|
||||
var orderedList = "1. " + selectedText;
|
||||
gitbucket.editor.insert(orderedList);
|
||||
break;
|
||||
default:
|
||||
var orderedList = selectedText.replace(lineType[1][0], "1. ");
|
||||
gitbucket.editor.insert(orderedList);
|
||||
}
|
||||
}
|
||||
markdownListOl.blur();
|
||||
gitbucket.editor.focus();
|
||||
});
|
||||
|
||||
/**
|
||||
* Event Handler to capture markdown task button clicks
|
||||
*/
|
||||
markdownTask.addEventListener('click', (e) => {
|
||||
var range = gitbucket.editor.getSelectionRange();
|
||||
for (row = range.start.row; row <= range.end.row; row++) {
|
||||
gitbucket.editor.clearSelection();
|
||||
gitbucket.editor.selection.moveCursorTo(row, 0);
|
||||
gitbucket.editor.selection.selectLineEnd();
|
||||
var selectedText = gitbucket.editor.getSelectedText();
|
||||
var lineType = getLineType(selectedText);
|
||||
var taskList = "";
|
||||
switch(lineType[0]) {
|
||||
case TYPE_DONE_TASK:
|
||||
taskList = selectedText.replace("[x]", "[ ]");
|
||||
break;
|
||||
case TYPE_TASK:
|
||||
taskList = selectedText.replace("[ ]", "[x]");
|
||||
break;
|
||||
case TYPE_NONE:
|
||||
taskList = "- [ ] " + selectedText;
|
||||
break;
|
||||
default:
|
||||
taskList = selectedText.replace(lineType[1][0], "- [ ] ");
|
||||
}
|
||||
gitbucket.editor.insert(taskList);
|
||||
}
|
||||
markdownTask.blur();
|
||||
gitbucket.editor.focus();
|
||||
});
|
||||
|
||||
/**
|
||||
* Event Handler to capture markdown code block button
|
||||
*/
|
||||
markdownCodeBlock.addEventListener('click', (e) => {
|
||||
var range = gitbucket.editor.getSelectionRange();
|
||||
gitbucket.editor.clearSelection();
|
||||
gitbucket.editor.selection.moveCursorTo(range.start.row, 0);
|
||||
gitbucket.editor.selection.selectLineEnd();
|
||||
gitbucket.editor.selection.moveCursorTo(range.end.row, 0);
|
||||
gitbucket.editor.selection.selectLineEnd();
|
||||
var codeBlockText = "```\n" + gitbucket.editor.getSelectedText() + "\n```";
|
||||
gitbucket.editor.insert(codeBlockText);
|
||||
gitbucket.editor.selection.moveCursorTo(range.start.row, 3);
|
||||
markdownCodeBlock.blur();
|
||||
gitbucket.editor.focus();
|
||||
});
|
||||
|
||||
var originalCompleter = null;
|
||||
|
||||
/**
|
||||
* A function to update the file mode of the Ace editor
|
||||
*/
|
||||
const updateFileMode = function () {
|
||||
if (gitbucket.getFileName() != "") {
|
||||
var modelist = ace.require("ace/ext/modelist");
|
||||
var mode = modelist.getModeForPath(gitbucket.getFileName());
|
||||
gitbucket.editor.getSession().setMode(mode.mode);
|
||||
if (mode.name == "markdown") {
|
||||
markdownToolBar.style.display = "block";
|
||||
gitbucket.editor.completers = mdCompleters ?? [];
|
||||
} else {
|
||||
markdownToolBar.style.display = "none";
|
||||
gitbucket.editor.completers = originalCompleter ?? [];
|
||||
}
|
||||
} else {
|
||||
markdownToolBar.style.display = "none";
|
||||
gitbucket.editor.completers = originalCompleter ?? [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A function to update status of commit button
|
||||
*/
|
||||
const updateCommitButtonStatus = function () {
|
||||
if (gitbucket.editor.getValue() == $('#initial').val() && $('#newFileName').val() == $('#oldFileName').val()) {
|
||||
$('#commitButton').attr('disabled', true);
|
||||
} else {
|
||||
$('#commitButton').attr('disabled', false);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the editor contents
|
||||
$('#editor').text($('#initial').val());
|
||||
|
||||
// Initializing Ace editor
|
||||
const langtools = ace.require("ace/ext/language_tools");
|
||||
gitbucket.editor = ace.edit("editor");
|
||||
|
||||
// Initialize Ace editor keyboard handler
|
||||
var aceKeyboard = localStorage.getItem("gitbucket:editor:keyboard") || "";
|
||||
gitbucket.editor.setKeyboardHandler(aceKeyboard == "" ? null : aceKeyboard);
|
||||
|
||||
var aceKeyboardSelect = document.getElementById("aceKeyboardSelect");
|
||||
aceKeyboardSelect.value = aceKeyboard;
|
||||
|
||||
// Event handler to change the keyboard handler for the Ace editor
|
||||
aceKeyboardSelect.addEventListener('change', () => {
|
||||
gitbucket.editor.setKeyboardHandler(aceKeyboardSelect.value == "" ? null : aceKeyboardSelect.value);
|
||||
localStorage.setItem("gitbucket:editor:keyboard", aceKeyboardSelect.value);
|
||||
}, true)
|
||||
|
||||
// Initialize the Ace editor theme
|
||||
if (typeof localStorage.getItem('gitbucket:editor:theme') == "string") {
|
||||
$('#theme').val(localStorage.getItem('gitbucket:editor:theme'));
|
||||
}
|
||||
|
||||
gitbucket.editor.setTheme("ace/theme/" + $('#theme').val());
|
||||
|
||||
// Event handler to change the Ace editor theme
|
||||
$('#theme').change(function () {
|
||||
gitbucket.editor.setTheme("ace/theme/" + $('#theme').val());
|
||||
localStorage.setItem('gitbucket:editor:theme', $('#theme').val());
|
||||
});
|
||||
|
||||
// Initialize text wrapping for Ace editor
|
||||
if (localStorage.getItem('gitbucket:editor:wrap') == 'true') {
|
||||
gitbucket.editor.getSession().setUseWrapMode(true);
|
||||
$('#wrap').val('true');
|
||||
}
|
||||
|
||||
// Event handler to change text wrapping for Ace editor
|
||||
$('#wrap').change(function () {
|
||||
if ($('#wrap option:selected').val() == 'true') {
|
||||
gitbucket.editor.getSession().setUseWrapMode(true);
|
||||
localStorage.setItem('gitbucket:editor:wrap', 'true');
|
||||
} else {
|
||||
gitbucket.editor.getSession().setUseWrapMode(false);
|
||||
localStorage.setItem('gitbucket:editor:wrap', 'false');
|
||||
}
|
||||
});
|
||||
|
||||
// enable auto completion
|
||||
gitbucket.editor.setOptions({
|
||||
enableBasicAutocompletion: true,
|
||||
enableSnippets: true,
|
||||
enableLiveAutocompletion: true
|
||||
});
|
||||
originalCompleter = gitbucket.editor.completers;
|
||||
|
||||
// Initialize file mode for Ace editor
|
||||
updateFileMode();
|
||||
|
||||
// Determine whether Ace Editor can be edited
|
||||
if (gitbucket.protected) {
|
||||
gitbucket.editor.setReadOnly(true);
|
||||
}
|
||||
|
||||
// Initialize tabSize, newLineMode, and useSoftTabs for the Ace editor.
|
||||
gitbucket.editor.getSession().setOption("tabSize", gitbucket.tabSize);
|
||||
gitbucket.editor.getSession().setOption("newLineMode", gitbucket.newLineMode);
|
||||
gitbucket.editor.getSession().setOption("useSoftTabs", gitbucket.useSoftTabs);
|
||||
|
||||
// Controls the activation of the commit button when using the repository file editor.
|
||||
if (!document.location.href.endsWith("/_edit")) {
|
||||
gitbucket.editor.on('change', function () {
|
||||
updateCommitButtonStatus();
|
||||
});
|
||||
|
||||
$('#newFileName').watch(function () {
|
||||
updateCommitButtonStatus();
|
||||
updateFileMode();
|
||||
});
|
||||
}
|
||||
|
||||
// When the Commit button is clicked, the form content will be overwritten with the editor content.
|
||||
$('#commitButton').click(function () {
|
||||
$('#content').val(gitbucket.editor.getValue());
|
||||
});
|
||||
|
||||
// An event handler that defines what happens when the code button is clicked.
|
||||
$('#btn-code').click(function () {
|
||||
$('#editor').show();
|
||||
$('#preview').hide();
|
||||
$('#btn-preview').removeClass('active');
|
||||
if (clickable) clickable.style.display = "block";
|
||||
});
|
||||
|
||||
// An event handler that defines what happens when the preview button is clicked.
|
||||
$('#btn-preview').click(function () {
|
||||
$('#editor').hide();
|
||||
$('#preview').show();
|
||||
$('#btn-code').removeClass('active');
|
||||
if (clickable) clickable.style.display = "none";
|
||||
|
||||
// Determine if rendering is possible
|
||||
$.get(gitbucket.isRenderableUrl + gitbucket.getFileName(), function (data) {
|
||||
if (data === 'true') {
|
||||
// update preview
|
||||
$('#preview').html(gitbucket.previewTemplate);
|
||||
$.post(gitbucket.previewUrl, {
|
||||
content: gitbucket.editor.getValue(),
|
||||
enableWikiLink: gitbucket.enableWikiLink,
|
||||
filename: gitbucket.getFilePath(),
|
||||
enableRefsLink: false,
|
||||
enableLineBreaks: false,
|
||||
enableTaskList: false
|
||||
}, function (data) {
|
||||
$('#preview').empty().append(
|
||||
$('<div class="markdown-body" style="padding-left: 20px; padding-right: 20px;">').html(data));
|
||||
prettyPrint();
|
||||
});
|
||||
} else {
|
||||
// Show diff
|
||||
$('#preview').empty()
|
||||
.append($('<div id="diffText">'))
|
||||
.append($('<textarea id="newText" style="display: none;">').data('file-name', $("#newFileName").val()).data('val', gitbucket.editor.getValue()))
|
||||
.append($('<textarea id="oldText" style="display: none;">').data('file-name', $("#oldFileName").val()).data('val', $('#initial').val()));
|
||||
diffUsingJS('oldText', 'newText', 'diffText', 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// In the case of a Wiki editor, it controls the parts for attachments and delete button.
|
||||
if (document.location.href.endsWith("/_edit")) {
|
||||
try {
|
||||
// Event handler to capture drag and drop into the editor
|
||||
$('#editor').dropzone({
|
||||
url: gitbucket.uploadUrl,
|
||||
maxFilesize: gitbucket.maxFilesize,
|
||||
timeout: gitbucket.timeout,
|
||||
clickable: false,
|
||||
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
|
||||
success: function (file, id) {
|
||||
var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) + '](' + file.name + ')';
|
||||
gitbucket.editor.session.insert(gitbucket.editor.selection.getCursor(), attachFile);
|
||||
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Event handler to capture drag and drop into the clickable
|
||||
$('.clickable').dropzone({
|
||||
url: gitbucket.uploadUrl,
|
||||
maxFilesize: gitbucket.maxFilesize,
|
||||
timeout: gitbucket.timeout,
|
||||
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
|
||||
success: function (file, id) {
|
||||
var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) + '](' + file.name + ')';
|
||||
gitbucket.editor.session.insert(gitbucket.editor.selection.getCursor(), attachFile);
|
||||
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.message !== "Dropzone already attached.") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// An event handler to capture clicks on the Delete button
|
||||
$('#delete').click(function () {
|
||||
return confirm('Are you sure you want to delete this page?');
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
262
src/main/webapp/assets/common/js/textarea.js
Normal file
262
src/main/webapp/assets/common/js/textarea.js
Normal file
@@ -0,0 +1,262 @@
|
||||
(function () {
|
||||
/**
|
||||
* constants for line type
|
||||
*/
|
||||
const headingPattern = /^#{1,6} /;
|
||||
const quotePattern = /^((>+ )+|>+ )/;
|
||||
const nestedQuotePattern = /^(> ?){2,}/;
|
||||
const taskPattern = /^ *- \[ \] /;
|
||||
const doneTaskPattern = /^ *- \[x\] /;
|
||||
const ulPattern = /^ *- /;
|
||||
const olPattern = /^ *[0-9]. /;
|
||||
|
||||
const TYPE_NONE = 0;
|
||||
const TYPE_HEADING = 1;
|
||||
const TYPE_QUOTE = 2;
|
||||
const TYPE_TASK = 3;
|
||||
const TYPE_DONE_TASK = 4;
|
||||
const TYPE_UL = 5;
|
||||
const TYPE_OL = 6;
|
||||
|
||||
/**
|
||||
* Determine the type of line
|
||||
*
|
||||
* @param {*} line line of editor contents
|
||||
* @returns [TYPE, match]
|
||||
*/
|
||||
const getLineType = function (line) {
|
||||
if (m = line.match(headingPattern)) {
|
||||
return [TYPE_HEADING, m];
|
||||
} else if (m = line.match(quotePattern)) {
|
||||
return [TYPE_QUOTE, m];
|
||||
} else if (m = line.match(taskPattern)) {
|
||||
return [TYPE_TASK, m];
|
||||
} else if (m = line.match(doneTaskPattern)) {
|
||||
return [TYPE_DONE_TASK, m];
|
||||
} else if (m = line.match(ulPattern)) {
|
||||
return [TYPE_UL, m];
|
||||
} else if (m = line.match(olPattern)) {
|
||||
return [TYPE_OL, m];
|
||||
} else {
|
||||
return [TYPE_NONE, null];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Wraps the textarea selection with the specified string
|
||||
*
|
||||
* @param {Element} textarea Target textarea element
|
||||
* @param {string} prefix The string to insert before the selection
|
||||
* @param {string} suffix The string to insert after the selection
|
||||
* @param {boolean} allowNoSelectRange Allow insert even if not selected
|
||||
*/
|
||||
window.surroundSelection = function (textarea, prefix, suffix, allowNoSelectRange) {
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const selectedText = textarea.value.substring(start, end);
|
||||
if (!(selectedText.length == 0 && allowNoSelectRange == false)) {
|
||||
textarea.value = textarea.value.substring(0, start) + prefix + selectedText + suffix + textarea.value.substring(end);
|
||||
const newPosition = start + prefix.length + selectedText.length + suffix.length + (allowNoSelectRange ? -1 : 0);
|
||||
textarea.setSelectionRange(newPosition, newPosition);
|
||||
}
|
||||
textarea.focus();
|
||||
};
|
||||
|
||||
/**
|
||||
* Extend selection to entireLine in textarea
|
||||
*
|
||||
* @param {Element} textarea Target textarea element
|
||||
*/
|
||||
window.extendSelectionToEntireLine = function (textarea) {
|
||||
const text = textarea.value;
|
||||
// Forward line break position (if not found it is -1, so add +1 to make it 0th character)
|
||||
const start = text.lastIndexOf('\n', textarea.selectionStart - 1) + 1;
|
||||
// Rear line break position (if not found, use end of string)
|
||||
let end = text.indexOf('\n', textarea.selectionEnd);
|
||||
if (end === -1) end = text.length;
|
||||
// Update selection to entire row
|
||||
textarea.setSelectionRange(start, end);
|
||||
};
|
||||
|
||||
/**
|
||||
* Make the textarea selection a heading element
|
||||
*
|
||||
* @param {Element} textarea
|
||||
*/
|
||||
window.setHeadding = function (textarea) {
|
||||
extendSelectionToEntireLine(textarea);
|
||||
var newText = "";
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const selectedText = textarea.value.substring(start, end);
|
||||
selectedText.split('\n').forEach((line) => {
|
||||
var headingText = "";
|
||||
var lineType = getLineType(line);
|
||||
switch(lineType[0]) {
|
||||
case TYPE_HEADING:
|
||||
var level = selectedText.indexOf(' ');
|
||||
if (level < 6) {
|
||||
headingText = "#" + line;
|
||||
} else {
|
||||
headingText = line.replace("###### ","");
|
||||
}
|
||||
break;
|
||||
case TYPE_NONE:
|
||||
headingText = "# " + line;
|
||||
break;
|
||||
default:
|
||||
headingText = line.replace(lineType[1][0], "# ");
|
||||
}
|
||||
newText = newText + headingText + "\n";
|
||||
});
|
||||
textarea.value = textarea.value.substring(0, start) + newText.trimEnd() + textarea.value.substring(end);
|
||||
textarea.setSelectionRange(start, start + newText.trimEnd().length);
|
||||
textarea.focus();
|
||||
};
|
||||
|
||||
/**
|
||||
* Make the textarea selection a quote element
|
||||
*
|
||||
* @param {Element} textarea
|
||||
*/
|
||||
window.setQuote = function (textarea) {
|
||||
extendSelectionToEntireLine(textarea);
|
||||
var newText = "";
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const selectedText = textarea.value.substring(start, end);
|
||||
selectedText.split('\n').forEach((line) => {
|
||||
var quoteText = "";
|
||||
var lineType = getLineType(line);
|
||||
switch(lineType[0]) {
|
||||
case TYPE_QUOTE:
|
||||
if (m = line.match(nestedQuotePattern)) {
|
||||
quoteText = line.replace(m[0], "");
|
||||
} else {
|
||||
quoteText = "> " + line;
|
||||
}
|
||||
break;
|
||||
case TYPE_NONE:
|
||||
quoteText = "> " + line;
|
||||
break;
|
||||
default:
|
||||
quoteText = line.replace(lineType[1][0], "> ");
|
||||
}
|
||||
newText = newText + quoteText + "\n";
|
||||
});
|
||||
textarea.value = textarea.value.substring(0, start) + newText.trimEnd() + textarea.value.substring(end);
|
||||
textarea.setSelectionRange(start, start + newText.trimEnd().length);
|
||||
textarea.focus();
|
||||
};
|
||||
|
||||
/**
|
||||
* Make the textarea selection a unordered list element
|
||||
*
|
||||
* @param {Element} textarea
|
||||
*/
|
||||
window.setUnorderedList = function (textarea) {
|
||||
extendSelectionToEntireLine(textarea);
|
||||
var newText = "";
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const selectedText = textarea.value.substring(start, end);
|
||||
selectedText.split('\n').forEach((line) => {
|
||||
var unorderedList = "";
|
||||
var lineType = getLineType(selectedText);
|
||||
switch(lineType[0]) {
|
||||
case TYPE_UL:
|
||||
unorderedList = line.replace(lineType[1][0], "");
|
||||
break;
|
||||
case TYPE_NONE:
|
||||
unorderedList = "- " + line;
|
||||
break;
|
||||
default:
|
||||
unorderedList = line.replace(lineType[1][0], "- ");
|
||||
}
|
||||
newText = newText + unorderedList + "\n";
|
||||
});
|
||||
textarea.value = textarea.value.substring(0, start) + newText.trimEnd() + textarea.value.substring(end);
|
||||
textarea.setSelectionRange(start, start + newText.trimEnd().length);
|
||||
textarea.focus();
|
||||
};
|
||||
|
||||
/**
|
||||
* Make the textarea selection a ordered list element
|
||||
*
|
||||
* @param {Element} textarea
|
||||
*/
|
||||
window.setOrderedList = function (textarea) {
|
||||
extendSelectionToEntireLine(textarea);
|
||||
var newText = "";
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const selectedText = textarea.value.substring(start, end);
|
||||
selectedText.split('\n').forEach((line) => {
|
||||
var orderedList = "";
|
||||
var lineType = getLineType(selectedText);
|
||||
switch(lineType[0]) {
|
||||
case TYPE_OL:
|
||||
orderedList = line.replace(lineType[1][0], "");
|
||||
break;
|
||||
case TYPE_NONE:
|
||||
orderedList = "1. " + line;
|
||||
break;
|
||||
default:
|
||||
orderedList = line.replace(lineType[1][0], "1. ");
|
||||
}
|
||||
newText = newText + orderedList + "\n";
|
||||
});
|
||||
textarea.value = textarea.value.substring(0, start) + newText.trimEnd() + textarea.value.substring(end);
|
||||
textarea.setSelectionRange(start, start + newText.trimEnd().length);
|
||||
textarea.focus();
|
||||
};
|
||||
|
||||
/**
|
||||
* Make the textarea selection a task list element
|
||||
*
|
||||
* @param {Element} textarea
|
||||
*/
|
||||
window.setTaskList = function (textarea) {
|
||||
extendSelectionToEntireLine(textarea);
|
||||
var newText = "";
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const selectedText = textarea.value.substring(start, end);
|
||||
selectedText.split('\n').forEach((line) => {
|
||||
var taskList = "";
|
||||
var lineType = getLineType(line);
|
||||
switch(lineType[0]) {
|
||||
case TYPE_DONE_TASK:
|
||||
taskList = line.replace("[x]", "[ ]");
|
||||
break;
|
||||
case TYPE_TASK:
|
||||
taskList = line.replace("[ ]", "[x]");
|
||||
break;
|
||||
case TYPE_NONE:
|
||||
taskList = "- [ ] " + line;
|
||||
break;
|
||||
default:
|
||||
taskList = line.replace(lineType[1][0], "- [ ] ");
|
||||
}
|
||||
newText = newText + taskList + "\n";
|
||||
});
|
||||
textarea.value = textarea.value.substring(0, start) + newText.trimEnd() + textarea.value.substring(end);
|
||||
textarea.setSelectionRange(start, start + newText.trimEnd().length);
|
||||
textarea.focus();
|
||||
};
|
||||
/**
|
||||
* Make the textarea selection a unordered list element
|
||||
*
|
||||
* @param {Element} textarea
|
||||
*/
|
||||
window.setCodeBlock = function (textarea) {
|
||||
extendSelectionToEntireLine(textarea);
|
||||
var newText = "";
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const selectedText = textarea.value.substring(start, end);
|
||||
textarea.value = textarea.value.substring(0, start) + "```\n" + selectedText + "\n```\n" + textarea.value.substring(end);
|
||||
textarea.setSelectionRange(start + 3, start + 3);
|
||||
textarea.focus();
|
||||
};
|
||||
})(window);
|
||||
24726
src/main/webapp/assets/vendors/ace/ace.js
vendored
24726
src/main/webapp/assets/vendors/ace/ace.js
vendored
File diff suppressed because one or more lines are too long
656
src/main/webapp/assets/vendors/ace/ext-beautify.js
vendored
656
src/main/webapp/assets/vendors/ace/ext-beautify.js
vendored
@@ -1,333 +1,343 @@
|
||||
define("ace/ext/beautify/php_rules",["require","exports","module","ace/token_iterator"], function(require, exports, module) {
|
||||
define("ace/ext/beautify",["require","exports","module","ace/token_iterator"], function(require, exports, module){/**
|
||||
* ## Code beautification and formatting extension.
|
||||
*
|
||||
* **This extension is considered outdated.** For better formatting support with modern language servers
|
||||
* and advanced formatting capabilities, consider using [ace-linters](https://github.com/mkslanc/ace-linters)
|
||||
* which provides comprehensive language support including formatting, linting, and IntelliSense features.
|
||||
*
|
||||
* This legacy extension provides basic formatting for HTML, CSS, JavaScript, and PHP code with support for
|
||||
* proper indentation, whitespace management, line breaks, and bracket alignment. It handles various language
|
||||
* constructs including HTML tags, CSS selectors, JavaScript operators, control structures, and maintains
|
||||
* consistent code style throughout the document.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
"use strict";
|
||||
var TokenIterator = require("ace/token_iterator").TokenIterator;
|
||||
exports.newLines = [{
|
||||
type: 'support.php_tag',
|
||||
value: '<?php'
|
||||
}, {
|
||||
type: 'support.php_tag',
|
||||
value: '<?'
|
||||
}, {
|
||||
type: 'support.php_tag',
|
||||
value: '?>'
|
||||
}, {
|
||||
type: 'paren.lparen',
|
||||
value: '{',
|
||||
indent: true
|
||||
}, {
|
||||
type: 'paren.rparen',
|
||||
breakBefore: true,
|
||||
value: '}',
|
||||
indent: false
|
||||
}, {
|
||||
type: 'paren.rparen',
|
||||
breakBefore: true,
|
||||
value: '})',
|
||||
indent: false,
|
||||
dontBreak: true
|
||||
}, {
|
||||
type: 'comment'
|
||||
}, {
|
||||
type: 'text',
|
||||
value: ';'
|
||||
}, {
|
||||
type: 'text',
|
||||
value: ':',
|
||||
context: 'php'
|
||||
}, {
|
||||
type: 'keyword',
|
||||
value: 'case',
|
||||
indent: true,
|
||||
dontBreak: true
|
||||
}, {
|
||||
type: 'keyword',
|
||||
value: 'default',
|
||||
indent: true,
|
||||
dontBreak: true
|
||||
}, {
|
||||
type: 'keyword',
|
||||
value: 'break',
|
||||
indent: false,
|
||||
dontBreak: true
|
||||
}, {
|
||||
type: 'punctuation.doctype.end',
|
||||
value: '>'
|
||||
}, {
|
||||
type: 'meta.tag.punctuation.end',
|
||||
value: '>'
|
||||
}, {
|
||||
type: 'meta.tag.punctuation.begin',
|
||||
value: '<',
|
||||
blockTag: true,
|
||||
indent: true,
|
||||
dontBreak: true
|
||||
}, {
|
||||
type: 'meta.tag.punctuation.begin',
|
||||
value: '</',
|
||||
indent: false,
|
||||
breakBefore: true,
|
||||
dontBreak: true
|
||||
}, {
|
||||
type: 'punctuation.operator',
|
||||
value: ';'
|
||||
}];
|
||||
|
||||
exports.spaces = [{
|
||||
type: 'xml-pe',
|
||||
prepend: true
|
||||
},{
|
||||
type: 'entity.other.attribute-name',
|
||||
prepend: true
|
||||
}, {
|
||||
type: 'storage.type',
|
||||
value: 'var',
|
||||
append: true
|
||||
}, {
|
||||
type: 'storage.type',
|
||||
value: 'function',
|
||||
append: true
|
||||
}, {
|
||||
type: 'keyword.operator',
|
||||
value: '='
|
||||
}, {
|
||||
type: 'keyword',
|
||||
value: 'as',
|
||||
prepend: true,
|
||||
append: true
|
||||
}, {
|
||||
type: 'keyword',
|
||||
value: 'function',
|
||||
append: true
|
||||
}, {
|
||||
type: 'support.function',
|
||||
next: /[^\(]/,
|
||||
append: true
|
||||
}, {
|
||||
type: 'keyword',
|
||||
value: 'or',
|
||||
append: true,
|
||||
prepend: true
|
||||
}, {
|
||||
type: 'keyword',
|
||||
value: 'and',
|
||||
append: true,
|
||||
prepend: true
|
||||
}, {
|
||||
type: 'keyword',
|
||||
value: 'case',
|
||||
append: true
|
||||
}, {
|
||||
type: 'keyword.operator',
|
||||
value: '||',
|
||||
append: true,
|
||||
prepend: true
|
||||
}, {
|
||||
type: 'keyword.operator',
|
||||
value: '&&',
|
||||
append: true,
|
||||
prepend: true
|
||||
}];
|
||||
exports.singleTags = ['!doctype','area','base','br','hr','input','img','link','meta'];
|
||||
|
||||
exports.transform = function(iterator, maxPos, context) {
|
||||
var token = iterator.getCurrentToken();
|
||||
|
||||
var newLines = exports.newLines;
|
||||
var spaces = exports.spaces;
|
||||
var singleTags = exports.singleTags;
|
||||
|
||||
var code = '';
|
||||
|
||||
var indentation = 0;
|
||||
var dontBreak = false;
|
||||
var tag;
|
||||
var lastTag;
|
||||
var lastToken = {};
|
||||
var nextTag;
|
||||
var nextToken = {};
|
||||
var breakAdded = false;
|
||||
var value = '';
|
||||
|
||||
while (token!==null) {
|
||||
|
||||
if( !token ){
|
||||
token = iterator.stepForward();
|
||||
continue;
|
||||
}
|
||||
if( token.type == 'support.php_tag' && token.value != '?>' ){
|
||||
context = 'php';
|
||||
}
|
||||
else if( token.type == 'support.php_tag' && token.value == '?>' ){
|
||||
context = 'html';
|
||||
}
|
||||
else if( token.type == 'meta.tag.name.style' && context != 'css' ){
|
||||
context = 'css';
|
||||
}
|
||||
else if( token.type == 'meta.tag.name.style' && context == 'css' ){
|
||||
context = 'html';
|
||||
}
|
||||
else if( token.type == 'meta.tag.name.script' && context != 'js' ){
|
||||
context = 'js';
|
||||
}
|
||||
else if( token.type == 'meta.tag.name.script' && context == 'js' ){
|
||||
context = 'html';
|
||||
}
|
||||
|
||||
nextToken = iterator.stepForward();
|
||||
if (nextToken && nextToken.type.indexOf('meta.tag.name') == 0) {
|
||||
nextTag = nextToken.value;
|
||||
}
|
||||
if ( lastToken.type == 'support.php_tag' && lastToken.value == '<?=') {
|
||||
dontBreak = true;
|
||||
}
|
||||
if (token.type == 'meta.tag.name') {
|
||||
token.value = token.value.toLowerCase();
|
||||
}
|
||||
if (token.type == 'text') {
|
||||
token.value = token.value.trim();
|
||||
}
|
||||
if (!token.value) {
|
||||
token = nextToken;
|
||||
continue;
|
||||
}
|
||||
value = token.value;
|
||||
for (var i in spaces) {
|
||||
if (
|
||||
token.type == spaces[i].type &&
|
||||
(!spaces[i].value || token.value == spaces[i].value) &&
|
||||
(
|
||||
nextToken &&
|
||||
(!spaces[i].next || spaces[i].next.test(nextToken.value))
|
||||
)
|
||||
) {
|
||||
if (spaces[i].prepend) {
|
||||
value = ' ' + token.value;
|
||||
}
|
||||
|
||||
if (spaces[i].append) {
|
||||
value += ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (token.type.indexOf('meta.tag.name') == 0) {
|
||||
tag = token.value;
|
||||
}
|
||||
breakAdded = false;
|
||||
for (i in newLines) {
|
||||
if (
|
||||
token.type == newLines[i].type &&
|
||||
(
|
||||
!newLines[i].value ||
|
||||
token.value == newLines[i].value
|
||||
) &&
|
||||
(
|
||||
!newLines[i].blockTag ||
|
||||
singleTags.indexOf(nextTag) === -1
|
||||
) &&
|
||||
(
|
||||
!newLines[i].context ||
|
||||
newLines[i].context === context
|
||||
)
|
||||
) {
|
||||
if (newLines[i].indent === false) {
|
||||
indentation--;
|
||||
}
|
||||
|
||||
if (
|
||||
newLines[i].breakBefore &&
|
||||
( !newLines[i].prev || newLines[i].prev.test(lastToken.value) )
|
||||
) {
|
||||
code += "\n";
|
||||
breakAdded = true;
|
||||
for (i = 0; i < indentation; i++) {
|
||||
code += "\t";
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dontBreak===false) {
|
||||
for (i in newLines) {
|
||||
if (
|
||||
lastToken.type == newLines[i].type &&
|
||||
(
|
||||
!newLines[i].value || lastToken.value == newLines[i].value
|
||||
) &&
|
||||
(
|
||||
!newLines[i].blockTag ||
|
||||
singleTags.indexOf(tag) === -1
|
||||
) &&
|
||||
(
|
||||
!newLines[i].context ||
|
||||
newLines[i].context === context
|
||||
)
|
||||
) {
|
||||
if (newLines[i].indent === true) {
|
||||
indentation++;
|
||||
}
|
||||
|
||||
if (!newLines[i].dontBreak && !breakAdded) {
|
||||
code += "\n";
|
||||
for (i = 0; i < indentation; i++) {
|
||||
code += "\t";
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
code += value;
|
||||
if ( lastToken.type == 'support.php_tag' && lastToken.value == '?>' ) {
|
||||
dontBreak = false;
|
||||
}
|
||||
lastTag = tag;
|
||||
|
||||
lastToken = token;
|
||||
|
||||
token = nextToken;
|
||||
|
||||
if (token===null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return code;
|
||||
var TokenIterator = require("../token_iterator").TokenIterator;
|
||||
function is(token, type) {
|
||||
return token.type.lastIndexOf(type + ".xml") > -1;
|
||||
}
|
||||
exports.singletonTags = ["area", "base", "br", "col", "command", "embed", "hr", "html", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"];
|
||||
exports.blockTags = ["article", "aside", "blockquote", "body", "div", "dl", "fieldset", "footer", "form", "head", "header", "html", "nav", "ol", "p", "script", "section", "style", "table", "tbody", "tfoot", "thead", "ul"];
|
||||
exports.formatOptions = {
|
||||
lineBreaksAfterCommasInCurlyBlock: true
|
||||
};
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
define("ace/ext/beautify",["require","exports","module","ace/token_iterator","ace/ext/beautify/php_rules"], function(require, exports, module) {
|
||||
"use strict";
|
||||
var TokenIterator = require("ace/token_iterator").TokenIterator;
|
||||
|
||||
var phpTransform = require("./beautify/php_rules").transform;
|
||||
|
||||
exports.beautify = function(session) {
|
||||
exports.beautify = function (session) {
|
||||
var iterator = new TokenIterator(session, 0, 0);
|
||||
var token = iterator.getCurrentToken();
|
||||
|
||||
var context = session.$modeId.split("/").pop();
|
||||
|
||||
var code = phpTransform(iterator, context);
|
||||
var tabString = session.getTabString();
|
||||
var singletonTags = exports.singletonTags;
|
||||
var blockTags = exports.blockTags;
|
||||
var formatOptions = exports.formatOptions || {};
|
||||
var nextToken;
|
||||
var breakBefore = false;
|
||||
var spaceBefore = false;
|
||||
var spaceAfter = false;
|
||||
var code = "";
|
||||
var value = "";
|
||||
var tagName = "";
|
||||
var depth = 0;
|
||||
var lastDepth = 0;
|
||||
var lastIndent = 0;
|
||||
var indent = 0;
|
||||
var unindent = 0;
|
||||
var roundDepth = 0;
|
||||
var curlyDepth = 0;
|
||||
var row;
|
||||
var curRow = 0;
|
||||
var rowsToAdd = 0;
|
||||
var rowTokens = [];
|
||||
var abort = false;
|
||||
var i;
|
||||
var indentNextLine = false;
|
||||
var inTag = false;
|
||||
var inCSS = false;
|
||||
var inBlock = false;
|
||||
var levels = { 0: 0 };
|
||||
var parents = [];
|
||||
var caseBody = false;
|
||||
var trimNext = function () {
|
||||
if (nextToken && nextToken.value && nextToken.type !== 'string.regexp')
|
||||
nextToken.value = nextToken.value.replace(/^\s*/, "");
|
||||
};
|
||||
var trimLine = function () {
|
||||
var end = code.length - 1;
|
||||
while (true) {
|
||||
if (end == 0)
|
||||
break;
|
||||
if (code[end] !== " ")
|
||||
break;
|
||||
end = end - 1;
|
||||
}
|
||||
code = code.slice(0, end + 1);
|
||||
};
|
||||
var trimCode = function () {
|
||||
code = code.trimRight();
|
||||
breakBefore = false;
|
||||
};
|
||||
while (token !== null) {
|
||||
curRow = iterator.getCurrentTokenRow();
|
||||
rowTokens = iterator.$rowTokens;
|
||||
nextToken = iterator.stepForward();
|
||||
if (typeof token !== "undefined") {
|
||||
value = token.value;
|
||||
unindent = 0;
|
||||
inCSS = (tagName === "style" || session.$modeId === "ace/mode/css");
|
||||
if (is(token, "tag-open")) {
|
||||
inTag = true;
|
||||
if (nextToken)
|
||||
inBlock = (blockTags.indexOf(nextToken.value) !== -1);
|
||||
if (value === "</") {
|
||||
if (inBlock && !breakBefore && rowsToAdd < 1)
|
||||
rowsToAdd++;
|
||||
if (inCSS)
|
||||
rowsToAdd = 1;
|
||||
unindent = 1;
|
||||
inBlock = false;
|
||||
}
|
||||
}
|
||||
else if (is(token, "tag-close")) {
|
||||
inTag = false;
|
||||
}
|
||||
else if (is(token, "comment.start")) {
|
||||
inBlock = true;
|
||||
}
|
||||
else if (is(token, "comment.end")) {
|
||||
inBlock = false;
|
||||
}
|
||||
if (!inTag && !rowsToAdd && token.type === "paren.rparen" && token.value.substr(0, 1) === "}") {
|
||||
rowsToAdd++;
|
||||
}
|
||||
if (curRow !== row) {
|
||||
rowsToAdd = curRow;
|
||||
if (row)
|
||||
rowsToAdd -= row;
|
||||
}
|
||||
if (rowsToAdd) {
|
||||
trimCode();
|
||||
for (; rowsToAdd > 0; rowsToAdd--)
|
||||
code += "\n";
|
||||
breakBefore = true;
|
||||
if (!is(token, "comment") && !token.type.match(/^(comment|string)$/))
|
||||
value = value.trimLeft();
|
||||
}
|
||||
if (value) {
|
||||
if (token.type === "keyword" && value.match(/^(if|else|elseif|for|foreach|while|switch)$/)) {
|
||||
parents[depth] = value;
|
||||
trimNext();
|
||||
spaceAfter = true;
|
||||
if (value.match(/^(else|elseif)$/)) {
|
||||
if (code.match(/\}[\s]*$/)) {
|
||||
trimCode();
|
||||
spaceBefore = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (token.type === "paren.lparen") {
|
||||
trimNext();
|
||||
if (value.substr(-1) === "{") {
|
||||
spaceAfter = true;
|
||||
indentNextLine = false;
|
||||
if (!inTag)
|
||||
rowsToAdd = 1;
|
||||
}
|
||||
if (value.substr(0, 1) === "{") {
|
||||
spaceBefore = true;
|
||||
if (code.substr(-1) !== '[' && code.trimRight().substr(-1) === '[') {
|
||||
trimCode();
|
||||
spaceBefore = false;
|
||||
}
|
||||
else if (code.trimRight().substr(-1) === ')') {
|
||||
trimCode();
|
||||
}
|
||||
else {
|
||||
trimLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (token.type === "paren.rparen") {
|
||||
unindent = 1;
|
||||
if (value.substr(0, 1) === "}") {
|
||||
if (parents[depth - 1] === 'case')
|
||||
unindent++;
|
||||
if (code.trimRight().substr(-1) === '{') {
|
||||
trimCode();
|
||||
}
|
||||
else {
|
||||
spaceBefore = true;
|
||||
if (inCSS)
|
||||
rowsToAdd += 2;
|
||||
}
|
||||
}
|
||||
if (value.substr(0, 1) === "]") {
|
||||
if (code.substr(-1) !== '}' && code.trimRight().substr(-1) === '}') {
|
||||
spaceBefore = false;
|
||||
indent++;
|
||||
trimCode();
|
||||
}
|
||||
}
|
||||
if (value.substr(0, 1) === ")") {
|
||||
if (code.substr(-1) !== '(' && code.trimRight().substr(-1) === '(') {
|
||||
spaceBefore = false;
|
||||
indent++;
|
||||
trimCode();
|
||||
}
|
||||
}
|
||||
trimLine();
|
||||
}
|
||||
else if ((token.type === "keyword.operator" || token.type === "keyword") && value.match(/^(=|==|===|!=|!==|&&|\|\||and|or|xor|\+=|.=|>|>=|<|<=|=>)$/)) {
|
||||
trimCode();
|
||||
trimNext();
|
||||
spaceBefore = true;
|
||||
spaceAfter = true;
|
||||
}
|
||||
else if (token.type === "punctuation.operator" && value === ';') {
|
||||
trimCode();
|
||||
trimNext();
|
||||
spaceAfter = true;
|
||||
if (inCSS)
|
||||
rowsToAdd++;
|
||||
}
|
||||
else if (token.type === "punctuation.operator" && value.match(/^(:|,)$/)) {
|
||||
trimCode();
|
||||
trimNext();
|
||||
if (value.match(/^(,)$/) && curlyDepth > 0 && roundDepth === 0 && formatOptions.lineBreaksAfterCommasInCurlyBlock) {
|
||||
rowsToAdd++;
|
||||
}
|
||||
else {
|
||||
spaceAfter = true;
|
||||
breakBefore = false;
|
||||
}
|
||||
}
|
||||
else if (token.type === "support.php_tag" && value === "?>" && !breakBefore) {
|
||||
trimCode();
|
||||
spaceBefore = true;
|
||||
}
|
||||
else if (is(token, "attribute-name") && code.substr(-1).match(/^\s$/)) {
|
||||
spaceBefore = true;
|
||||
}
|
||||
else if (is(token, "attribute-equals")) {
|
||||
trimLine();
|
||||
trimNext();
|
||||
}
|
||||
else if (is(token, "tag-close")) {
|
||||
trimLine();
|
||||
if (value === "/>")
|
||||
spaceBefore = true;
|
||||
}
|
||||
else if (token.type === "keyword" && value.match(/^(case|default)$/)) {
|
||||
if (caseBody)
|
||||
unindent = 1;
|
||||
}
|
||||
if (breakBefore && !(token.type.match(/^(comment)$/) && !value.substr(0, 1).match(/^[/#]$/)) && !(token.type.match(/^(string)$/) && !value.substr(0, 1).match(/^['"@]$/))) {
|
||||
indent = lastIndent;
|
||||
if (depth > lastDepth) {
|
||||
indent++;
|
||||
for (i = depth; i > lastDepth; i--)
|
||||
levels[i] = indent;
|
||||
}
|
||||
else if (depth < lastDepth)
|
||||
indent = levels[depth];
|
||||
lastDepth = depth;
|
||||
lastIndent = indent;
|
||||
if (unindent)
|
||||
indent -= unindent;
|
||||
if (indentNextLine && !roundDepth) {
|
||||
indent++;
|
||||
indentNextLine = false;
|
||||
}
|
||||
for (i = 0; i < indent; i++)
|
||||
code += tabString;
|
||||
}
|
||||
if (token.type === "keyword" && value.match(/^(case|default)$/)) {
|
||||
if (caseBody === false) {
|
||||
parents[depth] = value;
|
||||
depth++;
|
||||
caseBody = true;
|
||||
}
|
||||
}
|
||||
else if (token.type === "keyword" && value.match(/^(break)$/)) {
|
||||
if (parents[depth - 1] && parents[depth - 1].match(/^(case|default)$/)) {
|
||||
depth--;
|
||||
caseBody = false;
|
||||
}
|
||||
}
|
||||
if (token.type === "paren.lparen") {
|
||||
roundDepth += (value.match(/\(/g) || []).length;
|
||||
curlyDepth += (value.match(/\{/g) || []).length;
|
||||
depth += value.length;
|
||||
}
|
||||
if (token.type === "keyword" && value.match(/^(if|else|elseif|for|while)$/)) {
|
||||
indentNextLine = true;
|
||||
roundDepth = 0;
|
||||
}
|
||||
else if (!roundDepth && value.trim() && token.type !== "comment")
|
||||
indentNextLine = false;
|
||||
if (token.type === "paren.rparen") {
|
||||
roundDepth -= (value.match(/\)/g) || []).length;
|
||||
curlyDepth -= (value.match(/\}/g) || []).length;
|
||||
for (i = 0; i < value.length; i++) {
|
||||
depth--;
|
||||
if (value.substr(i, 1) === '}' && parents[depth] === 'case') {
|
||||
depth--;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (token.type == "text")
|
||||
value = value.replace(/\s+$/, " ");
|
||||
if (spaceBefore && !breakBefore) {
|
||||
trimLine();
|
||||
if (code.substr(-1) !== "\n")
|
||||
code += " ";
|
||||
}
|
||||
code += value;
|
||||
if (spaceAfter)
|
||||
code += " ";
|
||||
breakBefore = false;
|
||||
spaceBefore = false;
|
||||
spaceAfter = false;
|
||||
if ((is(token, "tag-close") && (inBlock || blockTags.indexOf(tagName) !== -1)) || (is(token, "doctype") && value === ">")) {
|
||||
if (inBlock && nextToken && nextToken.value === "</")
|
||||
rowsToAdd = -1;
|
||||
else
|
||||
rowsToAdd = 1;
|
||||
}
|
||||
if (nextToken && singletonTags.indexOf(nextToken.value) === -1) {
|
||||
if (is(token, "tag-open") && value === "</") {
|
||||
depth--;
|
||||
}
|
||||
else if (is(token, "tag-open") && value === "<") {
|
||||
depth++;
|
||||
}
|
||||
else if (is(token, "tag-close") && value === "/>") {
|
||||
depth--;
|
||||
}
|
||||
}
|
||||
if (is(token, "tag-name")) {
|
||||
tagName = value;
|
||||
}
|
||||
row = curRow;
|
||||
}
|
||||
}
|
||||
token = nextToken;
|
||||
}
|
||||
code = code.trim();
|
||||
session.doc.setValue(code);
|
||||
};
|
||||
|
||||
exports.commands = [{
|
||||
name: "beautify",
|
||||
exec: function(editor) {
|
||||
exports.beautify(editor.session);
|
||||
},
|
||||
bindKey: "Ctrl-Shift-B"
|
||||
}];
|
||||
name: "beautify",
|
||||
description: "Format selection (Beautify)",
|
||||
exec: function (editor) {
|
||||
exports.beautify(editor.session);
|
||||
},
|
||||
bindKey: "Ctrl-Shift-B"
|
||||
}];
|
||||
|
||||
});
|
||||
(function() {
|
||||
window.require(["ace/ext/beautify"], function() {});
|
||||
}); (function() {
|
||||
window.require(["ace/ext/beautify"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
204
src/main/webapp/assets/vendors/ace/ext-code_lens.js
vendored
Normal file
204
src/main/webapp/assets/vendors/ace/ext-code_lens.js
vendored
Normal file
@@ -0,0 +1,204 @@
|
||||
define("ace/ext/code_lens",["require","exports","module","ace/lib/event","ace/lib/lang","ace/lib/dom","ace/editor","ace/config"], function(require, exports, module){/**
|
||||
* ## Code Lens extension.
|
||||
*
|
||||
* Displaying contextual information and clickable commands above code lines. Supports registering custom providers,
|
||||
* rendering lens widgets with proper positioning and styling, and handling user interactions with lens commands.
|
||||
* @module
|
||||
*/
|
||||
"use strict";
|
||||
var event = require("../lib/event");
|
||||
var lang = require("../lib/lang");
|
||||
var dom = require("../lib/dom");
|
||||
function clearLensElements(renderer) {
|
||||
var textLayer = renderer.$textLayer;
|
||||
var lensElements = textLayer.$lenses;
|
||||
if (lensElements)
|
||||
lensElements.forEach(function (el) { el.remove(); });
|
||||
textLayer.$lenses = null;
|
||||
}
|
||||
function renderWidgets(changes, renderer) {
|
||||
var changed = changes & renderer.CHANGE_LINES
|
||||
|| changes & renderer.CHANGE_FULL
|
||||
|| changes & renderer.CHANGE_SCROLL
|
||||
|| changes & renderer.CHANGE_TEXT;
|
||||
if (!changed)
|
||||
return;
|
||||
var session = renderer.session;
|
||||
var lineWidgets = renderer.session.lineWidgets;
|
||||
var textLayer = renderer.$textLayer;
|
||||
var lensElements = textLayer.$lenses;
|
||||
if (!lineWidgets) {
|
||||
if (lensElements)
|
||||
clearLensElements(renderer);
|
||||
return;
|
||||
}
|
||||
var textCells = renderer.$textLayer.$lines.cells;
|
||||
var config = renderer.layerConfig;
|
||||
var padding = renderer.$padding;
|
||||
if (!lensElements)
|
||||
lensElements = textLayer.$lenses = [];
|
||||
var index = 0;
|
||||
for (var i = 0; i < textCells.length; i++) {
|
||||
var row = textCells[i].row;
|
||||
var widget = lineWidgets[row];
|
||||
var lenses = widget && widget.lenses;
|
||||
if (!lenses || !lenses.length)
|
||||
continue;
|
||||
var lensContainer = lensElements[index];
|
||||
if (!lensContainer) {
|
||||
lensContainer = lensElements[index]
|
||||
= dom.buildDom(["div", { class: "ace_codeLens" }], renderer.container);
|
||||
}
|
||||
lensContainer.style.height = config.lineHeight + "px";
|
||||
index++;
|
||||
for (var j = 0; j < lenses.length; j++) {
|
||||
var el = lensContainer.childNodes[2 * j];
|
||||
if (!el) {
|
||||
if (j != 0)
|
||||
lensContainer.appendChild(dom.createTextNode("\xa0|\xa0"));
|
||||
el = dom.buildDom(["a"], lensContainer);
|
||||
}
|
||||
el.textContent = lenses[j].title;
|
||||
(el).lensCommand = lenses[j];
|
||||
}
|
||||
while (lensContainer.childNodes.length > 2 * j - 1)
|
||||
lensContainer.lastChild.remove();
|
||||
var top = renderer.$cursorLayer.getPixelPosition({
|
||||
row: row,
|
||||
column: 0
|
||||
}, true).top - config.lineHeight * widget.rowsAbove - config.offset;
|
||||
lensContainer.style.top = top + "px";
|
||||
var left = renderer.gutterWidth;
|
||||
var indent = session.getLine(row).search(/\S|$/);
|
||||
if (indent == -1)
|
||||
indent = 0;
|
||||
left += indent * config.characterWidth;
|
||||
lensContainer.style.paddingLeft = padding + left + "px";
|
||||
}
|
||||
while (index < lensElements.length)
|
||||
lensElements.pop().remove();
|
||||
}
|
||||
function clearCodeLensWidgets(session) {
|
||||
if (!session.lineWidgets)
|
||||
return;
|
||||
var widgetManager = session.widgetManager;
|
||||
session.lineWidgets.forEach(function (widget) {
|
||||
if (widget && widget.lenses)
|
||||
widgetManager.removeLineWidget(widget);
|
||||
});
|
||||
}
|
||||
exports.setLenses = function (session, lenses) {
|
||||
var firstRow = Number.MAX_VALUE;
|
||||
clearCodeLensWidgets(session);
|
||||
lenses && lenses.forEach(function (lens) {
|
||||
var row = lens.start.row;
|
||||
var column = lens.start.column;
|
||||
var widget = session.lineWidgets && session.lineWidgets[row];
|
||||
if (!widget || !widget.lenses) {
|
||||
widget = session.widgetManager.$registerLineWidget({
|
||||
rowCount: 1,
|
||||
rowsAbove: 1,
|
||||
row: row,
|
||||
column: column,
|
||||
lenses: []
|
||||
});
|
||||
}
|
||||
widget.lenses.push(lens.command);
|
||||
if (row < firstRow)
|
||||
firstRow = row;
|
||||
});
|
||||
session._emit("changeFold", { data: { start: { row: firstRow } } });
|
||||
return firstRow;
|
||||
};
|
||||
function attachToEditor(editor) {
|
||||
editor.codeLensProviders = [];
|
||||
editor.renderer.on("afterRender", renderWidgets);
|
||||
if (!editor.$codeLensClickHandler) {
|
||||
editor.$codeLensClickHandler = function (e) {
|
||||
var command = e.target.lensCommand;
|
||||
if (!command)
|
||||
return;
|
||||
editor.execCommand(command.id, command.arguments);
|
||||
editor._emit("codeLensClick", e);
|
||||
};
|
||||
event.addListener(editor.container, "click", editor.$codeLensClickHandler, editor);
|
||||
}
|
||||
editor.$updateLenses = function () {
|
||||
var session = editor.session;
|
||||
if (!session)
|
||||
return;
|
||||
var providersToWaitNum = editor.codeLensProviders.length;
|
||||
var lenses = [];
|
||||
editor.codeLensProviders.forEach(function (provider) {
|
||||
provider.provideCodeLenses(session, function (err, payload) {
|
||||
if (err)
|
||||
return;
|
||||
payload.forEach(function (lens) {
|
||||
lenses.push(lens);
|
||||
});
|
||||
providersToWaitNum--;
|
||||
if (providersToWaitNum == 0) {
|
||||
applyLenses();
|
||||
}
|
||||
});
|
||||
});
|
||||
function applyLenses() {
|
||||
var cursor = session.selection.cursor;
|
||||
var oldRow = session.documentToScreenRow(cursor);
|
||||
var scrollTop = session.getScrollTop();
|
||||
var firstRow = exports.setLenses(session, lenses);
|
||||
var lastDelta = session.$undoManager && session.$undoManager.$lastDelta;
|
||||
if (lastDelta && lastDelta.action == "remove" && lastDelta.lines.length > 1)
|
||||
return;
|
||||
var row = session.documentToScreenRow(cursor);
|
||||
var lineHeight = editor.renderer.layerConfig.lineHeight;
|
||||
var top = session.getScrollTop() + (row - oldRow) * lineHeight;
|
||||
if (firstRow == 0 && scrollTop < lineHeight / 4 && scrollTop > -lineHeight / 4) {
|
||||
top = -lineHeight;
|
||||
}
|
||||
session.setScrollTop(top);
|
||||
}
|
||||
};
|
||||
var updateLenses = lang.delayedCall(editor.$updateLenses);
|
||||
editor.$updateLensesOnInput = function () {
|
||||
updateLenses.delay(250);
|
||||
};
|
||||
editor.on("input", editor.$updateLensesOnInput);
|
||||
}
|
||||
function detachFromEditor(editor) {
|
||||
editor.off("input", editor.$updateLensesOnInput);
|
||||
editor.renderer.off("afterRender", renderWidgets);
|
||||
if (editor.$codeLensClickHandler)
|
||||
editor.container.removeEventListener("click", editor.$codeLensClickHandler);
|
||||
}
|
||||
exports.registerCodeLensProvider = function (editor, codeLensProvider) {
|
||||
editor.setOption("enableCodeLens", true);
|
||||
editor.codeLensProviders.push(codeLensProvider);
|
||||
editor.$updateLensesOnInput();
|
||||
};
|
||||
exports.clear = function (session) {
|
||||
exports.setLenses(session, null);
|
||||
};
|
||||
var Editor = require("../editor").Editor;
|
||||
require("../config").defineOptions(Editor.prototype, "editor", {
|
||||
enableCodeLens: {
|
||||
set: function (val) {
|
||||
if (val) {
|
||||
attachToEditor(this);
|
||||
}
|
||||
else {
|
||||
detachFromEditor(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
dom.importCssString("\n.ace_codeLens {\n position: absolute;\n color: #aaa;\n font-size: 88%;\n background: inherit;\n width: 100%;\n display: flex;\n align-items: flex-end;\n pointer-events: none;\n}\n.ace_codeLens > a {\n cursor: pointer;\n pointer-events: auto;\n}\n.ace_codeLens > a:hover {\n color: #0000ff;\n text-decoration: underline;\n}\n.ace_dark > .ace_codeLens > a:hover {\n color: #4e94ce;\n}\n", "codelense.css", false);
|
||||
|
||||
}); (function() {
|
||||
window.require(["ace/ext/code_lens"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user