Compare commits

..

264 Commits

Author SHA1 Message Date
Naoki Takezoe
baf560d532 Release GitBucket 4.46.1 (#4008) 2026-04-18 11:09:43 +09:00
gitbucket-bot[bot]
448b3ac144 Update sbt-scalafmt to 2.6.0 2026-04-14 03:46:50 +09:00
gitbucket-bot[bot]
82be26dbae Update scalafmt-core to 3.11.0 2026-04-13 11:20:16 +09:00
gitbucket-bot[bot]
102c9e8245 Update sbt, sbt-dependency-tree, ... to 1.12.9 2026-04-08 03:49:49 +09:00
Shreejan Shrestha
2587f2bd19 Improve pull request compare performance by making mergeability and diff loading manually triggerable, with configurable default mode (#3996) 2026-04-05 20:40:14 +09:00
gitbucket-bot[bot]
9c4c2dfc81 Update scala3-library to 3.8.3 2026-04-02 06:38:11 +09:00
Naoki Takezoe
6ff90aae1a Fix NullPointerException in the commits page (#4000) 2026-04-01 09:57:52 +09:00
gitbucket-bot[bot]
79a9cfb3bb Update oauth2-oidc-sdk to 11.37 2026-03-30 03:41:39 +09:00
gitbucket-bot[bot]
c218cf70b6 Update oauth2-oidc-sdk to 11.36 2026-03-29 18:22:14 +09:00
gitbucket-bot[bot]
4b4810438e Update sbt, sbt-dependency-tree, ... to 1.12.8 2026-03-25 06:59:55 +09:00
gitbucket-bot[bot]
371913a9d5 Update oauth2-oidc-sdk to 11.35 2026-03-24 18:57:20 +09:00
gitbucket-bot[bot]
3d13ae705a Update sbt, sbt-dependency-tree, ... to 1.12.7 2026-03-24 17:49:37 +09:00
gitbucket-bot[bot]
abd94807fe Update tika-core to 3.3.0 2026-03-24 07:39:04 +09:00
gitbucket-bot[bot]
eea43c30d2 Update testcontainers-mysql, ... to 2.0.4 (#3991)
Co-authored-by: gitbucket-bot[bot] <256891351+gitbucket-bot[bot]@users.noreply.github.com>
2026-03-20 02:59:44 +09:00
gitbucket-bot[bot]
32bf51ff1a Update commons-net to 3.13.0 (#3990)
Co-authored-by: gitbucket-bot[bot] <256891351+gitbucket-bot[bot]@users.noreply.github.com>
2026-03-20 02:59:19 +09:00
gitbucket-bot[bot]
e32c68c7d7 Update sbt, sbt-dependency-tree, ... to 1.12.6 2026-03-17 06:56:34 +09:00
gitbucket-bot[bot]
c6691e1766 Update mockito-core to 5.23.0 2026-03-13 05:29:09 +09:00
gitbucket-bot[bot]
860813174a Update oauth2-oidc-sdk to 11.34 (#3983)
Co-authored-by: gitbucket-bot[bot] <256891351+gitbucket-bot[bot]@users.noreply.github.com>
2026-03-08 02:47:35 +09:00
Naoki Takezoe
d10736387b Suppress JGitUtil's debug logs in test (#3982) 2026-03-07 12:50:48 +09:00
Naoki Takezoe
90b9c17d6e Release GitBucket 4.46.0 (#3981) 2026-03-07 11:01:41 +09:00
Naoki Takezoe
f9e4500bbb Support for revert pull request (#3819) 2026-03-07 08:57:12 +09:00
Naoki Takezoe
df60395ea2 Fix CSS for discussion items in issue and pull request (#3979) 2026-03-07 03:22:33 +09:00
Yasumichi Akahoshi
3111ab3234 Add text completion (use SuggestionProvider) (#3974) 2026-03-04 02:24:37 +09:00
gitbucket-bot[bot]
74a9aaa31d Update sbt, sbt-dependency-tree, ... to 1.12.5 2026-03-03 06:43:34 +09:00
dependabot[bot]
175232cac1 Bump actions/upload-artifact from 6 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 19:18:42 +09:00
gitbucket-bot[bot]
cce9c62591 Update xz to 1.12 2026-03-02 07:23:07 +09:00
gitbucket-bot[bot]
35d670843f Update mockito-core to 5.22.0 2026-02-28 10:01:45 +09:00
gitbucket-bot[bot]
40555c8464 Update scala3-library to 3.8.2 2026-02-26 04:44:32 +09:00
gitbucket-bot[bot]
28dadef118 Update typesafe:config to 1.4.6 2026-02-25 03:12:02 +09:00
gitbucket-bot[bot]
b697fdf189 Update sbt, sbt-dependency-tree, ... to 1.12.4 2026-02-24 05:10:43 +09:00
Yasumichi Akahoshi
f72617769b Fix potential error in Markdown preview introduced in #3907 (#3967) 2026-02-21 09:52:48 +09:00
Naoki Takezoe
b355599a96 Tweak markdown editor and tool bar styles (#3966) 2026-02-19 08:30:38 +09:00
gitbucket-bot[bot]
af6e99d832 Update logback-classic to 1.5.32 2026-02-17 06:40:10 +09:00
gitbucket-bot[bot]
312927efda Update logback-classic to 1.5.31 2026-02-16 07:45:04 +09:00
gitbucket-bot[bot]
366074ec55 Update sbt, sbt-dependency-tree, ... to 1.12.3 2026-02-16 03:51:57 +09:00
gitbucket-bot[bot]
7d679781a8 Update logback-classic to 1.5.30 2026-02-15 08:17:39 +09:00
ziggystar
8f2e0f8505 Add SHA-256 support for WebHookService by adding header X-Hub-Signature-256 (#3962)
Co-authored-by: Thomas Geier <thomas.geier@solidat.de>
2026-02-14 13:47:14 +09:00
gitbucket-bot[bot]
73ccfd0788 Update postgresql to 42.7.10 2026-02-13 06:37:09 +09:00
gitbucket-bot[bot]
c25145b793 Update scalafmt-core to 3.10.7 2026-02-12 07:23:19 +09:00
RIVOIRA
1b7fbcb59d Fix UTF-8 BOM preservation when editing files in browser (fixes #2188) (#3954)
* Fix UTF-8 BOM preservation when editing files in browser (fixes #2188)

When editing a file encoded in UTF-8 with BOM through the web interface,
the BOM was lost during save, making it impossible to use this feature
for files requiring UTF-8 BOM encoding.

This fix:
- Detects UTF-8 BOM when reading file content
- Preserves BOM information through the edit form
- Restores BOM when writing file content back to repository

Changes:
- Add hasUtf8Bom() function to detect BOM in byte arrays
- Add hasBom field to ContentInfo case class
- Update getContentInfo to detect and store BOM information
- Add hasBom hidden field in editor form
- Update EditorForm and commitFile to handle BOM preservation
- Add unit tests for BOM detection
2026-02-11 23:21:02 +09:00
gitbucket-bot[bot]
03760f126b Update logback-classic to 1.5.29 (#3960)
Co-authored-by: gitbucket-bot[bot] <256891351+gitbucket-bot[bot]@users.noreply.github.com>
2026-02-11 09:59:51 +09:00
gitbucket-bot[bot]
3826b690cf Update oauth2-oidc-sdk to 11.33 2026-02-09 06:53:25 +09:00
Naoki Takezoe
3f3b111afc Fix redirect after creating or editing in web editor (#3958) 2026-02-08 23:02:12 +09:00
Naoki Takezoe
379c86ba9d Restore pagination in commit history (#3957) 2026-02-08 22:31:18 +09:00
Yasumichi Akahoshi
6dce8672ab add markdown toolbar to comment (#3952) 2026-02-08 19:27:46 +09:00
gitbucket-bot[bot]
85d8432755 Update logback-classic to 1.5.28 2026-02-07 07:14:34 +09:00
gitbucket-bot[bot]
edbec530f3 Update sbt, sbt-dependency-tree, ... to 1.12.2 2026-02-04 15:44:37 +09:00
gitbucket-bot[bot]
952bfcc37b Update scalafmt-core to 3.10.6 2026-02-01 07:46:47 +09:00
Yasumichi Akahoshi
0f48c3c926 Add debug logs to JGitUtil (#3940) 2026-01-31 22:38:54 +09:00
Yasumichi Akahoshi
ed0a6ef4ff Add markdown toolbar (#3949) 2026-01-31 08:46:33 +09:00
Yasumichi Akahoshi
0bdc765198 Fixed inconsistencies in branch names on the Wiki (#3929) 2026-01-31 08:28:24 +09:00
gitbucket-bot[bot]
9a613bb678 Update logback-classic to 1.5.27 2026-01-31 06:33:18 +09:00
gitbucket-bot[bot]
06ea83579d Update json4s-jackson to 4.1.0 (#3934)
Co-authored-by: gitbucket-bot[bot] <256891351+gitbucket-bot[bot]@users.noreply.github.com>
2026-01-30 13:57:35 +09:00
gitbucket-bot[bot]
a84e501099 Update cache2k-api, cache2k-core to 2.6.1.Final (#3946)
Co-authored-by: gitbucket-bot[bot] <256891351+gitbucket-bot[bot]@users.noreply.github.com>
2026-01-30 13:57:17 +09:00
Yasumichi Akahoshi
17b3587263 share ace related script (#3936) 2026-01-30 13:56:35 +09:00
gitbucket-bot[bot]
6cce445dcb Update oauth2-oidc-sdk to 11.32 2026-01-30 06:53:12 +09:00
Yasumichi Akahoshi
22c09c2cb1 fix-cache2k-warning (#3944) 2026-01-29 09:57:12 +09:00
Yasumichi Akahoshi
241d46847b Migrated from helpers.markdown() to helpers.renderMarkup(). (#3939) 2026-01-29 09:51:46 +09:00
Yasumichi Akahoshi
6b145ea760 Recommended use of BOMInputStream (#3938) 2026-01-29 09:48:24 +09:00
gitbucket-bot[bot]
5fc3dd74a8 Update scalafmt-core to 3.10.5 2026-01-29 07:25:04 +09:00
gitbucket-bot[bot]
6ee03843c5 Update apache-sshd to 2.17.1 2026-01-28 06:52:15 +09:00
gitbucket-bot[bot]
516f67f812 Update sbt, sbt-dependency-tree, ... to 1.12.1 2026-01-27 07:32:22 +09:00
gitbucket-bot[bot]
2b3b26d542 Update logback-classic to 1.5.26 2026-01-27 07:31:44 +09:00
Yasumichi Akahoshi
858de58cd0 Add ace keybinds (#3935) 2026-01-24 17:06:23 +09:00
Yasumichi Akahoshi
3248bef98d update ace 1.43.5 (#3930) 2026-01-24 13:33:52 +09:00
Yasumichi Akahoshi
0c9863f956 Apply Ace Editor to Wiki editing with keyboard handler support (#3925) 2026-01-23 09:09:40 +09:00
Scala Steward
afc5c2d3d0 Update scala3-library to 3.8.1 2026-01-22 12:21:54 +09:00
Scala Steward
544a291dac Update logback-classic to 1.5.25 2026-01-18 10:45:10 +09:00
Scala Steward
9e8d5b89c7 Update scalafmt-core to 3.10.4 2026-01-18 09:34:30 +09:00
Scala Steward
86f069c980 Update postgresql to 42.7.9 2026-01-16 05:25:41 +09:00
Scala Steward
69bd70b381 Update scala3-library to 3.8.0 2026-01-14 06:05:07 +09:00
Scala Steward
f1d0b0c080 Update sbt-scoverage to 2.4.4 2026-01-14 06:01:47 +09:00
Scala Steward
45ae0cfd62 Update oauth2-oidc-sdk to 11.31.1 2026-01-10 23:30:44 +09:00
Naoki Takezoe
d2233e78e4 Fix release date of GitBucket 4.45.0 (#3917) 2026-01-10 13:33:39 +09:00
Naoki Takezoe
8ab6b386a3 Release 4.45.0 (#3916) 2026-01-10 11:55:30 +09:00
Naoki Takezoe
2dd2c4f568 Deprecate helpers.markdown() (#3913) 2026-01-09 22:15:50 +09:00
Naoki Takezoe
36f7011ebf Fix warnings in PluginRegistry (#3914) 2026-01-09 21:46:28 +09:00
Yasumichi Akahoshi
fbcf962630 Check existence of Wiki page for Wiki links (#3907) 2026-01-09 11:22:51 +09:00
Naoki Takezoe
3b9b261878 Remove unused imports (#3912) 2026-01-09 08:58:48 +09:00
Naoki Takezoe
b334809e3e Add option to display fullname instead of username on UI (#3910) 2026-01-08 14:21:23 +09:00
Scala Steward
9a1324a870 Update logback-classic to 1.5.24 2026-01-07 09:10:47 +09:00
Scala Steward
b7c39e90d2 Update sbt, sbt-dependency-tree, ... to 1.12.0 2026-01-05 16:38:47 +09:00
Scala Steward
63b4f25687 Update oauth2-oidc-sdk to 11.31 2026-01-04 19:53:19 +09:00
Yasumichi Akahoshi
b582af4469 Fix bug that #3894 doesn't work with context path (#3906) 2026-01-03 20:54:42 +09:00
Yasumichi Akahoshi
ecd3f5b4eb Fix system menu of plugins (#3902)
In the system settings, the menu-item-hover class has been added to the plugin menu.
2025-12-31 20:41:12 +09:00
Yasumichi Akahoshi
7be433d331 Remove unnecessary CSS style (#3899) 2025-12-31 20:40:00 +09:00
Scala Steward
154be0f425 Update oauth2-oidc-sdk to 11.30.2 2025-12-30 20:17:02 +09:00
Scala Steward
9ddd2065b7 Update scalafmt-core to 3.10.3 2025-12-24 06:54:30 +09:00
Naoki Takezoe
6917cbf224 Fix typo in #3896 (#3898) 2025-12-23 11:48:06 +09:00
Naoki Takezoe
637b033782 Use Commons IO's IOUtils instead of Commons Compress's one (#3897) 2025-12-23 11:03:19 +09:00
Naoki Takezoe
7aee451a55 Include path in filePath param for preview in the online editor (#3896) 2025-12-23 10:59:08 +09:00
Scala Steward
e1e369d653 Update logback-classic to 1.5.23 2025-12-22 12:06:20 +09:00
Naoki Takezoe
c2ad66438c Preview based on the correct filename on the editor (#3894) 2025-12-21 13:23:30 +09:00
Yasumichi Akahoshi
c88e5adac2 Add support for an alternative renderer to commit comments, wikis, and issues. (#3882) 2025-12-21 12:17:34 +09:00
Scala Steward
703fb4a650 Update mariadb-java-client to 2.7.13 2025-12-18 07:31:16 +09:00
Scala Steward
465282580f Update testcontainers-mysql, ... to 2.0.3 2025-12-16 11:51:40 +09:00
dependabot[bot]
e041f8ffa0 Bump actions/upload-artifact from 5 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 13:36:38 +09:00
dependabot[bot]
094386bb65 Bump actions/cache from 4 to 5
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 13:36:25 +09:00
Scala Steward
b9e830f06e Update logback-classic to 1.5.22 2025-12-12 08:12:41 +09:00
Scala Steward
bdbfbc62a9 Update sbt-scoverage to 2.4.3 2025-12-11 07:25:38 +09:00
Scala Steward
5787a02daa Update mockito-core to 5.21.0 2025-12-10 07:02:13 +09:00
Yasumichi Akahoshi
0d6e5af8d7 Tentative fix for issue #2456 (#3883)
Co-authored-by: Naoki Takezoe <takezoe@gmail.com>
2025-12-07 01:26:37 +09:00
scala-steward-bot
88a8973685 Update ec4j-core to 1.2.0 (#3884) 2025-12-04 02:10:48 +09:00
kenji yoshida
e640cec323 Update CODEOWNERS 2025-11-30 08:52:00 +09:00
Scala Steward
5a4226db1d Update scalafmt-core to 3.10.2 2025-11-30 08:51:04 +09:00
dependabot[bot]
fb48c2a874 Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 12:11:07 +09:00
Scala Steward
28da703052 Update xz to 1.11 2025-11-20 06:01:50 +09:00
Scala Steward
2568171083 Update scala-library to 2.13.18 2025-11-18 07:33:52 +09:00
Scala Steward
2dff7f1b9e Update sbt-scoverage to 2.4.2 2025-11-18 07:33:40 +09:00
Scala Steward
8a79de8b50 Update testcontainers-mysql, ... to 2.0.2 2025-11-14 07:08:37 +09:00
Scala Steward
1b763c753c Update logback-classic to 1.5.21 2025-11-11 07:00:54 +09:00
Scala Steward
3bb1b1269c Update commons-io to 2.21.0 2025-11-08 07:23:38 +09:00
Scala Steward
dbbaf3cc9c Update scala3-library to 3.7.4 2025-11-08 07:01:23 +09:00
Scala Steward
4465a62b5f Update oauth2-oidc-sdk to 11.30.1 2025-11-07 07:19:19 +09:00
Scala Steward
fb48e75ecf Update sbt-scoverage to 2.4.1 2025-11-07 05:16:14 +09:00
Naoki Takezoe
a79142074f Fix community plugins link in README (#3867)
Updated the link to the GitBucket community plugins page.
2025-11-01 15:53:22 +09:00
Scala Steward
9f58c6dce7 Update sbt-scalafmt to 2.5.6 2025-10-31 07:27:37 +09:00
dependabot[bot]
6fe903afab Bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 13:23:19 +09:00
Scala Steward
0d94865633 Update sbt-scoverage to 2.4.0 2025-10-22 08:07:20 +09:00
xuwei-k
2fbdead64b Update testcontainers 2025-10-20 07:59:52 +09:00
Scala Steward
72a354931e Update scalafmt-core to 3.10.1 2025-10-20 07:28:21 +09:00
Scala Steward
cb8affcd0d Update logback-classic to 1.5.20 2025-10-20 06:53:52 +09:00
scala-steward-bot
a5a997eb40 Update scalafmt-core to 3.10.0 (#3857) 2025-10-18 10:47:07 +09:00
Naoki Takezoe
b4220aab68 Test with MySQL 8.4 (#3860) 2025-10-18 10:40:16 +09:00
Scala Steward
c8a4798f86 Update oauth2-oidc-sdk to 11.30 2025-10-16 20:05:41 +09:00
xuwei-k
6b8e9e8892 add JDK 25 CI 2025-10-13 17:57:35 +09:00
xuwei-k
9ab9363d0b fix warnings 2025-10-13 17:56:55 +09:00
Scala Steward
1edb18a147 Update sbt, sbt-dependency-tree, ... to 1.11.7 2025-10-06 06:59:52 +09:00
Scala Steward
d8b4bf3033 Update thumbnailator to 0.4.21 2025-10-03 06:32:36 +09:00
Scala Steward
6597c4490b Update logback-classic to 1.5.19 2025-10-01 06:22:46 +09:00
Scala Steward
daaf1696ad Update oauth2-oidc-sdk to 11.29.2 2025-09-30 21:44:47 +09:00
Scala Steward
53a1ca7874 Update scala-library to 2.13.17 2025-09-30 18:43:32 +09:00
Scala Steward
f045fa4c6a Update h2 to 2.4.240 2025-09-25 06:58:37 +09:00
Scala Steward
53a7c5adf8 Update sbt-license-report to 1.9.0 2025-09-25 04:25:34 +09:00
takezoe
b142eca9a5 Update change log for 4.44.0 2025-09-23 09:56:19 +09:00
Naoki Takezoe
ba753a373b Fix release doc (#3844) 2025-09-23 09:46:49 +09:00
Naoki Takezoe
95ceca75a4 Update change log for 4.44.0 (#3845) 2025-09-23 09:46:35 +09:00
Naoki Takezoe
f16cc117a9 Release 4.44.0 (#3842) 2025-09-23 09:02:24 +09:00
Naoki Takezoe
af66f8f746 Fix Repository Contents Upload API for nested path (#3843) 2025-09-23 09:01:30 +09:00
Scala Steward
d9cc57e8e0 Update oauth2-oidc-sdk to 11.29.1 2025-09-23 06:51:06 +09:00
Scala Steward
72b6dad3a2 Update mockito-core to 5.20.0 2025-09-21 06:10:09 +09:00
Scala Steward
18f396b4a2 Update postgresql to 42.7.8 2025-09-19 13:46:36 +09:00
Scala Steward
9f3fde8de2 Update tika-core to 3.2.3 2025-09-17 07:14:08 +09:00
scala-steward-bot
dfd6f80b63 Update scalafmt-core to 3.9.10 (#3838) 2025-09-16 22:15:57 +09:00
Scala Steward
119d91210c Update typesafe:config to 1.4.5 2025-09-11 17:29:06 +09:00
Scala Steward
75ef30ee03 Update sbt-license-report to 1.8.0 2025-09-11 06:32:21 +09:00
Scala Steward
d1cf9dd600 Update scala3-library to 3.7.3 2025-09-09 06:18:29 +09:00
Scala Steward
9c9fea908c Update sbt, sbt-dependency-tree, ... to 1.11.6 2025-09-07 08:51:05 +09:00
kenji yoshida
1145c4d0f6 update build.sbt. prepare sbt 2 2025-09-05 20:03:01 +09:00
Scala Steward
d847fc6e0f Update github-api to 1.330 2025-09-05 05:36:45 +09:00
Scala Steward
bb9585f7a6 Update oauth2-oidc-sdk to 11.28 2025-08-31 06:58:03 +09:00
Scala Steward
e91411fa45 Update sbt, sbt-dependency-tree, ... to 1.11.5 2025-08-26 08:30:43 +09:00
dependabot[bot]
adbc065a6f Bump actions/setup-java from 4 to 5
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4 to 5.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-25 17:59:37 +09:00
Scala Steward
862283b729 Update apache-sshd to 2.16.0 2025-08-24 06:44:33 +09:00
Naoki Takezoe
217df7012c Fix Wiki branch resolution in blob endpoint (#3826) 2025-08-23 17:11:11 +09:00
Naoki Takezoe
e672d41e77 Fix downloading branch that contains slash (#3825) 2025-08-23 16:49:18 +09:00
Scala Steward
d975700bd4 Update HikariCP to 7.0.2 2025-08-20 03:24:57 +09:00
dependabot[bot]
f65e41561a Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-18 17:51:52 +09:00
Scala Steward
dab4f33ed9 Update oauth2-oidc-sdk to 11.27.1 2025-08-16 12:30:05 +09:00
Scala Steward
5ff45ef5ae Update jetty-http, jetty-io, jetty-runner, ... to 10.0.26 2025-08-16 06:35:41 +09:00
Scala Steward
af7c622647 Update mockito-core to 5.19.0 2025-08-15 18:58:23 +09:00
Scala Steward
f7027e57df Update HikariCP to 7.0.1 2025-08-09 12:01:30 +09:00
Scala Steward
6a4719469d Update tika-core to 3.2.2 2025-08-08 19:03:31 +09:00
Scala Steward
6ebc865ba5 Update sbt, sbt-dependency-tree, ... to 1.11.4 2025-08-05 07:04:59 +09:00
Naoki Takezoe
0c1e8b932b Reject direct push to branch if branch protection is enabled (#3791) 2025-08-03 11:58:28 +09:00
scala-steward-bot
fda67a32e2 Update scalafmt-core to 3.9.9 (#3815) 2025-08-03 01:31:06 +09:00
Scala Steward
14d7e9ee90 Update commons-net to 3.12.0 2025-08-02 06:56:11 +09:00
Naoki Takezoe
b7b7322cce Fix branch selector in repository viewer (#3813) 2025-08-01 02:33:05 +09:00
Scala Steward
5eb44398d0 Update oauth2-oidc-sdk to 11.27 2025-07-30 10:09:17 +09:00
Scala Steward
be7bb255c3 Update commons-compress to 1.28.0 2025-07-30 09:54:03 +09:00
Scala Steward
cb9522d416 Update github-api to 1.329 2025-07-30 06:56:36 +09:00
Scala Steward
d80afb473b Update scala3-library to 3.7.2 2025-07-30 06:49:49 +09:00
Scala Steward
607d85c661 Update HikariCP to 7.0.0 2025-07-29 05:59:20 +09:00
Scala Steward
a5fab3bc96 Update oauth2-oidc-sdk to 11.26.1 2025-07-25 18:55:51 +09:00
Scala Steward
eb403ada58 Update HikariCP to 6.3.2 2025-07-24 17:34:00 +09:00
Naoki Takezoe
911c102f39 Improve logging in initialization process (#3804) 2025-07-21 12:10:56 +09:00
Naoki Takezoe
bf23e854f8 Exclude sshd-spring-sftp (#3803) 2025-07-21 11:58:22 +09:00
Scala Steward
52427c0a1e Update HikariCP to 6.3.1 2025-07-21 06:15:54 +09:00
Naoki Takezoe
d8e5ac585c Disable blank issue (#3801) 2025-07-20 10:52:14 +09:00
Naoki Takezoe
2fbeef73b0 Move issue_template.yml to .github directory (#3800) 2025-07-20 10:42:25 +09:00
Naoki Takezoe
15e39572dd Update issue template (#3799) 2025-07-20 10:33:40 +09:00
Scala Steward
9eac4f42c5 Update commons-io to 2.20.0 2025-07-20 06:04:11 +09:00
scala-steward-bot
01d18bb5c3 Update typesafe:config to 1.4.4 (#3796) 2025-07-13 08:52:18 +09:00
scala-steward-bot
00258e9125 Update tika-core to 3.2.1 (#3795) 2025-07-13 08:52:02 +09:00
Scala Steward
046b337337 Update java-diff-utils to 4.16 2025-07-08 17:02:18 +09:00
Naoki Takezoe
46cc7b6fd3 Update H2 database migration guide (#3793) 2025-07-06 21:24:28 +09:00
Scala Steward
59344b4f05 Update sbt, sbt-dependency-tree, ... to 1.11.3 2025-07-06 07:11:59 +09:00
Naoki Takezoe
c4d8af02b2 Fix wrong redirect after sign-in when user-defined CSS is used (#3789) 2025-07-05 12:08:20 +09:00
Scala Steward
a10bc3687a Update sbt-twirl, twirl-api to 2.0.9 2025-07-01 06:36:38 +09:00
Scala Steward
b0d21dee42 Update sbt-scalafmt to 2.5.5 2025-06-30 17:35:49 +09:00
Naoki Takezoe
13ea0e7507 Update release procedure doc (#3785) 2025-06-29 13:57:16 +09:00
takezoe
d66fdaede5 Update README and CHANGELOG 2025-06-29 13:33:29 +09:00
takezoe
d6217d89eb Update README and CHANGELOG 2025-06-29 13:01:17 +09:00
Naoki Takezoe
c99ff1cf0f Release 4.43.0 (#3784) 2025-06-29 13:00:21 +09:00
Scala Steward
001b9ae2ae Update mysql, postgresql to 1.21.3 2025-06-29 07:49:01 +09:00
Scala Steward
f63493f1c0 Update scalafmt-core to 3.9.8 2025-06-29 07:48:45 +09:00
Naoki Takezoe
c9095722f8 Redirect from sign-in page to top page if already authenticated (#3781) 2025-06-22 21:23:15 +09:00
Naoki Takezoe
b9d2efa582 UI: Fix bottom margin of branch deletion box in pull request (#3780) 2025-06-21 17:23:23 +09:00
Naoki Takezoe
9c2e09020a Fix warnings in service test cases (#3779) 2025-06-21 17:01:54 +09:00
Naoki Takezoe
1ffcf8c1e9 Bump h2 to 2.3.232 (#3746) 2025-06-21 13:03:48 +09:00
Naoki Takezoe
0124091840 Fix the issue template (#3778) 2025-06-21 12:59:50 +09:00
Naoki Takezoe
2a28a7b35b Migrate templates for issue request (#3777) 2025-06-21 12:48:06 +09:00
Scala Steward
2a68ffc8dc Update mysql, postgresql to 1.21.2 2025-06-19 20:42:59 +09:00
Scala Steward
6b47c49cdd Update oauth2-oidc-sdk to 11.26 2025-06-18 05:15:40 +09:00
Scala Steward
b634967776 Update postgresql to 42.7.7 2025-06-11 19:52:43 +09:00
Scala Steward
172bed760d Update sbt, sbt-dependency-tree, ... to 1.11.2 2025-06-08 07:42:23 +09:00
Naoki Takezoe
1b7eb69083 Migrate from OSSRH to Central Portal (#3770) 2025-06-08 00:17:29 +09:00
scala-steward-bot
185c01db99 Update sbt, sbt-dependency-tree, ... to 1.11.1 (#3767) 2025-06-07 23:56:26 +09:00
Scala Steward
ab548d8c25 Update scala3-library to 3.7.1 2025-06-03 05:24:59 +09:00
kenji yoshida
983975620b Merge pull request #3766 from scala-steward-bot/update/scalafmt-core-3.9.7
Update scalafmt-core to 3.9.7
2025-06-01 08:27:00 +09:00
Scala Steward
b512b08256 Add 'Reformat with scalafmt 3.9.7' to .git-blame-ignore-revs 2025-05-30 14:45:26 +00:00
Scala Steward
a54fb4960f Reformat with scalafmt 3.9.7
Executed command: scalafmt --non-interactive
2025-05-30 14:45:26 +00:00
Scala Steward
93de53d717 Update scalafmt-core to 3.9.7 2025-05-30 14:45:19 +00:00
Scala Steward
f262c0a9eb Update mysql, postgresql to 1.21.1 2025-05-30 17:44:07 +09:00
Scala Steward
e5d15569df Update postgresql to 42.7.6 2025-05-28 20:41:26 +09:00
Scala Steward
369b08eae3 Update tika-core to 3.2.0 2025-05-27 06:49:24 +09:00
Scala Steward
8e6fcb022b Update sbt, sbt-dependency-tree, ... to 1.11.0 2025-05-24 15:52:45 +09:00
Scala Steward
456b1f6571 Update org.eclipse.jgit.archive, ... to 6.10.1.202505221210-r 2025-05-23 11:28:36 +09:00
Scala Steward
6e459ad225 Update oauth2-oidc-sdk to 11.25 2025-05-23 05:51:55 +09:00
Scala Steward
cece4c1c7d Update mockito-core to 5.18.0 2025-05-21 06:47:39 +09:00
Scala Steward
42e7a9fa9f Update ec4j-core to 1.1.1 2025-05-18 06:45:58 +09:00
Scala Steward
f9510aba8e Update scalafmt-core to 3.9.6 2025-05-15 11:47:04 +09:00
Scala Steward
8a7d719025 Update oauth2-oidc-sdk to 11.24 2025-05-07 06:53:07 +09:00
Scala Steward
1293a21450 Update scala3-library to 3.7.0 2025-05-06 06:34:29 +09:00
scala-steward-bot
65dd597ab7 Update scalatra-forms-javax, ... to 3.1.2 (#3752) 2025-05-03 18:42:17 +09:00
Scala Steward
e145b5151e Update scalafmt-core to 3.9.5 2025-04-30 06:38:32 +09:00
Scala Steward
b9684c277b Update mysql, postgresql to 1.21.0 2025-04-24 11:47:26 +09:00
Scala Steward
4accb77533 Update commons-io to 2.19.0 2025-04-12 14:10:18 +09:00
Scala Steward
9eef961025 Update mockito-core to 5.17.0 2025-04-05 07:10:28 +09:00
Scala Steward
546b40cdd1 Update HikariCP to 6.3.0 2025-03-25 06:24:54 +09:00
Scala Steward
274a08c14c Update logback-classic to 1.5.18 2025-03-20 06:35:18 +09:00
Scala Steward
eed4b51189 Update jetty-http, jetty-io, jetty-runner, ... to 10.0.25 2025-03-18 08:00:26 +09:00
Scala Steward
5c2f84367b Update sbt, sbt-dependency-tree, ... to 1.10.11 2025-03-17 19:21:55 +09:00
Scala Steward
d5b625e43f Update mockito-core to 5.16.1 2025-03-16 07:10:45 +09:00
scala-steward-bot
27f9e3dec9 Update scalafmt-core to 3.9.4 (#3737) 2025-03-15 12:53:33 +09:00
Scala Steward
00ef4db9a7 Update testcontainers-scala to 0.43.0 2025-03-12 19:32:26 +09:00
Scala Steward
bf4f814389 Update scalafmt-core to 3.9.3 2025-03-07 19:13:22 +09:00
Scala Steward
23e45afd7f Update scala3-library to 3.6.4 2025-03-07 19:12:26 +09:00
Scala Steward
bfb02eef62 Update mysql, postgresql to 1.20.6 2025-03-05 06:57:03 +09:00
Scala Steward
c129aae73a Update sbt, sbt-dependency-tree, ... to 1.10.10 2025-03-04 16:01:56 +09:00
Scala Steward
a955856cef Update sbt, sbt-dependency-tree, ... to 1.10.9 2025-03-04 07:19:27 +09:00
Scala Steward
a43a3fa55c Update sbt, sbt-dependency-tree, ... to 1.10.8 2025-03-03 20:03:42 +09:00
Scala Steward
a6254ab955 Update mockito-core to 5.16.0 2025-03-03 20:03:13 +09:00
scala-steward-bot
b505c3dc12 Update scalafmt-core to 3.9.2 (#3726) 2025-03-01 15:35:33 +09:00
mnival
9d69b9e980 Add bearer authentification (#3725)
Co-authored-by: mnival <extern.nival_michael@allianz.com>
2025-02-28 09:30:45 +09:00
Scala Steward
44b2320644 Update scalafmt-core to 3.9.1 2025-02-26 19:12:54 +09:00
Scala Steward
8fb9643ea5 Update oauth2-oidc-sdk to 11.23.1 2025-02-26 19:12:31 +09:00
Scala Steward
10ea988298 Update logback-classic to 1.5.17 2025-02-26 04:00:17 +09:00
Scala Steward
7896945519 Update apache-sshd to 2.15.0 2025-02-25 03:36:14 +09:00
Scala Steward
210342d2bc Update sbt-twirl, twirl-api to 2.0.8 2025-02-24 06:45:06 +09:00
Scala Steward
fdacea858b Update sbt-scoverage to 2.3.1 2025-02-21 06:19:43 +09:00
Scala Steward
6825028d37 Update mysql, postgresql to 1.20.5 2025-02-20 07:54:32 +09:00
Scala Steward
2089882d41 Update oauth2-oidc-sdk to 11.23 2025-02-18 06:52:15 +09:00
Scala Steward
8e1d938155 Update github-api to 1.327 2025-02-13 23:30:25 +09:00
Scala Steward
39eb4cef04 Update oauth2-oidc-sdk to 11.22.2 2025-02-13 13:33:44 +09:00
Scala Steward
d4e3adafa6 Update oauth2-oidc-sdk to 11.22.1 2025-02-09 17:45:03 +09:00
Scala Steward
a7b8326499 Update oauth2-oidc-sdk to 11.22 2025-02-07 04:19:11 +09:00
Naoki Takezoe
249f8738d3 Update command to run GitBucket in debug.md (#3708) 2025-02-05 22:52:49 +09:00
Scala Steward
fefe6ef74f Update scalafmt-core to 3.8.6 2025-02-01 17:22:55 +09:00
Scala Steward
9ca6cd1d90 Update tika-core to 3.1.0 2025-02-01 07:18:24 +09:00
Scala Steward
bff7b7c460 Update oauth2-oidc-sdk to 11.21.3 2025-02-01 07:18:01 +09:00
Scala Steward
cfff79758b Update oauth2-oidc-sdk to 11.21.2 2025-01-22 03:38:05 +09:00
Scala Steward
ded8ceb2c6 Update sbt-assembly to 2.3.1 2025-01-21 08:50:51 +09:00
Scala Steward
c502ebfc16 Update testcontainers-scala to 0.41.8 2025-01-21 08:13:08 +09:00
594 changed files with 198912 additions and 132479 deletions

View File

@@ -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
View File

@@ -3,3 +3,4 @@
build.sbt @xuwei-k
project/* @xuwei-k
.github/workflows/* @xuwei-k
.scalafmt.conf @xuwei-k

View File

@@ -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
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View 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

View File

@@ -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.*

View File

@@ -1,4 +1,4 @@
version = "3.8.5"
version = "3.11.0"
project.git = true
maxColumn = 120

View File

@@ -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

View File

@@ -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.

View File

@@ -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 := { _ =>

View File

@@ -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.

View File

@@ -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.

View File

@@ -1 +1 @@
sbt.version=1.10.7
sbt.version=1.12.9

View File

@@ -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

View File

@@ -1,4 +1,4 @@
notifications:1.11.0
gist:4.23.0
gist:4.24.0
emoji:4.6.0
pages:1.10.0

View File

@@ -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"/>

View 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>

View 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>

View File

@@ -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)
}

View File

@@ -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 =

View File

@@ -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])
}

View File

@@ -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 =>
(

View File

@@ -44,7 +44,7 @@ object JsonFormat {
FieldSerializer[ApiCommits.File]() +
FieldSerializer[ApiRelease]() +
FieldSerializer[ApiReleaseAsset]() +
ApiBranchProtection.enforcementLevelSerializer
ApiBranchProtectionResponse.enforcementLevelSerializer
def apiPathSerializer(c: Context) =
new CustomSerializer[ApiPath](_ =>

View File

@@ -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"

View File

@@ -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 ""
})

View File

@@ -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
)
)
}

View File

@@ -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)
}

View File

@@ -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
}
}
}

View File

@@ -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.")

View File

@@ -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)
})

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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()
}
})

View File

@@ -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) =>

View File

@@ -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)

View File

@@ -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]
)

View File

@@ -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")

View File

@@ -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
)

View File

@@ -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"

View File

@@ -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

View File

@@ -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), _) =>

View File

@@ -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])
}

View File

@@ -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
)
}
}
}

View File

@@ -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
}
}

View File

@@ -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))

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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"))
}
}

View File

@@ -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
}
}
}

View File

@@ -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")

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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")

View File

@@ -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.
*

View File

@@ -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
)
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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 = {

View File

@@ -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);

View File

@@ -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>

View File

@@ -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 -->

View File

@@ -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>

View 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>

View File

@@ -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');

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>&nbsp;
<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 {

View File

@@ -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){

View File

@@ -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>
}

View File

@@ -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>

View File

@@ -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>
}
}
@*

View File

@@ -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>
}

View File

@@ -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>
}

View File

@@ -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>

View File

@@ -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)
}

View File

@@ -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;">

View File

@@ -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 {

View File

@@ -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>
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"){

View File

@@ -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;">

View File

@@ -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>
}
}
}

View File

@@ -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>

View File

@@ -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>
}
}
}

View File

@@ -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>

View File

@@ -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(',');

View File

@@ -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 &amp; 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>
}

View File

@@ -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{

View File

@@ -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;
}

View 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?');
});
}
});
})();

View 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);

File diff suppressed because one or more lines are too long

View File

@@ -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;
}
});
})();

View 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