Compare commits

...

170 Commits

Author SHA1 Message Date
takezoe
753403b4c8 Implement /meta API endpoint 2022-10-30 21:02:23 +09:00
Naoki Takezoe
5a0f9f8bbb Merge 4.38.3 release notes into master 2022-10-30 11:02:49 +09:00
takezoe
8fa22b4de2 Enhance .gitignore 2022-10-30 10:58:07 +09:00
Naoki Takezoe
d17cae16fd Revert "Fix IllegalStateException when returning unknown avatar image (#3158)" (#3179)
This reverts commit a0be02ce2f.
2022-10-30 10:18:49 +09:00
Naoki Takezoe
c4c48962cf Fix an issue that assignees are not saved in PR creation (#3178) 2022-10-30 09:54:33 +09:00
Naoki Takezoe
4140e92f0b Fix an issue that assignees are not saved in PR creation (#3178) 2022-10-30 09:54:18 +09:00
Scala Steward
887e560a1b Update oauth2-oidc-sdk to 10.1 2022-10-28 07:22:22 +09:00
pea-sys
e2d70181e8 png optimization (#3176) 2022-10-27 22:32:08 +09:00
Scala Steward
148c453dbc Update oauth2-oidc-sdk to 10.0 2022-10-24 20:52:36 +09:00
Scala Steward
f6ee9d311d Update thumbnailator to 0.4.18 2022-10-24 08:45:56 +09:00
Scala Steward
35209e43bb Update mockito-core to 4.8.1 2022-10-22 06:50:07 +09:00
Scala Steward
4a3ecf063d Update sbt-assembly to 2.0.0 2022-10-18 06:54:35 +09:00
Naoki Takezoe
4c79101624 Fix duplications in issue labels and assignees (#3168) 2022-10-17 01:00:22 +09:00
Naoki Takezoe
921b01661b Upgrade Scalatra to 2.8.4 (#3165) 2022-10-16 16:49:47 +09:00
Scala Steward
c63301d8e6 Update testcontainers-scala to 0.40.11 2022-10-11 12:22:12 +09:00
Naoki Takezoe
c9ed9d2237 Hide large diffs by default and show on demand (#3157) 2022-10-10 15:49:37 +09:00
Naoki Takezoe
ca55cbe456 Disable scalafmt on compile (#3160) 2022-10-10 03:15:59 +09:00
Scala Steward
d4828613ee Update scala-library to 2.13.10 2022-10-09 05:13:06 +09:00
Naoki Takezoe
a0be02ce2f Fix IllegalStateException when returning unknown avatar image (#3158) 2022-10-08 11:24:03 +09:00
Scala Steward
9b8016a4d5 Update mysql, postgresql to 1.17.5 2022-10-05 06:25:59 +09:00
Scala Steward
8fdd3bfd21 Update tika-core to 2.5.0 2022-10-04 06:11:07 +09:00
Scala Steward
695a061e3c Update sbt, sbt-dependency-tree to 1.7.2 2022-10-03 16:05:29 +09:00
Scala Steward
bd50d9218e Update mysql, postgresql to 1.17.4 2022-09-30 06:13:37 +09:00
Scala Steward
d8f13bc1ce Update json4s-jackson to 4.0.6 2022-09-29 17:14:45 +09:00
Scala Steward
ed84d1a3c9 Update github-api to 1.313 2022-09-27 18:50:29 +09:00
Scala Steward
3c765d879c Update mariadb-java-client to 3.0.8 2022-09-21 01:05:42 +09:00
Naoki Takezoe
4c76b6dd96 Release 4.38.2 (#3141) 2022-09-20 17:52:14 +09:00
Scala Steward
7e66917993 Update scala-library to 2.13.9 2022-09-20 07:35:35 +09:00
Scala Steward
eb0f985399 Update jetty-continuation, jetty-http, ... to 9.4.49.v20220914 2022-09-16 07:13:32 +09:00
Naoki Takezoe
3a2908c3a3 Resurrect assignee icons on the issue list (#3136) 2022-09-13 23:19:38 +09:00
Naoki Takezoe
f13c10859f Fix warnings in AccountService (#3135) 2022-09-13 15:42:43 +09:00
Scala Steward
8e8a314c91 Update oauth2-oidc-sdk to 9.43.1 2022-09-10 07:07:24 +09:00
Naoki Takezoe
dad0478b87 Remove unused import statement (#3134) 2022-09-10 03:06:21 +09:00
Naoki Takezoe
240b34016d Release 4.38.1 (#3133) 2022-09-10 02:43:06 +09:00
yurafuca
2c164d630c Fix comment diff in Chrome 105 (#3131) 2022-09-10 02:06:13 +09:00
Scala Steward
3d12a9038f Update mockito-core to 4.8.0 2022-09-08 06:51:44 +09:00
Scala Steward
71fc02b5cd Update apache-sshd to 2.9.1 2022-09-08 00:03:30 +09:00
Naoki Takezoe
74caaa3d94 Fix Markdown table CSS (#3126) 2022-09-06 00:11:53 +09:00
Naoki Takezoe
e024c12521 Fix HTML rendering of multiple asignees (#3125) 2022-09-05 22:39:36 +09:00
Scala Steward
1d0c364947 Update oauth2-oidc-sdk to 9.43 2022-09-05 18:53:55 +09:00
Naoki Takezoe
88e59405c5 Release 4.38.0 (#3123) 2022-09-04 00:36:03 +09:00
Naoki Takezoe
8160c93c15 Fix ref and ssh_url in webhook payload (#3122) 2022-09-03 13:15:37 +09:00
Naoki Takezoe
3c8113d607 Minor code cleanup for GitRepositoryServlet (#3121) 2022-09-03 12:27:06 +09:00
scala-steward-bot
8e2f421e76 Update markedj to 1.0.17 (#3120) 2022-09-03 12:19:37 +09:00
Scala Steward
56df833b4a Update oauth2-oidc-sdk to 9.42 2022-09-02 06:27:00 +09:00
Naoki Takezoe
119d7d0be0 Revert "Drop unused ActivityComponent (#3024)" (#3117)
This reverts commit bd06e6d4dc.
2022-08-28 03:33:19 +09:00
Scala Steward
d4d2ca19a1 Update oauth2-oidc-sdk to 9.41.1 2022-08-27 07:59:51 +09:00
Naoki Takezoe
4ce5597d67 Remove a link to the demo site from README (#3115) 2022-08-26 14:33:12 +09:00
Scala Steward
17888327b3 Update postgresql to 42.5.0 2022-08-25 07:01:14 +09:00
Naoki Takezoe
997def54ab Set UTF-8 as the default commit charset at the online text editor (#3112) 2022-08-21 22:50:12 +09:00
Naoki Takezoe
a407e75459 Remove unused import (#3111) 2022-08-21 21:52:55 +09:00
Scala Steward
cece53aad9 Update github-api to 1.308 2022-08-19 08:40:16 +09:00
Scala Steward
a2d63bc5f8 Update postgresql to 42.4.2 2022-08-18 07:26:45 +09:00
Scala Steward
0c81c63ab7 Update mockito-core to 4.7.0 2022-08-13 22:35:03 +09:00
Scala Steward
3e8f1eee22 Update oauth2-oidc-sdk to 9.41 2022-08-12 07:52:55 +09:00
Scala Steward
a91d1def45 Update oauth2-oidc-sdk to 9.40 2022-08-09 22:49:48 +09:00
Scala Steward
032e6d1c57 Update oauth2-oidc-sdk to 9.39.1 2022-08-09 17:31:16 +09:00
yurafuca
93eb133cba Fix milestone count (#3103) 2022-08-09 12:28:07 +09:00
Scala Steward
b7b78842da Update mariadb-java-client to 3.0.7 2022-08-05 05:48:20 +09:00
Scala Steward
8100ccaf5f Update postgresql to 42.4.1 2022-08-04 07:13:05 +09:00
Scala Steward
936e1c0e96 Update testcontainers-scala to 0.40.10 2022-07-30 12:07:10 +09:00
Scala Steward
1a7274e475 Update solidbase to 1.0.5 2022-07-25 18:59:59 +09:00
Scala Steward
ac7b83b6ed Update java-diff-utils to 4.12 2022-07-23 12:32:01 +09:00
Scala Steward
e298279032 Update apache-sshd to 2.9.0 2022-07-23 07:55:52 +09:00
Scala Steward
f7e58c28c6 Update oauth2-oidc-sdk to 9.39 2022-07-22 16:52:09 +09:00
Scala Steward
18f3bc4056 Update testcontainers-scala to 0.40.9 2022-07-14 22:32:58 +09:00
Scala Steward
4638f82889 Update oauth2-oidc-sdk to 9.38.1 2022-07-14 05:01:11 +09:00
Naoki Takezoe
4396b6eab5 Horizontal scroll for too wide tables in Markdown (#3087) 2022-07-13 21:24:58 +09:00
Zain Aftab
594ae2bb7e #2983 fixed css for table (#3082) 2022-07-13 08:19:21 +09:00
Scala Steward
429cf54bd4 Update sbt, sbt-dependency-tree to 1.7.1 2022-07-12 14:14:05 +09:00
Scala Steward
274357abc4 Update sbt, sbt-dependency-tree to 1.7.0 2022-07-11 07:24:10 +09:00
Scala Steward
0e21247755 Update oauth2-oidc-sdk to 9.38 2022-07-06 18:52:46 +09:00
Scala Steward
37f2868ff3 Update oauth2-oidc-sdk to 9.37.3 2022-07-04 17:00:26 +09:00
Scala Steward
6a85e25a62 Update mariadb-java-client to 3.0.6 2022-06-30 06:33:58 +09:00
Scala Steward
a91b1c7e79 Update mysql, postgresql to 1.17.3 2022-06-29 18:03:50 +09:00
Scala Steward
bfd9ecd130 Update github-api to 1.307 2022-06-27 18:33:09 +09:00
Scala Steward
ec888e94fd Update sbt-scoverage to 2.0.0 2022-06-26 06:30:23 +09:00
Scala Steward
a19c80bef2 Update jetty-continuation, jetty-http, ... to 9.4.48.v20220622 2022-06-23 07:01:52 +09:00
Scala Steward
7b9ade2698 Update jetty-continuation, jetty-http, ... to 9.4.47.v20220610 2022-06-21 06:25:34 +09:00
Scala Steward
d03f28eeb9 Update tika-core to 2.4.1 2022-06-18 07:22:37 +09:00
Scala Steward
288dd788bc Update org.eclipse.jgit.archive, ... to 5.13.1.202206130422-r 2022-06-13 20:47:46 +09:00
Scala Steward
2069a5a931 Update postgresql to 42.4.0 2022-06-10 02:10:28 +09:00
Scala Steward
8a434cbd15 Update mockito-core to 4.6.1 2022-06-03 07:10:47 +09:00
Scala Steward
faafe49848 Update oauth2-oidc-sdk to 9.37.2 2022-06-02 19:33:26 +09:00
Scala Steward
17a72292ce Update oauth2-oidc-sdk to 9.37.1 2022-06-01 19:03:35 +09:00
Scala Steward
9ae09f1801 Update testcontainers-scala to 0.40.8 2022-06-01 06:54:21 +09:00
Scala Steward
abbba17e4b Update oauth2-oidc-sdk to 9.37 2022-05-31 06:29:58 +09:00
Scala Steward
479d5da17e Update mockito-core to 4.6.0 2022-05-28 07:59:15 +09:00
Scala Steward
dec7890d04 Update oauth2-oidc-sdk to 9.36 2022-05-26 19:45:42 +09:00
Scala Steward
61910a6667 Update mariadb-java-client to 3.0.5 2022-05-26 07:06:52 +09:00
Scala Steward
ac2119ac3b Update postgresql to 42.3.6 2022-05-24 23:05:58 +09:00
Scala Steward
8c71cd7369 Update mysql, postgresql to 1.17.2 2022-05-20 08:20:48 +09:00
porkotron
ec8b6ff18f Implement missing parameters on commits api (#3061)
* Add missing leading slash html_url api path
* Use HEAD as default ref
* Implement missing parameters

Co-authored-by: Petri Pyy <petri.pyy@abako.fi>
2022-05-15 01:51:16 +09:00
Naoki Takezoe
e214804d57 Bump mariadb-java-client to 3.0.4 (#3059) 2022-05-08 21:57:25 +09:00
Scala Steward
39448f2e3c Update postgresql to 42.3.5 2022-05-05 07:37:40 +09:00
Naoki Takezoe
79dc6fc247 Support multiple assignees for Issues and Pull requests (#3055) 2022-05-03 22:51:54 +09:00
Naoki Takezoe
a800f305f2 Refactor old migrations (#3056) 2022-05-03 22:24:14 +09:00
Scala Steward
67e0fb6b3f Update tika-core to 2.4.0 2022-05-03 03:41:42 +09:00
Naoki Takezoe
e401d30159 Fix wrong overwriting of updatedDate of Issues and Pull requests (#3053) 2022-05-02 22:11:46 +09:00
Scala Steward
504e651828 Update testcontainers-scala to 0.40.7 2022-04-28 06:40:34 +09:00
Scala Steward
7a8da2f074 Update oauth2-oidc-sdk to 9.35 2022-04-27 01:22:23 +09:00
Scala Steward
1e47e96634 Update testcontainers-scala to 0.40.6 2022-04-25 07:11:30 +09:00
Scala Steward
69502480b4 Update github-api to 1.306 2022-04-22 03:39:39 +09:00
Scala Steward
5833e3fcca Update mockito-core to 4.5.1 2022-04-21 22:31:44 +09:00
Scala Steward
4e47ffb1d2 Update github-api to 1.305 2022-04-20 09:28:21 +09:00
Scala Steward
3056333a7c Update github-api to 1.304 2022-04-20 06:08:23 +09:00
Scala Steward
ad0cd7f625 Update mockito-core to 4.5.0 2022-04-20 06:08:10 +09:00
kenji yoshida
dedaa263ce Scala 3.1.2 2022-04-18 07:02:22 +09:00
Scala Steward
bccf729102 Update oauth2-oidc-sdk to 9.34 2022-04-18 06:46:15 +09:00
onukura
a244c61e56 Hide header content on signin and register page (#2589)
Co-authored-by: Naoki Takezoe <takezoe@gmail.com>
2022-04-18 01:43:03 +09:00
Naoki Takezoe
37594935f0 Custom fields for issues and pull requests (#3034) 2022-04-18 01:01:15 +09:00
Scala Steward
e521c2158b Update postgresql to 42.3.4 2022-04-16 07:06:11 +09:00
Scala Steward
b1c8a8c76e Update mysql, postgresql to 1.17.1 2022-04-14 01:44:20 +09:00
Scala Steward
8d92f210f0 Update oauth2-oidc-sdk to 9.33 2022-04-13 06:02:00 +09:00
Scala Steward
287281923c Update mysql, postgresql to 1.17.0 2022-04-12 22:58:11 +09:00
dependabot[bot]
6f18135574 Bump actions/setup-java from 2 to 3 (#3037)
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 2 to 3.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v2...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-12 17:22:01 +09:00
dependabot[bot]
58cf9bec3b Bump actions/upload-artifact from 2 to 3 (#3038)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-12 17:21:43 +09:00
Scala Steward
b7a45e9901 Update json4s-jackson to 4.0.5 2022-04-05 18:26:05 +09:00
Scala Steward
f5585b39c6 Update testcontainers-scala to 0.40.5 2022-04-04 05:32:48 +09:00
Scala Steward
9c3ef37274 Update jetty-continuation, jetty-http, ... to 9.4.46.v20220331 2022-04-02 07:27:09 +09:00
Scala Steward
e904c0b689 Update oauth2-oidc-sdk to 9.32 2022-03-30 18:30:21 +09:00
Scala Steward
237f97f069 Update testcontainers-scala to 0.40.4 2022-03-30 00:37:40 +09:00
Scala Steward
1eb2a04090 Update oauth2-oidc-sdk to 9.31.1 2022-03-29 20:18:47 +09:00
Naoki Takezoe
cda6106858 Allow to configure Jetty idle timeout in standalone mode (#3027) 2022-03-29 07:44:14 +09:00
Naoki Takezoe
8c4ce5e5f4 Hash password when reset it (#3026) 2022-03-29 07:16:12 +09:00
Naoki Takezoe
b5ee6431c4 Reset password email by users themselves (#3023) 2022-03-29 01:04:57 +09:00
Naoki Takezoe
bd06e6d4dc Drop unused ActivityComponent (#3024) 2022-03-28 22:24:34 +09:00
dependabot[bot]
3b607e74ed Bump actions/cache from 2.1.7 to 3 (#3025)
Bumps [actions/cache](https://github.com/actions/cache) from 2.1.7 to 3.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v2.1.7...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-28 21:55:49 +09:00
Scala Steward
dc16474ed3 Update oauth2-oidc-sdk to 9.31 2022-03-19 19:51:24 +09:00
Scala Steward
dc0f088be0 Update oauth2-oidc-sdk to 9.30 2022-03-19 09:21:48 +09:00
Scala Steward
2718e10044 Update oauth2-oidc-sdk to 9.29 2022-03-17 06:15:25 +09:00
Scala Steward
4213dcb8a2 Update testcontainers-scala to 0.40.3 2022-03-16 11:49:15 +09:00
Scala Steward
39afb31200 Update solidbase to 1.0.4 2022-03-14 06:30:05 +09:00
dependabot[bot]
8e1ce1c96a Bump actions/checkout from 2 to 3 (#3011)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [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/v2...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-13 17:52:16 +09:00
Scala Steward
fdfd8293d6 Update oauth2-oidc-sdk to 9.28 2022-03-12 08:34:36 +09:00
Scala Steward
b7d3be6a14 Update oauth2-oidc-sdk to 9.27.1 2022-03-09 05:42:19 +09:00
Scala Steward
bb5c809749 Update mockito-core to 4.4.0 2022-03-09 05:41:08 +09:00
Scala Steward
a2cbe7cbf5 Update logback-classic to 1.2.11 2022-03-06 20:37:40 +09:00
Scala Steward
b1fb09ac99 Update github-api to 1.303 2022-03-06 08:05:46 +09:00
Scala Steward
201ae7e7db Update github-api to 1.302 2022-03-02 12:23:53 +09:00
ziggystar
2978decb84 fix: the URLs returned in tag api to point to something that exists (#2998)
Co-authored-by: Thomas Geier <thomas.geier@solidat.de>
2022-02-24 14:19:24 +09:00
Scala Steward
5fbbd4d5f3 Update testcontainers-scala to 0.40.2 2022-02-21 06:39:36 +09:00
Scala Steward
4abd363f62 Update oauth2-oidc-sdk to 9.27 2022-02-18 06:19:36 +09:00
Scala Steward
8288db60a9 Update postgresql to 42.3.3 2022-02-16 05:08:44 +09:00
Scala Steward
6cdf679571 Update sbt-assembly to 1.2.0 2022-02-13 18:11:57 +09:00
Scala Steward
51179acc12 Update sbt-assembly to 1.1.1 2022-02-13 17:35:25 +09:00
Scala Steward
1ff27d3e17 Update thumbnailator to 0.4.17 2022-02-12 06:52:22 +09:00
Scala Steward
1e9bce3d5a Update oauth2-oidc-sdk to 9.25 2022-02-10 18:47:18 +09:00
Scala Steward
8811ea2b58 Update jetty-continuation, jetty-http, ... to 9.4.45.v20220203 2022-02-08 03:05:22 +09:00
Scala Steward
be7cdd0a28 Update tika-core to 2.3.0 2022-02-08 03:03:55 +09:00
Scala Steward
611a05aa48 Update testcontainers-scala to 0.40.1 2022-02-06 07:42:19 +09:00
Naoki Takezoe
9f95a757a2 Update CHANGELOG of 4.37.2 2022-02-05 08:47:17 +09:00
Scala Steward
58b577453c Update oauth2-oidc-sdk to 9.24 2022-02-05 06:57:49 +09:00
Naoki Takezoe
78acfd87e8 Update CHANGELOG.md 2022-02-03 11:41:40 +09:00
Scala Steward
bf9cb26164 Update typesafe:config to 1.4.2 2022-02-03 07:29:36 +09:00
Scala Steward
68a99ada0b Update postgresql to 42.3.2 2022-02-02 01:37:26 +09:00
Scala Steward
1d841ce171 Update sbt, sbt-dependency-tree to 1.6.2 2022-02-01 15:00:58 +09:00
Naoki Takezoe
ccfdbfd276 Mention that GitBucket doesn't support JakartaEE in README (#2986) 2022-01-31 23:51:44 +09:00
Naoki Takezoe
70d584d56d Report commit failure caused by hook (#2984) 2022-01-30 21:38:15 +09:00
kenji yoshida
a49ffb07be update Scala 3 version 2022-01-30 16:14:57 +09:00
Scala Steward
baa9caf010 Update testcontainers-scala to 0.40.0 2022-01-28 14:11:19 +09:00
Scala Steward
22025c875f Update oauth2-oidc-sdk to 9.22.2 2022-01-26 06:46:20 +09:00
Scala Steward
3806995a50 Update mockito-core to 4.3.1 2022-01-26 06:44:32 +09:00
Scala Steward
94d2650491 Update mockito-core to 4.3.0 2022-01-24 22:05:03 +09:00
Scala Steward
844f24afa4 Update json4s-jackson to 4.0.4 2022-01-23 12:27:59 +09:00
kenji yoshida
083396a8e4 update maven badge 2022-01-22 16:25:53 +09:00
kenji yoshida
e02769056b remove Resolver.jcenterRepo
- 00eab5d584
- debbc21bf3
- https://repo1.maven.org/maven2/io/github/java-diff-utils/java-diff-utils/
2022-01-22 15:49:21 +09:00
Scala Steward
024f8e8d80 Update oauth2-oidc-sdk to 9.22.1 2022-01-21 22:51:38 +09:00
Scala Steward
3b4a7c3c47 Update mariadb-java-client to 2.7.5 2022-01-20 08:22:53 +09:00
Scala Steward
d9838d9cba Update mysql, postgresql to 1.16.3 2022-01-19 23:59:28 +09:00
100 changed files with 2372 additions and 973 deletions

View File

@@ -10,9 +10,9 @@ jobs:
matrix: matrix:
java: [8, 11, 17] java: [8, 11, 17]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Cache - name: Cache
uses: actions/cache@v2.1.7 uses: actions/cache@v3
env: env:
cache-name: cache-sbt-libs cache-name: cache-sbt-libs
with: with:
@@ -22,18 +22,18 @@ jobs:
~/.cache/coursier/v1 ~/.cache/coursier/v1
key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }} key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }}
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v2 uses: actions/setup-java@v3
with: with:
java-version: ${{ matrix.java }} java-version: ${{ matrix.java }}
distribution: adopt distribution: adopt
- name: Run tests - name: Run tests
run: sbt scalafmtSbtCheck scalafmtCheck test:scalafmtCheck test run: sbt scalafmtSbtCheck scalafmtCheck test:scalafmtCheck test
- name: Scala 3 - name: Scala 3
run: sbt '++ 3.0.1!' update # TODO run: sbt '++ 3.1.2!' update # TODO
- name: Build executable - name: Build executable
run: sbt executable run: sbt executable
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: gitbucket-java${{ matrix.java }}-${{ github.sha }} name: gitbucket-java${{ matrix.java }}-${{ github.sha }}
path: ./target/executable/gitbucket.* path: ./target/executable/gitbucket.*

2
.gitignore vendored
View File

@@ -2,6 +2,8 @@
*.log *.log
.ensime .ensime
.ensime_cache .ensime_cache
.DS_Store
.java-version
# sbt specific # sbt specific
dist/* dist/*

View File

@@ -1,14 +1,37 @@
# Changelog # Changelog
All changes to the project will be documented in this file. All changes to the project will be documented in this file.
### 4.37.2 - 16 Jan 2022 ## 4.38.3 - 30 Oct 2022
- Security fix - Fix several issues around multiple assignees in issues and pull requests
- Fix IllegalStateException when returning unknown avatar image
### 4.37.1 - 14 Dec 2021 ## 4.38.2 - 20 Sep 2022
- Resurrect assignee icons on the issue list
## 4.38.1 - 10 Sep 2022
- Fix comment diff in Chrome 105
- Fix Markdown table CSS
- Fix HTML rendering of multiple asignees
## 4.38.0 - 3 Sep 2022
- Support multiple assignees for Issues and Pull requests
- Custom fields for issues and pull requests
- Reset password by users
- Allow to configure Jetty idle timeout in standalone mode
- Horizontal scroll for too wide tables in Markdown
- Hide header content on signin and register page
- Fix the default charset of the online editor in the repository viewer
- Fix the milestone count
- Some improvements and bugfixes for WebAPI and WebHook
## 4.37.2 - 16 Jan 2022
- Fixed a security issue reported by [Positive Technologies](https://www.ptsecurity.com/ww-en/). Great thanks for their detailed report and close support!
## 4.37.1 - 14 Dec 2021
- Update gist-plugin and notification-plugin - Update gist-plugin and notification-plugin
- Fix SSHCommand extension point for apache-sshd 2.x - Fix SSHCommand extension point for apache-sshd 2.x
### 4.37.0 - 11 Dec 2021 ## 4.37.0 - 11 Dec 2021
- Enhance Git Reference APIs - Enhance Git Reference APIs
- Add milestone data to issue list API - Add milestone data to issue list API
- Support "all" in issue list API - Support "all" in issue list API
@@ -17,32 +40,32 @@ All changes to the project will be documented in this file.
- Relax max passward length limitation - Relax max passward length limitation
- Relax max webhook url length limitation - Relax max webhook url length limitation
### 4.36.2 - 16 Aug 2021 ## 4.36.2 - 16 Aug 2021
- Escape user name in avatar image tag - Escape user name in avatar image tag
### 4.36.1 - 22 Jul 2021 ## 4.36.1 - 22 Jul 2021
- Bump gitbucket-gist-plugin to 4.21.0 - Bump gitbucket-gist-plugin to 4.21.0
### 4.36.0 - 17 Jul 2021 ## 4.36.0 - 17 Jul 2021
- Tag selector in the repository viewer - Tag selector in the repository viewer
- Link issues/pull requests of other repositories - Link issues/pull requests of other repositories
- Files and lines can be linked in the diff view - Files and lines can be linked in the diff view
- Option to disable XSS protection - Option to disable XSS protection
### 4.35.3 - 14 Jan 2021 ## 4.35.3 - 14 Jan 2021
- Fix a bug that Wiki page cannot be deleted - Fix a bug that Wiki page cannot be deleted
- Fix a deployment issue on Tomcat - Fix a deployment issue on Tomcat
### 4.35.2 - 30 Dec 2020 ## 4.35.2 - 30 Dec 2020
- Upgrade gitbucket-notifications-plugin to 1.10.0 - Upgrade gitbucket-notifications-plugin to 1.10.0
- Upgrade oauth2-oidc-sdk to 8.29.1 to solve dependency issue - Upgrade oauth2-oidc-sdk to 8.29.1 to solve dependency issue
### 4.35.1 - 29 Dec 2020 ## 4.35.1 - 29 Dec 2020
- Fix database migration issue which happens if webhook is configured - Fix database migration issue which happens if webhook is configured
- Call push webhook when pull request is merged - Call push webhook when pull request is merged
- Show commit status at commits tab of pull request - Show commit status at commits tab of pull request
### 4.35.0 - 25 Dec 2020 ## 4.35.0 - 25 Dec 2020
- Editor and source viewer color theme - Editor and source viewer color theme
- Auto completion for issues and pull requests - Auto completion for issues and pull requests
- Upload image from clipboard - Upload image from clipboard

View File

@@ -1,4 +1,4 @@
GitBucket [![Gitter chat](https://badges.gitter.im/gitbucket/gitbucket.svg)](https://gitter.im/gitbucket/gitbucket) [![build](https://github.com/gitbucket/gitbucket/workflows/build/badge.svg?branch=master)](https://github.com/gitbucket/gitbucket/actions?query=workflow%3Abuild+branch%3Amaster) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.github.gitbucket/gitbucket_2.13/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.github.gitbucket/gitbucket_2.13) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/gitbucket/gitbucket/blob/master/LICENSE) GitBucket [![Gitter chat](https://badges.gitter.im/gitbucket/gitbucket.svg)](https://gitter.im/gitbucket/gitbucket) [![build](https://github.com/gitbucket/gitbucket/workflows/build/badge.svg?branch=master)](https://github.com/gitbucket/gitbucket/actions?query=workflow%3Abuild+branch%3Amaster) [![gitbucket Scala version support](https://index.scala-lang.org/gitbucket/gitbucket/gitbucket/latest-by-scala-version.svg)](https://index.scala-lang.org/gitbucket/gitbucket/gitbucket) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/gitbucket/gitbucket/blob/master/LICENSE)
========= =========
GitBucket is a Git web platform powered by Scala offering: GitBucket is a Git web platform powered by Scala offering:
@@ -10,8 +10,6 @@ GitBucket is a Git web platform powered by Scala offering:
![GitBucket](https://gitbucket.github.io/img/screenshots/screenshot-repository_viewer.png) ![GitBucket](https://gitbucket.github.io/img/screenshots/screenshot-repository_viewer.png)
You can try an [online demo](https://gitbucket.herokuapp.com/) *(ID: root / Pass: root)* of GitBucket, and also get the latest information at [GitBucket News](https://gitbucket.github.io/gitbucket-news/).
Features Features
-------- --------
The current version of GitBucket provides many features such as: The current version of GitBucket provides many features such as:
@@ -31,7 +29,7 @@ GitBucket requires **Java8**. You have to install it, if it is not already insta
1. Download the latest **gitbucket.war** from [the releases page](https://github.com/gitbucket/gitbucket/releases) and run it by `java -jar gitbucket.war`. 1. Download the latest **gitbucket.war** from [the releases page](https://github.com/gitbucket/gitbucket/releases) and run it by `java -jar gitbucket.war`.
2. Go to `http://[hostname]:8080/` and log in with ID: **root** / Pass: **root**. 2. Go to `http://[hostname]:8080/` and log in with ID: **root** / Pass: **root**.
You can also deploy `gitbucket.war` to a servlet container which supports Servlet 3.0 (like Jetty, Tomcat, JBoss, etc) You can also deploy `gitbucket.war` to a servlet container which supports Servlet 3.0 (like Jetty, Tomcat, JBoss, etc). Note that GitBucket doesn't support Jakarta EE yet.
For more information about installation on Mac or Windows Server (with IIS), or configuration of Apache or Nginx and also integration with other tools or services such as Jenkins or Slack, see [Wiki](https://github.com/gitbucket/gitbucket/wiki). For more information about installation on Mac or Windows Server (with IIS), or configuration of Apache or Nginx and also integration with other tools or services such as Jenkins or Slack, see [Wiki](https://github.com/gitbucket/gitbucket/wiki).
@@ -61,22 +59,29 @@ Support
- If you can't find same question and report, send it to our [Gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue. - If you can't find same question and report, send it to our [Gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles. - The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles.
What's New in 4.37.x What's New in 4.38.x
------------- -------------
### 4.37.2 - 16 Jan 2022 ## 4.38.3 - 30 Oct 2022
- Security fix - Fix several issues around multiple assignees in issues and pull requests
- Fix IllegalStateException when returning unknown avatar image
### 4.37.1 - 14 Dec 2021 ## 4.38.2 - 20 Sep 2022
- Update gist-plugin and notification-plugin - Resurrect assignee icons on the issue list
- Fix SSHCommand extension point for apache-sshd 2.x
### 4.37.0 - 11 Dec 2021 ## 4.38.1 - 10 Sep 2022
- Enhance Git Reference APIs - Fix comment diff in Chrome 105
- Add milestone data to issue list API - Fix Markdown table CSS
- Support "all" in issue list API - Fix HTML rendering of multiple asignees
- Support EDDSA in signed commit verification
- Support custom SSH url ## 4.38.0 - 3 Sep 2022
- Relax max passward length limitation - Support multiple assignees for Issues and Pull requests
- Relax max webhook url length limitation - Custom fields for issues and pull requests
- Reset password by users
- Allow to configure Jetty idle timeout in standalone mode
- Horizontal scroll for too wide tables in Markdown
- Hide header content on signin and register page
- Fix the default charset of the online editor in the repository viewer
- Fix the milestone count
- Some improvements and bugfixes for WebAPI and WebHook
See the [change log](CHANGELOG.md) for all of the updates. See the [change log](CHANGELOG.md) for all of the updates.

View File

@@ -3,10 +3,10 @@ import com.jsuereth.sbtpgp.PgpKeys._
val Organization = "io.github.gitbucket" val Organization = "io.github.gitbucket"
val Name = "gitbucket" val Name = "gitbucket"
val GitBucketVersion = "4.37.2" val GitBucketVersion = "4.38.3"
val ScalatraVersion = "2.8.2" val ScalatraVersion = "2.8.4"
val JettyVersion = "9.4.44.v20210927" val JettyVersion = "9.4.49.v20220914"
val JgitVersion = "5.13.0.202109080827-r" val JgitVersion = "5.13.1.202206130422-r"
lazy val root = (project in file(".")) lazy val root = (project in file("."))
.enablePlugins(SbtTwirl, ScalatraPlugin) .enablePlugins(SbtTwirl, ScalatraPlugin)
@@ -15,16 +15,15 @@ sourcesInBase := false
organization := Organization organization := Organization
name := Name name := Name
version := GitBucketVersion version := GitBucketVersion
scalaVersion := "2.13.8" scalaVersion := "2.13.10"
scalafmtOnCompile := true // scalafmtOnCompile := true
coverageExcludedPackages := ".*\\.html\\..*" coverageExcludedPackages := ".*\\.html\\..*"
// dependency settings // dependency settings
resolvers ++= Seq( resolvers ++= Seq(
Classpaths.typesafeReleases, Classpaths.typesafeReleases,
Resolver.jcenterRepo,
"sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/" "sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/"
) )
@@ -34,42 +33,42 @@ libraryDependencies ++= Seq(
"org.scalatra" %% "scalatra" % ScalatraVersion cross CrossVersion.for3Use2_13, "org.scalatra" %% "scalatra" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.scalatra" %% "scalatra-json" % ScalatraVersion cross CrossVersion.for3Use2_13, "org.scalatra" %% "scalatra-json" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.scalatra" %% "scalatra-forms" % ScalatraVersion cross CrossVersion.for3Use2_13, "org.scalatra" %% "scalatra-forms" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.json4s" %% "json4s-jackson" % "4.0.3" cross CrossVersion.for3Use2_13, "org.json4s" %% "json4s-jackson" % "4.0.6" cross CrossVersion.for3Use2_13,
"commons-io" % "commons-io" % "2.11.0", "commons-io" % "commons-io" % "2.11.0",
"io.github.gitbucket" % "solidbase" % "1.0.3", "io.github.gitbucket" % "solidbase" % "1.0.5",
"io.github.gitbucket" % "markedj" % "1.0.16", "io.github.gitbucket" % "markedj" % "1.0.17",
"org.apache.commons" % "commons-compress" % "1.21", "org.apache.commons" % "commons-compress" % "1.21",
"org.apache.commons" % "commons-email" % "1.5", "org.apache.commons" % "commons-email" % "1.5",
"commons-net" % "commons-net" % "3.8.0", "commons-net" % "commons-net" % "3.8.0",
"org.apache.httpcomponents" % "httpclient" % "4.5.13", "org.apache.httpcomponents" % "httpclient" % "4.5.13",
"org.apache.sshd" % "apache-sshd" % "2.8.0" exclude ("org.slf4j", "slf4j-jdk14") exclude ("org.apache.sshd", "sshd-mina") exclude ("org.apache.sshd", "sshd-netty"), "org.apache.sshd" % "apache-sshd" % "2.9.1" exclude ("org.slf4j", "slf4j-jdk14") exclude ("org.apache.sshd", "sshd-mina") exclude ("org.apache.sshd", "sshd-netty"),
"org.apache.tika" % "tika-core" % "2.2.1", "org.apache.tika" % "tika-core" % "2.5.0",
"com.github.takezoe" %% "blocking-slick-32" % "0.0.12" cross CrossVersion.for3Use2_13, "com.github.takezoe" %% "blocking-slick-32" % "0.0.12" cross CrossVersion.for3Use2_13,
"com.novell.ldap" % "jldap" % "2009-10-07", "com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.199", "com.h2database" % "h2" % "1.4.199",
"org.mariadb.jdbc" % "mariadb-java-client" % "2.7.4", "org.mariadb.jdbc" % "mariadb-java-client" % "3.0.8",
"org.postgresql" % "postgresql" % "42.3.1", "org.postgresql" % "postgresql" % "42.5.0",
"ch.qos.logback" % "logback-classic" % "1.2.10", "ch.qos.logback" % "logback-classic" % "1.2.11",
"com.zaxxer" % "HikariCP" % "4.0.3" exclude ("org.slf4j", "slf4j-api"), "com.zaxxer" % "HikariCP" % "4.0.3" exclude ("org.slf4j", "slf4j-api"),
"com.typesafe" % "config" % "1.4.1", "com.typesafe" % "config" % "1.4.2",
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0", "fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0",
"io.github.java-diff-utils" % "java-diff-utils" % "4.11", "io.github.java-diff-utils" % "java-diff-utils" % "4.12",
"org.cache2k" % "cache2k-all" % "1.6.0.Final", "org.cache2k" % "cache2k-all" % "1.6.0.Final",
"net.coobird" % "thumbnailator" % "0.4.16", "net.coobird" % "thumbnailator" % "0.4.18",
"com.github.zafarkhaja" % "java-semver" % "0.9.0", "com.github.zafarkhaja" % "java-semver" % "0.9.0",
"com.nimbusds" % "oauth2-oidc-sdk" % "9.22", "com.nimbusds" % "oauth2-oidc-sdk" % "10.1",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided", "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"junit" % "junit" % "4.13.2" % "test", "junit" % "junit" % "4.13.2" % "test",
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test" cross CrossVersion.for3Use2_13, "org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test" cross CrossVersion.for3Use2_13,
"org.mockito" % "mockito-core" % "4.2.0" % "test", "org.mockito" % "mockito-core" % "4.8.1" % "test",
"com.dimafeng" %% "testcontainers-scala" % "0.39.12" % "test", "com.dimafeng" %% "testcontainers-scala" % "0.40.11" % "test",
"org.testcontainers" % "mysql" % "1.16.2" % "test", "org.testcontainers" % "mysql" % "1.17.5" % "test",
"org.testcontainers" % "postgresql" % "1.16.2" % "test", "org.testcontainers" % "postgresql" % "1.17.5" % "test",
"net.i2p.crypto" % "eddsa" % "0.3.0", "net.i2p.crypto" % "eddsa" % "0.3.0",
"is.tagomor.woothee" % "woothee-java" % "1.11.0", "is.tagomor.woothee" % "woothee-java" % "1.11.0",
"org.ec4j.core" % "ec4j-core" % "0.3.0", "org.ec4j.core" % "ec4j-core" % "0.3.0",
"org.kohsuke" % "github-api" % "1.301" % "test" "org.kohsuke" % "github-api" % "1.313" % "test"
) )
libraryDependencies ~= { libraryDependencies ~= {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -1 +1 @@
sbt.version=1.6.1 sbt.version=1.7.2

View File

@@ -2,10 +2,10 @@ scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.5.1") addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.5.1")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.0.0")
addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.4") addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.4")
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2")
addSbtPlugin("com.typesafe.sbt" % "sbt-license-report" % "1.2.0") addSbtPlugin("com.typesafe.sbt" % "sbt-license-report" % "1.2.0")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.0")
addDependencyTreePlugin addDependencyTreePlugin

View File

@@ -61,6 +61,7 @@ public class JettyLauncher {
String redirectHttps = getEnvironmentVariable("gitbucket.redirectHttps"); String redirectHttps = getEnvironmentVariable("gitbucket.redirectHttps");
String contextPath = getEnvironmentVariable("gitbucket.prefix"); String contextPath = getEnvironmentVariable("gitbucket.prefix");
String tmpDirPath = getEnvironmentVariable("gitbucket.tempDir"); String tmpDirPath = getEnvironmentVariable("gitbucket.tempDir");
String jettyIdleTimeout = getEnvironmentVariable("gitbucket.jettyIdleTimeout");
boolean saveSessions = false; boolean saveSessions = false;
for(String arg: args) { for(String arg: args) {
@@ -107,6 +108,9 @@ public class JettyLauncher {
case "--plugin_dir": case "--plugin_dir":
System.setProperty("gitbucket.pluginDir", dim[1]); System.setProperty("gitbucket.pluginDir", dim[1]);
break; break;
case "--jetty_idle_timeout":
jettyIdleTimeout = dim[1];
break;
} }
} }
} }
@@ -130,6 +134,11 @@ public class JettyLauncher {
if (connectorsSet.contains(Connectors.HTTPS)) { if (connectorsSet.contains(Connectors.HTTPS)) {
httpConfig.setSecurePort(fallback(securePort, Defaults.HTTPS_PORT, Integer::parseInt)); httpConfig.setSecurePort(fallback(securePort, Defaults.HTTPS_PORT, Integer::parseInt));
} }
if (jettyIdleTimeout != null && jettyIdleTimeout.trim().length() != 0) {
httpConfig.setIdleTimeout(Long.parseLong(jettyIdleTimeout.trim()));
} else {
httpConfig.setIdleTimeout(300000L); // default is 5min
}
if (connectorsSet.contains(Connectors.HTTP)) { if (connectorsSet.contains(Connectors.HTTP)) {
final ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConfig)); final ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConfig));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,18 +0,0 @@
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
SELECT
A.USER_NAME,
A.REPOSITORY_NAME,
A.ISSUE_ID,
COALESCE(B.COMMENT_COUNT, 0) + COALESCE(C.COMMENT_COUNT, 0) AS COMMENT_COUNT
FROM ISSUE A
LEFT OUTER JOIN (
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
) B
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID)
LEFT OUTER JOIN (
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM COMMIT_COMMENT
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
) C
ON (A.USER_NAME = C.USER_NAME AND A.REPOSITORY_NAME = C.REPOSITORY_NAME AND A.ISSUE_ID = C.ISSUE_ID);

View File

@@ -343,4 +343,28 @@
<addPrimaryKey constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_PK" tableName="PROTECTED_BRANCH_REQUIRE_CONTEXT" columnNames="USER_NAME, REPOSITORY_NAME, BRANCH, CONTEXT"/> <addPrimaryKey constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_PK" tableName="PROTECTED_BRANCH_REQUIRE_CONTEXT" columnNames="USER_NAME, REPOSITORY_NAME, BRANCH, CONTEXT"/>
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_FK0" baseTableName="PROTECTED_BRANCH_REQUIRE_CONTEXT" baseColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" referencedTableName="PROTECTED_BRANCH" referencedColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" onDelete="CASCADE" onUpdate="CASCADE"/> <addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_FK0" baseTableName="PROTECTED_BRANCH_REQUIRE_CONTEXT" baseColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" referencedTableName="PROTECTED_BRANCH" referencedColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" onDelete="CASCADE" onUpdate="CASCADE"/>
<!--================================================================================================-->
<!-- ISSUE_OUTLINE_VIEW -->
<!--================================================================================================-->
<sql>
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
SELECT
A.USER_NAME,
A.REPOSITORY_NAME,
A.ISSUE_ID,
COALESCE(B.COMMENT_COUNT, 0) + COALESCE(C.COMMENT_COUNT, 0) AS COMMENT_COUNT
FROM ISSUE A
LEFT OUTER JOIN (
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
) B
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID)
LEFT OUTER JOIN (
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM COMMIT_COMMENT
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
) C
ON (A.USER_NAME = C.USER_NAME AND A.REPOSITORY_NAME = C.REPOSITORY_NAME AND A.ISSUE_ID = C.ISSUE_ID)
</sql>
</changeSet> </changeSet>

View File

@@ -1,26 +0,0 @@
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
SELECT
A.USER_NAME,
A.REPOSITORY_NAME,
A.ISSUE_ID,
COALESCE(B.COMMENT_COUNT, 0) + COALESCE(C.COMMENT_COUNT, 0) AS COMMENT_COUNT,
COALESCE(D.ORDERING, 9999) AS PRIORITY
FROM ISSUE A
LEFT OUTER JOIN (
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
) B
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID)
LEFT OUTER JOIN (
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM COMMIT_COMMENT
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
) C
ON (A.USER_NAME = C.USER_NAME AND A.REPOSITORY_NAME = C.REPOSITORY_NAME AND A.ISSUE_ID = C.ISSUE_ID)
LEFT OUTER JOIN PRIORITY D
ON (A.PRIORITY_ID = D.PRIORITY_ID);

View File

@@ -35,4 +35,28 @@
<column name="URL" type="varchar(200)" nullable="false"/> <column name="URL" type="varchar(200)" nullable="false"/>
<column name="EVENT" type="varchar(30)" nullable="false"/> <column name="EVENT" type="varchar(30)" nullable="false"/>
</createTable> </createTable>
<sql>
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
SELECT
A.USER_NAME,
A.REPOSITORY_NAME,
A.ISSUE_ID,
COALESCE(B.COMMENT_COUNT, 0) + COALESCE(C.COMMENT_COUNT, 0) AS COMMENT_COUNT,
COALESCE(D.ORDERING, 9999) AS PRIORITY
FROM ISSUE A
LEFT OUTER JOIN (
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
) B
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID)
LEFT OUTER JOIN (
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM COMMIT_COMMENT
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
) C
ON (A.USER_NAME = C.USER_NAME AND A.REPOSITORY_NAME = C.REPOSITORY_NAME AND A.ISSUE_ID = C.ISSUE_ID)
LEFT OUTER JOIN PRIORITY D
ON (A.PRIORITY_ID = D.PRIORITY_ID);
</sql>
</changeSet> </changeSet>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<!--================================================================================================-->
<!-- CUSTOM_FIELD -->
<!--================================================================================================-->
<createTable tableName="CUSTOM_FIELD">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="FIELD_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
<column name="FIELD_NAME" type="varchar(100)" nullable="false"/>
<column name="FIELD_TYPE" type="varchar(100)" nullable="false"/>
<column name="ENABLE_FOR_ISSUES" type="boolean" nullable="false"/>
<column name="ENABLE_FOR_PULL_REQUESTS" type="boolean" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_CUSTOM_FIELD_PK" tableName="CUSTOM_FIELD" columnNames="USER_NAME, REPOSITORY_NAME, FIELD_ID"/>
<addForeignKeyConstraint constraintName="IDX_CUSTOM_FIELD_FK0" baseTableName="CUSTOM_FIELD" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
<!--================================================================================================-->
<!-- ISSUE_CUSTOM_FIELD -->
<!--================================================================================================-->
<createTable tableName="ISSUE_CUSTOM_FIELD">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="ISSUE_ID" type="int" nullable="false"/>
<column name="FIELD_ID" type="int" nullable="false"/>
<column name="VALUE" type="varchar(200)" nullable="true"/>
</createTable>
<addPrimaryKey constraintName="IDX_ISSUE_CUSTOM_FIELD_PK" tableName="ISSUE_CUSTOM_FIELD" columnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID, FIELD_ID"/>
<addForeignKeyConstraint constraintName="IDX_ISSUE_CUSTOM_FIELD_FK0" baseTableName="ISSUE_CUSTOM_FIELD" baseColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID" referencedTableName="ISSUE" referencedColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID"/>
<addForeignKeyConstraint constraintName="IDX_ISSUE_CUSTOM_FIELD_FK1" baseTableName="ISSUE_CUSTOM_FIELD" baseColumnNames="USER_NAME, REPOSITORY_NAME, FIELD_ID" referencedTableName="CUSTOM_FIELD" referencedColumnNames="USER_NAME, REPOSITORY_NAME, FIELD_ID"/>
<!--================================================================================================-->
<!-- ISSUE_ASSIGNEE -->
<!--================================================================================================-->
<createTable tableName="ISSUE_ASSIGNEE">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="ISSUE_ID" type="int" nullable="false"/>
<column name="ASSIGNEE_USER_NAME" type="varchar(100)" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_ISSUE_ASSIGNEE_PK" tableName="ISSUE_ASSIGNEE" columnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID, ASSIGNEE_USER_NAME"/>
<addForeignKeyConstraint constraintName="IDX_ISSUE_ASSIGNEE_FK0" baseTableName="ISSUE_ASSIGNEE" baseColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID" referencedTableName="ISSUE" referencedColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID"/>
<!--
<addForeignKeyConstraint constraintName="IDX_ISSUE_ASSIGNEE_FK1" baseTableName="ISSUE_ASSIGNEE" baseColumnNames="ASSIGNEE_USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
-->
<sql>
INSERT INTO ISSUE_ASSIGNEE (USER_NAME, REPOSITORY_NAME, ISSUE_ID, ASSIGNEE_USER_NAME)
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, ASSIGNED_USER_NAME FROM ISSUE WHERE ASSIGNED_USER_NAME IS NOT NULL
</sql>
<dropColumn tableName="ISSUE" columnName="ASSIGNED_USER_NAME"/>
</changeSet>

View File

@@ -1,2 +0,0 @@
-- DELETE COLLABORATORS IN GROUP REPOSITORIES
DELETE FROM COLLABORATOR WHERE USER_NAME IN (SELECT USER_NAME FROM ACCOUNT WHERE GROUP_ACCOUNT = TRUE)

View File

@@ -30,4 +30,9 @@
<dropColumn tableName="REPOSITORY" columnName="ENABLE_WIKI"/> <dropColumn tableName="REPOSITORY" columnName="ENABLE_WIKI"/>
<dropColumn tableName="REPOSITORY" columnName="ALLOW_WIKI_EDITING"/> <dropColumn tableName="REPOSITORY" columnName="ALLOW_WIKI_EDITING"/>
<dropColumn tableName="REPOSITORY" columnName="ENABLE_ISSUES"/> <dropColumn tableName="REPOSITORY" columnName="ENABLE_ISSUES"/>
<!-- DELETE COLLABORATORS IN GROUP REPOSITORIES -->
<sql>
DELETE FROM COLLABORATOR WHERE USER_NAME IN (SELECT USER_NAME FROM ACCOUNT WHERE GROUP_ACCOUNT = TRUE)
</sql>
</changeSet> </changeSet>

View File

@@ -9,7 +9,7 @@ import gitbucket.core.model.Activity
import gitbucket.core.util.Directory.ActivityLog import gitbucket.core.util.Directory.ActivityLog
import gitbucket.core.util.JDBCUtil import gitbucket.core.util.JDBCUtil
import io.github.gitbucket.solidbase.Solidbase import io.github.gitbucket.solidbase.Solidbase
import io.github.gitbucket.solidbase.migration.{LiquibaseMigration, Migration, SqlMigration} import io.github.gitbucket.solidbase.migration.{LiquibaseMigration, Migration}
import io.github.gitbucket.solidbase.model.{Module, Version} import io.github.gitbucket.solidbase.model.{Module, Version}
import org.json4s.{Formats, NoTypeHints} import org.json4s.{Formats, NoTypeHints}
import org.json4s.jackson.Serialization import org.json4s.jackson.Serialization
@@ -20,11 +20,7 @@ import scala.util.Using
object GitBucketCoreModule object GitBucketCoreModule
extends Module( extends Module(
"gitbucket-core", "gitbucket-core",
new Version( new Version("4.0.0", new LiquibaseMigration("update/gitbucket-core_4.0.xml")),
"4.0.0",
new LiquibaseMigration("update/gitbucket-core_4.0.xml"),
new SqlMigration("update/gitbucket-core_4.0.sql")
),
new Version("4.1.0"), new Version("4.1.0"),
new Version("4.2.0", new LiquibaseMigration("update/gitbucket-core_4.2.xml")), new Version("4.2.0", new LiquibaseMigration("update/gitbucket-core_4.2.xml")),
new Version("4.2.1"), new Version("4.2.1"),
@@ -32,11 +28,7 @@ object GitBucketCoreModule
new Version("4.4.0"), new Version("4.4.0"),
new Version("4.5.0"), new Version("4.5.0"),
new Version("4.6.0", new LiquibaseMigration("update/gitbucket-core_4.6.xml")), new Version("4.6.0", new LiquibaseMigration("update/gitbucket-core_4.6.xml")),
new Version( new Version("4.7.0", new LiquibaseMigration("update/gitbucket-core_4.7.xml")),
"4.7.0",
new LiquibaseMigration("update/gitbucket-core_4.7.xml"),
new SqlMigration("update/gitbucket-core_4.7.sql")
),
new Version("4.7.1"), new Version("4.7.1"),
new Version("4.8"), new Version("4.8"),
new Version("4.9.0", new LiquibaseMigration("update/gitbucket-core_4.9.xml")), new Version("4.9.0", new LiquibaseMigration("update/gitbucket-core_4.9.xml")),
@@ -45,11 +37,7 @@ object GitBucketCoreModule
new Version("4.12.0"), new Version("4.12.0"),
new Version("4.12.1"), new Version("4.12.1"),
new Version("4.13.0"), new Version("4.13.0"),
new Version( new Version("4.14.0", new LiquibaseMigration("update/gitbucket-core_4.14.xml")),
"4.14.0",
new LiquibaseMigration("update/gitbucket-core_4.14.xml"),
new SqlMigration("update/gitbucket-core_4.14.sql")
),
new Version("4.14.1"), new Version("4.14.1"),
new Version("4.15.0"), new Version("4.15.0"),
new Version("4.16.0"), new Version("4.16.0"),
@@ -122,5 +110,9 @@ object GitBucketCoreModule
new Version("4.36.2"), new Version("4.36.2"),
new Version("4.37.0", new LiquibaseMigration("update/gitbucket-core_4.37.xml")), new Version("4.37.0", new LiquibaseMigration("update/gitbucket-core_4.37.xml")),
new Version("4.37.1"), new Version("4.37.1"),
new Version("4.37.2") new Version("4.37.2"),
new Version("4.38.0", new LiquibaseMigration("update/gitbucket-core_4.38.xml")),
new Version("4.38.1"),
new Version("4.38.2"),
new Version("4.38.3")
) )

View File

@@ -97,7 +97,7 @@ object ApiCommits {
ApiCommits( ApiCommits(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"), url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
sha = commitInfo.id, sha = commitInfo.id,
html_url = ApiPath(s"${repositoryName.fullName}/commit/${commitInfo.id}"), html_url = ApiPath(s"/${repositoryName.fullName}/commit/${commitInfo.id}"),
comment_url = ApiPath(""), // TODO no API for commit comment comment_url = ApiPath(""), // TODO no API for commit comment
commit = Commit( commit = Commit(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"), url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),

View File

@@ -12,7 +12,7 @@ case class ApiIssue(
number: Int, number: Int,
title: String, title: String,
user: ApiUser, user: ApiUser,
assignee: Option[ApiUser], assignees: List[ApiUser],
labels: List[ApiLabel], labels: List[ApiLabel],
state: String, state: String,
created_at: Date, created_at: Date,
@@ -21,7 +21,7 @@ case class ApiIssue(
milestone: Option[ApiMilestone] milestone: Option[ApiMilestone]
)(repositoryName: RepositoryName, isPullRequest: Boolean) { )(repositoryName: RepositoryName, isPullRequest: Boolean) {
val id = 0 // dummy id val id = 0 // dummy id
val assignees = List(assignee).flatten val assignee = assignees.headOption
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments") val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
val html_url = ApiPath(s"/${repositoryName.fullName}/${if (isPullRequest) { "pull" } else { "issues" }}/${number}") val html_url = ApiPath(s"/${repositoryName.fullName}/${if (isPullRequest) { "pull" } else { "issues" }}/${number}")
val pull_request = if (isPullRequest) { val pull_request = if (isPullRequest) {
@@ -43,7 +43,7 @@ object ApiIssue {
issue: Issue, issue: Issue,
repositoryName: RepositoryName, repositoryName: RepositoryName,
user: ApiUser, user: ApiUser,
assignee: Option[ApiUser], assignees: List[ApiUser],
labels: List[ApiLabel], labels: List[ApiLabel],
milestone: Option[ApiMilestone] milestone: Option[ApiMilestone]
): ApiIssue = ): ApiIssue =
@@ -51,7 +51,7 @@ object ApiIssue {
number = issue.issueId, number = issue.issueId,
title = issue.title, title = issue.title,
user = user, user = user,
assignee = assignee, assignees = assignees,
labels = labels, labels = labels,
milestone = milestone, milestone = milestone,
state = if (issue.closed) { "closed" } else { "open" }, state = if (issue.closed) { "closed" } else { "open" },

View File

@@ -21,10 +21,11 @@ case class ApiPullRequest(
body: String, body: String,
user: ApiUser, user: ApiUser,
labels: List[ApiLabel], labels: List[ApiLabel],
assignee: Option[ApiUser], assignees: List[ApiUser],
draft: Option[Boolean] draft: Option[Boolean]
) { ) {
val id = 0 // dummy id val id = 0 // dummy id
val assignee = assignees.headOption
val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}") val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
//val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff") //val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff")
//val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch") //val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch")
@@ -45,7 +46,7 @@ object ApiPullRequest {
baseRepo: ApiRepository, baseRepo: ApiRepository,
user: ApiUser, user: ApiUser,
labels: List[ApiLabel], labels: List[ApiLabel],
assignee: Option[ApiUser], assignees: List[ApiUser],
mergedComment: Option[(IssueComment, Account)] mergedComment: Option[(IssueComment, Account)]
): ApiPullRequest = ): ApiPullRequest =
ApiPullRequest( ApiPullRequest(
@@ -63,7 +64,7 @@ object ApiPullRequest {
body = issue.content.getOrElse(""), body = issue.content.getOrElse(""),
user = user, user = user,
labels = labels, labels = labels,
assignee = assignee, assignees = assignees,
draft = Some(pullRequest.isDraft) draft = Some(pullRequest.isDraft)
) )

View File

@@ -28,7 +28,7 @@ object ApiRef {
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/${ref.getName}"), url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/${ref.getName}"),
`object` = ApiRefCommit( `object` = ApiRefCommit(
sha = ref.getObjectId.getName, sha = ref.getObjectId.getName,
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/commits/${ref.getObjectId.getName}"), url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${ref.getObjectId.getName}"),
`type` = "commit" `type` = "commit"
) )
) )
@@ -40,10 +40,16 @@ object ApiRef {
ApiRef( ApiRef(
ref = s"refs/tags/${tagInfo.name}", ref = s"refs/tags/${tagInfo.name}",
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/refs/tags/${tagInfo.name}"), url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/refs/tags/${tagInfo.name}"),
//the GH api distinguishes between "releases" and plain git tags
//for "releases", the api returns a reference to the release object (with type `tag`)
//this would be something like s"/api/v3/repos/${repositoryName.fullName}/git/tags/<hash-of-tag>"
//with a hash for the tag, which I do not fully understand
//since this is not yet implemented in GB, we always return a link to the plain `commit` object,
//which GH does for tags that are not annotated
`object` = ApiRefCommit( `object` = ApiRefCommit(
sha = tagInfo.objectId, sha = tagInfo.objectId,
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/tags/${tagInfo.objectId}"), // TODO This URL is not yet available? url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${tagInfo.objectId}"),
`type` = "tag" `type` = "commit"
) )
) )
} }

View File

@@ -21,7 +21,7 @@ case class ApiRepository(
val url = ApiPath(s"/api/v3/repos/${full_name}") val url = ApiPath(s"/api/v3/repos/${full_name}")
val clone_url = ApiPath(s"/git/${full_name}.git") val clone_url = ApiPath(s"/git/${full_name}.git")
val html_url = ApiPath(s"/${full_name}") val html_url = ApiPath(s"/${full_name}")
val ssh_url = Some(SshPath(s":${full_name}.git")) val ssh_url = Some(SshPath(""))
} }
object ApiRepository { object ApiRepository {

View File

@@ -128,6 +128,15 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"highlighterTheme" -> trim(label("Theme", text(required))) "highlighterTheme" -> trim(label("Theme", text(required)))
)(SyntaxHighlighterThemeForm.apply) )(SyntaxHighlighterThemeForm.apply)
val resetPasswordEmailForm = mapping(
"mailAddress" -> trim(label("Email", text(required)))
)(ResetPasswordEmailForm.apply)
val resetPasswordForm = mapping(
"token" -> trim(label("Token", text(required))),
"password" -> trim(label("Password", text(required, maxlength(40))))
)(ResetPasswordForm.apply)
case class NewGroupForm( case class NewGroupForm(
groupName: String, groupName: String,
description: Option[String], description: Option[String],
@@ -143,6 +152,13 @@ trait AccountControllerBase extends AccountManagementControllerBase {
members: String, members: String,
clearImage: Boolean clearImage: Boolean
) )
case class ResetPasswordEmailForm(
mailAddress: String
)
case class ResetPasswordForm(
token: String,
password: String
)
val newGroupForm = mapping( val newGroupForm = mapping(
"groupName" -> trim(label("Group name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))), "groupName" -> trim(label("Group name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
@@ -602,7 +618,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
}) })
get("/register") { get("/register") {
if (context.settings.allowAccountRegistration) { if (context.settings.basicBehavior.allowAccountRegistration) {
if (context.loginAccount.isDefined) { if (context.loginAccount.isDefined) {
redirect("/") redirect("/")
} else { } else {
@@ -612,7 +628,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} }
post("/register", newForm) { form => post("/register", newForm) { form =>
if (context.settings.allowAccountRegistration) { if (context.settings.basicBehavior.allowAccountRegistration) {
createAccount( createAccount(
form.userName, form.userName,
pbkdf2_sha256(form.password), pbkdf2_sha256(form.password),
@@ -628,6 +644,63 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} else NotFound() } else NotFound()
} }
get("/reset") {
if (context.settings.basicBehavior.allowResetPassword) {
html.reset()
} else NotFound()
}
post("/reset", resetPasswordEmailForm) { form =>
if (context.settings.basicBehavior.allowResetPassword) {
getAccountByMailAddress(form.mailAddress).foreach { account =>
val token = generateResetPasswordToken(form.mailAddress)
val mailer = new Mailer(context.settings)
mailer.send(
form.mailAddress,
"Reset password",
s"""Hello, ${account.fullName}!
|
|You requested to reset the password for your GitBucket account.
|If you are not sure about the request, you can ignore this email.
|Otherwise, click the following link to set the new password:
|${context.baseUrl}/reset/form/${token}
|""".stripMargin
)
}
redirect("/reset/sent")
} else NotFound()
}
get("/reset/sent") {
if (context.settings.basicBehavior.allowResetPassword) {
html.resetsent()
} else NotFound()
}
get("/reset/form/:token") {
if (context.settings.basicBehavior.allowResetPassword) {
val token = params("token")
decodeResetPasswordToken(token)
.map { _ =>
html.resetform(token)
}
.getOrElse(NotFound())
} else NotFound()
}
post("/reset/form", resetPasswordForm) { form =>
if (context.settings.basicBehavior.allowResetPassword) {
decodeResetPasswordToken(form.token)
.flatMap { mailAddress =>
getAccountByMailAddress(mailAddress).map { account =>
updateAccount(account.copy(password = pbkdf2_sha256(form.password)))
html.resetcomplete()
}
}
.getOrElse(NotFound())
} else NotFound()
}
get("/groups/new")(usersOnly { get("/groups/new")(usersOnly {
context.withLoginAccount { loginAccount => context.withLoginAccount { loginAccount =>
html.creategroup(List(GroupMember("", loginAccount.userName, true))) html.creategroup(List(GroupMember("", loginAccount.userName, true)))
@@ -713,7 +786,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
*/ */
get("/new")(usersOnly { get("/new")(usersOnly {
context.withLoginAccount { loginAccount => context.withLoginAccount { loginAccount =>
html.newrepo(getGroupsByUserName(loginAccount.userName), context.settings.isCreateRepoOptionPublic) html.newrepo(getGroupsByUserName(loginAccount.userName), context.settings.basicBehavior.isCreateRepoOptionPublic)
} }
}) })
@@ -723,7 +796,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
post("/new", newRepositoryForm)(usersOnly { form => post("/new", newRepositoryForm)(usersOnly { form =>
context.withLoginAccount { context.withLoginAccount {
loginAccount => loginAccount =>
if (context.settings.repositoryOperation.create || loginAccount.isAdmin) { if (context.settings.basicBehavior.repositoryOperation.create || loginAccount.isAdmin) {
LockUtil.lock(s"${form.owner}/${form.name}") { LockUtil.lock(s"${form.owner}/${form.name}") {
if (getRepository(form.owner, form.name).isDefined) { if (getRepository(form.owner, form.name).isDefined) {
// redirect to the repository if repository already exists // redirect to the repository if repository already exists
@@ -753,7 +826,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:owner/:repository/fork")(readableUsersOnly { repository => get("/:owner/:repository/fork")(readableUsersOnly { repository =>
context.withLoginAccount { context.withLoginAccount {
loginAccount => loginAccount =>
if (repository.repository.options.allowFork && (context.settings.repositoryOperation.fork || loginAccount.isAdmin)) { if (repository.repository.options.allowFork && (context.settings.basicBehavior.repositoryOperation.fork || loginAccount.isAdmin)) {
val loginUserName = loginAccount.userName val loginUserName = loginAccount.userName
val groups = getGroupsByUserName(loginUserName) val groups = getGroupsByUserName(loginUserName)
groups match { groups match {
@@ -780,7 +853,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount { context.withLoginAccount {
loginAccount => loginAccount =>
if (repository.repository.options.allowFork && (context.settings.repositoryOperation.fork || loginAccount.isAdmin)) { if (repository.repository.options.allowFork && (context.settings.basicBehavior.repositoryOperation.fork || loginAccount.isAdmin)) {
val loginUserName = loginAccount.userName val loginUserName = loginAccount.userName
val accountName = form.accountName val accountName = form.accountName

View File

@@ -100,4 +100,16 @@ trait ApiControllerBase extends ControllerBase {
get("/api/v3/gitbucket/plugins") { get("/api/v3/gitbucket/plugins") {
PluginRegistry().getPlugins().map { ApiPlugin(_) } PluginRegistry().getPlugins().map { ApiPlugin(_) }
} }
/**
* https://docs.github.com/en/enterprise-server@2.21/rest/reference/meta#get-github-enterprise-server-meta-information
*/
get("/api/v3/meta") {
JsonFormat(
Map(
"https://api.github.com/meta" -> context.loginAccount.isDefined,
"installed_version" -> "2.21.0"
)
)
}
} }

View File

@@ -40,7 +40,7 @@ trait DashboardControllerBase extends ControllerBase {
context.loginAccount, context.loginAccount,
None, None,
withoutPhysicalInfo = true, withoutPhysicalInfo = true,
limit = context.settings.limitVisibleRepositories limit = context.settings.basicBehavior.limitVisibleRepositories
) )
html.repos(getGroupNames(loginAccount.userName), repos, repos) html.repos(getGroupNames(loginAccount.userName), repos, repos)
} }
@@ -129,7 +129,7 @@ trait DashboardControllerBase extends ControllerBase {
context.loginAccount, context.loginAccount,
None, None,
withoutPhysicalInfo = true, withoutPhysicalInfo = true,
limit = context.settings.limitVisibleRepositories limit = context.settings.basicBehavior.limitVisibleRepositories
) )
) )
} }
@@ -171,7 +171,7 @@ trait DashboardControllerBase extends ControllerBase {
context.loginAccount, context.loginAccount,
None, None,
withoutPhysicalInfo = true, withoutPhysicalInfo = true,
limit = context.settings.limitVisibleRepositories limit = context.settings.basicBehavior.limitVisibleRepositories
) )
) )
} }

View File

@@ -69,7 +69,7 @@ trait IndexControllerBase extends ControllerBase {
Some(account), Some(account),
None, None,
withoutPhysicalInfo = true, withoutPhysicalInfo = true,
limit = context.settings.limitVisibleRepositories limit = context.settings.basicBehavior.limitVisibleRepositories
), ),
showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken( showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken(
account.userName account.userName
@@ -289,11 +289,11 @@ trait IndexControllerBase extends ControllerBase {
context.loginAccount, context.loginAccount,
None, None,
withoutPhysicalInfo = true, withoutPhysicalInfo = true,
limit = context.settings.limitVisibleRepositories limit = context.settings.basicBehavior.limitVisibleRepositories
) )
val repositories = { val repositories = {
context.settings.limitVisibleRepositories match { context.settings.basicBehavior.limitVisibleRepositories match {
case true => case true =>
getVisibleRepositories( getVisibleRepositories(
context.loginAccount, context.loginAccount,

View File

@@ -1,7 +1,7 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.issues.html import gitbucket.core.issues.html
import gitbucket.core.model.Account import gitbucket.core.model.{Account, CustomFieldBehavior}
import gitbucket.core.service.IssuesService._ import gitbucket.core.service.IssuesService._
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
@@ -21,6 +21,7 @@ class IssuesController
with ActivityService with ActivityService
with HandleCommentService with HandleCommentService
with IssueCreationService with IssueCreationService
with CustomFieldsService
with ReadableUsersAuthenticator with ReadableUsersAuthenticator
with ReferrerAuthenticator with ReferrerAuthenticator
with WritableUsersAuthenticator with WritableUsersAuthenticator
@@ -41,6 +42,7 @@ trait IssuesControllerBase extends ControllerBase {
with ActivityService with ActivityService
with HandleCommentService with HandleCommentService
with IssueCreationService with IssueCreationService
with CustomFieldsService
with ReadableUsersAuthenticator with ReadableUsersAuthenticator
with ReferrerAuthenticator with ReferrerAuthenticator
with WritableUsersAuthenticator with WritableUsersAuthenticator
@@ -51,7 +53,7 @@ trait IssuesControllerBase extends ControllerBase {
case class IssueCreateForm( case class IssueCreateForm(
title: String, title: String,
content: Option[String], content: Option[String],
assignedUserName: Option[String], assigneeUserNames: Option[String],
milestoneId: Option[Int], milestoneId: Option[Int],
priorityId: Option[Int], priorityId: Option[Int],
labelNames: Option[String] labelNames: Option[String]
@@ -62,7 +64,7 @@ trait IssuesControllerBase extends ControllerBase {
val issueCreateForm = mapping( val issueCreateForm = mapping(
"title" -> trim(label("Title", text(required))), "title" -> trim(label("Title", text(required))),
"content" -> trim(optional(text())), "content" -> trim(optional(text())),
"assignedUserName" -> trim(optional(text())), "assigneeUserNames" -> trim(optional(text())),
"milestoneId" -> trim(optional(number())), "milestoneId" -> trim(optional(number())),
"priorityId" -> trim(optional(number())), "priorityId" -> trim(optional(number())),
"labelNames" -> trim(optional(text())) "labelNames" -> trim(optional(text()))
@@ -105,10 +107,12 @@ trait IssuesControllerBase extends ControllerBase {
issue, issue,
getComments(repository.owner, repository.name, issueId.toInt), getComments(repository.owner, repository.name, issueId.toInt),
getIssueLabels(repository.owner, repository.name, issueId.toInt), getIssueLabels(repository.owner, repository.name, issueId.toInt),
getIssueAssignees(repository.owner, repository.name, issueId.toInt),
getAssignableUserNames(repository.owner, repository.name), getAssignableUserNames(repository.owner, repository.name),
getMilestonesWithIssueCount(repository.owner, repository.name), getMilestonesWithIssueCount(repository.owner, repository.name),
getPriorities(repository.owner, repository.name), getPriorities(repository.owner, repository.name),
getLabels(repository.owner, repository.name), getLabels(repository.owner, repository.name),
getCustomFieldsWithValue(repository.owner, repository.name, issueId.toInt).filter(_._1.enableForIssues),
isIssueEditable(repository), isIssueEditable(repository),
isIssueManageable(repository), isIssueManageable(repository),
isIssueCommentManageable(repository), isIssueCommentManageable(repository),
@@ -126,6 +130,7 @@ trait IssuesControllerBase extends ControllerBase {
getPriorities(repository.owner, repository.name), getPriorities(repository.owner, repository.name),
getDefaultPriority(repository.owner, repository.name), getDefaultPriority(repository.owner, repository.name),
getLabels(repository.owner, repository.name), getLabels(repository.owner, repository.name),
getCustomFields(repository.owner, repository.name).filter(_.enableForIssues),
isIssueManageable(repository), isIssueManageable(repository),
getContentTemplate(repository, "ISSUE_TEMPLATE"), getContentTemplate(repository, "ISSUE_TEMPLATE"),
repository repository
@@ -141,12 +146,31 @@ trait IssuesControllerBase extends ControllerBase {
repository, repository,
form.title, form.title,
form.content, form.content,
form.assignedUserName, form.assigneeUserNames.toSeq.flatMap(_.split(",")),
form.milestoneId, form.milestoneId,
form.priorityId, form.priorityId,
form.labelNames.toSeq.flatMap(_.split(",")), form.labelNames.toSeq.flatMap(_.split(",")),
loginAccount loginAccount
) )
// Insert custom field values
params.toMap.foreach {
case (key, value) =>
if (key.startsWith("custom-field-")) {
getCustomField(
repository.owner,
repository.name,
key.replaceFirst("^custom-field-", "").toInt
).foreach { field =>
CustomFieldBehavior.validate(field, value, messages) match {
case None =>
insertOrUpdateCustomFieldValue(field, repository.owner, repository.name, issue.issueId, value)
case Some(_) => halt(400)
}
}
}
}
redirect(s"/${issue.userName}/${issue.repositoryName}/issues/${issue.issueId}") redirect(s"/${issue.userName}/${issue.repositoryName}/issues/${issue.issueId}")
} else Unauthorized() } else Unauthorized()
} }
@@ -235,7 +259,7 @@ trait IssuesControllerBase extends ControllerBase {
loginAccount => loginAccount =>
getComment(repository.owner, repository.name, params("id")).map { comment => getComment(repository.owner, repository.name, params("id")).map { comment =>
if (isEditableContent(repository.owner, repository.name, comment.commentedUserName, loginAccount)) { if (isEditableContent(repository.owner, repository.name, comment.commentedUserName, loginAccount)) {
updateComment(comment.issueId, comment.commentId, form.content) updateComment(repository.owner, repository.name, comment.issueId, comment.commentId, form.content)
redirect(s"/${repository.owner}/${repository.name}/issue_comments/_data/${comment.commentId}") redirect(s"/${repository.owner}/${repository.name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized() } else Unauthorized()
} getOrElse NotFound() } getOrElse NotFound()
@@ -333,15 +357,16 @@ trait IssuesControllerBase extends ControllerBase {
html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
}) })
ajaxPost("/:owner/:repository/issues/:id/assign")(writableUsersOnly { repository => ajaxPost("/:owner/:repository/issues/:id/assignee/new")(writableUsersOnly { repository =>
updateAssignedUserName( val issueId = params("id").toInt
repository.owner, registerIssueAssignee(repository.owner, repository.name, issueId, params("assigneeUserName"), true)
repository.name, Ok()
params("id").toInt, })
assignedUserName("assignedUserName"),
true ajaxPost("/:owner/:repository/issues/:id/assignee/delete")(writableUsersOnly { repository =>
) val issueId = params("id").toInt
Ok("updated") deleteIssueAssignee(repository.owner, repository.name, issueId, params("assigneeUserName"), true)
Ok()
}) })
ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { repository => ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { repository =>
@@ -362,6 +387,35 @@ trait IssuesControllerBase extends ControllerBase {
Ok("updated") Ok("updated")
}) })
ajaxPost("/:owner/:repository/issues/customfield_validation/:fieldId")(writableUsersOnly { repository =>
val fieldId = params("fieldId").toInt
val value = params("value")
getCustomField(repository.owner, repository.name, fieldId)
.flatMap { field =>
CustomFieldBehavior.validate(field, value, messages).map { error =>
Ok(error)
}
}
.getOrElse(Ok())
})
ajaxPost("/:owner/:repository/issues/:id/customfield/:fieldId")(writableUsersOnly { repository =>
val issueId = params("id").toInt
val fieldId = params("fieldId").toInt
val value = params("value")
for {
_ <- getIssue(repository.owner, repository.name, issueId.toString)
field <- getCustomField(repository.owner, repository.name, fieldId)
} {
CustomFieldBehavior.validate(field, value, messages) match {
case None => insertOrUpdateCustomFieldValue(field, repository.owner, repository.name, issueId, value)
case Some(_) => halt(400)
}
}
Ok(value)
})
post("/:owner/:repository/issues/batchedit/state")(writableUsersOnly { repository => post("/:owner/:repository/issues/batchedit/state")(writableUsersOnly { repository =>
val action = params.get("value") val action = params.get("value")
action match { action match {
@@ -403,7 +457,13 @@ trait IssuesControllerBase extends ControllerBase {
post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository => post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository =>
val value = assignedUserName("value") val value = assignedUserName("value")
executeBatch(repository) { executeBatch(repository) {
updateAssignedUserName(repository.owner, repository.name, _, value, true) //updateAssignedUserName(repository.owner, repository.name, _, value, true)
value match {
case Some(assignedUserName) =>
registerIssueAssignee(repository.owner, repository.name, _, assignedUserName, true)
case None =>
deleteAllIssueAssignees(repository.owner, repository.name, _, true)
}
} }
if (params("uri").nonEmpty) { if (params("uri").nonEmpty) {
redirect(params("uri")) redirect(params("uri"))

View File

@@ -44,7 +44,7 @@ trait LabelsControllerBase extends ControllerBase {
get("/:owner/:repository/issues/labels")(referrersOnly { repository => get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
html.list( html.list(
getLabels(repository.owner, repository.name), getLabels(repository.owner, repository.name),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository, repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount) hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
) )
@@ -59,7 +59,7 @@ trait LabelsControllerBase extends ControllerBase {
html.label( html.label(
getLabel(repository.owner, repository.name, labelId).get, getLabel(repository.owner, repository.name, labelId).get,
// TODO futility // TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository, repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount) hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
) )
@@ -76,7 +76,7 @@ trait LabelsControllerBase extends ControllerBase {
html.label( html.label(
getLabel(repository.owner, repository.name, params("labelId").toInt).get, getLabel(repository.owner, repository.name, params("labelId").toInt).get,
// TODO futility // TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository, repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount) hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
) )

View File

@@ -29,7 +29,7 @@ trait PreProcessControllerBase extends ControllerBase {
* If anonymous access is allowed, pass all requests. * If anonymous access is allowed, pass all requests.
* But if it's not allowed, demands authentication except some paths. * But if it's not allowed, demands authentication except some paths.
*/ */
get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) { get(!context.settings.basicBehavior.allowAnonymousAccess, context.loginAccount.isEmpty) {
if (!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") && if (!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
!context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs") && !context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs") &&
!context.currentPath.startsWith("/plugin-assets") && !context.currentPath.startsWith("/plugin-assets") &&

View File

@@ -45,7 +45,7 @@ trait PrioritiesControllerBase extends ControllerBase {
get("/:owner/:repository/issues/priorities")(referrersOnly { repository => get("/:owner/:repository/issues/priorities")(referrersOnly { repository =>
html.list( html.list(
getPriorities(repository.owner, repository.name), getPriorities(repository.owner, repository.name),
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository, repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount) hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
) )
@@ -60,7 +60,7 @@ trait PrioritiesControllerBase extends ControllerBase {
createPriority(repository.owner, repository.name, form.priorityName, form.description, form.color.substring(1)) createPriority(repository.owner, repository.name, form.priorityName, form.description, form.color.substring(1))
html.priority( html.priority(
getPriority(repository.owner, repository.name, priorityId).get, getPriority(repository.owner, repository.name, priorityId).get,
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository, repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount) hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
) )
@@ -84,7 +84,7 @@ trait PrioritiesControllerBase extends ControllerBase {
) )
html.priority( html.priority(
getPriority(repository.owner, repository.name, params("priorityId").toInt).get, getPriority(repository.owner, repository.name, params("priorityId").toInt).get,
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository, repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount) hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
) )

View File

@@ -25,6 +25,7 @@ class PullRequestsController
with PullRequestService with PullRequestService
with MilestonesService with MilestonesService
with LabelsService with LabelsService
with CustomFieldsService
with CommitsService with CommitsService
with ActivityService with ActivityService
with WebHookPullRequestService with WebHookPullRequestService
@@ -44,6 +45,7 @@ trait PullRequestsControllerBase extends ControllerBase {
with IssuesService with IssuesService
with MilestonesService with MilestonesService
with LabelsService with LabelsService
with CustomFieldsService
with CommitsService with CommitsService
with ActivityService with ActivityService
with PullRequestService with PullRequestService
@@ -67,7 +69,7 @@ trait PullRequestsControllerBase extends ControllerBase {
"commitIdFrom" -> trim(text(required, maxlength(40))), "commitIdFrom" -> trim(text(required, maxlength(40))),
"commitIdTo" -> trim(text(required, maxlength(40))), "commitIdTo" -> trim(text(required, maxlength(40))),
"isDraft" -> trim(boolean(required)), "isDraft" -> trim(boolean(required)),
"assignedUserName" -> trim(optional(text())), "assigneeUserNames" -> trim(optional(text())),
"milestoneId" -> trim(optional(number())), "milestoneId" -> trim(optional(number())),
"priorityId" -> trim(optional(number())), "priorityId" -> trim(optional(number())),
"labelNames" -> trim(optional(text())) "labelNames" -> trim(optional(text()))
@@ -90,7 +92,7 @@ trait PullRequestsControllerBase extends ControllerBase {
commitIdFrom: String, commitIdFrom: String,
commitIdTo: String, commitIdTo: String,
isDraft: Boolean, isDraft: Boolean,
assignedUserName: Option[String], assigneeUserNames: Option[String],
milestoneId: Option[Int], milestoneId: Option[Int],
priorityId: Option[Int], priorityId: Option[Int],
labelNames: Option[String] labelNames: Option[String]
@@ -129,10 +131,12 @@ trait PullRequestsControllerBase extends ControllerBase {
getPullRequestComments(repository.owner, repository.name, issue.issueId, commits.flatten), getPullRequestComments(repository.owner, repository.name, issue.issueId, commits.flatten),
diffs.size, diffs.size,
getIssueLabels(repository.owner, repository.name, issueId), getIssueLabels(repository.owner, repository.name, issueId),
getIssueAssignees(repository.owner, repository.name, issueId),
getAssignableUserNames(repository.owner, repository.name), getAssignableUserNames(repository.owner, repository.name),
getMilestonesWithIssueCount(repository.owner, repository.name), getMilestonesWithIssueCount(repository.owner, repository.name),
getPriorities(repository.owner, repository.name), getPriorities(repository.owner, repository.name),
getLabels(repository.owner, repository.name), getLabels(repository.owner, repository.name),
getCustomFieldsWithValue(repository.owner, repository.name, issueId).filter(_._1.enableForPullRequests),
isEditable(repository), isEditable(repository),
isManageable(repository), isManageable(repository),
hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount), hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount),
@@ -140,25 +144,6 @@ trait PullRequestsControllerBase extends ControllerBase {
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName), getRepository(pullreq.requestUserName, pullreq.requestRepositoryName),
flash.iterator.map(f => f._1 -> f._2.toString).toMap flash.iterator.map(f => f._1 -> f._2.toString).toMap
) )
// html.pullreq(
// issue,
// pullreq,
// comments,
// getIssueLabels(owner, name, issueId),
// getAssignableUserNames(owner, name),
// getMilestonesWithIssueCount(owner, name),
// getPriorities(owner, name),
// getLabels(owner, name),
// commits,
// diffs,
// isEditable(repository),
// isManageable(repository),
// hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount),
// repository,
// getRepository(pullreq.requestUserName, pullreq.requestRepositoryName),
// flash.toMap.map(f => f._1 -> f._2.toString)
// )
} }
} getOrElse NotFound() } getOrElse NotFound()
}) })
@@ -392,9 +377,9 @@ trait PullRequestsControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/compare")(referrersOnly { forkedRepository => get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
val headBranch: Option[String] = params.get("head") val headBranch = params.get("head")
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => { case (Some(originUserName), Some(originRepositoryName)) =>
getRepository(originUserName, originRepositoryName).map { getRepository(originUserName, originRepositoryName).map {
originRepository => originRepository =>
Using.resources( Using.resources(
@@ -411,8 +396,7 @@ trait PullRequestsControllerBase extends ControllerBase {
) )
} }
} getOrElse NotFound() } getOrElse NotFound()
} case _ =>
case _ => {
Using.resource(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))) { git => Using.resource(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))) { git =>
JGitUtil.getDefaultBranch(git, forkedRepository).map { JGitUtil.getDefaultBranch(git, forkedRepository).map {
case (_, defaultBranch) => case (_, defaultBranch) =>
@@ -423,41 +407,48 @@ trait PullRequestsControllerBase extends ControllerBase {
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}") redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
} }
} }
}
} }
}) })
private def getOriginRepositoryName(
originOwner: String,
forkedOwner: String,
forkedRepository: RepositoryInfo
): Option[String] = {
if (originOwner == forkedOwner) {
// Self repository
Some(forkedRepository.name)
} else if (forkedRepository.repository.originUserName.isEmpty) {
// when ForkedRepository is the original repository
getForkedRepositories(forkedRepository.owner, forkedRepository.name)
.find(_.userName == originOwner)
.map(_.repositoryName)
} else if (Some(originOwner) == forkedRepository.repository.originUserName) {
// Original repository
forkedRepository.repository.originRepositoryName
} else {
// Sibling repository
getUserRepositories(originOwner)
.find { x =>
x.repository.originUserName == forkedRepository.repository.originUserName &&
x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName
}
.map(_.repository.repositoryName)
}
}
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository => get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat") val Seq(origin, forked) = multiParams("splat")
val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner) val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner)
val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner) val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner)
(for (originRepositoryName <- if (originOwner == forkedOwner) { (for (originRepositoryName <- getOriginRepositoryName(originOwner, forkedOwner, forkedRepository);
// Self repository
Some(forkedRepository.name)
} else if (forkedRepository.repository.originUserName.isEmpty) {
// when ForkedRepository is the original repository
getForkedRepositories(forkedRepository.owner, forkedRepository.name)
.find(_.userName == originOwner)
.map(_.repositoryName)
} else if (Some(originOwner) == forkedRepository.repository.originUserName) {
// Original repository
forkedRepository.repository.originRepositoryName
} else {
// Sibling repository
getUserRepositories(originOwner)
.find { x =>
x.repository.originUserName == forkedRepository.repository.originUserName &&
x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName
}
.map(_.repository.repositoryName)
};
originRepository <- getRepository(originOwner, originRepositoryName)) yield { originRepository <- getRepository(originOwner, originRepositoryName)) yield {
val (oldId, newId) = val (oldId, newId) =
getPullRequestCommitFromTo(originRepository, forkedRepository, originId, forkedId) getPullRequestCommitFromTo(originRepository, forkedRepository, originId, forkedId)
(oldId, newId) match { (oldId, newId) match {
case (Some(oldId), Some(newId)) => { case (Some(oldId), Some(newId)) =>
val (commits, diffs) = getRequestCompareInfo( val (commits, diffs) = getRequestCompareInfo(
originRepository.owner, originRepository.owner,
originRepository.name, originRepository.name,
@@ -505,9 +496,9 @@ trait PullRequestsControllerBase extends ControllerBase {
getMilestones(originRepository.owner, originRepository.name), getMilestones(originRepository.owner, originRepository.name),
getPriorities(originRepository.owner, originRepository.name), getPriorities(originRepository.owner, originRepository.name),
getDefaultPriority(originRepository.owner, originRepository.name), getDefaultPriority(originRepository.owner, originRepository.name),
getLabels(originRepository.owner, originRepository.name) getLabels(originRepository.owner, originRepository.name),
getCustomFields(originRepository.owner, originRepository.name).filter(_.enableForPullRequests)
) )
}
case (oldId, newId) => case (oldId, newId) =>
redirect( redirect(
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/" + s"/${forkedRepository.owner}/${forkedRepository.name}/compare/" +
@@ -519,6 +510,54 @@ trait PullRequestsControllerBase extends ControllerBase {
}) getOrElse NotFound() }) getOrElse NotFound()
}) })
ajaxGet("/:owner/:repository/diff/:id")(referrersOnly { repository =>
(for {
commitId <- params.get("id")
path <- params.get("path")
diff <- getSingleDiff(repository.owner, repository.name, commitId, path)
} yield {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map(
"oldContent" -> diff.oldContent,
"newContent" -> diff.newContent
)
)
}) getOrElse NotFound()
})
ajaxGet("/:owner/:repository/diff/*...*")(referrersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner)
val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner)
(for {
path <- params.get("path")
originRepositoryName <- getOriginRepositoryName(originOwner, forkedOwner, forkedRepository)
originRepository <- getRepository(originOwner, originRepositoryName)
(oldId, newId) = getPullRequestCommitFromTo(originRepository, forkedRepository, originId, forkedId)
oldId <- oldId
newId <- newId
diff <- getSingleDiff(
originRepository.owner,
originRepository.name,
oldId.getName,
forkedRepository.owner,
forkedRepository.name,
newId.getName,
path
)
} yield {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map(
"oldContent" -> diff.oldContent,
"newContent" -> diff.newContent
)
)
}) getOrElse NotFound()
})
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(readableUsersOnly { forkedRepository => ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(readableUsersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat") val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifier(origin, forkedRepository.owner) val (originOwner, tmpOriginBranch) = parseCompareIdentifier(origin, forkedRepository.owner)
@@ -567,7 +606,6 @@ trait PullRequestsControllerBase extends ControllerBase {
loginUser = loginAccount.userName, loginUser = loginAccount.userName,
title = form.title, title = form.title,
content = form.content, content = form.content,
assignedUserName = if (manageable) form.assignedUserName else None,
milestoneId = if (manageable) form.milestoneId else None, milestoneId = if (manageable) form.milestoneId else None,
priorityId = if (manageable) form.priorityId else None, priorityId = if (manageable) form.priorityId else None,
isPullRequest = true isPullRequest = true
@@ -587,8 +625,14 @@ trait PullRequestsControllerBase extends ControllerBase {
settings = context.settings settings = context.settings
) )
// insert labels
if (manageable) { if (manageable) {
// insert assignees
form.assigneeUserNames.foreach { value =>
value.split(",").foreach { userName =>
registerIssueAssignee(repository.owner, repository.name, issueId, userName)
}
}
// insert labels
form.labelNames.foreach { value => form.labelNames.foreach { value =>
val labels = getLabels(repository.owner, repository.name) val labels = getLabels(repository.owner, repository.name)
value.split(",").foreach { labelName => value.split(",").foreach { labelName =>

View File

@@ -2,7 +2,6 @@ package gitbucket.core.controller
import java.time.{LocalDateTime, ZoneOffset} import java.time.{LocalDateTime, ZoneOffset}
import java.util.Date import java.util.Date
import gitbucket.core.settings.html import gitbucket.core.settings.html
import gitbucket.core.model.{RepositoryWebHook, WebHook} import gitbucket.core.model.{RepositoryWebHook, WebHook}
import gitbucket.core.service._ import gitbucket.core.service._
@@ -21,7 +20,7 @@ import org.eclipse.jgit.lib.Constants
import org.eclipse.jgit.lib.ObjectId import org.eclipse.jgit.lib.ObjectId
import scala.util.Using import scala.util.Using
import org.scalatra.Forbidden import org.scalatra.{Forbidden, Ok}
class RepositorySettingsController class RepositorySettingsController
extends RepositorySettingsControllerBase extends RepositorySettingsControllerBase
@@ -31,6 +30,7 @@ class RepositorySettingsController
with ProtectedBranchService with ProtectedBranchService
with CommitStatusService with CommitStatusService
with DeployKeyService with DeployKeyService
with CustomFieldsService
with ActivityService with ActivityService
with OwnerAuthenticator with OwnerAuthenticator
with UsersAuthenticator with UsersAuthenticator
@@ -43,6 +43,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
with ProtectedBranchService with ProtectedBranchService
with CommitStatusService with CommitStatusService
with DeployKeyService with DeployKeyService
with CustomFieldsService
with ActivityService with ActivityService
with OwnerAuthenticator with OwnerAuthenticator
with UsersAuthenticator => with UsersAuthenticator =>
@@ -121,6 +122,21 @@ trait RepositorySettingsControllerBase extends ControllerBase {
"newOwner" -> trim(label("New owner", text(required, transferUser))) "newOwner" -> trim(label("New owner", text(required, transferUser)))
)(TransferOwnerShipForm.apply) )(TransferOwnerShipForm.apply)
// for custom field
case class CustomFieldForm(
fieldName: String,
fieldType: String,
enableForIssues: Boolean,
enableForPullRequests: Boolean
)
val customFieldForm = mapping(
"fieldName" -> trim(label("Field name", text(required, maxlength(100)))),
"fieldType" -> trim(label("Field type", text(required))),
"enableForIssues" -> trim(label("Enable for issues", boolean(required))),
"enableForPullRequests" -> trim(label("Enable for pull requests", boolean(required))),
)(CustomFieldForm.apply)
/** /**
* Redirect to the Options page. * Redirect to the Options page.
*/ */
@@ -390,7 +406,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) =>
context.withLoginAccount { context.withLoginAccount {
loginAccount => loginAccount =>
if (context.settings.repositoryOperation.rename || loginAccount.isAdmin) { if (context.settings.basicBehavior.repositoryOperation.rename || loginAccount.isAdmin) {
if (repository.name != form.repositoryName) { if (repository.name != form.repositoryName) {
// Update database and move git repository // Update database and move git repository
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName) renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
@@ -414,7 +430,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
context.withLoginAccount { context.withLoginAccount {
loginAccount => loginAccount =>
if (context.settings.repositoryOperation.transfer || loginAccount.isAdmin) { if (context.settings.basicBehavior.repositoryOperation.transfer || loginAccount.isAdmin) {
// Change repository owner // Change repository owner
if (repository.owner != form.newOwner) { if (repository.owner != form.newOwner) {
// Update database and move git repository // Update database and move git repository
@@ -438,7 +454,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
*/ */
post("/:owner/:repository/settings/delete")(ownerOnly { repository => post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
context.withLoginAccount { loginAccount => context.withLoginAccount { loginAccount =>
if (context.settings.repositoryOperation.delete || loginAccount.isAdmin) { if (context.settings.basicBehavior.repositoryOperation.delete || loginAccount.isAdmin) {
// Delete the repository and related files // Delete the repository and related files
deleteRepository(repository.repository) deleteRepository(repository.repository)
redirect(s"/${repository.owner}") redirect(s"/${repository.owner}")
@@ -477,6 +493,58 @@ trait RepositorySettingsControllerBase extends ControllerBase {
redirect(s"/${repository.owner}/${repository.name}/settings/deploykey") redirect(s"/${repository.owner}/${repository.name}/settings/deploykey")
}) })
/** Custom fields for issues and pull requests */
get("/:owner/:repository/settings/issues")(ownerOnly { repository =>
val customFields = getCustomFields(repository.owner, repository.name)
html.issues(customFields, repository)
})
/** New custom field form */
get("/:owner/:repository/settings/issues/fields/new")(ownerOnly { repository =>
html.issuesfieldform(None, repository)
})
/** Add custom field */
ajaxPost("/:owner/:repository/settings/issues/fields/new", customFieldForm)(ownerOnly { (form, repository) =>
val fieldId = createCustomField(
repository.owner,
repository.name,
form.fieldName,
form.fieldType,
form.enableForIssues,
form.enableForPullRequests
)
html.issuesfield(getCustomField(repository.owner, repository.name, fieldId).get)
})
/** Edit custom field form */
ajaxGet("/:owner/:repository/settings/issues/fields/:fieldId/edit")(ownerOnly { repository =>
getCustomField(repository.owner, repository.name, params("fieldId").toInt).map { customField =>
html.issuesfieldform(Some(customField), repository)
} getOrElse NotFound()
})
/** Update custom field */
ajaxPost("/:owner/:repository/settings/issues/fields/:fieldId/edit", customFieldForm)(ownerOnly {
(form, repository) =>
updateCustomField(
repository.owner,
repository.name,
params("fieldId").toInt,
form.fieldName,
form.fieldType,
form.enableForIssues,
form.enableForPullRequests
)
html.issuesfield(getCustomField(repository.owner, repository.name, params("fieldId").toInt).get)
})
/** Delete custom field */
ajaxPost("/:owner/:repository/settings/issues/fields/:fieldId/delete")(ownerOnly { repository =>
deleteCustomField(repository.owner, repository.name, params("fieldId").toInt)
Ok()
})
/** /**
* Provides duplication check for web hook url. * Provides duplication check for web hook url.
*/ */

View File

@@ -329,55 +329,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/upload", uploadForm)(writableUsersOnly { (form, repository) => post("/:owner/:repository/upload", uploadForm)(writableUsersOnly { (form, repository) =>
context.withLoginAccount {
loginAccount =>
val files = form.uploadFiles
.split("\n")
.map { line =>
val i = line.indexOf(':')
CommitFile(line.substring(0, i).trim, line.substring(i + 1).trim)
}
.toSeq
val newFiles = files.map { file =>
file.copy(name = if (form.path.length == 0) file.name else s"${form.path}/${file.name}")
}
if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
val objectId = _commit(newBranchName, newFiles, loginAccount)
val issueId =
createIssueAndPullRequest(
repository,
form.branch,
newBranchName,
form.commit,
objectId.name,
form.message,
loginAccount
)
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} else {
_commit(form.branch, newFiles, loginAccount)
if (form.path.length == 0) {
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}")
} else {
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}/${form.path}")
}
}
}
def _commit( def _commit(
branchName: String, branchName: String,
//files: Seq[CommitFile],
newFiles: Seq[CommitFile], newFiles: Seq[CommitFile],
loginAccount: Account loginAccount: Account
): ObjectId = { ): Either[String, ObjectId] = {
commitFiles( commitFiles(
repository = repository, repository = repository,
branch = branchName, branch = branchName,
//path = form.path,
//files = files.toIndexedSeq,
message = form.message.getOrElse("Add files via upload"), message = form.message.getOrElse("Add files via upload"),
loginAccount = loginAccount, loginAccount = loginAccount,
settings = context.settings settings = context.settings
@@ -399,6 +358,52 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
} }
} }
context.withLoginAccount {
loginAccount =>
val files = form.uploadFiles
.split("\n")
.map { line =>
val i = line.indexOf(':')
CommitFile(line.substring(0, i).trim, line.substring(i + 1).trim)
}
.toSeq
val newFiles = files.map { file =>
file.copy(name = if (form.path.length == 0) file.name else s"${form.path}/${file.name}")
}
if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
_commit(newBranchName, newFiles, loginAccount) match {
case Right(objectId) =>
val issueId =
createIssueAndPullRequest(
repository,
form.branch,
newBranchName,
form.commit,
objectId.name,
form.message,
loginAccount
)
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
case Left(error) => Forbidden(gitbucket.core.html.error(error))
}
} else {
_commit(form.branch, newFiles, loginAccount) match {
case Right(_) =>
if (form.path.length == 0) {
redirect(s"/${repository.owner}/${repository.name}/tree/${encodeRefName(form.branch)}")
} else {
redirect(
s"/${repository.owner}/${repository.name}/tree/${encodeRefName(form.branch)}/${encodeRefName(form.path)}"
)
}
case Left(error) => Forbidden(gitbucket.core.html.error(error))
}
}
}
}) })
get("/:owner/:repository/edit/*")(writableUsersOnly { repository => get("/:owner/:repository/edit/*")(writableUsersOnly { repository =>
@@ -456,32 +461,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/create", editorForm)(writableUsersOnly { (form, repository) => post("/:owner/:repository/create", editorForm)(writableUsersOnly { (form, repository) =>
context.withLoginAccount { def _commit(branchName: String, loginAccount: Account): Either[String, ObjectId] = {
loginAccount =>
if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
val objectId = _commit(newBranchName, loginAccount)
val issueId =
createIssueAndPullRequest(
repository,
form.branch,
newBranchName,
form.commit,
objectId.name,
form.message,
loginAccount
)
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} else {
_commit(form.branch, loginAccount)
redirect(
s"/${repository.owner}/${repository.name}/blob/${form.branch}/${if (form.path.length == 0) urlEncode(form.newFileName)
else s"${form.path}/${urlEncode(form.newFileName)}"}"
)
}
}
def _commit(branchName: String, loginAccount: Account): ObjectId = {
commitFile( commitFile(
repository = repository, repository = repository,
branch = branchName, branch = branchName,
@@ -494,37 +474,48 @@ trait RepositoryViewerControllerBase extends ControllerBase {
commit = form.commit, commit = form.commit,
loginAccount = loginAccount, loginAccount = loginAccount,
settings = context.settings settings = context.settings
)._1 ).map(_._1)
} }
})
post("/:owner/:repository/update", editorForm)(writableUsersOnly { (form, repository) =>
context.withLoginAccount { context.withLoginAccount {
loginAccount => loginAccount =>
if (form.newBranch) { if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount) val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
val objectId = _commit(newBranchName, loginAccount) _commit(newBranchName, loginAccount) match {
val issueId = case Right(objectId) =>
createIssueAndPullRequest( val issueId =
repository, createIssueAndPullRequest(
form.branch, repository,
newBranchName, form.branch,
form.commit, newBranchName,
objectId.name, form.commit,
form.message, objectId.name,
loginAccount form.message,
) loginAccount
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") )
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
case Left(error) => Forbidden(gitbucket.core.html.error(error))
}
} else { } else {
_commit(form.branch, loginAccount) _commit(form.branch, loginAccount) match {
redirect( case Right(_) =>
s"/${repository.owner}/${repository.name}/blob/${urlEncode(form.branch)}/${if (form.path.length == 0) urlEncode(form.newFileName) if (form.path.length == 0) {
else s"${form.path}/${urlEncode(form.newFileName)}"}" redirect(
) s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${urlEncode(form.newFileName)}"
)
} else {
redirect(
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${encodeRefName(form.path)}/${urlEncode(form.newFileName)}"
)
}
case Left(error) => Forbidden(gitbucket.core.html.error(error))
}
} }
} }
})
def _commit(branchName: String, loginAccount: Account): ObjectId = { post("/:owner/:repository/update", editorForm)(writableUsersOnly { (form, repository) =>
def _commit(branchName: String, loginAccount: Account): Either[String, ObjectId] = {
commitFile( commitFile(
repository = repository, repository = repository,
branch = branchName, branch = branchName,
@@ -541,37 +532,48 @@ trait RepositoryViewerControllerBase extends ControllerBase {
commit = form.commit, commit = form.commit,
loginAccount = loginAccount, loginAccount = loginAccount,
settings = context.settings settings = context.settings
)._1 ).map(_._1)
} }
})
post("/:owner/:repository/remove", deleteForm)(writableUsersOnly { (form, repository) =>
context.withLoginAccount { context.withLoginAccount {
loginAccount => loginAccount =>
if (form.newBranch) { if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount) val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
val objectId = _commit(newBranchName, loginAccount) _commit(newBranchName, loginAccount) match {
val issueId = case Right(objectId) =>
createIssueAndPullRequest( val issueId =
repository, createIssueAndPullRequest(
form.branch, repository,
newBranchName, form.branch,
form.commit, newBranchName,
objectId.name, form.commit,
form.message, objectId.name,
loginAccount form.message,
) loginAccount
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") )
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
case Left(error) => Forbidden(gitbucket.core.html.error(error))
}
} else { } else {
_commit(form.branch, loginAccount) _commit(form.branch, loginAccount) match {
redirect( case Right(_) =>
s"/${repository.owner}/${repository.name}/tree/${form.branch}${if (form.path.length == 0) "" if (form.path.length == 0) {
else "/" + form.path}" redirect(
) s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${urlEncode(form.newFileName)}"
)
} else {
redirect(
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${encodeRefName(form.path)}/${urlEncode(form.newFileName)}"
)
}
case Left(error) => Forbidden(gitbucket.core.html.error(error))
}
} }
} }
})
def _commit(branchName: String, loginAccount: Account): ObjectId = { post("/:owner/:repository/remove", deleteForm)(writableUsersOnly { (form, repository) =>
def _commit(branchName: String, loginAccount: Account): Either[String, ObjectId] = {
commitFile( commitFile(
repository = repository, repository = repository,
branch = branchName, branch = branchName,
@@ -584,7 +586,41 @@ trait RepositoryViewerControllerBase extends ControllerBase {
commit = form.commit, commit = form.commit,
loginAccount = loginAccount, loginAccount = loginAccount,
settings = context.settings settings = context.settings
)._1 ).map(_._1)
}
context.withLoginAccount {
loginAccount =>
if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
_commit(newBranchName, loginAccount) match {
case Right(objectId) =>
val issueId =
createIssueAndPullRequest(
repository,
form.branch,
newBranchName,
form.commit,
objectId.name,
form.message,
loginAccount
)
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
case Left(error) => Forbidden(gitbucket.core.html.error(error))
}
} else {
_commit(form.branch, loginAccount) match {
case Right(_) =>
if (form.path.isEmpty) {
redirect(s"/${repository.owner}/${repository.name}/tree/${encodeRefName(form.branch)}")
} else {
redirect(
s"/${repository.owner}/${repository.name}/tree/${encodeRefName(form.branch)}/${encodeRefName(form.path)}"
)
}
case Left(error) => Forbidden(gitbucket.core.html.error(error))
}
}
} }
}) })
@@ -640,7 +676,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
loginUser = loginAccount.userName, loginUser = loginAccount.userName,
title = requestBranch, title = requestBranch,
content = commitMessage, content = commitMessage,
assignedUserName = None,
milestoneId = None, milestoneId = None,
priorityId = None, priorityId = None,
isPullRequest = true isPullRequest = true

View File

@@ -34,19 +34,22 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
private val form = mapping( private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))), "baseUrl" -> trim(label("Base URL", optional(text()))),
"information" -> trim(label("Information", optional(text()))), "information" -> trim(label("Information", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())), "basicBehavior" -> mapping(
"allowAnonymousAccess" -> trim(label("Anonymous access", boolean())), "allowAccountRegistration" -> trim(label("Account registration", boolean())),
"isCreateRepoOptionPublic" -> trim(label("Default visibility of new repository", boolean())), "allowResetPassword" -> trim(label("Reset password", boolean())),
"repositoryOperation" -> mapping( "allowAnonymousAccess" -> trim(label("Anonymous access", boolean())),
"create" -> trim(label("Allow all users to create repository", boolean())), "isCreateRepoOptionPublic" -> trim(label("Default visibility of new repository", boolean())),
"delete" -> trim(label("Allow all users to delete repository", boolean())), "repositoryOperation" -> mapping(
"rename" -> trim(label("Allow all users to rename repository", boolean())), "create" -> trim(label("Allow all users to create repository", boolean())),
"transfer" -> trim(label("Allow all users to transfer repository", boolean())), "delete" -> trim(label("Allow all users to delete repository", boolean())),
"fork" -> trim(label("Allow all users to fork repository", boolean())) "rename" -> trim(label("Allow all users to rename repository", boolean())),
)(RepositoryOperation.apply), "transfer" -> trim(label("Allow all users to transfer repository", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())), "fork" -> trim(label("Allow all users to fork repository", boolean()))
"notification" -> trim(label("Notification", boolean())), )(RepositoryOperation.apply),
"limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())), "gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())),
"limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())),
)(BasicBehavior.apply),
"ssh" -> mapping( "ssh" -> mapping(
"enabled" -> trim(label("SSH access", boolean())), "enabled" -> trim(label("SSH access", boolean())),
"bindAddress" -> mapping( "bindAddress" -> mapping(
@@ -334,7 +337,12 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
post("/admin/system/sendmail", sendMailForm)(adminOnly { form => post("/admin/system/sendmail", sendMailForm)(adminOnly { form =>
try { try {
new Mailer(context.settings.copy(smtp = Some(form.smtp), notification = true)).send( new Mailer(
context.settings.copy(
smtp = Some(form.smtp),
basicBehavior = context.settings.basicBehavior.copy(notification = true)
)
).send(
to = form.testAddress, to = form.testAddress,
subject = "Test message from GitBucket", subject = "Test message from GitBucket",
textMsg = "This is a test message from GitBucket.", textMsg = "This is a test message from GitBucket.",

View File

@@ -29,9 +29,9 @@ trait ApiIssueControllerBase extends ControllerBase {
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
// TODO: more api spec condition // TODO: more api spec condition
val condition = IssueSearchCondition(request) val condition = IssueSearchCondition(request)
val baseOwner = getAccountByUserName(repository.owner).get //val baseOwner = getAccountByUserName(repository.owner).get
val issues: List[(Issue, Account, Option[Account])] = val issues: List[(Issue, Account, List[Account])] =
searchIssueByApi( searchIssueByApi(
condition = condition, condition = condition,
offset = (page - 1) * PullRequestLimit, offset = (page - 1) * PullRequestLimit,
@@ -40,12 +40,12 @@ trait ApiIssueControllerBase extends ControllerBase {
) )
JsonFormat(issues.map { JsonFormat(issues.map {
case (issue, issueUser, assignedUser) => case (issue, issueUser, assigneeUsers) =>
ApiIssue( ApiIssue(
issue = issue, issue = issue,
repositoryName = RepositoryName(repository), repositoryName = RepositoryName(repository),
user = ApiUser(issueUser), user = ApiUser(issueUser),
assignee = assignedUser.map(ApiUser(_)), assignees = assigneeUsers.map(ApiUser(_)),
labels = getIssueLabels(repository.owner, repository.name, issue.issueId) labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))), .map(ApiLabel(_, RepositoryName(repository))),
issue.milestoneId.flatMap { getApiMilestone(repository, _) } issue.milestoneId.flatMap { getApiMilestone(repository, _) }
@@ -61,7 +61,8 @@ trait ApiIssueControllerBase extends ControllerBase {
(for { (for {
issueId <- params("id").toIntOpt issueId <- params("id").toIntOpt
issue <- getIssue(repository.owner, repository.name, issueId.toString) issue <- getIssue(repository.owner, repository.name, issueId.toString)
users = getAccountsByUserNames(Set(issue.openedUserName) ++ issue.assignedUserName, Set()) assigneeUsers = getIssueAssignees(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(Set(issue.openedUserName) ++ assigneeUsers.map(_.assigneeUserName), Set())
openedUser <- users.get(issue.openedUserName) openedUser <- users.get(issue.openedUserName)
} yield { } yield {
JsonFormat( JsonFormat(
@@ -69,7 +70,7 @@ trait ApiIssueControllerBase extends ControllerBase {
issue, issue,
RepositoryName(repository), RepositoryName(repository),
ApiUser(openedUser), ApiUser(openedUser),
issue.assignedUserName.flatMap(users.get(_)).map(ApiUser(_)), assigneeUsers.flatMap(x => users.get(x.assigneeUserName)).map(ApiUser(_)),
getIssueLabels(repository.owner, repository.name, issue.issueId).map(ApiLabel(_, RepositoryName(repository))), getIssueLabels(repository.owner, repository.name, issue.issueId).map(ApiLabel(_, RepositoryName(repository))),
issue.milestoneId.flatMap { getApiMilestone(repository, _) } issue.milestoneId.flatMap { getApiMilestone(repository, _) }
) )
@@ -92,7 +93,7 @@ trait ApiIssueControllerBase extends ControllerBase {
repository, repository,
data.title, data.title,
data.body, data.body,
data.assignees.headOption, data.assignees,
milestone.map(_.milestoneId), milestone.map(_.milestoneId),
None, None,
data.labels, data.labels,
@@ -103,7 +104,9 @@ trait ApiIssueControllerBase extends ControllerBase {
issue, issue,
RepositoryName(repository), RepositoryName(repository),
ApiUser(loginAccount), ApiUser(loginAccount),
issue.assignedUserName.flatMap(getAccountByUserName(_)).map(ApiUser(_)), getIssueAssignees(repository.owner, repository.name, issue.issueId)
.flatMap(x => getAccountByUserName(x.assigneeUserName, false))
.map(ApiUser.apply),
getIssueLabels(repository.owner, repository.name, issue.issueId) getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))), .map(ApiLabel(_, RepositoryName(repository))),
issue.milestoneId.flatMap { getApiMilestone(repository, _) } issue.milestoneId.flatMap { getApiMilestone(repository, _) }

View File

@@ -40,7 +40,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
val condition = IssueSearchCondition(request) val condition = IssueSearchCondition(request)
val baseOwner = getAccountByUserName(repository.owner).get val baseOwner = getAccountByUserName(repository.owner).get
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] = val issues: List[(Issue, Account, Int, PullRequest, Repository, Account, List[Account])] =
searchPullRequestByApi( searchPullRequestByApi(
condition = condition, condition = condition,
offset = (page - 1) * PullRequestLimit, offset = (page - 1) * PullRequestLimit,
@@ -49,7 +49,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
) )
JsonFormat(issues.map { JsonFormat(issues.map {
case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignee) => case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignees) =>
ApiPullRequest( ApiPullRequest(
issue = issue, issue = issue,
pullRequest = pullRequest, pullRequest = pullRequest,
@@ -58,7 +58,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
user = ApiUser(issueUser), user = ApiUser(issueUser),
labels = getIssueLabels(repository.owner, repository.name, issue.issueId) labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))), .map(ApiLabel(_, RepositoryName(repository))),
assignee = assignee.map(ApiUser.apply), assignees = assignees.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId) mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
) )
}) })
@@ -99,7 +99,6 @@ trait ApiPullRequestControllerBase extends ControllerBase {
loginUser = context.loginAccount.get.userName, loginUser = context.loginAccount.get.userName,
title = createPullReq.title, title = createPullReq.title,
content = createPullReq.body, content = createPullReq.body,
assignedUserName = None,
milestoneId = None, milestoneId = None,
priorityId = None, priorityId = None,
isPullRequest = true isPullRequest = true
@@ -319,8 +318,8 @@ trait ApiPullRequestControllerBase extends ControllerBase {
baseOwner <- users.get(repository.owner) baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName) headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName) issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName => assignees = getIssueAssignees(repository.owner, repository.name, issueId).flatMap { assignedUser =>
getAccountByUserName(userName, false) getAccountByUserName(assignedUser.assigneeUserName, false)
} }
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName) headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield { } yield {
@@ -332,7 +331,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
user = ApiUser(issueUser), user = ApiUser(issueUser),
labels = getIssueLabels(repository.owner, repository.name, issue.issueId) labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))), .map(ApiLabel(_, RepositoryName(repository))),
assignee = assignee.map(ApiUser.apply), assignees = assignees.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId) mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
) )
} }

View File

@@ -9,9 +9,23 @@ import gitbucket.core.util.JGitUtil.{CommitInfo, getBranches, getBranchesOfCommi
import gitbucket.core.util.{JGitUtil, ReferrerAuthenticator, RepositoryName} import gitbucket.core.util.{JGitUtil, ReferrerAuthenticator, RepositoryName}
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.revwalk.filter.{
AndRevFilter,
AuthorRevFilter,
CommitTimeRevFilter,
MaxCountRevFilter,
RevFilter,
SkipRevFilter
}
import org.eclipse.jgit.treewalk.filter.{AndTreeFilter, PathFilterGroup, TreeFilter}
import scala.collection.mutable.ListBuffer
import scala.jdk.CollectionConverters._ import scala.jdk.CollectionConverters._
import scala.util.Using import scala.util.Using
import math.min
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter._
import java.util.Date
import java.time.ZoneOffset
trait ApiRepositoryCommitControllerBase extends ControllerBase { trait ApiRepositoryCommitControllerBase extends ControllerBase {
self: AccountService with CommitsService with ProtectedBranchService with ReferrerAuthenticator => self: AccountService with CommitsService with ProtectedBranchService with ReferrerAuthenticator =>
@@ -22,8 +36,13 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
get("/api/v3/repos/:owner/:repository/commits")(referrersOnly { repository => get("/api/v3/repos/:owner/:repository/commits")(referrersOnly { repository =>
val owner = repository.owner val owner = repository.owner
val name = repository.name val name = repository.name
// TODO: The following parameters need to be implemented. [:path, :author, :since, :until] val sha = params.get("sha").filter(_.nonEmpty).getOrElse("HEAD")
val sha = params.getOrElse("sha", "refs/heads/master") val page = params.get("page").filter(_.nonEmpty).getOrElse("1").toInt
val per_page = min(params.get("per_page").filter(_.nonEmpty).getOrElse("30").toInt, 100)
val author = params.get("author").filter(_.nonEmpty)
val path = params.get("path").filter(_.nonEmpty)
val since = params.get("since").filter(_.nonEmpty)
val until = params.get("until").filter(_.nonEmpty)
Using.resource(Git.open(getRepositoryDir(owner, name))) { Using.resource(Git.open(getRepositoryDir(owner, name))) {
git => git =>
val repo = git.getRepository val repo = git.getRepository
@@ -31,7 +50,37 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
revWalk => revWalk =>
val objectId = repo.resolve(sha) val objectId = repo.resolve(sha)
revWalk.markStart(revWalk.parseCommit(objectId)) revWalk.markStart(revWalk.parseCommit(objectId))
JsonFormat(revWalk.asScala.take(30).map { if (path.nonEmpty) {
revWalk.setTreeFilter(
AndTreeFilter.create(PathFilterGroup.createFromStrings(path.get), TreeFilter.ANY_DIFF)
)
}
val revfilters = new ListBuffer[(RevFilter)]()
if (author.nonEmpty) {
revfilters += AuthorRevFilter.create(author.get)
}
if (since.nonEmpty) {
revfilters += CommitTimeRevFilter.after(
Date.from(LocalDateTime.parse(since.get, ISO_DATE_TIME).toInstant(ZoneOffset.UTC))
)
}
if (until.nonEmpty) {
revfilters += CommitTimeRevFilter.before(
Date.from(LocalDateTime.parse(until.get, ISO_DATE_TIME).toInstant(ZoneOffset.UTC))
)
}
if (page > 1) {
revfilters += SkipRevFilter.create(page * per_page - 2)
}
revfilters += MaxCountRevFilter.create(per_page);
revWalk.setRevFilter(
if (revfilters.size > 1) {
AndRevFilter.create(revfilters.toArray)
} else {
revfilters(0)
}
)
JsonFormat(revWalk.asScala.map {
commit => commit =>
val commitInfo = new CommitInfo(commit) val commitInfo = new CommitInfo(commit)
ApiCommits( ApiCommits(

View File

@@ -157,7 +157,7 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
Some("https://docs.github.com/en/rest/reference/repos#create-or-update-file-contents") Some("https://docs.github.com/en/rest/reference/repos#create-or-update-file-contents")
) )
case _ => case _ =>
val (commitId, blobId) = commitFile( commitFile(
repository, repository,
branch, branch,
path, path,
@@ -170,12 +170,12 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
data.committer.map(_.name).getOrElse(loginAccount.fullName), data.committer.map(_.name).getOrElse(loginAccount.fullName),
data.committer.map(_.email).getOrElse(loginAccount.mailAddress), data.committer.map(_.email).getOrElse(loginAccount.mailAddress),
context.settings context.settings
) ) match {
case Left(error) =>
blobId match { ApiError(s"Failed to commit a file: ${error}", None)
case None => case Right((_, None)) =>
ApiError("Failed to commit a file.", None) ApiError("Failed to commit a file.", None)
case Some(blobId) => case Right((commitId, Some(blobId))) =>
Map( Map(
"content" -> ApiContents( "content" -> ApiContents(
"file", "file",

View File

@@ -0,0 +1,191 @@
package gitbucket.core.model
import gitbucket.core.controller.Context
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util.StringUtil
import gitbucket.core.view.helpers
import org.scalatra.i18n.Messages
trait CustomFieldComponent extends TemplateComponent { self: Profile =>
import profile.api._
lazy val CustomFields = TableQuery[CustomFields]
class CustomFields(tag: Tag) extends Table[CustomField](tag, "CUSTOM_FIELD") with BasicTemplate {
val fieldId = column[Int]("FIELD_ID", O AutoInc)
val fieldName = column[String]("FIELD_NAME")
val fieldType = column[String]("FIELD_TYPE")
val enableForIssues = column[Boolean]("ENABLE_FOR_ISSUES")
val enableForPullRequests = column[Boolean]("ENABLE_FOR_PULL_REQUESTS")
def * =
(userName, repositoryName, fieldId, fieldName, fieldType, enableForIssues, enableForPullRequests)
.<>(CustomField.tupled, CustomField.unapply)
def byPrimaryKey(userName: String, repositoryName: String, fieldId: Int) =
(this.userName === userName.bind) && (this.repositoryName === repositoryName.bind) && (this.fieldId === fieldId.bind)
}
}
case class CustomField(
userName: String,
repositoryName: String,
fieldId: Int = 0,
fieldName: String,
fieldType: String, // long, double, string, or date
enableForIssues: Boolean,
enableForPullRequests: Boolean
)
trait CustomFieldBehavior {
def createHtml(repository: RepositoryInfo, fieldId: Int)(implicit conext: Context): String
def fieldHtml(repository: RepositoryInfo, issueId: Int, fieldId: Int, value: String, editable: Boolean)(
implicit context: Context
): String
def validate(name: String, value: String, messages: Messages): Option[String]
}
object CustomFieldBehavior {
def validate(field: CustomField, value: String, messages: Messages): Option[String] = {
if (value.isEmpty) None
else {
CustomFieldBehavior(field.fieldType).flatMap { behavior =>
behavior.validate(field.fieldName, value, messages)
}
}
}
def apply(fieldType: String): Option[CustomFieldBehavior] = {
fieldType match {
case "long" => Some(LongFieldBehavior)
case "double" => Some(DoubleFieldBehavior)
case "string" => Some(StringFieldBehavior)
case "date" => Some(DateFieldBehavior)
case _ => None
}
}
case object LongFieldBehavior extends TextFieldBehavior {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
try {
value.toLong
None
} catch {
case _: NumberFormatException => Some(messages("error.number").format(name))
}
}
}
case object DoubleFieldBehavior extends TextFieldBehavior {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
try {
value.toDouble
None
} catch {
case _: NumberFormatException => Some(messages("error.number").format(name))
}
}
}
case object StringFieldBehavior extends TextFieldBehavior
case object DateFieldBehavior extends TextFieldBehavior {
private val pattern = "yyyy-MM-dd"
override protected val fieldType: String = "date"
override def validate(name: String, value: String, messages: Messages): Option[String] = {
try {
new java.text.SimpleDateFormat(pattern).parse(value)
None
} catch {
case _: java.text.ParseException =>
Some(messages("error.datePattern").format(name, pattern))
}
}
}
trait TextFieldBehavior extends CustomFieldBehavior {
protected val fieldType = "text"
def createHtml(repository: RepositoryInfo, fieldId: Int)(implicit context: Context): String = {
val sb = new StringBuilder
sb.append(
s"""<input type="$fieldType" class="form-control input-sm" id="custom-field-$fieldId" name="custom-field-$fieldId" data-field-id="$fieldId" style="width: 120px;"/>"""
)
sb.append(s"""<script>
|$$('#custom-field-$fieldId').focusout(function(){
| const $$this = $$(this);
| const fieldId = $$this.data('field-id');
| $$.post('${helpers.url(repository)}/issues/customfield_validation/' + fieldId,
| { value: $$this.val() },
| function(data){
| if (data != '') {
| $$('#custom-field-$fieldId-error').text(data);
| } else {
| $$('#custom-field-$fieldId-error').text('');
| }
| }
| );
|});
|</script>
|""".stripMargin)
sb.toString()
}
def fieldHtml(repository: RepositoryInfo, issueId: Int, fieldId: Int, value: String, editable: Boolean)(
implicit context: Context
): String = {
val sb = new StringBuilder
sb.append(
s"""<span id="custom-field-$fieldId-label" class="custom-field-label">${StringUtil
.escapeHtml(value)}</span>""".stripMargin
)
if (editable) {
sb.append(
s"""<input type="$fieldType" id="custom-field-$fieldId-editor" class="form-control input-sm custom-field-editor" data-field-id="$fieldId" style="width: 120px; display: none;"/>"""
)
sb.append(s"""<script>
|$$('#custom-field-$fieldId-label').click(function(){
| const $$this = $$(this);
| $$this.hide();
| $$this.next().val($$this.text()).show().focus();
|});
|
|$$('#custom-field-$fieldId-editor').focusout(function(){
| const $$this = $$(this);
| const fieldId = $$this.data('field-id');
| $$.post('${helpers.url(repository)}/issues/customfield_validation/' + fieldId,
| { value: $$this.val() },
| function(data){
| if (data != '') {
| $$('#custom-field-$fieldId-error').text(data);
| } else {
| $$('#custom-field-$fieldId-error').text('');
| $$.post('${helpers.url(repository)}/issues/$issueId/customfield/' + fieldId,
| { value: $$this.val() },
| function(data){
| $$this.hide();
| $$this.prev().text(data).show();
| }
| );
| }
| }
| );
|});
|
|// ESC key handling in text field
|$$('#custom-field-$fieldId-editor').keyup(function(e){
| if (e.keyCode == 27) {
| const $$this = $$(this);
| $$this.hide();
| $$this.prev().show();
| }
| if (e.keyCode == 13) {
| $$('#custom-field-$fieldId-editor').blur();
| }
|});
|</script>
|""".stripMargin)
}
sb.toString()
}
def validate(name: String, value: String, messages: Messages): Option[String] = None
}
}

View File

@@ -27,7 +27,6 @@ trait IssueComponent extends TemplateComponent { self: Profile =>
with MilestoneTemplate with MilestoneTemplate
with PriorityTemplate { with PriorityTemplate {
val openedUserName = column[String]("OPENED_USER_NAME") val openedUserName = column[String]("OPENED_USER_NAME")
val assignedUserName = column[String]("ASSIGNED_USER_NAME")
val title = column[String]("TITLE") val title = column[String]("TITLE")
val content = column[String]("CONTENT") val content = column[String]("CONTENT")
val closed = column[Boolean]("CLOSED") val closed = column[Boolean]("CLOSED")
@@ -42,7 +41,6 @@ trait IssueComponent extends TemplateComponent { self: Profile =>
openedUserName, openedUserName,
milestoneId.?, milestoneId.?,
priorityId.?, priorityId.?,
assignedUserName.?,
title, title,
content.?, content.?,
closed, closed,
@@ -62,7 +60,6 @@ case class Issue(
openedUserName: String, openedUserName: String,
milestoneId: Option[Int], milestoneId: Option[Int],
priorityId: Option[Int], priorityId: Option[Int],
assignedUserName: Option[String],
title: String, title: String,
content: Option[String], content: Option[String],
closed: Boolean, closed: Boolean,

View File

@@ -0,0 +1,26 @@
package gitbucket.core.model
trait IssueAssigneeComponent extends TemplateComponent { self: Profile =>
import profile.api._
import self._
lazy val IssueAssignees = TableQuery[IssueAssignees]
class IssueAssignees(tag: Tag) extends Table[IssueAssignee](tag, "ISSUE_ASSIGNEE") with IssueTemplate {
val assigneeUserName = column[String]("ASSIGNEE_USER_NAME")
def * =
(userName, repositoryName, issueId, assigneeUserName)
.<>(IssueAssignee.tupled, IssueAssignee.unapply)
def byPrimaryKey(owner: String, repository: String, issueId: Int, assigneeUserName: String) = {
byIssue(owner, repository, issueId) && this.assigneeUserName === assigneeUserName.bind
}
}
}
case class IssueAssignee(
userName: String,
repositoryName: String,
issueId: Int,
assigneeUserName: String
)

View File

@@ -0,0 +1,31 @@
package gitbucket.core.model
trait IssueCustomFieldComponent extends TemplateComponent { self: Profile =>
import profile.api._
import self._
lazy val IssueCustomFields = TableQuery[IssueCustomFields]
class IssueCustomFields(tag: Tag) extends Table[IssueCustomField](tag, "ISSUE_CUSTOM_FIELD") {
val userName = column[String]("USER_NAME", O.PrimaryKey)
val repositoryName = column[String]("REPOSITORY_NAME", O.PrimaryKey)
val issueId = column[Int]("ISSUE_ID", O.PrimaryKey)
val fieldId = column[Int]("FIELD_ID", O.PrimaryKey)
val value = column[String]("VALUE")
def * =
(userName, repositoryName, issueId, fieldId, value)
.<>(IssueCustomField.tupled, IssueCustomField.unapply)
def byPrimaryKey(owner: String, repository: String, issueId: Int, fieldId: Int) = {
this.userName === owner.bind && this.repositoryName === repository.bind && this.issueId === issueId.bind && this.fieldId === fieldId.bind
}
}
}
case class IssueCustomField(
userName: String,
repositoryName: String,
issueId: Int,
fieldId: Int,
value: String
)

View File

@@ -73,5 +73,8 @@ trait CoreProfile
with ReleaseAssetComponent with ReleaseAssetComponent
with AccountExtraMailAddressComponent with AccountExtraMailAddressComponent
with AccountPreferenceComponent with AccountPreferenceComponent
with CustomFieldComponent
with IssueCustomFieldComponent
with IssueAssigneeComponent
object Profile extends CoreProfile object Profile extends CoreProfile

View File

@@ -7,9 +7,14 @@ import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.model.Profile.dateColumnType import gitbucket.core.model.Profile.dateColumnType
import gitbucket.core.util.{LDAPUtil, StringUtil} import gitbucket.core.util.{LDAPUtil, StringUtil}
import StringUtil._ import StringUtil._
import com.nimbusds.jose.{JWSAlgorithm, JWSHeader}
import com.nimbusds.jose.crypto.{MACSigner, MACVerifier}
import com.nimbusds.jwt.{JWTClaimsSet, SignedJWT}
import gitbucket.core.plugin.PluginRegistry import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService.SystemSettings import gitbucket.core.service.SystemSettingsService.SystemSettings
import java.security.SecureRandom
trait AccountService { trait AccountService {
private val logger = LoggerFactory.getLogger(classOf[AccountService]) private val logger = LoggerFactory.getLogger(classOf[AccountService])
@@ -60,28 +65,28 @@ trait AccountService {
case Right(ldapUserInfo) => { case Right(ldapUserInfo) => {
// Create or update account by LDAP information // Create or update account by LDAP information
getAccountByUserName(ldapUserInfo.userName, true) match { getAccountByUserName(ldapUserInfo.userName, true) match {
case Some(x) if (!x.isRemoved) => { case Some(x) =>
if (settings.ldap.get.mailAttribute.getOrElse("").isEmpty) { if (x.isRemoved) {
updateAccount(x.copy(fullName = ldapUserInfo.fullName)) logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
defaultAuthentication(userName, password)
} else { } else {
updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName)) if (settings.ldap.get.mailAttribute.getOrElse("").isEmpty) {
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
} else {
updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
}
getAccountByUserName(ldapUserInfo.userName)
} }
getAccountByUserName(ldapUserInfo.userName)
}
case Some(x) if (x.isRemoved) => {
logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
defaultAuthentication(userName, password)
}
case None => case None =>
getAccountByMailAddress(ldapUserInfo.mailAddress, true) match { getAccountByMailAddress(ldapUserInfo.mailAddress, true) match {
case Some(x) if (!x.isRemoved) => { case Some(x) =>
updateAccount(x.copy(fullName = ldapUserInfo.fullName)) if (x.isRemoved) {
getAccountByUserName(ldapUserInfo.userName) logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
} defaultAuthentication(userName, password)
case Some(x) if (x.isRemoved) => { } else {
logger.info("LDAP Authentication Failed: Account is already registered but disabled.") updateAccount(x.copy(fullName = ldapUserInfo.fullName))
defaultAuthentication(userName, password) getAccountByUserName(ldapUserInfo.userName)
} }
case None => { case None => {
createAccount( createAccount(
ldapUserInfo.userName, ldapUserInfo.userName,
@@ -337,6 +342,33 @@ trait AccountService {
} }
} }
def generateResetPasswordToken(mailAddress: String): String = {
val claimsSet = new JWTClaimsSet.Builder()
.claim("mailAddress", mailAddress)
.expirationTime(new java.util.Date(System.currentTimeMillis() + 10 * 1000))
.build()
val signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet)
signedJWT.sign(new MACSigner(AccountService.jwtSecretKey))
signedJWT.serialize()
}
def decodeResetPasswordToken(token: String): Option[String] = {
try {
val signedJWT = SignedJWT.parse(token)
val verifier = new MACVerifier(AccountService.jwtSecretKey)
if (signedJWT.verify(verifier) && new java.util.Date().before(signedJWT.getJWTClaimsSet().getExpirationTime())) {
Some(signedJWT.getPayload.toJSONObject.get("mailAddress").toString)
} else None
} catch {
case _: Exception => None
}
}
} }
object AccountService extends AccountService object AccountService extends AccountService {
// 256-bit key for HS256 which must be pre-shared
val jwtSecretKey = new Array[Byte](32)
new SecureRandom().nextBytes(jwtSecretKey)
}

View File

@@ -0,0 +1,94 @@
package gitbucket.core.service
import gitbucket.core.model.{CustomField, IssueCustomField}
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
trait CustomFieldsService {
def getCustomFields(owner: String, repository: String)(implicit s: Session): List[CustomField] =
CustomFields.filter(_.byRepository(owner, repository)).sortBy(_.fieldId asc).list
def getCustomFieldsWithValue(owner: String, repository: String, issueId: Int)(
implicit s: Session
): List[(CustomField, Option[IssueCustomField])] = {
CustomFields
.filter(_.byRepository(owner, repository))
.joinLeft(IssueCustomFields)
.on { case (t1, t2) => t1.fieldId === t2.fieldId && t2.issueId === issueId.bind }
.sortBy { case (t1, t2) => t1.fieldId }
.list
}
def getCustomField(owner: String, repository: String, fieldId: Int)(implicit s: Session): Option[CustomField] =
CustomFields.filter(_.byPrimaryKey(owner, repository, fieldId)).firstOption
def createCustomField(
owner: String,
repository: String,
fieldName: String,
fieldType: String,
enableForIssues: Boolean,
enableForPullRequests: Boolean
)(implicit s: Session): Int = {
CustomFields returning CustomFields.map(_.fieldId) insert CustomField(
userName = owner,
repositoryName = repository,
fieldName = fieldName,
fieldType = fieldType,
enableForIssues = enableForIssues,
enableForPullRequests = enableForPullRequests
)
}
def updateCustomField(
owner: String,
repository: String,
fieldId: Int,
fieldName: String,
fieldType: String,
enableForIssues: Boolean,
enableForPullRequests: Boolean
)(
implicit s: Session
): Unit =
CustomFields
.filter(_.byPrimaryKey(owner, repository, fieldId))
.map(t => (t.fieldName, t.fieldType, t.enableForIssues, t.enableForPullRequests))
.update((fieldName, fieldType, enableForIssues, enableForPullRequests))
def deleteCustomField(owner: String, repository: String, fieldId: Int)(implicit s: Session): Unit = {
IssueCustomFields
.filter(t => t.userName === owner.bind && t.repositoryName === repository.bind && t.fieldId === fieldId.bind)
.delete
CustomFields.filter(_.byPrimaryKey(owner, repository, fieldId)).delete
}
def getCustomFieldValues(
userName: String,
repositoryName: String,
issueId: Int,
)(implicit s: Session): List[IssueCustomField] = {
IssueCustomFields
.filter(t => t.userName === userName && t.repositoryName === repositoryName.bind && t.issueId === issueId.bind)
.list
}
def insertOrUpdateCustomFieldValue(
field: CustomField,
userName: String,
repositoryName: String,
issueId: Int,
value: String
)(implicit s: Session): Unit = {
IssueCustomFields.insertOrUpdate(
IssueCustomField(
userName = userName,
repositoryName = repositoryName,
issueId = issueId,
fieldId = field.fieldId,
value = value
)
)
}
}

View File

@@ -165,7 +165,7 @@ trait HandleCommentService {
content match { content match {
case Some(content) => case Some(content) =>
// Update comment // Update comment
val _commentId = Some(updateComment(issue.issueId, commentId.toInt, content)) val _commentId = Some(updateComment(owner, name, issue.issueId, commentId.toInt, content))
// Record comment activity // Record comment activity
val commentInfo = if (issue.isPullRequest) { val commentInfo = if (issue.isPullRequest) {
PullRequestCommentInfo(owner, name, userName, content, issue.issueId) PullRequestCommentInfo(owner, name, userName, content, issue.issueId)

View File

@@ -16,7 +16,7 @@ trait IssueCreationService {
repository: RepositoryInfo, repository: RepositoryInfo,
title: String, title: String,
body: Option[String], body: Option[String],
assignee: Option[String], assignees: Seq[String],
milestoneId: Option[Int], milestoneId: Option[Int],
priorityId: Option[Int], priorityId: Option[Int],
labelNames: Seq[String], labelNames: Seq[String],
@@ -35,16 +35,19 @@ trait IssueCreationService {
userName, userName,
title, title,
body, body,
if (manageable) assignee else None,
if (manageable) milestoneId else None, if (manageable) milestoneId else None,
if (manageable) priorityId else None if (manageable) priorityId else None
) )
val issue: Issue = getIssue(owner, name, issueId.toString).get val issue: Issue = getIssue(owner, name, issueId.toString).get
// insert labels
if (manageable) { if (manageable) {
// insert assignees
assignees.foreach { assignee =>
registerIssueAssignee(owner, name, issueId, assignee)
}
// insert labels
val labels = getLabels(owner, name) val labels = getLabels(owner, name)
labelNames.map { labelName => labelNames.foreach { labelName =>
labels.find(_.labelName == labelName).map { label => labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(owner, name, issueId, label.labelId) registerIssueLabel(owner, name, issueId, label.labelId)
} }

View File

@@ -5,7 +5,17 @@ import gitbucket.core.util.StringUtil._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.controller.Context import gitbucket.core.controller.Context
import gitbucket.core.model.{Account, Issue, IssueComment, IssueLabel, Label, PullRequest, Repository, Role} import gitbucket.core.model.{
Account,
Issue,
IssueAssignee,
IssueComment,
IssueLabel,
Label,
PullRequest,
Repository,
Role
}
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile._ import gitbucket.core.model.Profile.profile._
import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.model.Profile.profile.blockingApi._
@@ -118,8 +128,7 @@ trait IssuesService {
def countIssueGroupByLabels( def countIssueGroupByLabels(
owner: String, owner: String,
repository: String, repository: String,
condition: IssueSearchCondition, condition: IssueSearchCondition
filterUser: Map[String, String]
)(implicit s: Session): Map[String, Int] = { )(implicit s: Session): Map[String, Int] = {
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), IssueSearchOption.Issues) searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), IssueSearchOption.Issues)
@@ -138,7 +147,7 @@ trait IssuesService {
t3.labelName t3.labelName
} }
.map { .map {
case labelName ~ t => case (labelName, t) =>
labelName -> t.length labelName -> t.length
} }
.list .list
@@ -156,8 +165,7 @@ trait IssuesService {
def countIssueGroupByPriorities( def countIssueGroupByPriorities(
owner: String, owner: String,
repository: String, repository: String,
condition: IssueSearchCondition, condition: IssueSearchCondition
filterUser: Map[String, String]
)(implicit s: Session): Map[String, Int] = { )(implicit s: Session): Map[String, Int] = {
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), IssueSearchOption.Issues) searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), IssueSearchOption.Issues)
@@ -171,7 +179,7 @@ trait IssuesService {
t2.priorityName t2.priorityName
} }
.map { .map {
case priorityName ~ t => case (priorityName, t) =>
priorityName -> t.length priorityName -> t.length
} }
.list .list
@@ -207,9 +215,11 @@ trait IssuesService {
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t1.byPriority(t6.userName, t6.repositoryName, t6.priorityId) } .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t1.byPriority(t6.userName, t6.repositoryName, t6.priorityId) }
.joinLeft(PullRequests) .joinLeft(PullRequests)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t1.byIssue(t7.userName, t7.repositoryName, t7.issueId) } .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t1.byIssue(t7.userName, t7.repositoryName, t7.issueId) }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => i asc } .joinLeft(IssueAssignees)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => t1.byIssue(t8.userName, t8.repositoryName, t8.issueId) }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => i asc }
.map { .map {
case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 =>
( (
t1, t1,
t2.commentCount, t2.commentCount,
@@ -218,7 +228,8 @@ trait IssuesService {
t4.map(_.color), t4.map(_.color),
t5.map(_.title), t5.map(_.title),
t6.map(_.priorityName), t6.map(_.priorityName),
t7.map(_.commitIdTo) t7.map(_.commitIdTo),
t8.map(_.assigneeUserName)
) )
} }
.list .list
@@ -228,36 +239,51 @@ trait IssuesService {
result.map { issues => result.map { issues =>
issues.head match { issues.head match {
case (issue, commentCount, _, _, _, milestone, priority, commitId) => case (issue, commentCount, _, _, _, milestone, priority, commitId, _) =>
IssueInfo( IssueInfo(
issue, issue,
issues.flatMap { t => issues
t._3.map(Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)) .flatMap { t =>
} toList, t._3.map(Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get))
}
.distinct
.toList,
milestone, milestone,
priority, priority,
commentCount, commentCount,
commitId commitId,
issues.flatMap(_._9).distinct
) )
} }
} toList } toList
} }
/** for api /** for api
* @return (issue, issueUser, assignedUser) * @return (issue, issueUser, Seq(assigneeUsers))
*/ */
def searchIssueByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)( def searchIssueByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)(
implicit s: Session implicit s: Session
): List[(Issue, Account, Option[Account])] = { ): List[(Issue, Account, List[Account])] = {
// get issues and comment count and labels // get issues and comment count and labels
searchIssueQueryBase(condition, IssueSearchOption.Issues, offset, limit, repos) searchIssueQueryBase(condition, IssueSearchOption.Issues, offset, limit, repos)
.join(Accounts) .join(Accounts)
.on { case t1 ~ t2 ~ i ~ t3 => t3.userName === t1.openedUserName } .on { case t1 ~ t2 ~ i ~ t3 => t3.userName === t1.openedUserName }
.joinLeft(IssueAssignees)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t4.byIssue(t1.userName, t1.repositoryName, t1.issueId) }
.joinLeft(Accounts) .joinLeft(Accounts)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t4.userName === t1.assignedUserName } .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t4.map(_.assigneeUserName) }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 => i asc } .sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 => (t1, t3, t4) } .map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => (t1, t3, t5) }
.list .list
.groupBy {
case (issue, account, _) =>
(issue, account)
}
.map {
case (_, values) =>
(values.head._1, values.head._2, values.flatMap(_._3))
}
.toList
} }
/** for api /** for api
@@ -265,7 +291,7 @@ trait IssuesService {
*/ */
def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)( def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)(
implicit s: Session implicit s: Session
): List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] = { ): List[(Issue, Account, Int, PullRequest, Repository, Account, List[Account])] = {
// get issues and comment count and labels // get issues and comment count and labels
searchIssueQueryBase(condition, IssueSearchOption.PullRequests, offset, limit, repos) searchIssueQueryBase(condition, IssueSearchOption.PullRequests, offset, limit, repos)
.join(PullRequests) .join(PullRequests)
@@ -276,11 +302,30 @@ trait IssuesService {
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t1.openedUserName } .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t1.openedUserName }
.join(Accounts) .join(Accounts)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t6.userName === t4.userName } .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t6.userName === t4.userName }
.joinLeft(IssueAssignees)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t7.byIssue(t1.userName, t1.repositoryName, t1.issueId) }
.joinLeft(Accounts) .joinLeft(Accounts)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t7.userName === t1.assignedUserName } .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => t8.userName === t7.map(_.assigneeUserName) }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => i asc } .sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => (t1, t5, t2.commentCount, t3, t4, t6, t7) } .map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => (t1, t5, t2.commentCount, t3, t4, t6, t8) }
.list .list
.groupBy {
case (issue, openedUser, commentCount, pullRequest, repository, account, assignedUser) =>
(issue, openedUser, commentCount, pullRequest, repository, account)
}
.map {
case (_, values) =>
(
values.head._1,
values.head._2,
values.head._3,
values.head._4,
values.head._5,
values.head._6,
values.flatMap(_._7)
)
}
.toList
} }
private def searchIssueQueryBase( private def searchIssueQueryBase(
@@ -347,7 +392,7 @@ trait IssuesService {
case _ => t1.closed === true || t1.closed === false case _ => t1.closed === true || t1.closed === false
}).&&(t1.milestoneId.? isEmpty, condition.milestone == Some(None)) }).&&(t1.milestoneId.? isEmpty, condition.milestone == Some(None))
.&&(t1.priorityId.? isEmpty, condition.priority == Some(None)) .&&(t1.priorityId.? isEmpty, condition.priority == Some(None))
.&&(t1.assignedUserName.? isEmpty, condition.assigned == Some(None)) //.&&(t1.assignedUserName.? isEmpty, condition.assigned == Some(None))
.&&(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) && .&&(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
(searchOption match { (searchOption match {
case IssueSearchOption.Issues => t1.pullRequest === false case IssueSearchOption.Issues => t1.pullRequest === false
@@ -371,7 +416,13 @@ trait IssuesService {
condition.priority.flatten.isDefined condition.priority.flatten.isDefined
) )
// Assignee filter // Assignee filter
.&&(t1.assignedUserName === condition.assigned.get.get.bind, condition.assigned.flatten.isDefined) .&&(
IssueAssignees filter { a =>
a.byIssue(t1.userName, t1.repositoryName, t1.issueId) &&
a.assigneeUserName === condition.assigned.get.get.bind
} exists,
condition.assigned.flatten.isDefined
)
// Label filter // Label filter
.&&( .&&(
IssueLabels filter { t2 => IssueLabels filter { t2 =>
@@ -396,7 +447,9 @@ trait IssuesService {
.&&(t1.userName inSetBind condition.groups, condition.groups.nonEmpty) .&&(t1.userName inSetBind condition.groups, condition.groups.nonEmpty)
// Mentioned filter // Mentioned filter
.&&( .&&(
(t1.openedUserName === condition.mentioned.get.bind) || t1.assignedUserName === condition.mentioned.get.bind || (t1.openedUserName === condition.mentioned.get.bind) || (IssueAssignees filter { t1 =>
t1.byIssue(t1.userName, t1.repositoryName, t1.issueId) && t1.assigneeUserName === condition.mentioned.get.bind
} exists) ||
(IssueComments filter { t2 => (IssueComments filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind) (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind)
} exists), } exists),
@@ -410,7 +463,6 @@ trait IssuesService {
loginUser: String, loginUser: String,
title: String, title: String,
content: Option[String], content: Option[String],
assignedUserName: Option[String],
milestoneId: Option[Int], milestoneId: Option[Int],
priorityId: Option[Int], priorityId: Option[Int],
isPullRequest: Boolean = false isPullRequest: Boolean = false
@@ -427,7 +479,6 @@ trait IssuesService {
loginUser, loginUser,
milestoneId, milestoneId,
priorityId, priorityId,
assignedUserName,
title, title,
content, content,
false, false,
@@ -509,7 +560,7 @@ trait IssuesService {
content: String, content: String,
action: String action: String
)(implicit s: Session): Int = { )(implicit s: Session): Int = {
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate) Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(_.updatedDate).update(currentDate)
IssueComments returning IssueComments.map(_.commentId) insert IssueComment( IssueComments returning IssueComments.map(_.commentId) insert IssueComment(
userName = owner, userName = owner,
repositoryName = repository, repositoryName = repository,
@@ -542,35 +593,91 @@ trait IssuesService {
.update(true) .update(true)
} }
def updateAssignedUserName( def getIssueAssignees(owner: String, repository: String, issueId: Int)(
implicit s: Session
): List[IssueAssignee] = {
IssueAssignees.filter(_.byIssue(owner, repository, issueId)).sortBy(_.assigneeUserName).list
}
def registerIssueAssignee(
owner: String, owner: String,
repository: String, repository: String,
issueId: Int, issueId: Int,
assignedUserName: Option[String], assigneeUserName: String,
insertComment: Boolean = false insertComment: Boolean = false
)(implicit context: Context, s: Session): Int = { )(
val oldAssigned = getIssue(owner, repository, s"${issueId}").get.assignedUserName implicit context: Context,
val assigned = assignedUserName s: Session
): Int = {
val assigner = context.loginAccount.map(_.userName) val assigner = context.loginAccount.map(_.userName)
if (insertComment) { if (insertComment) {
IssueComments insert IssueComment( IssueComments insert IssueComment(
userName = owner, userName = owner,
repositoryName = repository, repositoryName = repository,
issueId = issueId, issueId = issueId,
action = "assign", action = "add_assignee",
commentedUserName = assigner.getOrElse("Unknown user"), commentedUserName = assigner.getOrElse("Unknown user"),
content = s"""${oldAssigned.getOrElse("Not assigned")}:${assigned.getOrElse("Not assigned")}""", content = assigneeUserName,
registeredDate = currentDate, registeredDate = currentDate,
updatedDate = currentDate updatedDate = currentDate
) )
} }
for (issue <- getIssue(owner, repository, issueId.toString); repo <- getRepository(owner, repository)) { for (issue <- getIssue(owner, repository, issueId.toString); repo <- getRepository(owner, repository)) {
PluginRegistry().getIssueHooks.foreach(_.assigned(issue, repo, assigner, assigned, oldAssigned)) PluginRegistry().getIssueHooks.foreach(_.assigned(issue, repo, assigner, Some(assigneeUserName), None))
} }
Issues IssueAssignees insert IssueAssignee(owner, repository, issueId, assigneeUserName)
.filter(_.byPrimaryKey(owner, repository, issueId)) }
.map(t => (t.assignedUserName ?, t.updatedDate))
.update(assignedUserName, currentDate) def deleteIssueAssignee(
owner: String,
repository: String,
issueId: Int,
assigneeUserName: String,
insertComment: Boolean = false
)(
implicit context: Context,
s: Session
): Int = {
val assigner = context.loginAccount.map(_.userName)
if (insertComment) {
IssueComments insert IssueComment(
userName = owner,
repositoryName = repository,
issueId = issueId,
action = "delete_assignee",
commentedUserName = assigner.getOrElse("Unknown user"),
content = assigneeUserName,
registeredDate = currentDate,
updatedDate = currentDate
)
}
// TODO Notify plugins of unassignment as doing in registerIssueAssignee()?
IssueAssignees filter (_.byPrimaryKey(owner, repository, issueId, assigneeUserName)) delete
}
def deleteAllIssueAssignees(owner: String, repository: String, issueId: Int, insertComment: Boolean = false)(
implicit context: Context,
s: Session
): Int = {
val assigner = context.loginAccount.map(_.userName)
if (insertComment) {
IssueComments insert IssueComment(
userName = owner,
repositoryName = repository,
issueId = issueId,
action = "delete_assign",
commentedUserName = assigner.getOrElse("Unknown user"),
content = "All assignees",
registeredDate = currentDate,
updatedDate = currentDate
)
}
// TODO Notify plugins of unassignment as doing in registerIssueAssignee()?
IssueAssignees filter (_.byIssue(owner, repository, issueId)) delete
} }
def updateMilestoneId( def updateMilestoneId(
@@ -635,8 +742,10 @@ trait IssuesService {
.update(priorityId, currentDate) .update(priorityId, currentDate)
} }
def updateComment(issueId: Int, commentId: Int, content: String)(implicit s: Session): Int = { def updateComment(owner: String, repository: String, issueId: Int, commentId: Int, content: String)(
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate) implicit s: Session
): Int = {
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(_.updatedDate).update(currentDate)
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.updatedDate)).update(content, currentDate) IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.updatedDate)).update(content, currentDate)
} }
@@ -644,13 +753,13 @@ trait IssuesService {
implicit context: Context, implicit context: Context,
s: Session s: Session
): Int = { ): Int = {
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate) Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(_.updatedDate).update(currentDate)
IssueComments.filter(_.byPrimaryKey(commentId)).firstOption match { IssueComments.filter(_.byPrimaryKey(commentId)).first match {
case Some(c) if c.action == "reopen_comment" => case c if c.action == "reopen_comment" =>
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.action)).update("Reopen", "reopen") IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.action)).update("Reopen", "reopen")
case Some(c) if c.action == "close_comment" => case c if c.action == "close_comment" =>
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.action)).update("Close", "close") IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.action)).update("Close", "close")
case Some(_) => case _ =>
IssueComments.filter(_.byPrimaryKey(commentId)).delete IssueComments.filter(_.byPrimaryKey(commentId)).delete
IssueComments insert IssueComment( IssueComments insert IssueComment(
userName = owner, userName = owner,
@@ -981,7 +1090,8 @@ object IssuesService {
milestone: Option[String], milestone: Option[String],
priority: Option[String], priority: Option[String],
commentCount: Int, commentCount: Int,
commitId: Option[String] commitId: Option[String],
assignees: Seq[String]
) )
} }

View File

@@ -472,6 +472,40 @@ trait PullRequestService {
} }
} }
def getSingleDiff(
userName: String,
repositoryName: String,
commitId: String,
path: String
): Option[DiffInfo] = {
Using.resource(
Git.open(getRepositoryDir(userName, repositoryName))
) { git =>
val newId = git.getRepository.resolve(commitId)
JGitUtil.getDiff(git, None, newId.getName, path)
}
}
def getSingleDiff(
userName: String,
repositoryName: String,
branch: String,
requestUserName: String,
requestRepositoryName: String,
requestCommitId: String,
path: String
): Option[DiffInfo] = {
Using.resources(
Git.open(getRepositoryDir(userName, repositoryName)),
Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
) { (oldGit, newGit) =>
val oldId = oldGit.getRepository.resolve(branch)
val newId = newGit.getRepository.resolve(requestCommitId)
JGitUtil.getDiff(newGit, Some(oldId.getName), newId.getName, path)
}
}
def getRequestCompareInfo( def getRequestCompareInfo(
userName: String, userName: String,
repositoryName: String, repositoryName: String,

View File

@@ -36,10 +36,10 @@ trait RepositoryCommitFileService {
settings: SystemSettings settings: SystemSettings
)( )(
f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => Unit f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => Unit
)(implicit s: Session, c: JsonFormat.Context): ObjectId = { )(implicit s: Session, c: JsonFormat.Context): Either[String, ObjectId] = {
_createFiles(repository, branch, message, loginAccount, loginAccount.fullName, loginAccount.mailAddress, settings)( _createFiles(repository, branch, message, loginAccount, loginAccount.fullName, loginAccount.mailAddress, settings)(
f f
)._1 ).map(_._1)
} }
/** /**
@@ -58,7 +58,7 @@ trait RepositoryCommitFileService {
commit: String, commit: String,
loginAccount: Account, loginAccount: Account,
settings: SystemSettings settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): (ObjectId, Option[ObjectId]) = { )(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, Option[ObjectId])] = {
commitFile( commitFile(
repository, repository,
branch, branch,
@@ -92,7 +92,7 @@ trait RepositoryCommitFileService {
committerName: String, committerName: String,
committerMailAddress: String, committerMailAddress: String,
settings: SystemSettings settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): (ObjectId, Option[ObjectId]) = { )(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, Option[ObjectId])] = {
val newPath = newFileName.map { newFileName => val newPath = newFileName.map { newFileName =>
if (path.length == 0) newFileName else s"${path}/${newFileName}" if (path.length == 0) newFileName else s"${path}/${newFileName}"
@@ -141,7 +141,7 @@ trait RepositoryCommitFileService {
settings: SystemSettings settings: SystemSettings
)( )(
f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => R f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => R
)(implicit s: Session, c: JsonFormat.Context): (ObjectId, R) = { )(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, R)] = {
LockUtil.lock(s"${repository.owner}/${repository.name}") { LockUtil.lock(s"${repository.owner}/${repository.name}") {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
@@ -177,11 +177,11 @@ trait RepositoryCommitFileService {
error match { error match {
case Some(error) => case Some(error) =>
// commit is rejected // commit is rejected
// TODO Notify commit failure to edited user
val refUpdate = git.getRepository.updateRef(headName) val refUpdate = git.getRepository.updateRef(headName)
refUpdate.setNewObjectId(headTip) refUpdate.setNewObjectId(headTip)
refUpdate.setForceUpdate(true) refUpdate.setForceUpdate(true)
refUpdate.update() refUpdate.update()
Left(error)
case None => case None =>
// update refs // update refs
@@ -242,8 +242,8 @@ trait RepositoryCommitFileService {
) )
} }
} }
Right((commitId, result))
} }
(commitId, result)
} }
} }
} }

View File

@@ -342,10 +342,7 @@ trait RepositoryService {
repository.originUserName.getOrElse(repository.userName), repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName) repository.originRepositoryName.getOrElse(repository.repositoryName)
), ),
getOpenMilestones( getOpenMilestones(repository.userName, repository.repositoryName),
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
),
getRepositoryManagers(repository.userName, repository.repositoryName) getRepositoryManagers(repository.userName, repository.repositoryName)
) )
} }

View File

@@ -18,17 +18,18 @@ trait SystemSettingsService {
val props = new java.util.Properties() val props = new java.util.Properties()
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", ""))) settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
settings.information.foreach(x => props.setProperty(Information, x)) settings.information.foreach(x => props.setProperty(Information, x))
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString) props.setProperty(AllowAccountRegistration, settings.basicBehavior.allowAccountRegistration.toString)
props.setProperty(AllowAnonymousAccess, settings.allowAnonymousAccess.toString) props.setProperty(AllowResetPassword, settings.basicBehavior.allowResetPassword.toString)
props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString) props.setProperty(AllowAnonymousAccess, settings.basicBehavior.allowAnonymousAccess.toString)
props.setProperty(RepositoryOperationCreate, settings.repositoryOperation.create.toString) props.setProperty(IsCreateRepoOptionPublic, settings.basicBehavior.isCreateRepoOptionPublic.toString)
props.setProperty(RepositoryOperationDelete, settings.repositoryOperation.delete.toString) props.setProperty(RepositoryOperationCreate, settings.basicBehavior.repositoryOperation.create.toString)
props.setProperty(RepositoryOperationRename, settings.repositoryOperation.rename.toString) props.setProperty(RepositoryOperationDelete, settings.basicBehavior.repositoryOperation.delete.toString)
props.setProperty(RepositoryOperationTransfer, settings.repositoryOperation.transfer.toString) props.setProperty(RepositoryOperationRename, settings.basicBehavior.repositoryOperation.rename.toString)
props.setProperty(RepositoryOperationFork, settings.repositoryOperation.fork.toString) props.setProperty(RepositoryOperationTransfer, settings.basicBehavior.repositoryOperation.transfer.toString)
props.setProperty(Gravatar, settings.gravatar.toString) props.setProperty(RepositoryOperationFork, settings.basicBehavior.repositoryOperation.fork.toString)
props.setProperty(Notification, settings.notification.toString) props.setProperty(Gravatar, settings.basicBehavior.gravatar.toString)
props.setProperty(LimitVisibleRepositories, settings.limitVisibleRepositories.toString) props.setProperty(Notification, settings.basicBehavior.notification.toString)
props.setProperty(LimitVisibleRepositories, settings.basicBehavior.limitVisibleRepositories.toString)
props.setProperty(SshEnabled, settings.ssh.enabled.toString) props.setProperty(SshEnabled, settings.ssh.enabled.toString)
settings.ssh.bindAddress.foreach { bindAddress => settings.ssh.bindAddress.foreach { bindAddress =>
props.setProperty(SshBindAddressHost, bindAddress.host.trim()) props.setProperty(SshBindAddressHost, bindAddress.host.trim())
@@ -109,19 +110,22 @@ trait SystemSettingsService {
SystemSettings( SystemSettings(
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")), getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
getOptionValue(props, Information, None), getOptionValue(props, Information, None),
getValue(props, AllowAccountRegistration, false), BasicBehavior(
getValue(props, AllowAnonymousAccess, true), getValue(props, AllowAccountRegistration, false),
getValue(props, IsCreateRepoOptionPublic, true), getValue(props, AllowResetPassword, false),
RepositoryOperation( getValue(props, AllowAnonymousAccess, true),
create = getValue(props, RepositoryOperationCreate, true), getValue(props, IsCreateRepoOptionPublic, true),
delete = getValue(props, RepositoryOperationDelete, true), RepositoryOperation(
rename = getValue(props, RepositoryOperationRename, true), create = getValue(props, RepositoryOperationCreate, true),
transfer = getValue(props, RepositoryOperationTransfer, true), delete = getValue(props, RepositoryOperationDelete, true),
fork = getValue(props, RepositoryOperationFork, true) rename = getValue(props, RepositoryOperationRename, true),
transfer = getValue(props, RepositoryOperationTransfer, true),
fork = getValue(props, RepositoryOperationFork, true)
),
getValue(props, Gravatar, false),
getValue(props, Notification, false),
getValue(props, LimitVisibleRepositories, false)
), ),
getValue(props, Gravatar, false),
getValue(props, Notification, false),
getValue(props, LimitVisibleRepositories, false),
Ssh( Ssh(
enabled = getValue(props, SshEnabled, false), enabled = getValue(props, SshEnabled, false),
bindAddress = { bindAddress = {
@@ -214,13 +218,7 @@ object SystemSettingsService {
case class SystemSettings( case class SystemSettings(
baseUrl: Option[String], baseUrl: Option[String],
information: Option[String], information: Option[String],
allowAccountRegistration: Boolean, basicBehavior: BasicBehavior,
allowAnonymousAccess: Boolean,
isCreateRepoOptionPublic: Boolean,
repositoryOperation: RepositoryOperation,
gravatar: Boolean,
notification: Boolean,
limitVisibleRepositories: Boolean,
ssh: Ssh, ssh: Ssh,
useSMTP: Boolean, useSMTP: Boolean,
smtp: Option[Smtp], smtp: Option[Smtp],
@@ -264,6 +262,17 @@ object SystemSettingsService {
ssh.getUrl(owner: String, name: String) ssh.getUrl(owner: String, name: String)
} }
case class BasicBehavior(
allowAccountRegistration: Boolean,
allowResetPassword: Boolean,
allowAnonymousAccess: Boolean,
isCreateRepoOptionPublic: Boolean,
repositoryOperation: RepositoryOperation,
gravatar: Boolean,
notification: Boolean,
limitVisibleRepositories: Boolean,
)
case class RepositoryOperation( case class RepositoryOperation(
create: Boolean, create: Boolean,
delete: Boolean, delete: Boolean,
@@ -383,6 +392,7 @@ object SystemSettingsService {
private val BaseURL = "base_url" private val BaseURL = "base_url"
private val Information = "information" private val Information = "information"
private val AllowAccountRegistration = "allow_account_registration" private val AllowAccountRegistration = "allow_account_registration"
private val AllowResetPassword = "allow_reset_password"
private val AllowAnonymousAccess = "allow_anonymous_access" private val AllowAnonymousAccess = "allow_anonymous_access"
private val IsCreateRepoOptionPublic = "is_create_repository_option_public" private val IsCreateRepoOptionPublic = "is_create_repository_option_public"
private val RepositoryOperationCreate = "repository_operation_create" private val RepositoryOperationCreate = "repository_operation_create"

View File

@@ -379,8 +379,12 @@ trait WebHookPullRequestService extends WebHookService {
settings: SystemSettings settings: SystemSettings
)(implicit s: Session, context: JsonFormat.Context): Unit = { )(implicit s: Session, context: JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, WebHook.Issues, settings) { callWebHookOf(repository.owner, repository.name, WebHook.Issues, settings) {
val assigneeUsers = getIssueAssignees(repository.owner, repository.name, issue.issueId)
val users = val users =
getAccountsByUserNames(Set(repository.owner, issue.openedUserName) ++ issue.assignedUserName, Set(sender)) getAccountsByUserNames(
Set(repository.owner, issue.openedUserName) ++ assigneeUsers.map(_.assigneeUserName),
Set(sender)
)
for { for {
repoOwner <- users.get(repository.owner) repoOwner <- users.get(repository.owner)
issueUser <- users.get(issue.openedUserName) issueUser <- users.get(issue.openedUserName)
@@ -393,7 +397,7 @@ trait WebHookPullRequestService extends WebHookService {
issue, issue,
RepositoryName(repository), RepositoryName(repository),
ApiUser(issueUser), ApiUser(issueUser),
issue.assignedUserName.flatMap(users.get(_)).map(ApiUser(_)), assigneeUsers.flatMap(x => users.get(x.assigneeUserName)).map(ApiUser(_)),
getIssueLabels(repository.owner, repository.name, issue.issueId) getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))), .map(ApiLabel(_, RepositoryName(repository))),
getApiMilestone(repository, issue.milestoneId getOrElse (0)) getApiMilestone(repository, issue.milestoneId getOrElse (0))
@@ -415,16 +419,15 @@ trait WebHookPullRequestService extends WebHookService {
callWebHookOf(repository.owner, repository.name, WebHook.PullRequest, settings) { callWebHookOf(repository.owner, repository.name, WebHook.PullRequest, settings) {
for { for {
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId) (issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
assignees = getIssueAssignees(repository.owner, repository.name, issueId)
users = getAccountsByUserNames( users = getAccountsByUserNames(
Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(repository.owner, pullRequest.requestUserName, issue.openedUserName) ++ assignees.map(_.assigneeUserName),
Set(sender) Set(sender)
) )
baseOwner <- users.get(repository.owner) baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName) headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName) issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName => assigneeUsers = assignees.flatMap(x => users.get(x.assigneeUserName))
getAccountByUserName(userName, false)
}
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName) headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
labels = getIssueLabels(repository.owner, repository.name, issue.issueId) labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))) .map(ApiLabel(_, RepositoryName(repository)))
@@ -433,7 +436,7 @@ trait WebHookPullRequestService extends WebHookService {
action = action, action = action,
issue = issue, issue = issue,
issueUser = issueUser, issueUser = issueUser,
assignee = assignee, assignees = assigneeUsers,
pullRequest = pullRequest, pullRequest = pullRequest,
headRepository = headRepo, headRepository = headRepo,
headOwner = headOwner, headOwner = headOwner,
@@ -481,8 +484,8 @@ trait WebHookPullRequestService extends WebHookService {
requestRepository.name, requestRepository.name,
requestBranch requestBranch
) )
assignee = issue.assignedUserName.flatMap { userName => assignees = getIssueAssignees(requestRepository.owner, requestRepository.name, issue.issueId).flatMap { x =>
getAccountByUserName(userName, false) getAccountByUserName(x.assigneeUserName, false)
} }
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName) baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName)
labels = getIssueLabels(pullRequest.userName, pullRequest.repositoryName, issue.issueId) labels = getIssueLabels(pullRequest.userName, pullRequest.repositoryName, issue.issueId)
@@ -492,7 +495,7 @@ trait WebHookPullRequestService extends WebHookService {
action = action, action = action,
issue = issue, issue = issue,
issueUser = issueUser, issueUser = issueUser,
assignee = assignee, assignees = assignees,
pullRequest = pullRequest, pullRequest = pullRequest,
headRepository = requestRepository, headRepository = requestRepository,
headOwner = headOwner, headOwner = headOwner,
@@ -522,15 +525,17 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
)(implicit s: Session, c: JsonFormat.Context): Unit = { )(implicit s: Session, c: JsonFormat.Context): Unit = {
import WebHookService._ import WebHookService._
callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment, settings) { callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment, settings) {
val assignees = getIssueAssignees(repository.owner, pullRequest.requestUserName, issue.issueId)
val users = val users =
getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender)) getAccountsByUserNames(
Set(repository.owner, pullRequest.requestUserName, issue.openedUserName) ++ assignees.map(_.assigneeUserName),
Set(sender)
)
for { for {
baseOwner <- users.get(repository.owner) baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName) headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName) issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName => assigneeUsers = assignees.flatMap(x => users.get(x.assigneeUserName))
getAccountByUserName(userName, false)
}
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName) headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
labels = getIssueLabels(pullRequest.userName, pullRequest.repositoryName, issue.issueId) labels = getIssueLabels(pullRequest.userName, pullRequest.repositoryName, issue.issueId)
.map(ApiLabel(_, RepositoryName(pullRequest.userName, pullRequest.repositoryName))) .map(ApiLabel(_, RepositoryName(pullRequest.userName, pullRequest.repositoryName)))
@@ -540,7 +545,7 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
comment = comment, comment = comment,
issue = issue, issue = issue,
issueUser = issueUser, issueUser = issueUser,
assignee = assignee, assignees = assigneeUsers,
pullRequest = pullRequest, pullRequest = pullRequest,
headRepository = headRepo, headRepository = headRepo,
headOwner = headOwner, headOwner = headOwner,
@@ -569,14 +574,17 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
callWebHookOf(repository.owner, repository.name, WebHook.IssueComment, settings) { callWebHookOf(repository.owner, repository.name, WebHook.IssueComment, settings) {
for { for {
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString()) issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
assignees = getIssueAssignees(repository.owner, repository.name, issue.issueId)
users = getAccountsByUserNames( users = getAccountsByUserNames(
Set(issue.openedUserName, repository.owner, issueComment.commentedUserName) ++ issue.assignedUserName, Set(issue.openedUserName, repository.owner, issueComment.commentedUserName) ++ assignees.map(
_.assigneeUserName
),
Set(sender) Set(sender)
) )
issueUser <- users.get(issue.openedUserName) issueUser <- users.get(issue.openedUserName)
repoOwner <- users.get(repository.owner) repoOwner <- users.get(repository.owner)
commenter <- users.get(issueComment.commentedUserName) commenter <- users.get(issueComment.commentedUserName)
assignedUser = issue.assignedUserName.flatMap(users.get(_)) assigneeUsers = assignees.flatMap(x => users.get(x.assigneeUserName))
labels = getIssueLabels(repository.owner, repository.name, issue.issueId) labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
milestone = getApiMilestone(repository, issue.milestoneId getOrElse (0)) milestone = getApiMilestone(repository, issue.milestoneId getOrElse (0))
} yield { } yield {
@@ -587,7 +595,7 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
commentUser = commenter, commentUser = commenter,
repository = repository, repository = repository,
repositoryUser = repoOwner, repositoryUser = repoOwner,
assignedUser = assignedUser, assignees = assigneeUsers,
sender = sender, sender = sender,
labels = labels, labels = labels,
milestone = milestone milestone = milestone
@@ -710,7 +718,7 @@ object WebHookService {
action: String, action: String,
issue: Issue, issue: Issue,
issueUser: Account, issueUser: Account,
assignee: Option[Account], assignees: List[Account],
pullRequest: PullRequest, pullRequest: PullRequest,
headRepository: RepositoryInfo, headRepository: RepositoryInfo,
headOwner: Account, headOwner: Account,
@@ -731,7 +739,7 @@ object WebHookService {
baseRepo = baseRepoPayload, baseRepo = baseRepoPayload,
user = ApiUser(issueUser), user = ApiUser(issueUser),
labels = labels, labels = labels,
assignee = assignee.map(ApiUser.apply), assignees = assignees.map(ApiUser.apply),
mergedComment = mergedComment mergedComment = mergedComment
) )
@@ -762,7 +770,7 @@ object WebHookService {
commentUser: Account, commentUser: Account,
repository: RepositoryInfo, repository: RepositoryInfo,
repositoryUser: Account, repositoryUser: Account,
assignedUser: Option[Account], assignees: List[Account],
sender: Account, sender: Account,
labels: List[Label], labels: List[Label],
milestone: Option[ApiMilestone] milestone: Option[ApiMilestone]
@@ -774,7 +782,7 @@ object WebHookService {
issue, issue,
RepositoryName(repository), RepositoryName(repository),
ApiUser(issueUser), ApiUser(issueUser),
assignedUser.map(ApiUser(_)), assignees.map(ApiUser(_)),
labels.map(ApiLabel(_, RepositoryName(repository))), labels.map(ApiLabel(_, RepositoryName(repository))),
milestone milestone
), ),
@@ -799,7 +807,7 @@ object WebHookService {
comment: CommitComment, comment: CommitComment,
issue: Issue, issue: Issue,
issueUser: Account, issueUser: Account,
assignee: Option[Account], assignees: List[Account],
pullRequest: PullRequest, pullRequest: PullRequest,
headRepository: RepositoryInfo, headRepository: RepositoryInfo,
headOwner: Account, headOwner: Account,
@@ -828,7 +836,7 @@ object WebHookService {
baseRepo = baseRepoPayload, baseRepo = baseRepoPayload,
user = ApiUser(issueUser), user = ApiUser(issueUser),
labels = labels, labels = labels,
assignee = assignee.map(ApiUser.apply), assignees = assignees.map(ApiUser.apply),
mergedComment = mergedComment mergedComment = mergedComment
), ),
repository = baseRepoPayload, repository = baseRepoPayload,

View File

@@ -98,29 +98,30 @@ class GitAuthenticationFilter extends Filter with RepositoryService with Account
Database() withSession { implicit session => Database() withSession { implicit session =>
getRepository(repositoryOwner, repositoryName.replaceFirst("(\\.wiki)?\\.git$", "")) match { getRepository(repositoryOwner, repositoryName.replaceFirst("(\\.wiki)?\\.git$", "")) match {
case Some(repository) => { case Some(repository) => {
val execute = if (!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess) { val execute =
// Authentication is not required if (!isUpdating && !repository.repository.isPrivate && settings.basicBehavior.allowAnonymousAccess) {
true // Authentication is not required
} else { true
// Authentication is required } else {
val passed = for { // Authentication is required
authorizationHeader <- Option(request.getHeader("Authorization")) val passed = for {
account <- authenticateByHeader(authorizationHeader, settings) authorizationHeader <- Option(request.getHeader("Authorization"))
} yield account <- authenticateByHeader(authorizationHeader, settings)
if (isUpdating) { } yield
if (hasDeveloperRole(repository.owner, repository.name, Some(account))) { if (isUpdating) {
request.setAttribute(Keys.Request.UserName, account.userName) if (hasDeveloperRole(repository.owner, repository.name, Some(account))) {
request.setAttribute(Keys.Request.RepositoryLockKey, s"${repository.owner}/${repository.name}") request.setAttribute(Keys.Request.UserName, account.userName)
true request.setAttribute(Keys.Request.RepositoryLockKey, s"${repository.owner}/${repository.name}")
} else false true
} else if (repository.repository.isPrivate) { } else false
if (hasGuestRole(repository.owner, repository.name, Some(account))) { } else if (repository.repository.isPrivate) {
request.setAttribute(Keys.Request.UserName, account.userName) if (hasGuestRole(repository.owner, repository.name, Some(account))) {
true request.setAttribute(Keys.Request.UserName, account.userName)
} else false true
} else true } else false
passed.getOrElse(false) } else true
} passed.getOrElse(false)
}
if (execute) { () => if (execute) { () =>
chain.doFilter(request, response) chain.doFilter(request, response)

View File

@@ -52,15 +52,11 @@ import org.json4s.jackson.Serialization._
*/ */
class GitRepositoryServlet extends GitServlet with SystemSettingsService { class GitRepositoryServlet extends GitServlet with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet])
private implicit val jsonFormats: Formats = gitbucket.core.api.JsonFormat.jsonFormats private implicit val jsonFormats: Formats = gitbucket.core.api.JsonFormat.jsonFormats
override def init(config: ServletConfig): Unit = { override def init(config: ServletConfig): Unit = {
setReceivePackFactory(new GitBucketReceivePackFactory()) setReceivePackFactory(new GitBucketReceivePackFactory())
val root: File = new File(Directory.RepositoryHome)
setRepositoryResolver(new GitBucketRepositoryResolver) setRepositoryResolver(new GitBucketRepositoryResolver)
super.init(config) super.init(config)
} }
@@ -420,7 +416,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
pusherAccount, pusherAccount,
repositoryInfo, repositoryInfo,
ownerAccount, ownerAccount,
ref = branchName, ref = command.getRefName,
refType = refType refType = refType
) )
} }

View File

@@ -38,8 +38,18 @@ object DatabaseConfig {
private lazy val dbUrl = getValue("db.url", config.getString) //config.getString("db.url") private lazy val dbUrl = getValue("db.url", config.getString) //config.getString("db.url")
def url(directory: Option[String]): String = def url(directory: Option[String]): String = {
dbUrl.replace("${DatabaseHome}", directory.getOrElse(DatabaseHome)) val sb = new StringBuilder()
sb.append(dbUrl)
if (dbUrl.startsWith("jdbc:mysql:") && dbUrl.indexOf("permitMysqlScheme") == -1) {
if (dbUrl.indexOf("?") == -1) {
sb.append("?permitMysqlScheme")
} else {
sb.append("&permitMysqlScheme")
}
}
sb.toString().replace("${DatabaseHome}", directory.getOrElse(DatabaseHome))
}
lazy val url: String = url(None) lazy val url: String = url(None)
lazy val user: String = getValue("db.user", config.getString) lazy val user: String = getValue("db.user", config.getString)

View File

@@ -37,7 +37,7 @@ object JGitUtil {
private val logger = LoggerFactory.getLogger(JGitUtil.getClass) private val logger = LoggerFactory.getLogger(JGitUtil.getClass)
implicit val objectDatabaseReleasable: Releasable[ObjectDatabase] = private implicit val objectDatabaseReleasable: Releasable[ObjectDatabase] =
_.close() _.close()
/** /**
@@ -690,7 +690,7 @@ object JGitUtil {
val toCommit = revWalk.parseCommit(git.getRepository.resolve(to)) val toCommit = revWalk.parseCommit(git.getRepository.resolve(to))
(from match { (from match {
case None => { case None =>
toCommit.getParentCount match { toCommit.getParentCount match {
case 0 => case 0 =>
df.scan( df.scan(
@@ -700,11 +700,9 @@ object JGitUtil {
.asScala .asScala
case _ => df.scan(toCommit.getParent(0), toCommit.getTree).asScala case _ => df.scan(toCommit.getParent(0), toCommit.getTree).asScala
} }
} case Some(from) =>
case Some(from) => {
val fromCommit = revWalk.parseCommit(git.getRepository.resolve(from)) val fromCommit = revWalk.parseCommit(git.getRepository.resolve(from))
df.scan(fromCommit.getTree, toCommit.getTree).asScala df.scan(fromCommit.getTree, toCommit.getTree).asScala
}
}).toSeq }).toSeq
} }
} }
@@ -719,6 +717,29 @@ object JGitUtil {
} }
} }
def getDiff(git: Git, from: Option[String], to: String, path: String): Option[DiffInfo] = {
getDiffEntries(git, from, to).find(_.getNewPath == path).map { diff =>
val oldIsImage = FileUtil.isImage(diff.getOldPath)
val newIsImage = FileUtil.isImage(diff.getNewPath)
val includeContent = oldIsImage || newIsImage
DiffInfo(
changeType = diff.getChangeType,
oldPath = diff.getOldPath,
newPath = diff.getNewPath,
oldContent = if (includeContent) None else getTextContent(git, diff.getOldId.toObjectId),
newContent = if (includeContent) None else getTextContent(git, diff.getNewId.toObjectId),
oldIsImage = oldIsImage,
newIsImage = newIsImage,
oldObjectId = Option(diff.getOldId).map(_.name),
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = false,
patch = None
)
}
}
def getDiffs( def getDiffs(
git: Git, git: Git,
from: Option[String], from: Option[String],
@@ -728,7 +749,7 @@ object JGitUtil {
): List[DiffInfo] = { ): List[DiffInfo] = {
val diffs = getDiffEntries(git, from, to) val diffs = getDiffEntries(git, from, to)
diffs.map { diff => diffs.map { diff =>
if (diffs.size > 100) { if (diffs.size > 100) { // Don't show diff if there are more than 100 files
DiffInfo( DiffInfo(
changeType = diff.getChangeType, changeType = diff.getChangeType,
oldPath = diff.getOldPath, oldPath = diff.getOldPath,
@@ -747,49 +768,35 @@ object JGitUtil {
} else { } else {
val oldIsImage = FileUtil.isImage(diff.getOldPath) val oldIsImage = FileUtil.isImage(diff.getOldPath)
val newIsImage = FileUtil.isImage(diff.getNewPath) val newIsImage = FileUtil.isImage(diff.getNewPath)
if (!fetchContent || oldIsImage || newIsImage) { val patch = if (oldIsImage || newIsImage) None else Some(makePatchFromDiffEntry(git, diff)) // TODO use DiffFormatter
DiffInfo( val tooLarge = patch.exists(_.count(_ == '\n') > 1000) // Don't show diff if the file has more than 1000 lines diff
changeType = diff.getChangeType, val includeContent = tooLarge || !fetchContent || oldIsImage || newIsImage
oldPath = diff.getOldPath, DiffInfo(
newPath = diff.getNewPath, changeType = diff.getChangeType,
oldContent = None, oldPath = diff.getOldPath,
newContent = None, newPath = diff.getNewPath,
oldIsImage = oldIsImage, oldContent = if (includeContent) None else getTextContent(git, diff.getOldId.toObjectId),
newIsImage = newIsImage, newContent = if (includeContent) None else getTextContent(git, diff.getNewId.toObjectId),
oldObjectId = Option(diff.getOldId).map(_.name), oldIsImage = oldIsImage,
newObjectId = Option(diff.getNewId).map(_.name), newIsImage = newIsImage,
oldMode = diff.getOldMode.toString, oldObjectId = Option(diff.getOldId).map(_.name),
newMode = diff.getNewMode.toString, newObjectId = Option(diff.getNewId).map(_.name),
tooLarge = false, oldMode = diff.getOldMode.toString,
patch = (if (makePatch) Some(makePatchFromDiffEntry(git, diff)) else None) // TODO use DiffFormatter newMode = diff.getNewMode.toString,
) tooLarge = tooLarge,
} else { patch = if (makePatch) patch else None
DiffInfo( )
changeType = diff.getChangeType,
oldPath = diff.getOldPath,
newPath = diff.getNewPath,
oldContent = JGitUtil
.getContentFromId(git, diff.getOldId.toObjectId, false)
.filter(FileUtil.isText)
.map(convertFromByteArray),
newContent = JGitUtil
.getContentFromId(git, diff.getNewId.toObjectId, false)
.filter(FileUtil.isText)
.map(convertFromByteArray),
oldIsImage = oldIsImage,
newIsImage = newIsImage,
oldObjectId = Option(diff.getOldId).map(_.name),
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = false,
patch = (if (makePatch) Some(makePatchFromDiffEntry(git, diff)) else None) // TODO use DiffFormatter
)
}
} }
}.toList }.toList
} }
private def getTextContent(git: Git, objectId: ObjectId): Option[String] = {
JGitUtil
.getContentFromId(git, objectId, false)
.filter(FileUtil.isText)
.map(convertFromByteArray)
}
private def makePatchFromDiffEntry(git: Git, diff: DiffEntry): String = { private def makePatchFromDiffEntry(git: Git, diff: DiffEntry): String = {
val out = new ByteArrayOutputStream() val out = new ByteArrayOutputStream()
Using.resource(new DiffFormatter(out)) { formatter => Using.resource(new DiffFormatter(out)) { formatter =>

View File

@@ -41,7 +41,7 @@ class Mailer(settings: SystemSettings) {
htmlMsg: Option[String] = None, htmlMsg: Option[String] = None,
loginAccount: Option[Account] = None loginAccount: Option[Account] = None
): Option[HtmlEmail] = { ): Option[HtmlEmail] = {
if (settings.notification) { if (settings.basicBehavior.notification) {
settings.smtp.map { smtp => settings.smtp.map { smtp =>
val email = new HtmlEmail val email = new HtmlEmail
email.setHostName(smtp.host) email.setHostName(smtp.host)

View File

@@ -74,6 +74,11 @@ object StringUtil {
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8") def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")
/**
* URL encode except '/'.
*/
def encodeRefName(value: String): String = urlEncode(value).replace("%2F", "/")
def splitWords(value: String): Array[String] = value.split("[ \\t ]+") def splitWords(value: String): Array[String] = value.split("[ \\t ]+")
def isInteger(value: String): Boolean = allCatch opt { value.toInt } map (_ => true) getOrElse (false) def isInteger(value: String): Boolean = allCatch opt { value.toInt } map (_ => true) getOrElse (false)
@@ -93,8 +98,8 @@ object StringUtil {
detector.handleData(content, 0, content.length) detector.handleData(content, 0, content.length)
detector.dataEnd() detector.dataEnd()
detector.getDetectedCharset match { detector.getDetectedCharset match {
case null => "UTF-8" case null | "US-ASCII" => "UTF-8"
case e => e case e => e
} }
} }

View File

@@ -18,7 +18,7 @@ trait AvatarImageProvider { self: RequestCache =>
val src = if (mailAddress.isEmpty) { val src = if (mailAddress.isEmpty) {
// by user name // by user name
getAccountByUserNameFromCache(userName).map { account => getAccountByUserNameFromCache(userName).map { account =>
if (account.image.isEmpty && context.settings.gravatar) { if (account.image.isEmpty && context.settings.basicBehavior.gravatar) {
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
} else { } else {
s"""${context.path}/${account.userName}/_avatar?${helpers.hashDate(account.updatedDate)}""" s"""${context.path}/${account.userName}/_avatar?${helpers.hashDate(account.updatedDate)}"""
@@ -29,13 +29,13 @@ trait AvatarImageProvider { self: RequestCache =>
} else { } else {
// by mail address // by mail address
getAccountByMailAddressFromCache(mailAddress).map { account => getAccountByMailAddressFromCache(mailAddress).map { account =>
if (account.image.isEmpty && context.settings.gravatar) { if (account.image.isEmpty && context.settings.basicBehavior.gravatar) {
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
} else { } else {
s"""${context.path}/${account.userName}/_avatar?${helpers.hashDate(account.updatedDate)}""" s"""${context.path}/${account.userName}/_avatar?${helpers.hashDate(account.updatedDate)}"""
} }
} getOrElse { } getOrElse {
if (context.settings.gravatar) { if (context.settings.basicBehavior.gravatar) {
s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
} else { } else {
s"""${context.path}/_unknown/_avatar""" s"""${context.path}/_unknown/_avatar"""

View File

@@ -276,7 +276,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
/** /**
* URL encode except '/'. * URL encode except '/'.
*/ */
def encodeRefName(value: String): String = StringUtil.urlEncode(value).replace("%2F", "/") def encodeRefName(value: String): String = StringUtil.encodeRefName(value)
def urlEncode(value: String): String = StringUtil.urlEncode(value) def urlEncode(value: String): String = StringUtil.urlEncode(value)

View File

@@ -0,0 +1,16 @@
@()(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Reset your password"){
<div class="content-wrapper main-center">
<div class="content body">
<h2>Reset your password</h2>
<form action="@context.path/reset" method="POST" validate="true" autocomplete="off">
<fieldset class="form-group">
Enter your email address to reset your password.
<input type="text" name="mailAddress" id="mailAddress" class="form-control" value="" value="" style="max-width: 400px;"/>
<span id="error-mailAddress" class="error"></span>
</fieldset>
<input type="submit" class="btn btn-success" value="Submit"/>
</form>
</div>
</div>
}

View File

@@ -0,0 +1,11 @@
@()(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Reset your password"){
<div class="content-wrapper main-center">
<div class="content body">
<h2>Reset your password</h2>
<p>
Password has been updated. <a href="@context.path/signin">Sign-in</a> with new password.
</p>
</div>
</div>
}

View File

@@ -0,0 +1,17 @@
@(token: String)(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Reset your password"){
<div class="content-wrapper main-center">
<div class="content body">
<h2>Reset your password</h2>
<form action="@context.path/reset/form" method="POST" validate="true" autocomplete="off">
<fieldset class="form-group">
Enter new password:
<input type="password" name="password" id="password" class="form-control" value="" style="max-width: 400px;"/>
<span id="error-password" class="error"></span>
</fieldset>
<input type="submit" class="btn btn-success" value="Submit"/>
<input type="hidden" name="token" id="token" value="@token"/>
</form>
</div>
</div>
}

View File

@@ -0,0 +1,11 @@
@()(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Reset your password"){
<div class="content-wrapper main-center">
<div class="content body">
<h2>Reset your password</h2>
<p>
Send an email to you. Check your mailbox.
</p>
</div>
</div>
}

View File

@@ -6,7 +6,7 @@
<label class="strong">Services</label> <label class="strong">Services</label>
<fieldset> <fieldset>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" name="gravatar"@if(context.settings.gravatar){ checked}/> <input type="checkbox" name="basicBehavior.gravatar"@if(context.settings.basicBehavior.gravatar){ checked}/>
Use Gravatar for profile images Use Gravatar for profile images
</label> </label>
</fieldset> </fieldset>
@@ -132,7 +132,7 @@
<label class="strong">Notifications</label> <label class="strong">Notifications</label>
<fieldset> <fieldset>
<label class="checkbox" for="notification"> <label class="checkbox" for="notification">
<input type="checkbox" id="notification" name="notification"@if(context.settings.notification){ checked}/> <input type="checkbox" id="notification" name="basicBehavior.notification"@if(context.settings.basicBehavior.notification){ checked}/>
Send notifications Send notifications
</label> </label>
</fieldset> </fieldset>

View File

@@ -108,15 +108,30 @@
<label class="strong">Account registration</label> <label class="strong">Account registration</label>
<fieldset> <fieldset>
<label class="radio"> <label class="radio">
<input type="radio" name="allowAccountRegistration" value="true"@if(context.settings.allowAccountRegistration){ checked}> <input type="radio" name="basicBehavior.allowAccountRegistration" value="true"@if(context.settings.basicBehavior.allowAccountRegistration){ checked}>
<span class="strong">Allow</span> <span class="normal">- Users can create accounts by themselves.</span> <span class="strong">Allow</span> <span class="normal">- Users can create accounts by themselves.</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" name="allowAccountRegistration" value="false"@if(!context.settings.allowAccountRegistration){ checked}> <input type="radio" name="basicBehavior.allowAccountRegistration" value="false"@if(!context.settings.basicBehavior.allowAccountRegistration){ checked}>
<span class="strong">Deny</span> <span class="normal">- Only administrators can create accounts.</span> <span class="strong">Deny</span> <span class="normal">- Only administrators can create accounts.</span>
</label> </label>
</fieldset> </fieldset>
<!--====================================================================--> <!--====================================================================-->
<!-- Reset password -->
<!--====================================================================-->
<hr>
<label class="strong">Reset password</label>
<fieldset>
<label class="radio">
<input type="radio" name="basicBehavior.allowResetPassword" value="true"@if(context.settings.basicBehavior.allowResetPassword){ checked}>
<span class="strong">Allow</span> <span class="normal">- Allow users to reset password. (SMTP setting is required)</span>
</label>
<label class="radio">
<input type="radio" name="basicBehavior.allowResetPassword" value="false"@if(!context.settings.basicBehavior.allowResetPassword){ checked}>
<span class="strong">Deny</span> <span class="normal">- Doesn't allow users to reset password.</span>
</label>
</fieldset>
<!--====================================================================-->
<!-- Repository operations --> <!-- Repository operations -->
<!--====================================================================--> <!--====================================================================-->
<hr> <hr>
@@ -126,11 +141,11 @@
<label class="control-label col-md-2">Create</label> <label class="control-label col-md-2">Create</label>
<div class="col-md-10"> <div class="col-md-10">
<label class="radio"> <label class="radio">
<input type="radio" name="repositoryOperation.create" value="true"@if(context.settings.repositoryOperation.create){ checked}> <input type="radio" name="basicBehavior.repositoryOperation.create" value="true"@if(context.settings.basicBehavior.repositoryOperation.create){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can create repository.</span> <span class="strong">All users</span> <span class="normal">- All users can create repository.</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" name="repositoryOperation.create" value="false"@if(!context.settings.repositoryOperation.create){ checked}> <input type="radio" name="basicBehavior.repositoryOperation.create" value="false"@if(!context.settings.basicBehavior.repositoryOperation.create){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can create repository.</span> <span class="strong">Admin only</span> <span class="normal">- Only administrators can create repository.</span>
</label> </label>
</div> </div>
@@ -139,11 +154,11 @@
<label class="control-label col-md-2">Delete</label> <label class="control-label col-md-2">Delete</label>
<div class="col-md-10"> <div class="col-md-10">
<label class="radio"> <label class="radio">
<input type="radio" name="repositoryOperation.delete" value="true"@if(context.settings.repositoryOperation.delete){ checked}> <input type="radio" name="basicBehavior.repositoryOperation.delete" value="true"@if(context.settings.basicBehavior.repositoryOperation.delete){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can delete repository.</span> <span class="strong">All users</span> <span class="normal">- All users can delete repository.</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" name="repositoryOperation.delete" value="false"@if(!context.settings.repositoryOperation.delete){ checked}> <input type="radio" name="basicBehavior.repositoryOperation.delete" value="false"@if(!context.settings.basicBehavior.repositoryOperation.delete){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can delete repository.</span> <span class="strong">Admin only</span> <span class="normal">- Only administrators can delete repository.</span>
</label> </label>
</div> </div>
@@ -152,11 +167,11 @@
<label class="control-label col-md-2">Rename</label> <label class="control-label col-md-2">Rename</label>
<div class="col-md-10"> <div class="col-md-10">
<label class="radio"> <label class="radio">
<input type="radio" name="repositoryOperation.rename" value="true"@if(context.settings.repositoryOperation.rename){ checked}> <input type="radio" name="basicBehavior.repositoryOperation.rename" value="true"@if(context.settings.basicBehavior.repositoryOperation.rename){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can rename repository.</span> <span class="strong">All users</span> <span class="normal">- All users can rename repository.</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" name="repositoryOperation.rename" value="false"@if(!context.settings.repositoryOperation.rename){ checked}> <input type="radio" name="basicBehavior.repositoryOperation.rename" value="false"@if(!context.settings.basicBehavior.repositoryOperation.rename){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can rename repository.</span> <span class="strong">Admin only</span> <span class="normal">- Only administrators can rename repository.</span>
</label> </label>
</div> </div>
@@ -165,11 +180,11 @@
<label class="control-label col-md-2">Transfer</label> <label class="control-label col-md-2">Transfer</label>
<div class="col-md-10"> <div class="col-md-10">
<label class="radio"> <label class="radio">
<input type="radio" name="repositoryOperation.transfer" value="true"@if(context.settings.repositoryOperation.transfer){ checked}> <input type="radio" name="basicBehavior.repositoryOperation.transfer" value="true"@if(context.settings.basicBehavior.repositoryOperation.transfer){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can transfer repository.</span> <span class="strong">All users</span> <span class="normal">- All users can transfer repository.</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" name="repositoryOperation.transfer" value="false"@if(!context.settings.repositoryOperation.transfer){ checked}> <input type="radio" name="basicBehavior.repositoryOperation.transfer" value="false"@if(!context.settings.basicBehavior.repositoryOperation.transfer){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can transfer repository.</span> <span class="strong">Admin only</span> <span class="normal">- Only administrators can transfer repository.</span>
</label> </label>
</div> </div>
@@ -178,11 +193,11 @@
<label class="control-label col-md-2">Fork</label> <label class="control-label col-md-2">Fork</label>
<div class="col-md-10"> <div class="col-md-10">
<label class="radio"> <label class="radio">
<input type="radio" name="repositoryOperation.fork" value="true"@if(context.settings.repositoryOperation.fork){ checked}> <input type="radio" name="basicBehavior.repositoryOperation.fork" value="true"@if(context.settings.basicBehavior.repositoryOperation.fork){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can fork repository.</span> <span class="strong">All users</span> <span class="normal">- All users can fork repository.</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" name="repositoryOperation.fork" value="false"@if(!context.settings.repositoryOperation.fork){ checked}> <input type="radio" name="basicBehavior.repositoryOperation.fork" value="false"@if(!context.settings.basicBehavior.repositoryOperation.fork){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can fork repository.</span> <span class="strong">Admin only</span> <span class="normal">- Only administrators can fork repository.</span>
</label> </label>
</div> </div>
@@ -192,11 +207,11 @@
<label class="strong">Default visibility when creating a new repository</label> <label class="strong">Default visibility when creating a new repository</label>
<fieldset> <fieldset>
<label class="radio"> <label class="radio">
<input type="radio" name="isCreateRepoOptionPublic" value="true"@if(context.settings.isCreateRepoOptionPublic){ checked}> <input type="radio" name="basicBehavior.isCreateRepoOptionPublic" value="true"@if(context.settings.basicBehavior.isCreateRepoOptionPublic){ checked}>
<span class="strong">Public</span> <span class="normal">- All users and guests can read the repository.</span> <span class="strong">Public</span> <span class="normal">- All users and guests can read the repository.</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" name="isCreateRepoOptionPublic" value="false"@if(!context.settings.isCreateRepoOptionPublic){ checked}> <input type="radio" name="basicBehavior.isCreateRepoOptionPublic" value="false"@if(!context.settings.basicBehavior.isCreateRepoOptionPublic){ checked}>
<span class="strong">Private</span> <span class="normal">- Only collaborators can read the repository.</span> <span class="strong">Private</span> <span class="normal">- Only collaborators can read the repository.</span>
</label> </label>
</fieldset> </fieldset>
@@ -207,11 +222,11 @@
<label class="strong">Anonymous access</label> <label class="strong">Anonymous access</label>
<fieldset> <fieldset>
<label class="radio"> <label class="radio">
<input type="radio" name="allowAnonymousAccess" value="true"@if(context.settings.allowAnonymousAccess){ checked}> <input type="radio" name="basicBehavior.allowAnonymousAccess" value="true"@if(context.settings.basicBehavior.allowAnonymousAccess){ checked}>
<span class="strong">Allow</span> <span class="normal">- Anyone can view public repositories and user/group profiles.</span> <span class="strong">Allow</span> <span class="normal">- Anyone can view public repositories and user/group profiles.</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" name="allowAnonymousAccess" value="false"@if(!context.settings.allowAnonymousAccess){ checked}> <input type="radio" name="basicBehavior.allowAnonymousAccess" value="false"@if(!context.settings.basicBehavior.allowAnonymousAccess){ checked}>
<span class="strong">Deny</span> <span class="normal">- Users must authenticate before viewing any information.</span> <span class="strong">Deny</span> <span class="normal">- Users must authenticate before viewing any information.</span>
</label> </label>
</fieldset> </fieldset>
@@ -272,11 +287,11 @@
<label><span class="strong">Show repositories in sidebar</span></label> <label><span class="strong">Show repositories in sidebar</span></label>
<fieldset> <fieldset>
<label class="radio"> <label class="radio">
<input type="radio" name="limitVisibleRepositories" value="false"@if(!context.settings.limitVisibleRepositories){ checked}> <input type="radio" name="basicBehavior.limitVisibleRepositories" value="false"@if(!context.settings.basicBehavior.limitVisibleRepositories){ checked}>
<span class="strong">All</span> <span class="normal">- Show all visible repositories in sidebar.</span> <span class="strong">All</span> <span class="normal">- Show all visible repositories in sidebar.</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" name="limitVisibleRepositories" value="true"@if(context.settings.limitVisibleRepositories){ checked}> <input type="radio" name="basicBehavior.limitVisibleRepositories" value="true"@if(context.settings.basicBehavior.limitVisibleRepositories){ checked}>
<span class="strong">Limited</span> <span class="normal">- Show only owned repositories in sidebar.</span> <span class="strong">Limited</span> <span class="normal">- Show only owned repositories in sidebar.</span>
</label> </label>
</fieldset> </fieldset>

View File

@@ -17,7 +17,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId), status) => @issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId, assignedUserNames), status) =>
<tr> <tr>
<td style="padding-top: 12px; padding-bottom: 12px;"> <td style="padding-top: 12px; padding-bottom: 12px;">
<a href="@context.path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a>&nbsp;&#xFF65; <a href="@context.path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a>&nbsp;&#xFF65;
@@ -33,7 +33,7 @@
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span> <span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
} }
<span class="pull-right muted"> <span class="pull-right muted">
@issue.assignedUserName.map { userName => @assignedUserNames.map { userName =>
@helpers.avatar(userName, 20, tooltip = true) @helpers.avatar(userName, 20, tooltip = true)
} }
@if(commentCount > 0){ @if(commentCount > 0){

View File

@@ -100,38 +100,48 @@
</tr> </tr>
<tr class="diff-collapse-@i collapse in"> <tr class="diff-collapse-@i collapse in">
<td style="padding: 0;"> <td style="padding: 0;">
@if(diff.oldObjectId == diff.newObjectId){ @if(diff.tooLarge) {
@if(diff.oldPath != diff.newPath){ <div style="padding: 12px;" id="show-diff-@i">
<div class="diff-same">File renamed without changes</div> @if(oldCommitId.isEmpty && newCommitId.isDefined) {
} else { Too large (<a href="javascript:showDiff(@i, '', '@newCommitId', '@diff.newPath')">Show diff</a>)
<div class="diff-same">File mode changed</div> }
} @if(oldCommitId.isDefined && newCommitId.isDefined) {
Too large (<a href="javascript:showDiff(@i, '@oldCommitId', '@newCommitId', '@diff.newPath')">Show diff</a>)
}
</div>
<div id="diffText-@i" class="diffText"></div>
<input type="hidden" id="newText-@i" data-file-name="@diff.newPath" data-val="">
<input type="hidden" id="oldText-@i" data-file-name="@diff.oldPath" data-val="">
} else { } else {
@if(diff.newContent != None || diff.oldContent != None){ @if(diff.oldObjectId == diff.newObjectId) {
<div id="diffText-@i" class="diffText"></div> @if(diff.oldPath != diff.newPath) {
<input type="hidden" id="newText-@i" data-file-name="@diff.newPath" data-val="@diff.newContent"> <div class="diff-same">File renamed without changes</div>
<input type="hidden" id="oldText-@i" data-file-name="@diff.oldPath" data-val="@diff.oldContent">
} else {
@if(diff.newIsImage || diff.oldIsImage){
<div class="diff-image-render diff2up">
@if(oldCommitId.isDefined && diff.oldIsImage){
<div class="diff-image-frame diff-old"><img src="@helpers.url(repository)/raw/@oldCommitId.get/@diff.oldPath" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
} else {
@if(diff.changeType != ChangeType.ADD){
<div style="padding: 12px;">Not supported</div>
}
}
@if(newCommitId.isDefined && diff.newIsImage){
<div class="diff-image-frame diff-new"><img src="@helpers.url(repository)/raw/@newCommitId.get/@diff.newPath" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
} else {
@if(diff.changeType != ChangeType.DELETE){
<div style="padding: 12px;">Not supported</div>
}
}
</div>
} else { } else {
@if(diff.tooLarge){ <div class="diff-same">File mode changed</div>
<div style="padding: 12px;">Too large</div> }
} else {
@if(diff.newContent != None || diff.oldContent != None){
<div id="diffText-@i" class="diffText"></div>
<input type="hidden" id="newText-@i" data-file-name="@diff.newPath" data-val="@diff.newContent">
<input type="hidden" id="oldText-@i" data-file-name="@diff.oldPath" data-val="@diff.oldContent">
} else {
@if(diff.newIsImage || diff.oldIsImage){
<div class="diff-image-render diff2up">
@if(oldCommitId.isDefined && diff.oldIsImage){
<div class="diff-image-frame diff-old"><img src="@helpers.url(repository)/raw/@oldCommitId.get/@diff.oldPath" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
} else {
@if(diff.changeType != ChangeType.ADD){
<div style="padding: 12px;">Not supported</div>
}
}
@if(newCommitId.isDefined && diff.newIsImage){
<div class="diff-image-frame diff-new"><img src="@helpers.url(repository)/raw/@newCommitId.get/@diff.newPath" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
} else {
@if(diff.changeType != ChangeType.DELETE){
<div style="padding: 12px;">Not supported</div>
}
}
</div>
} else { } else {
<div style="padding: 12px;">Not supported</div> <div style="padding: 12px;">Not supported</div>
} }
@@ -200,53 +210,10 @@ $(function(){
renderOneDiff($(this).closest("table").find(".diffText"), window.viewType, $(this).closest("table").find(".file-hash")[0].id); renderOneDiff($(this).closest("table").find(".diffText"), window.viewType, $(this).closest("table").find(".file-hash")[0].id);
}); });
function getInlineContainer(where) {
if (window.viewType == 0) {
if (where === 'new') {
return $('<tr class="not-diff"><td colspan="2"></td><td colspan="2" class="comment-box-container"></td></tr>');
} else {
return $('<tr class="not-diff"><td colspan="2" class="comment-box-container"></td><td colspan="2"></td></tr>');
}
}
return $('<tr class="not-diff"><td colspan="3" class="comment-box-container"></td></tr>');
}
if (typeof $('#show-notes')[0] !== 'undefined' && !$('#show-notes')[0].checked) { if (typeof $('#show-notes')[0] !== 'undefined' && !$('#show-notes')[0].checked) {
$('#comment-list').children('.inline-comment').hide(); $('#comment-list').children('.inline-comment').hide();
} }
function showCommentForm(commitId, fileName, oldLineNumber, newLineNumber, $tr){
// assemble Ajax url
let url = '@helpers.url(repository)/commit/' + commitId + '/comment/_form?fileName=' + fileName@issueId.map { id => + '&issueId=@id' };
if (!isNaN(oldLineNumber) && oldLineNumber) {
url += ('&oldLineNumber=' + oldLineNumber)
}
if (!isNaN(newLineNumber) && newLineNumber) {
url += ('&newLineNumber=' + newLineNumber)
}
// send Ajax request
$.get(url, { dataType : 'html' }, function(responseContent) {
// create container
let tmp;
if (!isNaN(oldLineNumber) && oldLineNumber) {
if (!isNaN(newLineNumber) && newLineNumber) {
tmp = getInlineContainer();
} else {
tmp = getInlineContainer('old');
}
} else {
tmp = getInlineContainer('new');
}
// add comment textarea
tmp.addClass('inline-comment-form').children('.comment-box-container').html(responseContent);
$tr.nextAll(':not(.not-diff):first').before(tmp);
// hide reply comment field
$(tmp).closest('.not-diff').prev().find('.reply-comment').closest('.not-diff').hide();
// focus textarea
tmp.find('textarea').focus();
});
}
// Add comment button // Add comment button
$('.diff-outside').on('click','table.diff .add-comment',function() { $('.diff-outside').on('click','table.diff .add-comment',function() {
const $this = $(this); const $this = $(this);
@@ -302,11 +269,156 @@ $(function(){
getSelection().empty(); getSelection().empty();
updateHighlighting(); updateHighlighting();
}); });
});
function getInlineContainer(where) {
if (window.viewType == 0) {
if (where === 'new') {
return $('<tr class="not-diff"><td colspan="2"></td><td colspan="2" class="comment-box-container"></td></tr>');
} else {
return $('<tr class="not-diff"><td colspan="2" class="comment-box-container"></td><td colspan="2"></td></tr>');
}
}
return $('<tr class="not-diff"><td colspan="3" class="comment-box-container"></td></tr>');
}
function showCommentForm(commitId, fileName, oldLineNumber, newLineNumber, $tr){
// assemble Ajax url
let url = '@helpers.url(repository)/commit/' + commitId + '/comment/_form?fileName=' + fileName@issueId.map { id => + '&issueId=@id' };
if (!isNaN(oldLineNumber) && oldLineNumber) {
url += ('&oldLineNumber=' + oldLineNumber)
}
if (!isNaN(newLineNumber) && newLineNumber) {
url += ('&newLineNumber=' + newLineNumber)
}
// send Ajax request
$.get(url, { dataType : 'html' }, function(responseContent) {
// create container
let tmp;
if (!isNaN(oldLineNumber) && oldLineNumber) {
if (!isNaN(newLineNumber) && newLineNumber) {
tmp = getInlineContainer();
} else {
tmp = getInlineContainer('old');
}
} else {
tmp = getInlineContainer('new');
}
// add comment textarea
tmp.addClass('inline-comment-form').children('.comment-box-container').html(responseContent);
$tr.nextAll(':not(.not-diff):first').before(tmp);
// hide reply comment field
$(tmp).closest('.not-diff').prev().find('.reply-comment').closest('.not-diff').hide();
// focus textarea
tmp.find('textarea').focus();
});
}
function renderOneCommitCommentIntoDiff($v, diff){
//var filename = $v.attr('filename');
const oldline = $v.attr('oldline');
const newline = $v.attr('newline');
let tmp;
if (typeof oldline !== 'undefined') {
if (typeof newline !== 'undefined') {
tmp = getInlineContainer();
} else {
tmp = getInlineContainer('old');
}
tmp.children('td:first').html($v.clone().show());
diff.find('table.diff').find('.oldline[line-number=' + oldline + ']').parent().nextAll(':not(.not-diff):first').before(tmp);
} else {
tmp = getInlineContainer('new');
tmp.children('td:last').html($v.clone().show());
diff.find('table.diff').find('.newline[line-number=' + newline + ']').parent().nextAll(':not(.not-diff):first').before(tmp);
}
if (!diff.find('.toggle-notes').prop('checked')) {
tmp.hide();
}
}
function renderStatBar(add, del){
if(add + del > 5){
if(add){
if(add < del){
add = Math.floor(1 + (add * 4 / (add + del)));
} else {
add = Math.ceil(1 + (add * 4 / (add + del)));
}
}
del = 5 - add;
}
const ret = $('<div class="diffstat-bar">');
for(let i = 0; i < 5; i++){
if(add){
ret.append('<span class="text-diff-added">■</span>');
add--;
} else if(del){
ret.append('<span class="text-diff-deleted">■</span>');
del--;
} else {
ret.append('■');
}
}
return ret;
}
function renderOneDiff(diffText, viewType, fileHash){
const table = diffText.closest("table[data-diff-id]");
const i = table.data("diff-id");
const ignoreWhiteSpace = table.find('.ignore-whitespace').prop('checked');
diffUsingJS('oldText-' + i, 'newText-' + i, diffText.attr('id'), viewType, ignoreWhiteSpace, fileHash);
const add = diffText.find("table").attr("add") * 1;
const del = diffText.find("table").attr("del") * 1;
table.find(".diffstat").text(add + del + " ").append(renderStatBar(add, del)).attr("title", add + " additions & " + del + " deletions").tooltip();
$('span.diffstat[data-diff-id="' + i + '"]')
.html('<span class="text-diff-added">+' + add + '</span><span class="text-diff-deleted">-' + del + '</span>')
.append(renderStatBar(add, del).attr('title', (add + del) + " lines changed").tooltip());
@if(hasWritePermission) {
diffText.find('.body').filter(function(i, e) {
return $(e).has('span').length > 0;
}).each(function(){
$('<b class="add-comment">+</b>').prependTo(this);
});
}
@if(showLineNotes){
const fileName = table.attr('filename');
$('.inline-comment').each(function(i, v) {
if($(this).attr('filename') == fileName){
renderOneCommitCommentIntoDiff($(this), table);
}
});
}
return table;
}
function renderReplyComment($table){
const elements = {};
let filename, newline, oldline;
$table.find('.comment-box-container .inline-comment').each(function(i, e){
filename = $(e).attr('filename');
newline = $(e).attr('newline');
oldline = $(e).attr('oldline');
const key = filename + '-' + newline + '-' + oldline;
elements[key] = {
element: $(e),
filename: filename,
newline: newline,
oldline: oldline
};
});
for(const key in elements){
filename = elements[key]['filename'];
oldline = elements[key]['oldline']; //? elements[key]['oldline'] : '';
newline = elements[key]['newline']; //? elements[key]['newline'] : '';
const $v = $('<div class="commit-comment-box reply-comment-box">')
.append($('<input type="text" class="form-control reply-comment" placeholder="Reply..." '
+ 'data-filename="' + filename + '" '
+ 'data-newline="' + newline + '" '
+ 'data-oldline="' + oldline + '">'));
function renderOneCommitCommentIntoDiff($v, diff){
//var filename = $v.attr('filename');
const oldline = $v.attr('oldline');
const newline = $v.attr('newline');
let tmp; let tmp;
if (typeof oldline !== 'undefined') { if (typeof oldline !== 'undefined') {
if (typeof newline !== 'undefined') { if (typeof newline !== 'undefined') {
@@ -314,134 +426,51 @@ $(function(){
} else { } else {
tmp = getInlineContainer('old'); tmp = getInlineContainer('old');
} }
tmp.children('td:first').html($v.clone().show()); tmp.children('td:first').html($v);
diff.find('table.diff').find('.oldline[line-number=' + oldline + ']').parent().nextAll(':not(.not-diff):first').before(tmp);
} else { } else {
tmp = getInlineContainer('new'); tmp = getInlineContainer('new');
tmp.children('td:last').html($v.clone().show()); tmp.children('td:last').html($v);
diff.find('table.diff').find('.newline[line-number=' + newline + ']').parent().nextAll(':not(.not-diff):first').before(tmp);
}
if (!diff.find('.toggle-notes').prop('checked')) {
tmp.hide();
} }
elements[key]['element'].closest('.not-diff').after(tmp);
} }
}
function renderStatBar(add, del){ function renderDiffs() {
if(add + del > 5){ const diffs = $('.diffText');
if(add){ let i = 0;
if(add < del){ function render(){
add = Math.floor(1 + (add * 4 / (add + del))); if (diffs[i]) {
} else { const $table = renderOneDiff($(diffs[i]), window.viewType, $('.file-hash')[i].id);
add = Math.ceil(1 + (add * 4 / (add + del))); @if(hasWritePermission) {
} renderReplyComment($table);
} }
del = 5 - add; i++;
} setTimeout(render);
const ret = $('<div class="diffstat-bar">'); } else {
for(let i = 0; i < 5; i++){ updateHighlighting();
if(add){
ret.append('<span class="text-diff-added">■</span>');
add--;
} else if(del){
ret.append('<span class="text-diff-deleted">■</span>');
del--;
} else {
ret.append('■');
}
}
return ret;
}
function renderOneDiff(diffText, viewType, fileHash){
const table = diffText.closest("table[data-diff-id]");
const i = table.data("diff-id");
const ignoreWhiteSpace = table.find('.ignore-whitespace').prop('checked');
diffUsingJS('oldText-' + i, 'newText-' + i, diffText.attr('id'), viewType, ignoreWhiteSpace, fileHash);
const add = diffText.find("table").attr("add") * 1;
const del = diffText.find("table").attr("del") * 1;
table.find(".diffstat").text(add + del + " ").append(renderStatBar(add, del)).attr("title", add + " additions & " + del + " deletions").tooltip();
$('span.diffstat[data-diff-id="' + i + '"]')
.html('<span class="text-diff-added">+' + add + '</span><span class="text-diff-deleted">-' + del + '</span>')
.append(renderStatBar(add, del).attr('title', (add + del) + " lines changed").tooltip());
@if(hasWritePermission) {
diffText.find('.body').filter(function(i, e) {
return $(e).has('span').length > 0;
}).each(function(){
$('<b class="add-comment">+</b>').prependTo(this);
});
}
@if(showLineNotes){
const fileName = table.attr('filename');
$('.inline-comment').each(function(i, v) {
if($(this).attr('filename') == fileName){
renderOneCommitCommentIntoDiff($(this), table);
}
});
}
return table;
}
function renderReplyComment($table){
const elements = {};
let filename, newline, oldline;
$table.find('.comment-box-container .inline-comment').each(function(i, e){
filename = $(e).attr('filename');
newline = $(e).attr('newline');
oldline = $(e).attr('oldline');
const key = filename + '-' + newline + '-' + oldline;
elements[key] = {
element: $(e),
filename: filename,
newline: newline,
oldline: oldline
};
});
for(const key in elements){
filename = elements[key]['filename'];
oldline = elements[key]['oldline']; //? elements[key]['oldline'] : '';
newline = elements[key]['newline']; //? elements[key]['newline'] : '';
const $v = $('<div class="commit-comment-box reply-comment-box">')
.append($('<input type="text" class="form-control reply-comment" placeholder="Reply..." '
+ 'data-filename="' + filename + '" '
+ 'data-newline="' + newline + '" '
+ 'data-oldline="' + oldline + '">'));
let tmp;
if (typeof oldline !== 'undefined') {
if (typeof newline !== 'undefined') {
tmp = getInlineContainer();
} else {
tmp = getInlineContainer('old');
}
tmp.children('td:first').html($v);
} else {
tmp = getInlineContainer('new');
tmp.children('td:last').html($v);
}
elements[key]['element'].closest('.not-diff').after(tmp);
} }
} }
render();
}
function renderDiffs() { function showDiff(index, fromId, toId, path){
let url = '@helpers.url(repository)/diff/';
if (fromId == '') {
url = url + encodeURIComponent(toId);
} else {
url = url + encodeURIComponent(fromId) + '...' + encodeURIComponent(toId);
}
$.get(url, { path : path }, function(data) {
$('#oldText-' + index).attr('data-val', data.oldContent);
$('#newText-' + index).attr('data-val', data.newContent);
const diffs = $('.diffText'); const diffs = $('.diffText');
let i = 0; const $table = renderOneDiff($(diffs[index]), window.viewType, $('.file-hash')[index].id);
function render(){ @if(hasWritePermission) {
if (diffs[i]) { renderReplyComment($table);
const $table = renderOneDiff($(diffs[i]), window.viewType, $('.file-hash')[i].id);
@if(hasWritePermission) {
renderReplyComment($table);
}
i++;
setTimeout(render);
} else {
updateHighlighting();
}
} }
render(); $('#show-diff-' + index).hide();
} });
}); }
function changeDisplaySetting(key, value){ function changeDisplaySetting(key, value){
let url = ''; let url = '';

View File

@@ -200,7 +200,7 @@
<span class="discussion-item-icon"><i class="octicon octicon-tag"></i></span> <span class="discussion-item-icon"><i class="octicon octicon-tag"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16) @helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong") @helpers.user(comment.commentedUserName, styleClass="username strong")
add the <code>@comment.content</code> label added the <code>@comment.content</code> label
@gitbucket.core.helper.html.datetimeago(comment.registeredDate) @gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div> </div>
</div> </div>
@@ -222,7 +222,7 @@
<span class="discussion-item-icon"><i class="octicon octicon-flame"></i></span> <span class="discussion-item-icon"><i class="octicon octicon-flame"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16) @helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong") @helpers.user(comment.commentedUserName, styleClass="username strong")
change priority from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code> changed priority from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate) @gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div> </div>
</div> </div>
@@ -233,18 +233,40 @@
<span class="discussion-item-icon"><i class="octicon octicon-milestone"></i></span> <span class="discussion-item-icon"><i class="octicon octicon-milestone"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16) @helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong") @helpers.user(comment.commentedUserName, styleClass="username strong")
change milestone from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code> changed milestone from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate) @gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div> </div>
</div> </div>
} }
case "assign" => { case "assign" => { @* for backward compatibility *@
<div class="discussion-item discussion-item-assign"> <div class="discussion-item discussion-item-assign">
<div class="discussion-item-header"> <div class="discussion-item-header">
<span class="discussion-item-icon"><i class="octicon octicon-person"></i></span> <span class="discussion-item-icon"><i class="octicon octicon-person"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16) @helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong") @helpers.user(comment.commentedUserName, styleClass="username strong")
change assignee from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code> changed assignee from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
}
case "add_assignee" => {
<div class="discussion-item discussion-item-assign">
<div class="discussion-item-header">
<span class="discussion-item-icon"><i class="octicon octicon-person"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
assigned <code>@comment.content</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
}
case "delete_assignee" => {
<div class="discussion-item discussion-item-assign">
<div class="discussion-item-header">
<span class="discussion-item-icon"><i class="octicon octicon-person"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
unassigned <code>@comment.content</code></code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate) @gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div> </div>
</div> </div>
@@ -255,7 +277,7 @@
<span class="discussion-item-icon"><i class="octicon octicon-pencil"></i></span> <span class="discussion-item-icon"><i class="octicon octicon-pencil"></i></span>
@helpers.avatar(comment.commentedUserName, 16) @helpers.avatar(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong") @helpers.user(comment.commentedUserName, styleClass="username strong")
change title from <code>@convertLineSeparator(comment.content, "LF").split("\n")(0)</code> to <code>@convertLineSeparator(comment.content, "LF").split("\n")(1)</code> changed title from <code>@convertLineSeparator(comment.content, "LF").split("\n")(0)</code> to <code>@convertLineSeparator(comment.content, "LF").split("\n")(1)</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate) @gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div> </div>
</div> </div>
@@ -266,7 +288,7 @@
<span class="discussion-item-icon"><i class="octicon octicon-pencil"></i></span> <span class="discussion-item-icon"><i class="octicon octicon-pencil"></i></span>
@helpers.avatar(comment.commentedUserName, 16) @helpers.avatar(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong") @helpers.user(comment.commentedUserName, styleClass="username strong")
change base branch from <code>@convertLineSeparator(comment.content, "LF").split("\n")(0)</code> to <code>@convertLineSeparator(comment.content, "LF").split("\n")(1)</code> changed base branch from <code>@convertLineSeparator(comment.content, "LF").split("\n")(0)</code> to <code>@convertLineSeparator(comment.content, "LF").split("\n")(1)</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate) @gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div> </div>
</div> </div>
@@ -328,9 +350,9 @@
$(function(){ $(function(){
@if(issue.isDefined){ @if(issue.isDefined){
$('.issue-comment-box i.octicon-pencil').click(function(){ $('.issue-comment-box i.octicon-pencil').click(function(){
var id = $(this).closest('a').data('comment-id'); let id = $(this).closest('a').data('comment-id');
var url = '@helpers.url(repository)/issue_comments/_data/' + id; let url = '@helpers.url(repository)/issue_comments/_data/' + id;
var $content = $('#commentContent-' + id); let $content = $('#commentContent-' + id);
if(!id){ if(!id){
id = $(this).closest('a').data('issue-id'); id = $(this).closest('a').data('issue-id');
@@ -345,7 +367,7 @@ $(function(){
}); });
$('.issue-comment-box i.octicon-x').click(function(){ $('.issue-comment-box i.octicon-x').click(function(){
if(confirm('Are you sure you want to delete this?')) { if(confirm('Are you sure you want to delete this?')) {
var id = $(this).closest('a').data('comment-id'); const id = $(this).closest('a').data('comment-id');
$.post('@helpers.url(repository)/issue_comments/delete/' + id, function(data){ $.post('@helpers.url(repository)/issue_comments/delete/' + id, function(data){
if(data > 0) { if(data > 0) {
$('#comment-' + id).remove(); $('#comment-' + id).remove();
@@ -356,9 +378,9 @@ $(function(){
}); });
} }
$(document).on('click', '.commit-comment-box i.octicon-pencil', function(){ $(document).on('click', '.commit-comment-box i.octicon-pencil', function(){
var id = $(this).closest('a').data('comment-id'); const id = $(this).closest('a').data('comment-id');
var url = '@helpers.url(repository)/commit_comments/_data/' + id; const url = '@helpers.url(repository)/commit_comments/_data/' + id;
var $content = $('.commit-commentContent-' + id, $(this).closest('.commit-comment-box')); const $content = $('.commit-commentContent-' + id, $(this).closest('.commit-comment-box'));
$.get(url, { dataType : 'html' }, function(data){ $.get(url, { dataType : 'html' }, function(data){
$content.empty().html(data); $content.empty().html(data);
@@ -369,14 +391,14 @@ $(function(){
$(document).on('click', '.commit-comment-box i.octicon-x', function(){ $(document).on('click', '.commit-comment-box i.octicon-x', function(){
if(confirm('Are you sure you want to delete this?')) { if(confirm('Are you sure you want to delete this?')) {
var id = $(this).closest('a').data('comment-id'); const id = $(this).closest('a').data('comment-id');
$.post('@helpers.url(repository)/commit_comments/delete/' + id, $.post('@helpers.url(repository)/commit_comments/delete/' + id,
function(data){ function(data){
if(data > 0) { if(data > 0) {
var comment = $('.commit-comment-' + id); const comment = $('.commit-comment-' + id);
// diff view // diff view
var tr = comment.closest('.not-diff'); const tr = comment.closest('.not-diff');
if(tr.length > 0){ 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.next('.not-diff:has(.reply-comment)').remove();
@@ -385,7 +407,7 @@ $(function(){
} }
// comment list view // comment list view
var panel = comment.closest('div.panel:has(.commit-comment-box)'); const panel = comment.closest('div.panel:has(.commit-comment-box)');
if(panel.length > 0){ if(panel.length > 0){
comment.parent('.commit-comment-box').remove(); comment.parent('.commit-comment-box').remove();
if(panel.has('.commit-comment-box').length == 0){ if(panel.has('.commit-comment-box').length == 0){
@@ -401,7 +423,7 @@ $(function(){
}); });
$('div[class*=commit-commentContent-]').on('click', ':checkbox', function(ev){ $('div[class*=commit-commentContent-]').on('click', ':checkbox', function(ev){
var $commentContent = $(ev.target).parents('div[class*=commit-commentContent-]'), const $commentContent = $(ev.target).parents('div[class*=commit-commentContent-]'),
commentId = $commentContent.attr('class').match(/commit-commentContent-.+/)[0].replace(/commit-commentContent-/, ''), commentId = $commentContent.attr('class').match(/commit-commentContent-.+/)[0].replace(/commit-commentContent-/, ''),
checkboxes = $commentContent.find(':checkbox'); checkboxes = $commentContent.find(':checkbox');
$.get('@helpers.url(repository)/commit_comments/_data/' + commentId, { dataType : 'html' }, $.get('@helpers.url(repository)/commit_comments/_data/' + commentId, { dataType : 'html' },
@@ -421,9 +443,9 @@ $(function(){
); );
}); });
@if(issue.isDefined){ @if(issue.isDefined){
$('#issueContent').on('click', ':checkbox', function(ev){ $('#issueContent').on('click', ':checkbox', function(ev){
var checkboxes = $('#issueContent :checkbox'); const checkboxes = $('#issueContent :checkbox');
$.get('@helpers.url(repository)/issues/_data/@issue.get.issueId', { dataType : 'html' }, $.get('@helpers.url(repository)/issues/_data/@issue.get.issueId', { dataType : 'html' },
function(responseContent){ function(responseContent){
$.ajax({ $.ajax({
@@ -439,7 +461,7 @@ $(function(){
}); });
$('div[id^=commentContent-]').on('click', ':checkbox', function(ev){ $('div[id^=commentContent-]').on('click', ':checkbox', function(ev){
var $commentContent = $(ev.target).parents('div[id^=commentContent-]'), const $commentContent = $(ev.target).parents('div[id^=commentContent-]'),
commentId = $commentContent.attr('id').replace(/commentContent-/, ''), commentId = $commentContent.attr('id').replace(/commentContent-/, ''),
checkboxes = $commentContent.find(':checkbox'); checkboxes = $commentContent.find(':checkbox');
$.get('@helpers.url(repository)/issue_comments/_data/' + commentId, { dataType : 'html' }, $.get('@helpers.url(repository)/issue_comments/_data/' + commentId, { dataType : 'html' },
@@ -455,9 +477,7 @@ $(function(){
} }
); );
}); });
}
}
}); });
</script> </script>
} }

View File

@@ -3,6 +3,7 @@
priorities: List[gitbucket.core.model.Priority], priorities: List[gitbucket.core.model.Priority],
defaultPriority: Option[gitbucket.core.model.Priority], defaultPriority: Option[gitbucket.core.model.Priority],
labels: List[gitbucket.core.model.Label], labels: List[gitbucket.core.model.Label],
customFields: List[gitbucket.core.model.CustomField],
isManageable: Boolean, isManageable: Boolean,
content: String, content: String,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@@ -27,13 +28,37 @@
elastic = true elastic = true
) )
<div class="align-right"> <div class="align-right">
<input type="submit" class="btn btn-success" value="Submit new issue"/> <input type="submit" class="btn btn-success" value="Submit new issue" onclick="javascript:return checkCustomFieldErrors();"/>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
@gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map(x => (x, 0, 0)), priorities, defaultPriority, labels, isManageable, repository) @gitbucket.core.issues.html.issueinfo(
issue = None,
comments = Nil,
issueLabels = Nil,
issueAssignees = Nil,
collaborators = collaborators,
milestones = milestones.map(x => (x, 0, 0)),
priorities= priorities,
defaultPriority = defaultPriority,
labels = labels,
customFields = customFields.map((_, None)),
isManageable = isManageable,
repository = repository
)
</div> </div>
</div> </div>
</form> </form>
<script>
function checkCustomFieldErrors() {
let error = false;
$('.custom-field-error').each(function(i, e) {
if ($(e).text() != '') {
error = true;
}
});
return !error;
}
</script>
} }
} }

View File

@@ -1,10 +1,12 @@
@(issue: gitbucket.core.model.Issue, @(issue: gitbucket.core.model.Issue,
comments: List[gitbucket.core.model.IssueComment], comments: List[gitbucket.core.model.IssueComment],
issueLabels: List[gitbucket.core.model.Label], issueLabels: List[gitbucket.core.model.Label],
issueAssignees: List[gitbucket.core.model.IssueAssignee],
collaborators: List[String], collaborators: List[String],
milestones: List[(gitbucket.core.model.Milestone, Int, Int)], milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
priorities: List[gitbucket.core.model.Priority], priorities: List[gitbucket.core.model.Priority],
labels: List[gitbucket.core.model.Label], labels: List[gitbucket.core.model.Label],
customFields: List[(gitbucket.core.model.CustomField, Option[gitbucket.core.model.IssueCustomField])],
isEditable: Boolean, isEditable: Boolean,
isManageable: Boolean, isManageable: Boolean,
isCommentManageable: Boolean, isCommentManageable: Boolean,
@@ -56,7 +58,20 @@
@gitbucket.core.issues.html.commentform(issue, true, isEditable, isManageable, repository) @gitbucket.core.issues.html.commentform(issue, true, isEditable, isManageable, repository)
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
@gitbucket.core.issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, priorities, None, labels, isManageable, repository) @gitbucket.core.issues.html.issueinfo(
issue = Some(issue),
comments = comments,
issueLabels = issueLabels,
issueAssignees = issueAssignees,
collaborators = collaborators,
milestones = milestones,
priorities = priorities,
defaultPriority = None,
labels = labels,
customFields = customFields,
isManageable = isManageable,
repository = repository
)
</div> </div>
</div> </div>
} }

View File

@@ -1,11 +1,14 @@
@import org.json4s.scalap.scalasig.ClassFileParser.field
@(issue: Option[gitbucket.core.model.Issue], @(issue: Option[gitbucket.core.model.Issue],
comments: List[gitbucket.core.model.Comment], comments: List[gitbucket.core.model.Comment],
issueLabels: List[gitbucket.core.model.Label], issueLabels: List[gitbucket.core.model.Label],
issueAssignees: List[gitbucket.core.model.IssueAssignee],
collaborators: List[String], collaborators: List[String],
milestones: List[(gitbucket.core.model.Milestone, Int, Int)], milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
priorities: List[gitbucket.core.model.Priority], priorities: List[gitbucket.core.model.Priority],
defaultPriority: Option[gitbucket.core.model.Priority], defaultPriority: Option[gitbucket.core.model.Priority],
labels: List[gitbucket.core.model.Label], labels: List[gitbucket.core.model.Label],
customFields: List[(gitbucket.core.model.CustomField, Option[gitbucket.core.model.IssueCustomField])],
isManageable: Boolean, isManageable: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers @import gitbucket.core.view.helpers
@@ -125,11 +128,10 @@
@if(isManageable){ @if(isManageable){
<div class="pull-right"> <div class="pull-right">
@gitbucket.core.helper.html.dropdown("Edit", right = true, filter = ("assignee", "Filter Assignee")) { @gitbucket.core.helper.html.dropdown("Edit", right = true, filter = ("assignee", "Filter Assignee")) {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="octicon octicon-x"></i> Clear assignee</a></li>
@collaborators.map { collaborator => @collaborators.map { collaborator =>
<li> <li>
<a href="javascript:void(0);" class="assign" data-name="@collaborator"> <a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator">
@gitbucket.core.helper.html.checkicon(issue.exists(_.assignedUserName.exists(_ == collaborator)))@helpers.avatar(collaborator, 20) @collaborator @gitbucket.core.helper.html.checkicon(issueAssignees.exists(_.assigneeUserName == collaborator))@helpers.avatar(collaborator, 20) @collaborator
</a> </a>
</li> </li>
} }
@@ -138,15 +140,35 @@
} }
</div> </div>
<span id="label-assigned"> <span id="label-assigned">
@issue.flatMap(_.assignedUserName).map { userName => @issueAssignees.map { asignee =>
@helpers.avatarLink(userName, 20) @helpers.user(userName, styleClass="username strong small") <div>@helpers.avatarLink(asignee.assigneeUserName, 20) @helpers.user(asignee.assigneeUserName, styleClass="username strong small")</div>
}.getOrElse{ }
<span class="muted small">No one</span> @if(issueAssignees.isEmpty) {
<span class="muted small">No one assigned</span>
} }
</span> </span>
@if(issue.isEmpty){ @if(issue.isEmpty){
<input type="hidden" name="assignedUserName" value=""/> <input type="hidden" name="assigneeUserNames" value=""/>
} }
@customFields.map { case (field, value) =>
<hr/>
<div style="margin-bottom: 14px;">
<span class="muted small strong">@field.fieldName</span>
<div class="pull-right">
@gitbucket.core.model.CustomFieldBehavior(field.fieldType).map { behavior =>
@if(issue.nonEmpty) {
@Html(behavior.fieldHtml(repository, issue.get.issueId, field.fieldId, value.map(_.value).getOrElse(""), isManageable))
}
@if(issue.isEmpty) {
@Html(behavior.createHtml(repository, field.fieldId))
}
}
</div>
</div>
<span id="custom-field-@field.fieldId-error" class="error custom-field-error"></span>
}
@issue.map { issue => @issue.map { issue =>
@gitbucket.core.plugin.PluginRegistry().getIssueSidebars.map { sidebarComponent => @gitbucket.core.plugin.PluginRegistry().getIssueSidebars.map { sidebarComponent =>
@sidebarComponent(issue, repository, context) @sidebarComponent(issue, repository, context)
@@ -167,7 +189,7 @@
$(function(){ $(function(){
@issue.map { issue => @issue.map { issue =>
$('a.toggle-label').click(function(){ $('a.toggle-label').click(function(){
var path = switchLabel($(this)); const path = switchToggleOptions($(this));
$.post('@helpers.url(repository)/issues/@issue.issueId/label/' + path, $.post('@helpers.url(repository)/issues/@issue.issueId/label/' + path,
{ labelId : $(this).data('label-id') }, { labelId : $(this).data('label-id') },
function(data){ function(data){
@@ -178,8 +200,8 @@ $(function(){
}); });
$('a.milestone').click(function(){ $('a.milestone').click(function(){
var title = $(this).data('title'); const title = $(this).data('title');
var milestoneId = $(this).data('id'); const milestoneId = $(this).data('id');
$.post('@helpers.url(repository)/issues/@issue.issueId/milestone', $.post('@helpers.url(repository)/issues/@issue.issueId/milestone',
{ milestoneId: milestoneId }, { milestoneId: milestoneId },
function(data){ function(data){
@@ -189,11 +211,11 @@ $(function(){
}); });
$('a.priority').click(function(){ $('a.priority').click(function(){
var priorityName = $(this).data('name'); const priorityName = $(this).data('name');
var priorityId = $(this).data('id'); const priorityId = $(this).data('id');
var description = $(this).attr('title'); const description = $(this).attr('title');
var color = $(this).data('color'); const color = $(this).data('color');
var fontColor = $(this).data('font-color'); const fontColor = $(this).data('font-color');
$.post('@helpers.url(repository)/issues/@issue.issueId/priority', $.post('@helpers.url(repository)/issues/@issue.issueId/priority',
{ priorityId: priorityId }, { priorityId: priorityId },
function(data){ function(data){
@@ -202,20 +224,26 @@ $(function(){
); );
}); });
$('a.assign').click(function(){ $('a.toggle-assign').click(function(){
var $this = $(this); const path = switchToggleOptions($(this));
var userName = $this.data('name'); $.post('@helpers.url(repository)/issues/@issue.issueId/assignee/' + path,
$.post('@helpers.url(repository)/issues/@issue.issueId/assign', { assigneeUserName : $(this).data('name') },
{ assignedUserName: userName }, function(data){
function(){ const assignees = Array();
displayAssignee($this, userName); $('a.toggle-assign').each(function(i, e){
if($(e).children('i').hasClass('octicon-check') == true){
assignees.push($(e).text().trim());
}
});
displayAssignee(assignees);
} }
); );
return false;
}); });
}.getOrElse { }.getOrElse {
$('a.toggle-label').click(function(){ $('a.toggle-label').click(function(){
switchLabel($(this)); switchToggleOptions($(this));
var labelNames = Array(); const labelNames = Array();
$('a.toggle-label').each(function(i, e){ $('a.toggle-label').each(function(i, e){
if($(e).children('i').hasClass('octicon-check') == true){ if($(e).children('i').hasClass('octicon-check') == true){
labelNames.push($(e).text().trim()); labelNames.push($(e).text().trim());
@@ -232,32 +260,37 @@ $(function(){
}); });
$('a.milestone').click(function(){ $('a.milestone').click(function(){
var title = $(this).data('title'); const title = $(this).data('title');
var milestoneId = $(this).data('id'); const milestoneId = $(this).data('id');
displayMilestone(title, milestoneId); displayMilestone(title, milestoneId);
$('input[name=milestoneId]').val(milestoneId); $('input[name=milestoneId]').val(milestoneId);
}); });
$('a.priority').click(function(){ $('a.priority').click(function(){
var priorityName = $(this).data('name'); const priorityName = $(this).data('name');
var priorityId = $(this).data('id'); const priorityId = $(this).data('id');
var description = $(this).attr('title'); const description = $(this).attr('title');
var color = $(this).data('color'); const color = $(this).data('color');
var fontColor = $(this).data('font-color'); const fontColor = $(this).data('font-color');
displayPriority(priorityName, priorityId, description, color, fontColor); displayPriority(priorityName, priorityId, description, color, fontColor);
$('input[name=priorityId]').val(priorityId); $('input[name=priorityId]').val(priorityId);
}); });
$('a.assign').click(function(){ $('a.toggle-assign').click(function(){
var $this = $(this); switchToggleOptions($(this));
var userName = $this.data('name'); const assignees = Array();
displayAssignee($this, userName); $('a.toggle-assign').each(function(i, e){
$('input[name=assignedUserName]').val(userName); if($(e).children('i').hasClass('octicon-check') == true){
assignees.push($(e).text().trim());
}
});
$('input[name=assigneeUserNames]').val(assignees.join(','));
displayAssignee(assignees);
}); });
} }
function switchLabel($this){ function switchToggleOptions($this){
var i = $this.children('i'); const i = $this.children('i');
if(i.hasClass('octicon-check')){ if(i.hasClass('octicon-check')){
i.removeClass('octicon-check'); i.removeClass('octicon-check');
return 'delete'; return 'delete';
@@ -299,15 +332,18 @@ $(function(){
} }
} }
function displayAssignee($this, userName){ function displayAssignee(assignees){
$('a.assign i.octicon-check').removeClass('octicon-check'); $('a.assign i.octicon-check').removeClass('octicon-check');
if(userName == ''){ if(assignees.length == 0){
$('#label-assigned').html($('<span class="muted small">').text('No one')); $('#label-assigned').html($('<span class="muted small">').text('No one assigned'));
} else { } else {
$('#label-assigned').empty() $('#label-assigned').empty();
.append($this.find('img.avatar-mini').clone(false)).append(' ') for (const userName of assignees) {
.append($('<a class="username strong small">').attr('href', '@context.path/' + userName).text(userName)); $('#label-assigned').append($('<div>').append(
$('a.assign[data-name=' + jqSelectorEscape(userName.toString()) + '] i').addClass('octicon-check'); $('a.toggle-assign').parent().find("img.avatar-mini[alt='@@" + userName + "']").clone(false),
' ',
$('<a class="username strong small">').attr('href', '@context.path/' + userName).text(userName)));
}
} }
} }
}); });

View File

@@ -206,7 +206,7 @@
</td> </td>
</tr> </tr>
} }
@issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId), status) => @issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId, assignedUserNames), status) =>
<tr> <tr>
<td style="padding-top: 12px; padding-bottom: 12px;"> <td style="padding-top: 12px; padding-bottom: 12px;">
@if(isManageable){ @if(isManageable){
@@ -230,7 +230,7 @@
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span> <span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
} }
<span class="pull-right small"> <span class="pull-right small">
@issue.assignedUserName.map { userName => @assignedUserNames.map { userName =>
@helpers.avatar(userName, 20, tooltip = true) @helpers.avatar(userName, 20, tooltip = true)
} }
@if(commentCount > 0){ @if(commentCount > 0){

View File

@@ -62,7 +62,7 @@
</td> </td>
</tr> </tr>
} }
@issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId), status) => @issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId, assignedUserNames), status) =>
<tr> <tr>
<td style="padding-top: 12px; padding-bottom: 12px;"> <td style="padding-top: 12px; padding-bottom: 12px;">
@if(isManageable){ @if(isManageable){
@@ -90,7 +90,7 @@
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span> <span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
} }
<span class="pull-right small"> <span class="pull-right small">
@issue.assignedUserName.map { userName => @assignedUserNames.map { userName =>
@helpers.avatar(userName, 20, tooltip = true) @helpers.avatar(userName, 20, tooltip = true)
} }
@if(commentCount > 0){ @if(commentCount > 0){

View File

@@ -73,27 +73,27 @@
</a> </a>
<nav class="navbar navbar-static-top" role="navigation"> <nav class="navbar navbar-static-top" role="navigation">
<!-- Sidebar toggle button--> <!-- Sidebar toggle button-->
@if(body.toString.contains("main-sidebar")){ @if(body.toString.contains("main-sidebar")) {
<a href="#" class="sidebar-toggle" data-toggle="push-menu" role="button" title="Toggle navigation"> <a href="#" class="sidebar-toggle" data-toggle="push-menu" role="button" title="Toggle navigation">
<span class="sr-only">Toggle navigation</span> <span class="sr-only">Toggle navigation</span>
</a> </a>
} }
<form id="search" action="@context.path/search" method="GET" class="pc navbar-form navbar-left" role="search"> @if(!(context.loginAccount.isEmpty && !context.settings.basicBehavior.allowAnonymousAccess)) {
<div class="form-group"> <form id="search" action="@context.path/search" method="GET" class="pc navbar-form navbar-left" role="search">
<input type="text" name="query" id="navbar-search-input" class="form-control" placeholder="Find a repository" aria-label="Search"/> <div class="form-group">
</div> <input type="text" name="query" id="navbar-search-input" class="form-control" placeholder="Find a repository" aria-label="Search"/>
</form> </div>
<ul class="pc nav navbar-nav"> </form>
@if(context.loginAccount.isDefined){ <ul class="pc nav navbar-nav">
<li><a href="@context.path/dashboard/pulls">Pull requests</a></li> <li><a href="@context.path/dashboard/pulls">Pull requests</a></li>
<li><a href="@context.path/dashboard/issues">Issues</a></li> <li><a href="@context.path/dashboard/issues">Issues</a></li>
} @gitbucket.core.plugin.PluginRegistry().getGlobalMenus.map { menu =>
@gitbucket.core.plugin.PluginRegistry().getGlobalMenus.map { menu => @menu(context).map { link =>
@menu(context).map { link => <li><a href="@context.path/@link.path">@link.label</a></li>
<li><a href="@context.path/@link.path">@link.label</a></li> }
} }
} </ul>
</ul> }
<div class="navbar-custom-menu"> <div class="navbar-custom-menu">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
@if(context.loginAccount.isDefined){ @if(context.loginAccount.isDefined){
@@ -104,7 +104,7 @@
<ul class="dropdown-menu pull-right" style="width: auto;"> <ul class="dropdown-menu pull-right" style="width: auto;">
<li> <li>
<ul class="menu"> <ul class="menu">
@if(context.settings.repositoryOperation.create || context.loginAccount.get.isAdmin){ @if(context.settings.basicBehavior.repositoryOperation.create || context.loginAccount.get.isAdmin){
<li><a href="@context.path/new">New repository</a></li> <li><a href="@context.path/new">New repository</a></li>
} }
<li><a href="@context.path/groups/new">New group</a></li> <li><a href="@context.path/groups/new">New group</a></li>

View File

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

View File

@@ -16,7 +16,8 @@
milestones: List[gitbucket.core.model.Milestone], milestones: List[gitbucket.core.model.Milestone],
priorities: List[gitbucket.core.model.Priority], priorities: List[gitbucket.core.model.Priority],
defaultPriority: Option[gitbucket.core.model.Priority], defaultPriority: Option[gitbucket.core.model.Priority],
labels: List[gitbucket.core.model.Label])(implicit context: gitbucket.core.controller.Context) labels: List[gitbucket.core.model.Label],
customFields: List[gitbucket.core.model.CustomField])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers @import gitbucket.core.view.helpers
@gitbucket.core.html.main(s"Pull requests - ${repository.owner}/${repository.name}", Some(repository)){ @gitbucket.core.html.main(s"Pull requests - ${repository.owner}/${repository.name}", Some(repository)){
@gitbucket.core.html.menu("pulls", repository){ @gitbucket.core.html.menu("pulls", repository){
@@ -101,7 +102,20 @@
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
@gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map((_, 0, 0)), priorities, defaultPriority, labels, hasOriginWritePermission, repository) @gitbucket.core.issues.html.issueinfo(
issue = None,
comments = Nil,
issueLabels = Nil,
issueAssignees = Nil,
collaborators = collaborators,
milestones = milestones.map((_, 0, 0)),
priorities = priorities,
defaultPriority = defaultPriority,
labels = labels,
customFields = customFields.map((_, None)),
isManageable = hasOriginWritePermission,
repository = repository
)
</div> </div>
</div> </div>
</form> </form>

View File

@@ -4,10 +4,12 @@
comments: Seq[gitbucket.core.model.Comment], comments: Seq[gitbucket.core.model.Comment],
changedFileSize: Int, changedFileSize: Int,
issueLabels: List[gitbucket.core.model.Label], issueLabels: List[gitbucket.core.model.Label],
issueAssignees: List[gitbucket.core.model.IssueAssignee],
collaborators: List[String], collaborators: List[String],
milestones: List[(gitbucket.core.model.Milestone, Int, Int)], milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
priorities: List[gitbucket.core.model.Priority], priorities: List[gitbucket.core.model.Priority],
labels: List[gitbucket.core.model.Label], labels: List[gitbucket.core.model.Label],
customFields: List[(gitbucket.core.model.CustomField, Option[gitbucket.core.model.IssueCustomField])],
isEditable: Boolean, isEditable: Boolean,
isManageable: Boolean, isManageable: Boolean,
isManageableForkedRepository: Boolean, isManageableForkedRepository: Boolean,
@@ -50,7 +52,20 @@
} }
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
@gitbucket.core.issues.html.issueinfo(Some(issue), comments.toList, issueLabels, collaborators, milestones, priorities, None, labels, isManageable, repository) @gitbucket.core.issues.html.issueinfo(
Some(issue),
comments.toList,
issueLabels,
issueAssignees,
collaborators,
milestones,
priorities,
None,
labels,
customFields,
isManageable,
repository
)
</div> </div>
<script> <script>
$(function(){ $(function(){

View File

@@ -55,10 +55,10 @@
}); });
@if(newLineNumber.isDefined){ @if(newLineNumber.isDefined){
var diff = getDiffData($('table[filename="@fileName"] table.diff tr:has(th.line-num.newline[line-number=@newLineNumber])')); var diff = getDiffData($('table[filename="@fileName"] table.diff tr:has(th.line-num.newline[line-number="@newLineNumber"])'));
param['diff'] = JSON.stringify(diff); param['diff'] = JSON.stringify(diff);
} else if(oldLineNumber.isDefined){ } else if(oldLineNumber.isDefined){
var diff = getDiffData($('table[filename="@fileName"] table.diff tr:has(th.line-num.oldline[line-number=@oldLineNumber])')); var diff = getDiffData($('table[filename="@fileName"] table.diff tr:has(th.line-num.oldline[line-number="@oldLineNumber"])'));
param['diff'] = JSON.stringify(diff); param['diff'] = JSON.stringify(diff);
} }

View File

@@ -16,7 +16,7 @@
</div> </div>
</fieldset> </fieldset>
</form> </form>
@if(context.settings.repositoryOperation.rename || context.loginAccount.get.isAdmin){ @if(context.settings.basicBehavior.repositoryOperation.rename || context.loginAccount.get.isAdmin){
<form id="rename-form" method="post" action="@helpers.url(repository)/settings/rename" validate="true" autocomplete="off"> <form id="rename-form" method="post" action="@helpers.url(repository)/settings/rename" validate="true" autocomplete="off">
<fieldset class="border-top form-group"> <fieldset class="border-top form-group">
<label class="strong">Rename repository</label> <label class="strong">Rename repository</label>
@@ -33,7 +33,7 @@
</fieldset> </fieldset>
</form> </form>
} }
@if(context.settings.repositoryOperation.transfer || context.loginAccount.get.isAdmin){ @if(context.settings.basicBehavior.repositoryOperation.transfer || context.loginAccount.get.isAdmin){
<form id="transfer-form" method="post" action="@helpers.url(repository)/settings/transfer" validate="true" autocomplete="off"> <form id="transfer-form" method="post" action="@helpers.url(repository)/settings/transfer" validate="true" autocomplete="off">
<fieldset class="border-top form-group"> <fieldset class="border-top form-group">
<label class="strong">Transfer Ownership</label> <label class="strong">Transfer Ownership</label>
@@ -50,7 +50,7 @@
</fieldset> </fieldset>
</form> </form>
} }
@if(context.settings.repositoryOperation.delete || context.loginAccount.get.isAdmin){ @if(context.settings.basicBehavior.repositoryOperation.delete || context.loginAccount.get.isAdmin){
<form id="delete-form" method="post" action="@helpers.url(repository)/settings/delete"> <form id="delete-form" method="post" action="@helpers.url(repository)/settings/delete">
<fieldset class="border-top form-group"> <fieldset class="border-top form-group">
<label class="strong">Delete repository</label> <label class="strong">Delete repository</label>

View File

@@ -0,0 +1,55 @@
@(customFields: List[gitbucket.core.model.CustomField],
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main("Issues", Some(repository)) {
@gitbucket.core.html.menu("settings", repository) {
@gitbucket.core.settings.html.menu("issues", repository) {
<div class="pull-right" style="margin-bottom: 10px;">
<a class="btn btn-success" href="javascript:void(0);" id="new-field-button">New field</a>
</div>
<table class="table table-bordered table-hover table-issues" id="new-field-table" style="display: none;">
<tr><td></td></tr>
</table>
<table class="table table-bordered table-hover">
<tr id="field-row-header">
<th>@customFields.size custom fields</th>
</tr>
@customFields.map { customField =>
@gitbucket.core.settings.html.issuesfield(customField)
}
</table>
}
}
}
<script>
$(function(){
$('#new-field-button').click(function(e){
if($('#edit-field-area-new').length != 0){
$('div#edit-field-area-new').remove();
$('#new-field-table').hide();
} else {
$.get('@helpers.url(repository)/settings/issues/fields/new',
function(data){
$('#new-field-table').show().find('tr td').append(data);
}
);
}
});
});
function deleteField(fieldId){
if(confirm('Once you delete this field, there is no going back.\nAre you sure?')){
$.post('@helpers.url(repository)/settings/issues/fields/' + fieldId + '/delete', function(){
$('tr#field-row-' + fieldId).remove();
});
}
}
function editField(fieldId){
$.get('@helpers.url(repository)/settings/issues/fields/' + fieldId + '/edit',
function(data){
$('#field-' + fieldId).hide().parent().append(data);
}
);
}
</script>

View File

@@ -0,0 +1,28 @@
@(customField: gitbucket.core.model.CustomField)
<tr id="field-row-@customField.fieldId">
<td>
<div class="row" id="field-@customField.fieldId">
<div class="col-md-4 strong">
@customField.fieldName
</div>
<div class="col-md-4">
@customField.fieldType
</div>
<div class="col-md-2">
@if(customField.enableForIssues) {
Issues &nbsp;&nbsp;
}
@if(customField.enableForPullRequests) {
Pull requests
}
</div>
<div class="col-md-2">
<div class="pull-right">
<a href="javascript:void(0);" onclick="editField(@customField.fieldId)">Edit</a>
&nbsp;&nbsp;
<a href="javascript:void(0);" onclick="deleteField(@customField.fieldId)">Delete</a>
</div>
</div>
</div>
</td>
</tr>

View File

@@ -0,0 +1,66 @@
@(field: Option[gitbucket.core.model.CustomField],
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@defining(field.map(_.fieldId).getOrElse("new")){ fieldId =>
<div id="edit-field-area-@fieldId">
<form class="form-inline" autocomplete="off">
<input type="text" id="fieldName-@fieldId" style="width: 300px; float: left; margin-right: 4px;" class="form-control input-sm" value="@field.map(_.fieldName)"@if(fieldId == "new"){ placeholder="New field name"}/>
<select id="fieldType-@fieldId" class="form-control input-sm">
<option value="long" @if(field.map(_.fieldType == "long").getOrElse(false)){selected}>Long</option>
<option value="double" @if(field.map(_.fieldType == "double").getOrElse(false)){selected}>Double</option>
<option value="string" @if(field.map(_.fieldType == "string").getOrElse(false)){selected}>String</option>
<option value="date" @if(field.map(_.fieldType == "date").getOrElse(false)){selected}>Date</option>
</select>
<label for="enableForIssues-@fieldId" class="normal" style="margin-left: 4px;">
<input type="checkbox" id="enableForIssues-@fieldId" @if(field.map(_.enableForIssues).getOrElse(false)){checked}> Issues
</label>
<label for="enableForPullRequests-@fieldId" class="normal" style="margin-left: 4px;">
<input type="checkbox" id="enableForPullRequests-@fieldId" @if(field.map(_.enableForPullRequests).getOrElse(false)){checked}> Pull requests
</label>
<span class="pull-right">
<span id="field-error-@fieldId" class="error"></span>
<input type="button" id="cancel-@fieldId" class="btn btn-sm btn-default field-edit-cancel" value="Cancel">
<input type="button" id="submit-@fieldId" class="btn btn-sm btn-success" style="margin-bottom: 0px;" value="@(if(fieldId == "new") "Create field" else "Save changes")"/>
</span>
</form>
</div>
<script>
$(function(){
$('#submit-@fieldId').click(function(e){
$.post('@helpers.url(repository)/settings/issues/fields/@{if(fieldId == "new") "new" else s"$fieldId/edit"}', {
'fieldName' : $('#fieldName-@fieldId').val(),
'fieldType': $('#fieldType-@fieldId option:selected').val(),
'enableForIssues': $('#enableForIssues-@fieldId').prop('checked'),
'enableForPullRequests': $('#enableForPullRequests-@fieldId').prop('checked')
}, function(data, status){
$('div#edit-field-area-@fieldId').remove();
@if(fieldId == "new"){
$('#new-field-table').hide();
// Insert row into the top of table
$('#field-row-header').after(data);
} else {
// Replace table row
$('#field-row-@fieldId').after(data).remove();
}
}).fail(function(xhr, status, error){
const errors = JSON.parse(xhr.responseText);
if(errors.fieldName){
$('span#field-error-@fieldId').text(errors.fieldName);
} else {
$('span#field-error-@fieldId').text('error');
}
});
return false;
});
$('#cancel-@fieldId').click(function(e){
$('div#edit-field-area-@fieldId').remove();
@if(fieldId == "new"){
$('#new-field-table').hide();
} else {
$('#field-@fieldId').show();
}
});
});
</script>
}

View File

@@ -12,6 +12,9 @@
<a href="@helpers.url(repository)/settings/branches">Branches</a> <a href="@helpers.url(repository)/settings/branches">Branches</a>
</li> </li>
} }
<li@if(active=="issues"){ class="active"}>
<a href="@helpers.url(repository)/settings/issues">Issues</a>
</li>
<li@if(active=="hooks"){ class="active"}> <li@if(active=="hooks"){ class="active"}>
<a href="@helpers.url(repository)/settings/hooks">Service Hooks</a> <a href="@helpers.url(repository)/settings/hooks">Service Hooks</a>
</li> </li>

View File

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

View File

@@ -24,6 +24,9 @@
<label for="password">Password:</label> <label for="password">Password:</label>
<span id="error-password" class="error"></span> <span id="error-password" class="error"></span>
<input type="password" name="password" id="password" class="form-control" value="@password"/> <input type="password" name="password" id="password" class="form-control" value="@password"/>
@if(systemSettings.basicBehavior.allowResetPassword){
<a href="@context.path/reset">Forgot password?</a>
}
</div> </div>
<input type="hidden" name="hash"/> <input type="hidden" name="hash"/>
<div> <div>
@@ -33,11 +36,11 @@
</li> </li>
</ul> </ul>
</div> </div>
@if(systemSettings.allowAccountRegistration){ @if(systemSettings.basicBehavior.allowAccountRegistration){
<div class="panel panel-default"> <div class="panel panel-default">
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
<li class="list-group-item text-center"> <li class="list-group-item text-center">
Don't have an account? <a href="@context.path/register">Create one.</a> <a href="@context.path/register">Create account</a>
</li> </li>
</ul> </ul>
</div> </div>

View File

@@ -1526,8 +1526,10 @@ div.markdown-body code {
} }
div.markdown-body table { div.markdown-body table {
/*width: 100%;*/ max-width: 100%;
margin-bottom: 20px; width: max-content;
overflow: auto;
display: block;
} }
div.markdown-body table th, div.markdown-body table th,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 949 B

After

Width:  |  Height:  |  Size: 85 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 691 B

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -31,6 +31,7 @@ class GitBucketCoreModuleSpec extends AnyFunSuite {
val container = new MySQLContainer() { val container = new MySQLContainer() {
override val container = new org.testcontainers.containers.MySQLContainer(s"mysql:$tag") { override val container = new org.testcontainers.containers.MySQLContainer(s"mysql:$tag") {
override def getDriverClassName = "org.mariadb.jdbc.Driver" override def getDriverClassName = "org.mariadb.jdbc.Driver"
override def getJdbcUrl: String = super.getJdbcUrl + "?permitMysqlScheme"
} }
// TODO https://jira.mariadb.org/browse/CONJ-663 // TODO https://jira.mariadb.org/browse/CONJ-663
container.withCommand("mysqld --default-authentication-plugin=mysql_native_password") container.withCommand("mysqld --default-authentication-plugin=mysql_native_password")
@@ -38,7 +39,11 @@ class GitBucketCoreModuleSpec extends AnyFunSuite {
container.start() container.start()
try { try {
new Solidbase().migrate( new Solidbase().migrate(
DriverManager.getConnection(s"${container.jdbcUrl}?useSSL=false", container.username, container.password), DriverManager.getConnection(
container.jdbcUrl,
container.username,
container.password
),
Thread.currentThread().getContextClassLoader(), Thread.currentThread().getContextClassLoader(),
new MySQLDatabase(), new MySQLDatabase(),
new Module(GitBucketCoreModule.getModuleId, GitBucketCoreModule.getVersions) new Module(GitBucketCoreModule.getModuleId, GitBucketCoreModule.getVersions)

View File

@@ -156,7 +156,6 @@ class ApiIntegrationTest extends AnyFunSuite {
val ref = repo.getRef("tags/v1.0") val ref = repo.getRef("tags/v1.0")
assert(ref.getRef == "refs/tags/v1.0") assert(ref.getRef == "refs/tags/v1.0")
assert(ref.getUrl.toString == "http://localhost:19999/api/v3/repos/root/create_status_test/git/refs/tags/v1.0") assert(ref.getUrl.toString == "http://localhost:19999/api/v3/repos/root/create_status_test/git/refs/tags/v1.0")
assert(ref.getObject.getType == "tag")
} }
} }
} }

View File

@@ -116,7 +116,6 @@ object ApiSpecModels {
openedUserName = "bear", openedUserName = "bear",
milestoneId = None, milestoneId = None,
priorityId = None, priorityId = None,
assignedUserName = None,
title = "Found a bug", title = "Found a bug",
content = Some("I'm having a problem with this."), content = Some("I'm having a problem with this."),
closed = false, closed = false,
@@ -226,7 +225,7 @@ object ApiSpecModels {
issue = issue, issue = issue,
repositoryName = repo1Name, repositoryName = repo1Name,
user = apiUser, user = apiUser,
assignee = Some(apiUser), assignees = List(apiUser),
labels = List(apiLabel), labels = List(apiLabel),
milestone = Some(apiMilestone) milestone = Some(apiMilestone)
) )
@@ -235,7 +234,7 @@ object ApiSpecModels {
issue = issue, issue = issue,
repositoryName = repo1Name, repositoryName = repo1Name,
user = apiUser, user = apiUser,
assignee = None, assignees = List.empty,
labels = List(apiLabel), labels = List(apiLabel),
milestone = Some(apiMilestone) milestone = Some(apiMilestone)
) )
@@ -244,7 +243,7 @@ object ApiSpecModels {
issue = issuePR, issue = issuePR,
repositoryName = repo1Name, repositoryName = repo1Name,
user = apiUser, user = apiUser,
assignee = Some(apiUser), assignees = List(apiUser),
labels = List(apiLabel), labels = List(apiLabel),
milestone = Some(apiMilestone) milestone = Some(apiMilestone)
) )
@@ -272,7 +271,7 @@ object ApiSpecModels {
baseRepo = apiRepository, baseRepo = apiRepository,
user = apiUser, user = apiUser,
labels = List(apiLabel), labels = List(apiLabel),
assignee = Some(apiUser), assignees = List(apiUser),
mergedComment = Some((issueComment, account)) mergedComment = Some((issueComment, account))
) )
@@ -540,7 +539,7 @@ object ApiSpecModels {
|"number":1347, |"number":1347,
|"title":"Found a bug", |"title":"Found a bug",
|"user":$jsonUser, |"user":$jsonUser,
|"assignee":$jsonUser, |"assignees":[$jsonUser],
|"labels":[$jsonLabel], |"labels":[$jsonLabel],
|"state":"open", |"state":"open",
|"created_at":"2011-04-14T16:00:49Z", |"created_at":"2011-04-14T16:00:49Z",
@@ -548,7 +547,7 @@ object ApiSpecModels {
|"body":"I'm having a problem with this.", |"body":"I'm having a problem with this.",
|"milestone":$jsonMilestone, |"milestone":$jsonMilestone,
|"id":0, |"id":0,
|"assignees":[$jsonUser], |"assignee":$jsonUser,
|"comments_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/issues/1347/comments", |"comments_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/issues/1347/comments",
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347" |"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347"
|}""".stripMargin |}""".stripMargin
@@ -557,6 +556,7 @@ object ApiSpecModels {
|"number":1347, |"number":1347,
|"title":"Found a bug", |"title":"Found a bug",
|"user":$jsonUser, |"user":$jsonUser,
|"assignees":[],
|"labels":[$jsonLabel], |"labels":[$jsonLabel],
|"state":"open", |"state":"open",
|"created_at":"2011-04-14T16:00:49Z", |"created_at":"2011-04-14T16:00:49Z",
@@ -564,7 +564,6 @@ object ApiSpecModels {
|"body":"I'm having a problem with this.", |"body":"I'm having a problem with this.",
|"milestone":$jsonMilestone, |"milestone":$jsonMilestone,
|"id":0, |"id":0,
|"assignees":[],
|"comments_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/issues/1347/comments", |"comments_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/issues/1347/comments",
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347" |"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347"
|}""".stripMargin |}""".stripMargin
@@ -573,7 +572,7 @@ object ApiSpecModels {
|"number":1347, |"number":1347,
|"title":"new-feature", |"title":"new-feature",
|"user":$jsonUser, |"user":$jsonUser,
|"assignee":$jsonUser, |"assignees":[$jsonUser],
|"labels":[$jsonLabel], |"labels":[$jsonLabel],
|"state":"closed", |"state":"closed",
|"created_at":"2011-04-14T16:00:49Z", |"created_at":"2011-04-14T16:00:49Z",
@@ -581,12 +580,12 @@ object ApiSpecModels {
|"body":"Please pull these awesome changes", |"body":"Please pull these awesome changes",
|"milestone":$jsonMilestone, |"milestone":$jsonMilestone,
|"id":0, |"id":0,
|"assignees":[$jsonUser], |"assignee":$jsonUser,
|"comments_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/issues/1347/comments", |"comments_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/issues/1347/comments",
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347", |"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347",
|"pull_request":{ |"pull_request":{
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/pulls/1347", |"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/pulls/1347",
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347"} |"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347"}
|}""".stripMargin |}""".stripMargin
val jsonPullRequest = s"""{ val jsonPullRequest = s"""{
@@ -603,9 +602,10 @@ object ApiSpecModels {
|"body":"Please pull these awesome changes", |"body":"Please pull these awesome changes",
|"user":$jsonUser, |"user":$jsonUser,
|"labels":[$jsonLabel], |"labels":[$jsonLabel],
|"assignee":$jsonUser, |"assignees":[$jsonUser],
|"draft":true, |"draft":true,
|"id":0, |"id":0,
|"assignee":$jsonUser,
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347", |"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347",
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/pulls/1347", |"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/pulls/1347",
|"commits_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/pulls/1347/commits", |"commits_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/pulls/1347/commits",
@@ -680,7 +680,7 @@ object ApiSpecModels {
val jsonCommits = s"""{ val jsonCommits = s"""{
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", |"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e",
|"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e", |"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e",
|"html_url":"http://gitbucket.exmple.comoctocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e", |"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e",
|"comment_url":"http://gitbucket.exmple.com", |"comment_url":"http://gitbucket.exmple.com",
|"commit":{ |"commit":{
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", |"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e",

View File

@@ -8,11 +8,18 @@ import liquibase.database.jvm.JdbcConnection
import gitbucket.core.model._ import gitbucket.core.model._
import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.model.Profile.profile.blockingApi._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import java.sql.DriverManager import java.sql.DriverManager
import java.io.File import java.io.File
import gitbucket.core.controller.Context import gitbucket.core.controller.Context
import gitbucket.core.service.SystemSettingsService.{RepositoryOperation, RepositoryViewerSettings, Ssh, SystemSettings} import gitbucket.core.service.SystemSettingsService.{
BasicBehavior,
RepositoryOperation,
RepositoryViewerSettings,
Ssh,
SystemSettings
}
import javax.servlet.http.{HttpServletRequest, HttpSession} import javax.servlet.http.{HttpServletRequest, HttpSession}
import org.mockito.Mockito._ import org.mockito.Mockito._
@@ -32,19 +39,22 @@ trait ServiceSpecBase {
SystemSettings( SystemSettings(
baseUrl = None, baseUrl = None,
information = None, information = None,
allowAccountRegistration = false, basicBehavior = BasicBehavior(
allowAnonymousAccess = true, allowAccountRegistration = false,
isCreateRepoOptionPublic = true, allowResetPassword = false,
repositoryOperation = RepositoryOperation( allowAnonymousAccess = true,
create = true, isCreateRepoOptionPublic = true,
delete = true, repositoryOperation = RepositoryOperation(
rename = true, create = true,
transfer = true, delete = true,
fork = true rename = true,
transfer = true,
fork = true
),
gravatar = false,
notification = false,
limitVisibleRepositories = false,
), ),
gravatar = false,
notification = false,
limitVisibleRepositories = false,
ssh = Ssh( ssh = Ssh(
enabled = false, enabled = false,
bindAddress = None, bindAddress = None,
@@ -132,7 +142,6 @@ trait ServiceSpecBase {
loginUser = loginUser, loginUser = loginUser,
title = "issue title", title = "issue title",
content = None, content = None,
assignedUserName = None,
milestoneId = None, milestoneId = None,
priorityId = None, priorityId = None,
isPullRequest = true isPullRequest = true

View File

@@ -91,7 +91,7 @@ class WebHookJsonFormatSpec extends AnyFunSuite {
action = "closed", action = "closed",
issue = issuePR, issue = issuePR,
issueUser = account, issueUser = account,
assignee = Some(account), assignees = List(account),
pullRequest = pullRequest, pullRequest = pullRequest,
headRepository = repositoryInfo, headRepository = repositoryInfo,
headOwner = account, headOwner = account,
@@ -119,7 +119,7 @@ class WebHookJsonFormatSpec extends AnyFunSuite {
commentUser = account, commentUser = account,
repository = repositoryInfo, repository = repositoryInfo,
repositoryUser = account, repositoryUser = account,
assignedUser = Some(account), assignees = List(account),
sender = account, sender = account,
labels = List(label), labels = List(label),
milestone = Some(apiMilestone) milestone = Some(apiMilestone)
@@ -140,7 +140,7 @@ class WebHookJsonFormatSpec extends AnyFunSuite {
comment = commitComment, comment = commitComment,
issue = issuePR, issue = issuePR,
issueUser = account, issueUser = account,
assignee = Some(account), assignees = List(account),
pullRequest = pullRequest, pullRequest = pullRequest,
headRepository = repositoryInfo, headRepository = repositoryInfo,
headOwner = account, headOwner = account,

View File

@@ -2,12 +2,12 @@ package gitbucket.core.view
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import javax.servlet.http.{HttpServletRequest, HttpSession} import javax.servlet.http.{HttpServletRequest, HttpSession}
import gitbucket.core.controller.Context import gitbucket.core.controller.Context
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.service.RequestCache import gitbucket.core.service.RequestCache
import gitbucket.core.service.SystemSettingsService.{ import gitbucket.core.service.SystemSettingsService.{
BasicBehavior,
RepositoryOperation, RepositoryOperation,
RepositoryViewerSettings, RepositoryViewerSettings,
Ssh, Ssh,
@@ -151,19 +151,22 @@ class AvatarImageProviderSpec extends AnyFunSpec {
SystemSettings( SystemSettings(
baseUrl = None, baseUrl = None,
information = None, information = None,
allowAccountRegistration = false, basicBehavior = BasicBehavior(
allowAnonymousAccess = true, allowAccountRegistration = false,
isCreateRepoOptionPublic = true, allowResetPassword = false,
repositoryOperation = RepositoryOperation( allowAnonymousAccess = true,
create = true, isCreateRepoOptionPublic = true,
delete = true, repositoryOperation = RepositoryOperation(
rename = true, create = true,
transfer = true, delete = true,
fork = true rename = true,
transfer = true,
fork = true
),
gravatar = useGravatar,
notification = false,
limitVisibleRepositories = false
), ),
gravatar = useGravatar,
notification = false,
limitVisibleRepositories = false,
ssh = Ssh( ssh = Ssh(
enabled = false, enabled = false,
bindAddress = None, bindAddress = None,

View File

@@ -1,6 +1,5 @@
package gitbucket.core.view package gitbucket.core.view
import gitbucket.core.util.SyntaxSugars
import org.scalatest.funspec.AnyFunSpec import org.scalatest.funspec.AnyFunSpec
class PaginationSpec extends AnyFunSpec { class PaginationSpec extends AnyFunSpec {