Compare commits

...

89 Commits

Author SHA1 Message Date
Naoki Takezoe
df69f88186 Release 4.39.0 (#3277) 2023-04-29 01:46:13 +09:00
Naoki Takezoe
8d1323f354 Delete records in ISSUE_ASSIGNEE when repository is deleted (#3276) 2023-04-24 02:26:21 +09:00
Naoki Takezoe
2f598b618b Fix issues in git refs APIs (#3275) 2023-04-23 22:52:31 +09:00
Scala Steward
baf0b0b92c Update oauth2-oidc-sdk to 10.8 2023-04-23 03:07:02 +09:00
Scala Steward
27a75250a6 Update logback-classic to 1.3.7 2023-04-20 06:32:21 +09:00
Scala Steward
15f60402a5 Update oauth2-oidc-sdk to 10.7.2 2023-04-17 17:49:56 +09:00
Scala Steward
41c6fc90b3 Update testcontainers-scala to 0.40.15 2023-04-17 00:50:22 +09:00
Scala Steward
34356b04a8 Update mysql, postgresql to 1.18.0 2023-04-05 02:14:51 +09:00
Scala Steward
2ca02b6539 Update oauth2-oidc-sdk to 10.7.1 2023-03-31 03:06:07 +09:00
Scala Steward
cd0c71dffb Update commons-compress to 1.23.0 2023-03-23 07:28:32 +09:00
Scala Steward
a59120fe19 Update testcontainers-scala to 0.40.14 2023-03-22 06:59:19 +09:00
Naoki Takezoe
fdc35f48ed Update the developer doc (#3257) 2023-03-19 11:48:07 +09:00
Naoki Takezoe
bae9b7ddc3 Ignore signed-commit verification error (#3256) 2023-03-18 21:58:15 +09:00
Scala Steward
3dd9b7e587 Update postgresql to 42.6.0 2023-03-18 07:48:38 +09:00
Scala Steward
44c905bdab Update logback-classic to 1.3.6 2023-03-17 13:08:58 +09:00
Scala Steward
5214040257 Update jetty-continuation, jetty-http, ... to 9.4.51.v20230217 2023-02-28 07:00:10 +09:00
Scala Steward
7ad9f901dd Update github-api to 1.314 2023-02-27 07:09:28 +09:00
Naoki Takezoe
f472d52954 Add --disable_news_feed option to disable News Feed (#3246) 2023-02-27 02:17:52 +09:00
Naoki Takezoe
1e752af41b Update directory.md 2023-02-26 11:25:50 +09:00
Naoki Takezoe
3ba46c3fc6 Update directory.md 2023-02-26 11:25:30 +09:00
Naoki Takezoe
bf83da476f Update directory.md 2023-02-26 11:21:26 +09:00
Naoki Takezoe
6b8c4cf8d0 Add --disable_cache option to disable cache (#3245) 2023-02-24 09:42:17 +09:00
Scala Steward
445329c07a Update postgresql to 42.5.4 2023-02-17 04:14:01 +09:00
Scala Steward
8f370e19c6 Update oauth2-oidc-sdk to 10.7 2023-02-16 21:44:11 +09:00
Scala Steward
736dbcfb58 Update oauth2-oidc-sdk to 10.6 2023-02-16 03:53:15 +09:00
Scala Steward
c1cb7f87e0 Update oauth2-oidc-sdk to 10.5.2 2023-02-14 07:22:34 +09:00
Scala Steward
3c14fcefc9 Update sbt-assembly to 2.1.1 2023-02-12 15:54:07 +09:00
Scala Steward
831badf8db Update tika-core to 2.7.0 2023-02-04 09:06:43 +09:00
Scala Steward
5f8a6e8d24 Update postgresql to 42.5.3 2023-02-04 07:54:40 +09:00
Scala Steward
a3981493f7 Update postgresql to 42.5.2 2023-02-01 07:33:53 +09:00
Scala Steward
6919cf5d4d Update oauth2-oidc-sdk to 10.5.1 2023-01-27 09:50:55 +09:00
kenji yoshida
f6d1e6bdd6 pin mockito 4.x
https://github.com/mockito/mockito/pull/2804
2023-01-14 18:11:18 +09:00
Scala Steward
a13ff89acd Update oauth2-oidc-sdk to 10.5 2023-01-13 07:20:28 +09:00
Naoki Takezoe
cd5c76279a Fix input JSON schema of add/replace labels to an issue API (#3226) 2023-01-13 02:41:48 +09:00
Scala Steward
debff5e4b8 Update thumbnailator to 0.4.19 2023-01-01 06:56:02 +09:00
Scala Steward
433e207ec5 Update mockito-core to 4.11.0 2022-12-29 01:38:44 +09:00
Naoki Takezoe
3775f6a907 Fix NoSuchElementException in delete issue comment API (#3220) 2022-12-26 09:48:05 +09:00
Naoki Takezoe
10d611c0eb Call OpenID connect logout endpoint when signed-out on GitBucket (#3219) 2022-12-26 09:41:29 +09:00
takezoe
963bc4d672 Change the log file path in local dev mode 2022-12-18 22:53:20 +09:00
Naoki Takezoe
e68a21ee30 Enum support in custom fields (#3195) 2022-12-18 22:46:11 +09:00
Naoki Takezoe
d5c083b70f Upgrade logback-classic to 1.3.5 (#3218) 2022-12-18 19:39:06 +09:00
Scala Steward
2deb9cf417 Update mockito-core to 4.10.0 2022-12-15 07:49:15 +09:00
Scala Steward
fca0cfcdc7 Update oauth2-oidc-sdk to 10.4 2022-12-13 22:09:35 +09:00
Scala Steward
1466e1bdb3 Update oauth2-oidc-sdk to 10.3.1 2022-12-13 20:58:47 +09:00
Naoki Takezoe
dd48bc443a Add option to keep session in the local dev mode (#3205) 2022-12-12 08:22:41 +09:00
Scala Steward
f455738e5f Update sbt-assembly to 2.1.0 2022-12-10 07:37:40 +09:00
Scala Steward
85193803cd Update jetty-continuation, jetty-http, ... to 9.4.50.v20221201 2022-12-08 08:58:51 +09:00
Scala Steward
4e90a6074a Update oauth2-oidc-sdk to 10.3 2022-12-07 08:02:34 +09:00
Scala Steward
ca94fa5184 Update sbt-pgp to 2.2.1 2022-12-06 14:45:21 +09:00
Scala Steward
f14a7c996f Update testcontainers-scala to 0.40.12 2022-12-05 04:53:03 +09:00
Scala Steward
989d22f4d8 Update httpclient to 4.5.14 2022-12-04 22:46:38 +09:00
Scala Steward
400a812343 Update commons-net to 3.9.0 2022-12-02 09:11:27 +09:00
Scala Steward
97284f1ced Update postgresql to 42.5.1 2022-11-24 03:45:39 +09:00
Scala Steward
5e6a0d7e16 Update mysql, postgresql to 1.17.6 2022-11-16 23:28:52 +09:00
Scala Steward
599e11245f Update apache-sshd to 2.9.2 2022-11-16 07:35:21 +09:00
Scala Steward
538d714c96 Update sbt-scalafmt to 2.5.0 2022-11-15 08:53:31 +09:00
Scala Steward
953915ba2a Update mockito-core to 4.9.0 2022-11-15 05:12:27 +09:00
Naoki Takezoe
1a2f5da055 Suppress "scanned from multiple locations" warnings in development (#3194) 2022-11-12 20:17:56 +09:00
Scala Steward
749a469d37 Update tika-core to 2.6.0 2022-11-08 03:15:07 +09:00
scala-steward-bot
c7d084321a Update mariadb-java-client to 2.7.6 (#3190) 2022-11-07 13:33:23 +09:00
Naoki Takezoe
00a61cd6cf Downgrade and pin MariaDB JDBC driver version (#3189) 2022-11-05 15:58:18 +09:00
Naoki Takezoe
d9c6c13c62 Merge 4.38.4 release notes into master (#3187) 2022-11-02 13:30:02 +09:00
Scala Steward
5260c5e889 Update commons-compress to 1.22 2022-11-01 06:47:55 +09:00
Scala Steward
1700f96c62 Update sbt-pgp to 2.2.0 2022-10-30 20:47:40 +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
50 changed files with 1153 additions and 559 deletions

3
.gitignore vendored
View File

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

View File

@@ -3,7 +3,9 @@ updates.limit = 3
updates.includeScala = true
updates.pin = [
{ groupId = "org.mockito", version = "4." }
{ groupId = "org.eclipse.jetty", version = "9." }
{ groupId = "org.eclipse.jgit", version = "5." }
{ groupId = "com.zaxxer", version = "4." }
{ groupId = "org.mariadb.jdbc", version = "2." }
]

View File

@@ -1,6 +1,19 @@
# Changelog
All changes to the project will be documented in this file.
## 4.39.0 - 29 Apr 2023
- Support enum type in custom fields of Issues and Pull requests
- Hide large diffs by default
- Add new options to make it possible to run GitBucket using multiple machines
- Fix many API issues
## 4.38.4 - 2 Nov 2022
- Downgrade MariaDB JDBC drive to avoid unknown error
## 4.38.3 - 30 Oct 2022
- Fix several issues around multiple assignees in issues and pull requests
- Fix IllegalStateException when returning unknown avatar image
## 4.38.2 - 20 Sep 2022
- Resurrect assignee icons on the issue list

View File

@@ -59,25 +59,12 @@ Support
- If you can't find same question and report, send it to our [Gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles.
What's New in 4.38.x
What's New in 4.39.x
-------------
## 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.39.0 - 29 Apr 2023
- Support enum type in custom fields of Issues and Pull requests
- Hide large diffs by default
- Add new options to make it possible to run GitBucket using multiple machines
- Fix many API issues
See the [change log](CHANGELOG.md) for all of the updates.

View File

@@ -3,9 +3,9 @@ import com.jsuereth.sbtpgp.PgpKeys._
val Organization = "io.github.gitbucket"
val Name = "gitbucket"
val GitBucketVersion = "4.38.2"
val ScalatraVersion = "2.8.2"
val JettyVersion = "9.4.49.v20220914"
val GitBucketVersion = "4.39.0"
val ScalatraVersion = "2.8.4"
val JettyVersion = "9.4.51.v20230217"
val JgitVersion = "5.13.1.202206130422-r"
lazy val root = (project in file("."))
@@ -15,9 +15,9 @@ sourcesInBase := false
organization := Organization
name := Name
version := GitBucketVersion
scalaVersion := "2.13.9"
scalaVersion := "2.13.10"
scalafmtOnCompile := true
// scalafmtOnCompile := true
coverageExcludedPackages := ".*\\.html\\..*"
@@ -33,42 +33,42 @@ libraryDependencies ++= Seq(
"org.scalatra" %% "scalatra" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.scalatra" %% "scalatra-json" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.scalatra" %% "scalatra-forms" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.json4s" %% "json4s-jackson" % "4.0.5" cross CrossVersion.for3Use2_13,
"org.json4s" %% "json4s-jackson" % "4.0.6" cross CrossVersion.for3Use2_13,
"commons-io" % "commons-io" % "2.11.0",
"io.github.gitbucket" % "solidbase" % "1.0.5",
"io.github.gitbucket" % "markedj" % "1.0.17",
"org.apache.commons" % "commons-compress" % "1.21",
"org.apache.commons" % "commons-compress" % "1.23.0",
"org.apache.commons" % "commons-email" % "1.5",
"commons-net" % "commons-net" % "3.8.0",
"org.apache.httpcomponents" % "httpclient" % "4.5.13",
"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.4.1",
"commons-net" % "commons-net" % "3.9.0",
"org.apache.httpcomponents" % "httpclient" % "4.5.14",
"org.apache.sshd" % "apache-sshd" % "2.9.2" exclude ("org.slf4j", "slf4j-jdk14") exclude ("org.apache.sshd", "sshd-mina") exclude ("org.apache.sshd", "sshd-netty"),
"org.apache.tika" % "tika-core" % "2.7.0",
"com.github.takezoe" %% "blocking-slick-32" % "0.0.12" cross CrossVersion.for3Use2_13,
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.199",
"org.mariadb.jdbc" % "mariadb-java-client" % "3.0.7",
"org.postgresql" % "postgresql" % "42.5.0",
"ch.qos.logback" % "logback-classic" % "1.2.11",
"org.mariadb.jdbc" % "mariadb-java-client" % "2.7.6",
"org.postgresql" % "postgresql" % "42.6.0",
"ch.qos.logback" % "logback-classic" % "1.3.7",
"com.zaxxer" % "HikariCP" % "4.0.3" exclude ("org.slf4j", "slf4j-api"),
"com.typesafe" % "config" % "1.4.2",
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0",
"io.github.java-diff-utils" % "java-diff-utils" % "4.12",
"org.cache2k" % "cache2k-all" % "1.6.0.Final",
"net.coobird" % "thumbnailator" % "0.4.17",
"net.coobird" % "thumbnailator" % "0.4.19",
"com.github.zafarkhaja" % "java-semver" % "0.9.0",
"com.nimbusds" % "oauth2-oidc-sdk" % "9.43.1",
"com.nimbusds" % "oauth2-oidc-sdk" % "10.8",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"junit" % "junit" % "4.13.2" % "test",
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test" cross CrossVersion.for3Use2_13,
"org.mockito" % "mockito-core" % "4.8.0" % "test",
"com.dimafeng" %% "testcontainers-scala" % "0.40.10" % "test",
"org.testcontainers" % "mysql" % "1.17.3" % "test",
"org.testcontainers" % "postgresql" % "1.17.3" % "test",
"org.mockito" % "mockito-core" % "4.11.0" % "test",
"com.dimafeng" %% "testcontainers-scala" % "0.40.15" % "test",
"org.testcontainers" % "mysql" % "1.18.0" % "test",
"org.testcontainers" % "postgresql" % "1.18.0" % "test",
"net.i2p.crypto" % "eddsa" % "0.3.0",
"is.tagomor.woothee" % "woothee-java" % "1.11.0",
"org.ec4j.core" % "ec4j-core" % "0.3.0",
"org.kohsuke" % "github-api" % "1.308" % "test"
"org.kohsuke" % "github-api" % "1.314" % "test"
)
libraryDependencies ~= {
@@ -90,7 +90,6 @@ scalacOptions := Seq(
"-Wconf:cat=unused&src=twirl/.*:s,cat=unused&src=scala/gitbucket/core/model/[^/]+\\.scala:s"
)
compile / javacOptions ++= Seq("-target", "8", "-source", "8")
Jetty / javaOptions += "-Dlogback.configurationFile=/logback-dev.xml"
// Test settings
//testOptions in Test += Tests.Argument("-l", "ExternalDBTest")
@@ -286,6 +285,9 @@ Test / testOptions ++= {
}
Jetty / javaOptions ++= Seq(
"-Dlogback.configurationFile=/logback-dev.xml",
"-Xdebug",
"-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000"
"-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000",
"-Dorg.eclipse.jetty.annotations.AnnotationParser.LEVEL=OFF",
//"-Ddev-features=keep-session"
)

View File

@@ -18,7 +18,21 @@ $ sbt ~jetty:start
Then access `http://localhost:8080/` in your browser. The default administrator account is `root` and password is `root`.
Source code modifications are detected and a reloading happens automatically. You can modify the logging configuration by editing `src/main/resources/logback-dev.xml`.
Source code modifications are detected and a reloading happens automatically.
You can modify the logging configuration by editing `src/main/resources/logback-dev.xml`.
Note that HttpSession is cleared when auto-reloading happened.
This is a bit annoying when developing features that requires sign-in.
You can keep HttpSession even if GitBucket is restarted by enabling this configuration in `build.sbt`:
https://github.com/gitbucket/gitbucket/blob/d5c083b70f7f3748d080166252e9a3dcaf579648/build.sbt#L292
Or by launching GitBucket with the following command:
```shell
sbt '; set Jetty/javaOptions += "-Ddev-features=keep-session" ; ~jetty:start'
```
Note that this feature serializes HttpSession on the local disk and assigns all requests to the same session
which means you cannot test multi users behavior in this mode.
Build war file
--------
@@ -37,7 +51,8 @@ To build an executable war file, run
$ sbt executable
```
at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. We release this war file as release artifact.
at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`.
We release this war file as release artifact.
Run tests spec
---------

View File

@@ -5,13 +5,17 @@ GitBucket persists all data into __HOME/.gitbucket__ in default (In 1.9 or befor
This directory has following structure:
```
* /HOME/gitbucket
* /HOME/.gitbucket
* gitbucket.conf
* database.conf
* activity.log
* data.mv.db, data.trace.db (H2 data files if the default embed H2 is used)
* /repositories
* /USER_NAME
* /REPO_NAME.git (substance of repository. GitServlet sees this directory)
* /REPO_NAME.wiki.git (wiki repository)
* /REPO_NAME
* /issues (files which are attached to issue)
* /issues (files attached to issue)
* /lfs (LFS managed files)
* /data
* /USER_NAME
@@ -20,6 +24,8 @@ This directory has following structure:
* /plugins
* plugin.jar
* /.installed (copied available plugins from the parent directory automatically)
* /sessions
* HTTP session data created (if '--save_sessions' option is used in the standalone mode)
* /tmp
* /_upload
* /SESSION_ID (removed at session timeout)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

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

View File

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

View File

@@ -65,9 +65,15 @@ public class JettyLauncher {
boolean saveSessions = false;
for(String arg: args) {
if(arg.equals("--save_sessions")) {
if (arg.equals("--save_sessions")) {
saveSessions = true;
}
if (arg.equals("--disable_news_feed")) {
System.setProperty("gitbucket.disableNewsFeed", "true");
}
if (arg.equals("--disable_cache")) {
System.setProperty("gitbucket.disableCache", "true");
}
if(arg.startsWith("--") && arg.contains("=")) {
String[] dim = arg.split("=", 2);
if(dim.length == 2) {

View File

@@ -7,7 +7,7 @@
</appender>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>gitbucket.log</file>
<file>.tmp/gitbucket.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
@@ -23,4 +23,4 @@
<logger name="scala.slick.jdbc.JdbcBackend.statement" level="DEBUG" />
-->
</configuration>
</configuration>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<addColumn tableName="CUSTOM_FIELD">
<column name="CONSTRAINTS" type="varchar(200)" nullable="true"/>
</addColumn>
</changeSet>

View File

@@ -113,5 +113,8 @@ object GitBucketCoreModule
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.2"),
new Version("4.38.3"),
new Version("4.38.4"),
new Version("4.39.0", new LiquibaseMigration("update/gitbucket-core_4.39.xml")),
)

View File

@@ -0,0 +1,6 @@
package gitbucket.core.api
/**
* https://developer.github.com/v3/issues/labels/#add-labels-to-an-issue
*/
case class AddLabelsToAnIssue(labels: Seq[String])

View File

@@ -1,7 +1,6 @@
package gitbucket.core.controller
import java.io.{File, FileInputStream}
import java.io.{File, FileInputStream, FileOutputStream}
import gitbucket.core.api.{ApiError, JsonFormat}
import gitbucket.core.model.Account
import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService}
@@ -14,9 +13,9 @@ import org.scalatra._
import org.scalatra.i18n._
import org.scalatra.json._
import org.scalatra.forms._
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import javax.servlet.{FilterChain, ServletRequest, ServletResponse}
import is.tagomor.woothee.Classifier
import scala.util.Try
@@ -29,6 +28,9 @@ import org.eclipse.jgit.treewalk._
import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
import org.json4s.Formats
import org.json4s.jackson.Serialization
import java.nio.charset.StandardCharsets
/**
* Provides generic features for controller implementations.
@@ -93,8 +95,16 @@ abstract class ControllerBase
}
}
private def LoginAccount: Option[Account] =
request.getAs[Account](Keys.Session.LoginAccount).orElse(session.getAs[Account](Keys.Session.LoginAccount))
private def LoginAccount: Option[Account] = {
request
.getAs[Account](Keys.Session.LoginAccount)
.orElse(session.getAs[Account](Keys.Session.LoginAccount))
.orElse {
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
getLoginAccountFromLocalFile()
} else None
}
}
def ajaxGet(path: String)(action: => Any): Route =
super.get(path) {
@@ -277,6 +287,47 @@ abstract class ControllerBase
}
}
}
protected object DevFeatures {
val KeepSession = "keep-session"
}
private val loginAccountFile = new File(".tmp/login_account.json")
protected def isDevFeatureEnabled(feature: String): Boolean = {
Option(System.getProperty("dev-features")).getOrElse("").split(",").map(_.trim).contains(feature)
}
protected def getLoginAccountFromLocalFile(): Option[Account] = {
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
if (loginAccountFile.exists()) {
Using.resource(new FileInputStream(loginAccountFile)) { in =>
val json = IOUtils.toString(in, StandardCharsets.UTF_8)
val account = parse(json).extract[Account]
session.setAttribute(Keys.Session.LoginAccount, account)
Some(parse(json).extract[Account])
}
} else None
} else None
}
protected def saveLoginAccountToLocalFile(account: Account): Unit = {
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
if (!loginAccountFile.getParentFile.exists()) {
loginAccountFile.getParentFile.mkdirs()
}
Using.resource(new FileOutputStream(loginAccountFile)) { in =>
in.write(Serialization.write(account).getBytes(StandardCharsets.UTF_8))
}
}
}
protected def deleteLoginAccountFromLocalFile(): Unit = {
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
loginAccountFile.delete()
}
}
}
/**

View File

@@ -6,6 +6,7 @@ import gitbucket.core.service._
import gitbucket.core.util.{Keys, UsersAuthenticator}
import gitbucket.core.util.Implicits._
import gitbucket.core.service.IssuesService._
import gitbucket.core.service.ActivityService._
class DashboardController
extends DashboardControllerBase
@@ -42,7 +43,7 @@ trait DashboardControllerBase extends ControllerBase {
withoutPhysicalInfo = true,
limit = context.settings.basicBehavior.limitVisibleRepositories
)
html.repos(getGroupNames(loginAccount.userName), repos, repos)
html.repos(getGroupNames(loginAccount.userName), repos, repos, isNewsFeedEnabled())
}
})
@@ -130,7 +131,8 @@ trait DashboardControllerBase extends ControllerBase {
None,
withoutPhysicalInfo = true,
limit = context.settings.basicBehavior.limitVisibleRepositories
)
),
isNewsFeedEnabled()
)
}
@@ -172,7 +174,8 @@ trait DashboardControllerBase extends ControllerBase {
None,
withoutPhysicalInfo = true,
limit = context.settings.basicBehavior.limitVisibleRepositories
)
),
isNewsFeedEnabled()
)
}

View File

@@ -1,7 +1,8 @@
package gitbucket.core.controller
import java.net.URI
import com.nimbusds.jwt.JWT
import java.net.URI
import com.nimbusds.oauth2.sdk.id.State
import com.nimbusds.openid.connect.sdk.Nonce
import gitbucket.core.helper.xml
@@ -13,6 +14,8 @@ import gitbucket.core.view.helpers._
import org.scalatra.Ok
import org.scalatra.forms._
import gitbucket.core.service.ActivityService._
class IndexController
extends IndexControllerBase
with RepositoryService
@@ -57,30 +60,37 @@ trait IndexControllerBase extends ControllerBase {
//
// case class SearchForm(query: String, owner: String, repository: String)
case class OidcContext(state: State, nonce: Nonce, redirectBackURI: String)
case class OidcAuthContext(state: State, nonce: Nonce, redirectBackURI: String)
case class OidcSessionContext(token: JWT)
get("/") {
context.loginAccount
.map { account =>
val visibleOwnerSet: Set[String] = Set(account.userName) ++ getGroupsByUserName(account.userName)
gitbucket.core.html.index(
getRecentActivitiesByOwners(visibleOwnerSet),
getVisibleRepositories(
Some(account),
None,
withoutPhysicalInfo = true,
limit = context.settings.basicBehavior.limitVisibleRepositories
),
showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken(
account.userName
if (!isNewsFeedEnabled()) {
redirect("/dashboard/repos")
} else {
gitbucket.core.html.index(
activities = getRecentActivitiesByOwners(visibleOwnerSet),
recentRepositories = getVisibleRepositories(
Some(account),
None,
withoutPhysicalInfo = true,
limit = context.settings.basicBehavior.limitVisibleRepositories
),
showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken(
account.userName
),
enableNewsFeed = isNewsFeedEnabled()
)
)
}
}
.getOrElse {
gitbucket.core.html.index(
getRecentPublicActivities(),
getVisibleRepositories(None, withoutPhysicalInfo = true),
showBannerToCreatePersonalAccessToken = false
activities = getRecentPublicActivities(),
recentRepositories = getVisibleRepositories(None, withoutPhysicalInfo = true),
showBannerToCreatePersonalAccessToken = false,
enableNewsFeed = isNewsFeedEnabled()
)
}
}
@@ -120,8 +130,8 @@ trait IndexControllerBase extends ControllerBase {
case _ => "/"
}
session.setAttribute(
Keys.Session.OidcContext,
OidcContext(authenticationRequest.getState, authenticationRequest.getNonce, redirectBackURI)
Keys.Session.OidcAuthContext,
OidcAuthContext(authenticationRequest.getState, authenticationRequest.getNonce, redirectBackURI)
)
redirect(authenticationRequest.toURI.toString)
} getOrElse {
@@ -135,10 +145,12 @@ trait IndexControllerBase extends ControllerBase {
get("/signin/oidc") {
context.settings.oidc.map { oidc =>
val redirectURI = new URI(s"$baseUrl/signin/oidc")
session.get(Keys.Session.OidcContext) match {
case Some(context: OidcContext) =>
authenticate(params.toMap, redirectURI, context.state, context.nonce, oidc).map { account =>
signin(account, context.redirectBackURI)
session.get(Keys.Session.OidcAuthContext) match {
case Some(context: OidcAuthContext) =>
authenticate(params.toMap, redirectURI, context.state, context.nonce, oidc).map {
case (jwt, account) =>
session.setAttribute(Keys.Session.OidcSessionContext, OidcSessionContext(jwt))
signin(account, context.redirectBackURI)
} orElse {
flash.update("error", "Sorry, authentication failed. Please try again.")
session.invalidate()
@@ -155,7 +167,19 @@ trait IndexControllerBase extends ControllerBase {
}
get("/signout") {
context.settings.oidc.map { oidc =>
session.get(Keys.Session.OidcSessionContext).foreach {
case context: OidcSessionContext =>
val redirectURI = new URI(baseUrl)
val authenticationRequest = createOIDLogoutRequest(oidc.issuer, oidc.clientID, redirectURI, context.token)
session.invalidate
redirect(authenticationRequest.toURI.toString)
}
}
session.invalidate
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
deleteLoginAccountFromLocalFile()
}
redirect("/")
}
@@ -178,6 +202,9 @@ trait IndexControllerBase extends ControllerBase {
*/
private def signin(account: Account, redirectUrl: String = "/") = {
session.setAttribute(Keys.Session.LoginAccount, account)
if (isDevFeatureEnabled(DevFeatures.KeepSession)) {
saveLoginAccountToLocalFile(account)
}
updateLastLoginDate(account.userName)
if (LDAPUtil.isDummyMailAddress(account)) {

View File

@@ -69,7 +69,7 @@ trait PullRequestsControllerBase extends ControllerBase {
"commitIdFrom" -> trim(text(required, maxlength(40))),
"commitIdTo" -> trim(text(required, maxlength(40))),
"isDraft" -> trim(boolean(required)),
"assignedUserName" -> trim(optional(text())),
"assigneeUserNames" -> trim(optional(text())),
"milestoneId" -> trim(optional(number())),
"priorityId" -> trim(optional(number())),
"labelNames" -> trim(optional(text()))
@@ -92,7 +92,7 @@ trait PullRequestsControllerBase extends ControllerBase {
commitIdFrom: String,
commitIdTo: String,
isDraft: Boolean,
assignedUserNames: Option[String],
assigneeUserNames: Option[String],
milestoneId: Option[Int],
priorityId: Option[Int],
labelNames: Option[String]
@@ -144,25 +144,6 @@ trait PullRequestsControllerBase extends ControllerBase {
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName),
flash.iterator.map(f => f._1 -> f._2.toString).toMap
)
// html.pullreq(
// issue,
// pullreq,
// comments,
// getIssueLabels(owner, name, issueId),
// getAssignableUserNames(owner, name),
// getMilestonesWithIssueCount(owner, name),
// getPriorities(owner, name),
// getLabels(owner, name),
// commits,
// diffs,
// isEditable(repository),
// isManageable(repository),
// hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount),
// repository,
// getRepository(pullreq.requestUserName, pullreq.requestRepositoryName),
// flash.toMap.map(f => f._1 -> f._2.toString)
// )
}
} getOrElse NotFound()
})
@@ -396,9 +377,9 @@ trait PullRequestsControllerBase extends ControllerBase {
})
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
val headBranch: Option[String] = params.get("head")
val headBranch = params.get("head")
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => {
case (Some(originUserName), Some(originRepositoryName)) =>
getRepository(originUserName, originRepositoryName).map {
originRepository =>
Using.resources(
@@ -415,8 +396,7 @@ trait PullRequestsControllerBase extends ControllerBase {
)
}
} getOrElse NotFound()
}
case _ => {
case _ =>
Using.resource(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))) { git =>
JGitUtil.getDefaultBranch(git, forkedRepository).map {
case (_, defaultBranch) =>
@@ -427,41 +407,48 @@ trait PullRequestsControllerBase extends ControllerBase {
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
}
}
}
}
})
private def getOriginRepositoryName(
originOwner: String,
forkedOwner: String,
forkedRepository: RepositoryInfo
): Option[String] = {
if (originOwner == forkedOwner) {
// Self repository
Some(forkedRepository.name)
} else if (forkedRepository.repository.originUserName.isEmpty) {
// when ForkedRepository is the original repository
getForkedRepositories(forkedRepository.owner, forkedRepository.name)
.find(_.userName == originOwner)
.map(_.repositoryName)
} else if (Some(originOwner) == forkedRepository.repository.originUserName) {
// Original repository
forkedRepository.repository.originRepositoryName
} else {
// Sibling repository
getUserRepositories(originOwner)
.find { x =>
x.repository.originUserName == forkedRepository.repository.originUserName &&
x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName
}
.map(_.repository.repositoryName)
}
}
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner)
val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner)
(for (originRepositoryName <- if (originOwner == forkedOwner) {
// Self repository
Some(forkedRepository.name)
} else if (forkedRepository.repository.originUserName.isEmpty) {
// when ForkedRepository is the original repository
getForkedRepositories(forkedRepository.owner, forkedRepository.name)
.find(_.userName == originOwner)
.map(_.repositoryName)
} else if (Some(originOwner) == forkedRepository.repository.originUserName) {
// Original repository
forkedRepository.repository.originRepositoryName
} else {
// Sibling repository
getUserRepositories(originOwner)
.find { x =>
x.repository.originUserName == forkedRepository.repository.originUserName &&
x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName
}
.map(_.repository.repositoryName)
};
(for (originRepositoryName <- getOriginRepositoryName(originOwner, forkedOwner, forkedRepository);
originRepository <- getRepository(originOwner, originRepositoryName)) yield {
val (oldId, newId) =
getPullRequestCommitFromTo(originRepository, forkedRepository, originId, forkedId)
(oldId, newId) match {
case (Some(oldId), Some(newId)) => {
case (Some(oldId), Some(newId)) =>
val (commits, diffs) = getRequestCompareInfo(
originRepository.owner,
originRepository.name,
@@ -512,7 +499,6 @@ trait PullRequestsControllerBase extends ControllerBase {
getLabels(originRepository.owner, originRepository.name),
getCustomFields(originRepository.owner, originRepository.name).filter(_.enableForPullRequests)
)
}
case (oldId, newId) =>
redirect(
s"/${forkedRepository.owner}/${forkedRepository.name}/compare/" +
@@ -524,6 +510,54 @@ trait PullRequestsControllerBase extends ControllerBase {
}) getOrElse NotFound()
})
ajaxGet("/:owner/:repository/diff/:id")(referrersOnly { repository =>
(for {
commitId <- params.get("id")
path <- params.get("path")
diff <- getSingleDiff(repository.owner, repository.name, commitId, path)
} yield {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map(
"oldContent" -> diff.oldContent,
"newContent" -> diff.newContent
)
)
}) getOrElse NotFound()
})
ajaxGet("/:owner/:repository/diff/*...*")(referrersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner)
val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner)
(for {
path <- params.get("path")
originRepositoryName <- getOriginRepositoryName(originOwner, forkedOwner, forkedRepository)
originRepository <- getRepository(originOwner, originRepositoryName)
(oldId, newId) = getPullRequestCommitFromTo(originRepository, forkedRepository, originId, forkedId)
oldId <- oldId
newId <- newId
diff <- getSingleDiff(
originRepository.owner,
originRepository.name,
oldId.getName,
forkedRepository.owner,
forkedRepository.name,
newId.getName,
path
)
} yield {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map(
"oldContent" -> diff.oldContent,
"newContent" -> diff.newContent
)
)
}) getOrElse NotFound()
})
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(readableUsersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifier(origin, forkedRepository.owner)
@@ -593,7 +627,7 @@ trait PullRequestsControllerBase extends ControllerBase {
if (manageable) {
// insert assignees
form.assignedUserNames.foreach { value =>
form.assigneeUserNames.foreach { value =>
value.split(",").foreach { userName =>
registerIssueAssignee(repository.owner, repository.name, issueId, userName)
}

View File

@@ -126,6 +126,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
case class CustomFieldForm(
fieldName: String,
fieldType: String,
constraints: Option[String],
enableForIssues: Boolean,
enableForPullRequests: Boolean
)
@@ -133,6 +134,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
val customFieldForm = mapping(
"fieldName" -> trim(label("Field name", text(required, maxlength(100)))),
"fieldType" -> trim(label("Field type", text(required))),
"constraints" -> trim(label("Constraints", optional(text()))),
"enableForIssues" -> trim(label("Enable for issues", boolean(required))),
"enableForPullRequests" -> trim(label("Enable for pull requests", boolean(required))),
)(CustomFieldForm.apply)
@@ -511,6 +513,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
repository.name,
form.fieldName,
form.fieldType,
if (form.fieldType == "enum") form.constraints else None,
form.enableForIssues,
form.enableForPullRequests
)
@@ -533,6 +536,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
params("fieldId").toInt,
form.fieldName,
form.fieldType,
if (form.fieldType == "enum") form.constraints else None,
form.enableForIssues,
form.enableForPullRequests
)

View File

@@ -331,15 +331,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
post("/:owner/:repository/upload", uploadForm)(writableUsersOnly { (form, repository) =>
def _commit(
branchName: String,
//files: Seq[CommitFile],
newFiles: Seq[CommitFile],
loginAccount: Account
): Either[String, ObjectId] = {
commitFiles(
repository = repository,
branch = branchName,
//path = form.path,
//files = files.toIndexedSeq,
message = form.message.getOrElse("Add files via upload"),
loginAccount = loginAccount,
settings = context.settings
@@ -614,7 +611,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} else {
_commit(form.branch, loginAccount) match {
case Right(_) =>
if (form.path.length == 0) {
if (form.path.isEmpty) {
redirect(s"/${repository.owner}/${repository.name}/tree/${encodeRefName(form.branch)}")
} else {
redirect(

View File

@@ -57,22 +57,27 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
* iii. Create a reference
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-reference
*/
post("/api/v3/repos/:owner/:repository/git/refs")(referrersOnly { repository =>
post("/api/v3/repos/:owner/:repository/git/refs")(writableUsersOnly { repository =>
extractFromJsonBody[CreateARef].map {
data =>
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.owner))) { git =>
val ref = git.getRepository.findRef(data.ref)
if (ref == null) {
val update = git.getRepository.updateRef(data.ref)
update.setNewObjectId(ObjectId.fromString(data.sha))
val result = update.update()
result match {
case Result.NEW => JsonFormat(ApiRef.fromRef(RepositoryName(repository.owner, repository.name), ref))
case _ => UnprocessableEntity(result.name())
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
git =>
val ref = git.getRepository.findRef(data.ref)
if (ref == null) {
val update = git.getRepository.updateRef(data.ref)
update.setNewObjectId(ObjectId.fromString(data.sha))
val result = update.update()
result match {
case Result.NEW =>
JsonFormat(
ApiRef
.fromRef(RepositoryName(repository.owner, repository.name), git.getRepository.findRef(data.ref))
)
case _ => UnprocessableEntity(result.name())
}
} else {
UnprocessableEntity("Ref already exists.")
}
} else {
UnprocessableEntity("Ref already exists.")
}
}
} getOrElse BadRequest()
})
@@ -85,7 +90,7 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
val refName = multiParams("splat").mkString("/")
extractFromJsonBody[UpdateARef].map {
data =>
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.owner))) { git =>
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val ref = git.getRepository.findRef(refName)
if (ref == null) {
UnprocessableEntity("Ref does not exist.")
@@ -96,7 +101,7 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
val result = update.update()
result match {
case Result.FORCED | Result.FAST_FORWARD | Result.NO_CHANGE =>
JsonFormat(ApiRef.fromRef(RepositoryName(repository), update.getRef))
JsonFormat(ApiRef.fromRef(RepositoryName(repository), git.getRepository.findRef(refName)))
case _ => UnprocessableEntity(result.name())
}
}

View File

@@ -85,7 +85,7 @@ trait ApiIssueCommentControllerBase extends ControllerBase {
* iv. Delete a comment
* https://docs.github.com/en/rest/reference/issues#delete-an-issue-comment
*/
delete("/api/v3/repos/:owner/:repo/issues/comments/:id")(readableUsersOnly { repository =>
delete("/api/v3/repos/:owner/:repository/issues/comments/:id")(readableUsersOnly { repository =>
val maybeDeleteResponse: Option[Either[ActionResult, Option[Int]]] =
for {
commentId <- params("id").toIntOpt

View File

@@ -1,5 +1,5 @@
package gitbucket.core.controller.api
import gitbucket.core.api.{ApiError, ApiLabel, CreateALabel, JsonFormat}
import gitbucket.core.api.{AddLabelsToAnIssue, ApiError, ApiLabel, CreateALabel, JsonFormat}
import gitbucket.core.controller.ControllerBase
import gitbucket.core.service._
import gitbucket.core.util.Implicits._
@@ -121,10 +121,10 @@ trait ApiIssueLabelControllerBase extends ControllerBase {
*/
post("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository =>
JsonFormat(for {
data <- extractFromJsonBody[Seq[String]]
data <- extractFromJsonBody[AddLabelsToAnIssue]
issueId <- params("id").toIntOpt
} yield {
data.map { labelName =>
data.labels.map { labelName =>
val label = getLabel(repository.owner, repository.name, labelName).getOrElse(
getLabel(
repository.owner,
@@ -160,11 +160,11 @@ trait ApiIssueLabelControllerBase extends ControllerBase {
*/
put("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository =>
JsonFormat(for {
data <- extractFromJsonBody[Seq[String]]
data <- extractFromJsonBody[AddLabelsToAnIssue]
issueId <- params("id").toIntOpt
} yield {
deleteAllIssueLabels(repository.owner, repository.name, issueId, true)
data.map { labelName =>
data.labels.map { labelName =>
val label = getLabel(repository.owner, repository.name, labelName).getOrElse(
getLabel(
repository.owner,

View File

@@ -5,6 +5,7 @@ import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util.StringUtil
import gitbucket.core.view.helpers
import org.scalatra.i18n.Messages
import play.twirl.api.Html
trait CustomFieldComponent extends TemplateComponent { self: Profile =>
import profile.api._
@@ -15,10 +16,11 @@ trait CustomFieldComponent extends TemplateComponent { self: Profile =>
val fieldId = column[Int]("FIELD_ID", O AutoInc)
val fieldName = column[String]("FIELD_NAME")
val fieldType = column[String]("FIELD_TYPE")
val constraints = column[Option[String]]("CONSTRAINTS")
val enableForIssues = column[Boolean]("ENABLE_FOR_ISSUES")
val enableForPullRequests = column[Boolean]("ENABLE_FOR_PULL_REQUESTS")
def * =
(userName, repositoryName, fieldId, fieldName, fieldType, enableForIssues, enableForPullRequests)
(userName, repositoryName, fieldId, fieldName, fieldType, constraints, enableForIssues, enableForPullRequests)
.<>(CustomField.tupled, CustomField.unapply)
def byPrimaryKey(userName: String, repositoryName: String, fieldId: Int) =
@@ -31,17 +33,28 @@ case class CustomField(
repositoryName: String,
fieldId: Int = 0,
fieldName: String,
fieldType: String, // long, double, string, or date
fieldType: String, // long, double, string, date, or enum
constraints: Option[String],
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)(
def createHtml(repository: RepositoryInfo, fieldId: Int, fieldName: String, constraints: Option[String])(
implicit context: Context
): String
def validate(name: String, value: String, messages: Messages): Option[String]
def fieldHtml(
repository: RepositoryInfo,
issueId: Int,
fieldId: Int,
fieldName: String,
constraints: Option[String],
value: String,
editable: Boolean
)(
implicit context: Context
): String
def validate(name: String, constraints: Option[String], value: String, messages: Messages): Option[String]
}
object CustomFieldBehavior {
@@ -49,7 +62,7 @@ object CustomFieldBehavior {
if (value.isEmpty) None
else {
CustomFieldBehavior(field.fieldType).flatMap { behavior =>
behavior.validate(field.fieldName, value, messages)
behavior.validate(field.fieldName, field.constraints, value, messages)
}
}
}
@@ -60,12 +73,18 @@ object CustomFieldBehavior {
case "double" => Some(DoubleFieldBehavior)
case "string" => Some(StringFieldBehavior)
case "date" => Some(DateFieldBehavior)
case "enum" => Some(EnumFieldBehavior)
case _ => None
}
}
case object LongFieldBehavior extends TextFieldBehavior {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
override def validate(
name: String,
constraints: Option[String],
value: String,
messages: Messages
): Option[String] = {
try {
value.toLong
None
@@ -75,7 +94,12 @@ object CustomFieldBehavior {
}
}
case object DoubleFieldBehavior extends TextFieldBehavior {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
override def validate(
name: String,
constraints: Option[String],
value: String,
messages: Messages
): Option[String] = {
try {
value.toDouble
None
@@ -89,7 +113,12 @@ object CustomFieldBehavior {
private val pattern = "yyyy-MM-dd"
override protected val fieldType: String = "date"
override def validate(name: String, value: String, messages: Messages): Option[String] = {
override def validate(
name: String,
constraints: Option[String],
value: String,
messages: Messages
): Option[String] = {
try {
new java.text.SimpleDateFormat(pattern).parse(value)
None
@@ -100,10 +129,142 @@ object CustomFieldBehavior {
}
}
case object EnumFieldBehavior extends CustomFieldBehavior {
override def createHtml(repository: RepositoryInfo, fieldId: Int, fieldName: String, constraints: Option[String])(
implicit context: Context
): String = {
createPulldownHtml(repository, fieldId, fieldName, constraints, None, None)
}
override def fieldHtml(
repository: RepositoryInfo,
issueId: Int,
fieldId: Int,
fieldName: String,
constraints: Option[String],
value: String,
editable: Boolean
)(implicit context: Context): String = {
if (!editable) {
val sb = new StringBuilder
sb.append("""</div>""")
sb.append("""<div>""")
if (value == "") {
sb.append(s"""<span id="label-custom-field-$fieldId"><span class="muted small">No ${StringUtil.escapeHtml(
fieldName
)}</span></span>""")
} else {
sb.append(s"""<span id="label-custom-field-$fieldId"><span class="muted small">${StringUtil
.escapeHtml(value)}</span></span>""")
}
sb.toString()
} else {
createPulldownHtml(repository, fieldId, fieldName, constraints, Some(issueId), Some(value))
}
}
private def createPulldownHtml(
repository: RepositoryInfo,
fieldId: Int,
fieldName: String,
constraints: Option[String],
issueId: Option[Int],
value: Option[String]
)(implicit context: Context): String = {
val sb = new StringBuilder
sb.append("""<div class="pull-right">""")
sb.append(
gitbucket.core.helper.html
.dropdown("Edit", right = true, filter = (fieldName, s"Filter $fieldName")) {
val options = new StringBuilder()
options.append(
s"""<li><a href="javascript:void(0);" class="custom-field-option-$fieldId" data-value=""><i class="octicon octicon-x"></i> Clear ${StringUtil
.escapeHtml(fieldName)}</a></li>"""
)
constraints.foreach {
x =>
x.split(",").map(_.trim).foreach {
item =>
options.append(s"""<li>
| <a href="javascript:void(0);" class="custom-field-option-$fieldId" data-value="${StringUtil
.escapeHtml(item)}">
| ${gitbucket.core.helper.html.checkicon(value.contains(item))}
| ${StringUtil.escapeHtml(item)}
| </a>
|</li>
|""".stripMargin)
}
}
Html(options.toString())
}
.toString()
)
sb.append("""</div>""")
sb.append("""</div>""")
sb.append("""<div>""")
value match {
case None =>
sb.append(s"""<span id="label-custom-field-$fieldId"><span class="muted small">No ${StringUtil.escapeHtml(
fieldName
)}</span></span>""")
case Some(value) =>
sb.append(s"""<span id="label-custom-field-$fieldId"><span class="muted small">${StringUtil
.escapeHtml(value)}</span></span>""")
}
if (value.isEmpty || issueId.isEmpty) {
sb.append(s"""<input type="hidden" id="custom-field-$fieldId" name="custom-field-$fieldId" value=""/>""")
sb.append(s"""<script>
|$$('a.custom-field-option-$fieldId').click(function(){
| const value = $$(this).data('value');
| $$('a.custom-field-option-$fieldId i.octicon-check').removeClass('octicon-check');
| $$('#custom-field-$fieldId').val(value);
| if (value == '') {
| $$('#label-custom-field-$fieldId').html($$('<span class="muted small">').text('No ${StringUtil
.escapeHtml(fieldName)}'));
| } else {
| $$('#label-custom-field-$fieldId').html($$('<span class="muted small">').text(value));
| $$('a.custom-field-option-$fieldId[data-value=' + value + '] i').addClass('octicon-check');
| }
|});
|</script>""".stripMargin)
} else {
sb.append(s"""<script>
|$$('a.custom-field-option-$fieldId').click(function(){
| const value = $$(this).data('value');
| $$.post('${helpers.url(repository)}/issues/${issueId.get}/customfield/$fieldId',
| { value: value },
| function(data){
| $$('a.custom-field-option-$fieldId i.octicon-check').removeClass('octicon-check');
| if (value == '') {
| $$('#label-custom-field-$fieldId').html($$('<span class="muted small">').text('No ${StringUtil
.escapeHtml(fieldName)}'));
| } else {
| $$('#label-custom-field-$fieldId').html($$('<span class="muted small">').text(value));
| $$('a.custom-field-option-$fieldId[data-value=' + value + '] i').addClass('octicon-check');
| }
| }
| );
|});
|</script>
|""".stripMargin)
}
sb.toString()
}
override def validate(
name: String,
constraints: Option[String],
value: String,
messages: Messages
): Option[String] = None
}
trait TextFieldBehavior extends CustomFieldBehavior {
protected val fieldType = "text"
def createHtml(repository: RepositoryInfo, fieldId: Int)(implicit context: Context): String = {
override def createHtml(repository: RepositoryInfo, fieldId: Int, fieldName: String, constraints: Option[String])(
implicit context: Context
): String = {
val sb = new StringBuilder
sb.append(
s"""<input type="$fieldType" class="form-control input-sm" id="custom-field-$fieldId" name="custom-field-$fieldId" data-field-id="$fieldId" style="width: 120px;"/>"""
@@ -111,8 +272,7 @@ object CustomFieldBehavior {
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,
| $$.post('${helpers.url(repository)}/issues/customfield_validation/$fieldId',
| { value: $$this.val() },
| function(data){
| if (data != '') {
@@ -128,7 +288,15 @@ object CustomFieldBehavior {
sb.toString()
}
def fieldHtml(repository: RepositoryInfo, issueId: Int, fieldId: Int, value: String, editable: Boolean)(
override def fieldHtml(
repository: RepositoryInfo,
issueId: Int,
fieldId: Int,
fieldName: String,
constraints: Option[String],
value: String,
editable: Boolean
)(
implicit context: Context
): String = {
val sb = new StringBuilder
@@ -149,15 +317,14 @@ object CustomFieldBehavior {
|
|$$('#custom-field-$fieldId-editor').focusout(function(){
| const $$this = $$(this);
| const fieldId = $$this.data('field-id');
| $$.post('${helpers.url(repository)}/issues/customfield_validation/' + fieldId,
| $$.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,
| $$.post('${helpers.url(repository)}/issues/$issueId/customfield/$fieldId',
| { value: $$this.val() },
| function(data){
| $$this.hide();
@@ -186,6 +353,11 @@ object CustomFieldBehavior {
sb.toString()
}
def validate(name: String, value: String, messages: Messages): Option[String] = None
override def validate(
name: String,
constraints: Option[String],
value: String,
messages: Messages
): Option[String] = None
}
}

View File

@@ -9,10 +9,12 @@ import org.json4s.jackson.Serialization.{read, write}
import scala.util.Using
import java.io.FileOutputStream
import java.nio.charset.StandardCharsets
import gitbucket.core.controller.Context
import gitbucket.core.util.ConfigUtil
import org.apache.commons.io.input.ReversedLinesFileReader
import ActivityService._
import scala.collection.mutable.ListBuffer
trait ActivityService {
@@ -27,7 +29,7 @@ trait ActivityService {
}
def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit context: Context): List[Activity] = {
if (!ActivityLog.exists()) {
if (!isNewsFeedEnabled() || !ActivityLog.exists()) {
List.empty
} else {
val list = new ListBuffer[Activity]
@@ -51,7 +53,7 @@ trait ActivityService {
}
def getRecentPublicActivities()(implicit context: Context): List[Activity] = {
if (!ActivityLog.exists()) {
if (!isNewsFeedEnabled() || !ActivityLog.exists()) {
List.empty
} else {
val list = new ListBuffer[Activity]
@@ -69,7 +71,7 @@ trait ActivityService {
}
def getRecentActivitiesByOwners(owners: Set[String])(implicit context: Context): List[Activity] = {
if (!ActivityLog.exists()) {
if (!isNewsFeedEnabled() || !ActivityLog.exists()) {
List.empty
} else {
val list = new ListBuffer[Activity]
@@ -93,3 +95,8 @@ trait ActivityService {
writeLog(info.toActivity)
}
}
object ActivityService {
def isNewsFeedEnabled(): Boolean =
!ConfigUtil.getConfigValue[Boolean]("gitbucket.disableNewsFeed").getOrElse(false)
}

View File

@@ -28,6 +28,7 @@ trait CustomFieldsService {
repository: String,
fieldName: String,
fieldType: String,
constraints: Option[String],
enableForIssues: Boolean,
enableForPullRequests: Boolean
)(implicit s: Session): Int = {
@@ -36,6 +37,7 @@ trait CustomFieldsService {
repositoryName = repository,
fieldName = fieldName,
fieldType = fieldType,
constraints = constraints,
enableForIssues = enableForIssues,
enableForPullRequests = enableForPullRequests
)
@@ -47,6 +49,7 @@ trait CustomFieldsService {
fieldId: Int,
fieldName: String,
fieldType: String,
constraints: Option[String],
enableForIssues: Boolean,
enableForPullRequests: Boolean
)(
@@ -54,8 +57,8 @@ trait CustomFieldsService {
): Unit =
CustomFields
.filter(_.byPrimaryKey(owner, repository, fieldId))
.map(t => (t.fieldName, t.fieldType, t.enableForIssues, t.enableForPullRequests))
.update((fieldName, fieldType, enableForIssues, enableForPullRequests))
.map(t => (t.fieldName, t.fieldType, t.constraints, t.enableForIssues, t.enableForPullRequests))
.update((fieldName, fieldType, constraints, enableForIssues, enableForPullRequests))
def deleteCustomField(owner: String, repository: String, fieldId: Int)(implicit s: Session): Unit = {
IssueCustomFields

View File

@@ -242,14 +242,17 @@ trait IssuesService {
case (issue, commentCount, _, _, _, milestone, priority, commitId, _) =>
IssueInfo(
issue,
issues.flatMap { t =>
t._3.map(Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get))
} toList,
issues
.flatMap { t =>
t._3.map(Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get))
}
.distinct
.toList,
milestone,
priority,
commentCount,
commitId,
issues.flatMap(_._9)
issues.flatMap(_._9).distinct
)
}
} toList
@@ -961,39 +964,39 @@ object IssuesService {
def nonEmpty: Boolean = !isEmpty
def toFilterString: String =
(
List(
Some(s"is:${state}"),
author.map(author => s"author:${author}"),
assigned.map(assignee => s"assignee:${assignee}"),
mentioned.map(mentioned => s"mentions:${mentioned}")
).flatten ++
labels.map(label => s"label:${label}") ++
List(
milestone.map {
case Some(x) => s"milestone:${x}"
case None => "no:milestone"
},
priority.map {
case Some(x) => s"priority:${x}"
case None => "no:priority"
},
(sort, direction) match {
case ("created", "desc") => None
case ("created", "asc") => Some("sort:created-asc")
case ("comments", "desc") => Some("sort:comments-desc")
case ("comments", "asc") => Some("sort:comments-asc")
case ("updated", "desc") => Some("sort:updated-desc")
case ("updated", "asc") => Some("sort:updated-asc")
case ("priority", "desc") => Some("sort:priority-desc")
case ("priority", "asc") => Some("sort:priority-asc")
case x => throw new MatchError(x)
},
visibility.map(visibility => s"visibility:${visibility}")
).flatten ++
groups.map(group => s"group:${group}")
).mkString(" ")
// def toFilterString: String =
// (
// List(
// Some(s"is:${state}"),
// author.map(author => s"author:${author}"),
// assigned.map(assignee => s"assignee:${assignee}"),
// mentioned.map(mentioned => s"mentions:${mentioned}")
// ).flatten ++
// labels.map(label => s"label:${label}") ++
// List(
// milestone.map {
// case Some(x) => s"milestone:${x}"
// case None => "no:milestone"
// },
// priority.map {
// case Some(x) => s"priority:${x}"
// case None => "no:priority"
// },
// (sort, direction) match {
// case ("created", "desc") => None
// case ("created", "asc") => Some("sort:created-asc")
// case ("comments", "desc") => Some("sort:comments-desc")
// case ("comments", "asc") => Some("sort:comments-asc")
// case ("updated", "desc") => Some("sort:updated-desc")
// case ("updated", "asc") => Some("sort:updated-asc")
// case ("priority", "desc") => Some("sort:priority-desc")
// case ("priority", "asc") => Some("sort:priority-asc")
// case x => throw new MatchError(x)
// },
// visibility.map(visibility => s"visibility:${visibility}")
// ).flatten ++
// groups.map(group => s"group:${group}")
// ).mkString(" ")
def toURL: String =
"?" + List(

View File

@@ -1,11 +1,11 @@
package gitbucket.core.service
import java.net.URI
import com.nimbusds.jose.JWSAlgorithm.Family
import com.nimbusds.jose.proc.BadJOSEException
import com.nimbusds.jose.util.DefaultResourceRetriever
import com.nimbusds.jose.{JOSEException, JWSAlgorithm}
import com.nimbusds.jwt.JWT
import com.nimbusds.oauth2.sdk._
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic
import com.nimbusds.oauth2.sdk.id.{ClientID, Issuer, State}
@@ -52,6 +52,11 @@ trait OpenIDConnectService {
)
}
def createOIDLogoutRequest(issuer: Issuer, clientID: ClientID, redirectURI: URI, token: JWT): LogoutRequest = {
val metadata = OIDCProviderMetadata.resolve(issuer)
new LogoutRequest(metadata.getEndSessionEndpointURI, token, null, clientID, redirectURI, null, null)
}
/**
* Proceed the OpenID Connect authentication.
*
@@ -60,7 +65,7 @@ trait OpenIDConnectService {
* @param state State saved in the session
* @param nonce Nonce saved in the session
* @param oidc OIDC settings
* @return ID token
* @return (ID token, GitBucket account)
*/
def authenticate(
params: Map[String, String],
@@ -68,22 +73,25 @@ trait OpenIDConnectService {
state: State,
nonce: Nonce,
oidc: SystemSettingsService.OIDC
)(implicit s: Session): Option[Account] =
)(implicit s: Session): Option[(JWT, Account)] =
validateOIDCAuthenticationResponse(params, state, redirectURI) flatMap { authenticationResponse =>
obtainOIDCToken(authenticationResponse.getAuthorizationCode, nonce, redirectURI, oidc) flatMap { claims =>
Seq("email", "preferred_username", "name").map(k => Option(claims.getStringClaim(k))) match {
case Seq(Some(email), preferredUsername, name) =>
getOrCreateFederatedUser(
claims.getIssuer.getValue,
claims.getSubject.getValue,
email,
preferredUsername,
name
)
case _ =>
logger.info(s"OIDC ID token must have an email claim: claims=${claims.toJSONObject}")
None
}
obtainOIDCToken(authenticationResponse.getAuthorizationCode, nonce, redirectURI, oidc) flatMap {
case (jwt, claims) =>
Seq("email", "preferred_username", "name").map(k => Option(claims.getStringClaim(k))) match {
case Seq(Some(email), preferredUsername, name) =>
getOrCreateFederatedUser(
claims.getIssuer.getValue,
claims.getSubject.getValue,
email,
preferredUsername,
name
).map { account =>
(jwt, account)
}
case _ =>
logger.info(s"OIDC ID token must have an email claim: claims=${claims.toJSONObject}")
None
}
}
}
@@ -136,7 +144,7 @@ trait OpenIDConnectService {
nonce: Nonce,
redirectURI: URI,
oidc: SystemSettingsService.OIDC
): Option[IDTokenClaimsSet] = {
): Option[(JWT, IDTokenClaimsSet)] = {
val metadata = OIDCProviderMetadata.resolve(oidc.issuer)
val tokenRequest = new TokenRequest(
metadata.getTokenEndpointURI,
@@ -173,7 +181,7 @@ trait OpenIDConnectService {
metadata: OIDCProviderMetadata,
nonce: Nonce,
oidc: SystemSettingsService.OIDC
): Option[IDTokenClaimsSet] =
): Option[(JWT, IDTokenClaimsSet)] =
Option(response.getOIDCTokens.getIDToken) match {
case Some(jwt) =>
val validator = oidc.jwsAlgorithm map { jwsAlgorithm =>
@@ -188,7 +196,7 @@ trait OpenIDConnectService {
new IDTokenValidator(metadata.getIssuer, oidc.clientID)
}
try {
Some(validator.validate(jwt, nonce))
Some((jwt, validator.validate(jwt, nonce)))
} catch {
case e @ (_: BadJOSEException | _: JOSEException) =>
logger.info(s"OIDC ID token has error: $e")

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(
userName: String,
repositoryName: String,

View File

@@ -254,6 +254,7 @@ trait RepositoryService {
Labels.filter(_.byRepository(userName, repositoryName)).delete
IssueComments.filter(_.byRepository(userName, repositoryName)).delete
PullRequests.filter(_.byRepository(userName, repositoryName)).delete
IssueAssignees.filter(_.byRepository(userName, repositoryName)).delete
Issues.filter(_.byRepository(userName, repositoryName)).delete
Priorities.filter(_.byRepository(userName, repositoryName)).delete
IssueId.filter(_.byRepository(userName, repositoryName)).delete

View File

@@ -1,8 +1,7 @@
package gitbucket.core.util
import java.io.ByteArrayInputStream
import scala.jdk.CollectionConverters._
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
import org.bouncycastle.bcpg.ArmoredInputStream
@@ -34,29 +33,33 @@ object GpgUtil {
}
def verifySign(signInfo: JGitUtil.GpgSignInfo)(implicit s: Session): Option[JGitUtil.GpgVerifyInfo] = {
new BcPGPObjectFactory(new ArmoredInputStream(new ByteArrayInputStream(signInfo.signArmored)))
.iterator()
.asScala
.flatMap {
case signList: PGPSignatureList =>
signList
.iterator()
.asScala
.flatMap { sign =>
getGpgKey(sign.getKeyID)
.map { pubKey =>
sign.init(new BcPGPContentVerifierBuilderProvider, pubKey)
sign.update(signInfo.target)
(sign, pubKey)
}
.collect {
case (sign, pubKey) if sign.verify() =>
JGitUtil.GpgVerifyInfo(pubKey.getUserIDs.next, pubKey.getKeyID.toHexString.toUpperCase)
}
}
try {
new BcPGPObjectFactory(new ArmoredInputStream(new ByteArrayInputStream(signInfo.signArmored)))
.iterator()
.asScala
.flatMap {
case signList: PGPSignatureList =>
signList
.iterator()
.asScala
.flatMap { sign =>
getGpgKey(sign.getKeyID)
.map { pubKey =>
sign.init(new BcPGPContentVerifierBuilderProvider, pubKey)
sign.update(signInfo.target)
(sign, pubKey)
}
.collect {
case (sign, pubKey) if sign.verify() =>
JGitUtil.GpgVerifyInfo(pubKey.getUserIDs.next, pubKey.getKeyID.toHexString.toUpperCase)
}
}
}
.toList
.headOption
} catch {
case _: Throwable => None
}
}
.toList
.headOption
}
}

View File

@@ -1,7 +1,6 @@
package gitbucket.core.util
import java.io._
import gitbucket.core.service.RepositoryService
import org.eclipse.jgit.api.Git
import Directory._
@@ -18,10 +17,10 @@ import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.errors.{ConfigInvalidException, IncorrectObjectTypeException, MissingObjectException}
import org.eclipse.jgit.transport.RefSpec
import java.util.Date
import java.util.concurrent.TimeUnit
import org.cache2k.Cache2kBuilder
import org.cache2k.{Cache, Cache2kBuilder}
import org.eclipse.jgit.api.errors._
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter, RawTextComparator}
import org.eclipse.jgit.dircache.DirCacheEntry
@@ -37,9 +36,12 @@ object JGitUtil {
private val logger = LoggerFactory.getLogger(JGitUtil.getClass)
implicit val objectDatabaseReleasable: Releasable[ObjectDatabase] =
private implicit val objectDatabaseReleasable: Releasable[ObjectDatabase] =
_.close()
private def isCacheEnabled(): Boolean =
!ConfigUtil.getConfigValue[Boolean]("gitbucket.disableCache").getOrElse(false)
/**
* The repository data.
*
@@ -284,26 +286,34 @@ object JGitUtil {
revCommit
}
private val cache = new Cache2kBuilder[String, Int]() {}
.name("commit-count")
.expireAfterWrite(24, TimeUnit.HOURS)
.entryCapacity(10000)
.build()
private val cache: Cache[String, Int] = if (isCacheEnabled()) {
Cache2kBuilder
.of(classOf[String], classOf[Int])
.name("commit-count")
.expireAfterWrite(24, TimeUnit.HOURS)
.entryCapacity(10000)
.build()
} else null
private val objectCommitCache = new Cache2kBuilder[ObjectId, RevCommit]() {}
.name("object-commit")
.entryCapacity(10000)
.build()
private val objectCommitCache: Cache[ObjectId, RevCommit] = if (isCacheEnabled()) {
Cache2kBuilder
.of(classOf[ObjectId], classOf[RevCommit])
.name("object-commit")
.entryCapacity(10000)
.build()
} else null
def removeCache(git: Git): Unit = {
val dir = git.getRepository.getDirectory
val keyPrefix = dir.getAbsolutePath + "@"
if (isCacheEnabled()) {
val dir = git.getRepository.getDirectory
val keyPrefix = dir.getAbsolutePath + "@"
cache.keys.forEach(key => {
if (key.startsWith(keyPrefix)) {
cache.remove(key)
}
})
cache.keys.forEach(key => {
if (key.startsWith(keyPrefix)) {
cache.remove(key)
}
})
}
}
/**
@@ -312,16 +322,23 @@ object JGitUtil {
*/
def getCommitCount(git: Git, branch: String, max: Int = 10001): Int = {
val dir = git.getRepository.getDirectory
val key = dir.getAbsolutePath + "@" + branch
val entry = cache.getEntry(key)
if (entry == null) {
if (isCacheEnabled()) {
val key = dir.getAbsolutePath + "@" + branch
val entry = cache.getEntry(key)
if (entry == null) {
val commitId = git.getRepository.resolve(branch)
val commitCount = git.log.add(commitId).call.iterator.asScala.take(max).size
cache.put(key, commitCount)
commitCount
} else {
entry.getValue
}
} else {
val commitId = git.getRepository.resolve(branch)
val commitCount = git.log.add(commitId).call.iterator.asScala.take(max).size
cache.put(key, commitCount)
commitCount
} else {
entry.getValue
}
}
@@ -444,7 +461,7 @@ object JGitUtil {
(id, mode, name, path, opt, None)
} else if (commitCount < 10000) {
(id, mode, name, path, opt, Some(getCommit(path)))
} else {
} else if (isCacheEnabled()) {
// Use in-memory cache if the commit count is too big.
val cached = objectCommitCache.getEntry(id)
if (cached == null) {
@@ -454,6 +471,9 @@ object JGitUtil {
} else {
(id, mode, name, path, opt, Some(cached.getValue))
}
} else {
val commit = getCommit(path)
(id, mode, name, path, opt, Some(commit))
}
}
}
@@ -690,7 +710,7 @@ object JGitUtil {
val toCommit = revWalk.parseCommit(git.getRepository.resolve(to))
(from match {
case None => {
case None =>
toCommit.getParentCount match {
case 0 =>
df.scan(
@@ -700,11 +720,9 @@ object JGitUtil {
.asScala
case _ => df.scan(toCommit.getParent(0), toCommit.getTree).asScala
}
}
case Some(from) => {
case Some(from) =>
val fromCommit = revWalk.parseCommit(git.getRepository.resolve(from))
df.scan(fromCommit.getTree, toCommit.getTree).asScala
}
}).toSeq
}
}
@@ -719,6 +737,29 @@ object JGitUtil {
}
}
def getDiff(git: Git, from: Option[String], to: String, path: String): Option[DiffInfo] = {
getDiffEntries(git, from, to).find(_.getNewPath == path).map { diff =>
val oldIsImage = FileUtil.isImage(diff.getOldPath)
val newIsImage = FileUtil.isImage(diff.getNewPath)
val includeContent = oldIsImage || newIsImage
DiffInfo(
changeType = diff.getChangeType,
oldPath = diff.getOldPath,
newPath = diff.getNewPath,
oldContent = if (includeContent) None else getTextContent(git, diff.getOldId.toObjectId),
newContent = if (includeContent) None else getTextContent(git, diff.getNewId.toObjectId),
oldIsImage = oldIsImage,
newIsImage = newIsImage,
oldObjectId = Option(diff.getOldId).map(_.name),
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = false,
patch = None
)
}
}
def getDiffs(
git: Git,
from: Option[String],
@@ -728,7 +769,7 @@ object JGitUtil {
): List[DiffInfo] = {
val diffs = getDiffEntries(git, from, to)
diffs.map { diff =>
if (diffs.size > 100) {
if (diffs.size > 100) { // Don't show diff if there are more than 100 files
DiffInfo(
changeType = diff.getChangeType,
oldPath = diff.getOldPath,
@@ -747,49 +788,35 @@ object JGitUtil {
} else {
val oldIsImage = FileUtil.isImage(diff.getOldPath)
val newIsImage = FileUtil.isImage(diff.getNewPath)
if (!fetchContent || oldIsImage || newIsImage) {
DiffInfo(
changeType = diff.getChangeType,
oldPath = diff.getOldPath,
newPath = diff.getNewPath,
oldContent = None,
newContent = None,
oldIsImage = oldIsImage,
newIsImage = newIsImage,
oldObjectId = Option(diff.getOldId).map(_.name),
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = false,
patch = (if (makePatch) Some(makePatchFromDiffEntry(git, diff)) else None) // TODO use DiffFormatter
)
} else {
DiffInfo(
changeType = diff.getChangeType,
oldPath = diff.getOldPath,
newPath = diff.getNewPath,
oldContent = JGitUtil
.getContentFromId(git, diff.getOldId.toObjectId, false)
.filter(FileUtil.isText)
.map(convertFromByteArray),
newContent = JGitUtil
.getContentFromId(git, diff.getNewId.toObjectId, false)
.filter(FileUtil.isText)
.map(convertFromByteArray),
oldIsImage = oldIsImage,
newIsImage = newIsImage,
oldObjectId = Option(diff.getOldId).map(_.name),
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = false,
patch = (if (makePatch) Some(makePatchFromDiffEntry(git, diff)) else None) // TODO use DiffFormatter
)
}
val patch = if (oldIsImage || newIsImage) None else Some(makePatchFromDiffEntry(git, diff)) // TODO use DiffFormatter
val tooLarge = patch.exists(_.count(_ == '\n') > 1000) // Don't show diff if the file has more than 1000 lines diff
val includeContent = tooLarge || !fetchContent || oldIsImage || newIsImage
DiffInfo(
changeType = diff.getChangeType,
oldPath = diff.getOldPath,
newPath = diff.getNewPath,
oldContent = if (includeContent) None else getTextContent(git, diff.getOldId.toObjectId),
newContent = if (includeContent) None else getTextContent(git, diff.getNewId.toObjectId),
oldIsImage = oldIsImage,
newIsImage = newIsImage,
oldObjectId = Option(diff.getOldId).map(_.name),
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = tooLarge,
patch = if (makePatch) patch else None
)
}
}.toList
}
private def getTextContent(git: Git, objectId: ObjectId): Option[String] = {
JGitUtil
.getContentFromId(git, objectId, false)
.filter(FileUtil.isText)
.map(convertFromByteArray)
}
private def makePatchFromDiffEntry(git: Git, diff: DiffEntry): String = {
val out = new ByteArrayOutputStream()
Using.resource(new DiffFormatter(out)) { formatter =>

View File

@@ -28,7 +28,12 @@ object Keys {
/**
* Session key for the OpenID Connect authentication.
*/
val OidcContext = "oidcContext"
val OidcAuthContext = "oidcAuthContext"
/**
* Session key for the OpenID Connect token.
*/
val OidcSessionContext = "oidcSessionContext"
/**
* Generate session key for the issue search condition.

View File

@@ -5,10 +5,11 @@
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
filter: String,
groups: List[String],
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context)
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
enableNewsFeed: Boolean)(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Issues"){
@gitbucket.core.dashboard.html.sidebar(recentRepositories){
@gitbucket.core.dashboard.html.tab("issues")
@gitbucket.core.dashboard.html.tab(enableNewsFeed, "issues")
<div class="container">
@gitbucket.core.dashboard.html.issuesnavi("issues", filter, openCount, closedCount, condition)
@gitbucket.core.dashboard.html.issueslist(issues, page, openCount, closedCount, condition, filter, groups)

View File

@@ -5,10 +5,11 @@
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
filter: String,
groups: List[String],
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context)
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
enableNewsFeed: Boolean)(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Pull requests"){
@gitbucket.core.dashboard.html.sidebar(recentRepositories){
@gitbucket.core.dashboard.html.tab("pulls")
@gitbucket.core.dashboard.html.tab(enableNewsFeed, "pulls")
<div class="container">
@gitbucket.core.dashboard.html.issuesnavi("pulls", filter, openCount, closedCount, condition)
@gitbucket.core.dashboard.html.issueslist(issues, page, openCount, closedCount, condition, filter, groups)

View File

@@ -1,10 +1,11 @@
@(groups: List[String],
visibleRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context)
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
enableNewsFeed: Boolean)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main("Repositories"){
@gitbucket.core.dashboard.html.sidebar(recentRepositories){
@gitbucket.core.dashboard.html.tab("repos")
@gitbucket.core.dashboard.html.tab(enableNewsFeed, "repos")
<div class="container">
<div class="btn-group" id="owner-dropdown">
<button class="dropdown-toggle btn btn-default" data-toggle="dropdown" aria-expanded="false">

View File

@@ -1,14 +1,18 @@
@(active: String = "")(implicit context: gitbucket.core.controller.Context)
<ul class="nav nav-tabs" style="margin-bottom: 20px;">
<li @if(active == ""){ class="active"}><a href="@context.path/">News feed</a></li>
@if(context.loginAccount.isDefined){
<li @if(active == "repos" ){ class="active"}><a href="@context.path/dashboard/repos">Repositories</a></li>
<li @if(active == "pulls" ){ class="active"}><a href="@context.path/dashboard/pulls">Pull requests</a></li>
<li @if(active == "issues"){ class="active"}><a href="@context.path/dashboard/issues">Issues</a></li>
@gitbucket.core.plugin.PluginRegistry().getDashboardTabs.map { tab =>
@tab(context).map { link =>
<li @if(active == link.id){ class="active"}><a href="@context.path/@link.path">@link.label</a></li>
@(enableNewsFeed: Boolean, active: String = "")(implicit context: gitbucket.core.controller.Context)
@if(enableNewsFeed || context.loginAccount.isDefined) {
<ul class="nav nav-tabs" style="margin-bottom: 20px;">
@if(enableNewsFeed) {
<li @if(active == "") {class="active"}><a href="@context.path/">News feed</a></li>
}
@if(context.loginAccount.isDefined) {
<li @if(active == "repos") {class="active"}><a href="@context.path/dashboard/repos">Repositories</a></li>
<li @if(active == "pulls") {class="active"}><a href="@context.path/dashboard/pulls">Pull requests</a></li>
<li @if(active == "issues") {class="active"}><a href="@context.path/dashboard/issues">Issues</a></li>
@gitbucket.core.plugin.PluginRegistry().getDashboardTabs.map { tab =>
@tab(context).map { link =>
<li @if(active == link.id) {class="active"}><a href="@context.path/@link.path">@link.label</a></li>
}
}
}
}
</ul>
</ul>
}

View File

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

View File

@@ -1,6 +1,7 @@
@(activities: List[gitbucket.core.model.Activity],
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
showBannerToCreatePersonalAccessToken: Boolean)(implicit context: gitbucket.core.controller.Context)
showBannerToCreatePersonalAccessToken: Boolean,
enableNewsFeed: Boolean)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main("GitBucket"){
@gitbucket.core.dashboard.html.sidebar(recentRepositories){
@@ -19,12 +20,26 @@
</a> and use it in place of a password on the <code>git</code> command line.
</div>
}
@gitbucket.core.dashboard.html.tab()
@gitbucket.core.dashboard.html.tab(enableNewsFeed)
<div class="container">
<div class="pull-right">
<a href="@context.path/activities.atom"><img src="@helpers.assets("/common/images/feed.png")" alt="activities"></a>
</div>
@gitbucket.core.helper.html.activities(activities)
@if(enableNewsFeed) {
<div class="pull-right">
<a href="@context.path/activities.atom"><img src="@helpers.assets("/common/images/feed.png")" alt="activities"></a>
</div>
@gitbucket.core.helper.html.activities(activities)
} else {
<div class="signin-form">
@if(context.settings.basicBehavior.allowAnonymousAccess){
@context.settings.information.map { information =>
<div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
<button type="button" class="close" data-dismiss="alert">&times;</button>
@Html(information)
</div>
}
}
@gitbucket.core.html.signinform(context.settings)
</div>
}
</div>
}
}

View File

@@ -140,8 +140,8 @@
}
</div>
<span id="label-assigned">
@issueAssignees.map { asignee =>
<div>@helpers.avatarLink(asignee.assigneeUserName, 20) @helpers.user(asignee.assigneeUserName, styleClass="username strong small")</div>
@issueAssignees.map { assignee =>
<div>@helpers.avatarLink(assignee.assigneeUserName, 20) @helpers.user(assignee.assigneeUserName, styleClass="username strong small")</div>
}
@if(issueAssignees.isEmpty) {
<span class="muted small">No one assigned</span>
@@ -158,10 +158,10 @@
<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))
@Html(behavior.fieldHtml(repository, issue.get.issueId, field.fieldId, field.fieldName, field.constraints, value.map(_.value).getOrElse(""), isManageable))
}
@if(issue.isEmpty) {
@Html(behavior.createHtml(repository, field.fieldId))
@Html(behavior.createHtml(repository, field.fieldId, field.fieldName, field.constraints))
}
}
</div>

View File

@@ -4,7 +4,7 @@
comments: Seq[gitbucket.core.model.Comment],
changedFileSize: Int,
issueLabels: List[gitbucket.core.model.Label],
issueAsignees: List[gitbucket.core.model.IssueAssignee],
issueAssignees: List[gitbucket.core.model.IssueAssignee],
collaborators: List[String],
milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
priorities: List[gitbucket.core.model.Priority],
@@ -56,7 +56,7 @@
Some(issue),
comments.toList,
issueLabels,
issueAsignees,
issueAssignees,
collaborators,
milestones,
priorities,

View File

@@ -7,6 +7,9 @@
</div>
<div class="col-md-4">
@customField.fieldType
@customField.constraints.map { constraints =>
(@constraints)
}
</div>
<div class="col-md-2">
@if(customField.enableForIssues) {

View File

@@ -10,7 +10,9 @@
<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>
<option value="enum" @if(field.map(_.fieldType == "enum").getOrElse(false)){selected}>Enum</option>
</select>
<input type="text" id="constraints-@fieldId" style="width: 300px; @if(!field.exists(_.fieldType == "enum")){display: none;}" class="form-control input-sm" value="@field.map(_.constraints)" placeholder="Comma-separated enum values">
<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>
@@ -30,6 +32,7 @@
$.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(),
'constraints': $('#constraints-@fieldId').val(),
'enableForIssues': $('#enableForIssues-@fieldId').prop('checked'),
'enableForPullRequests': $('#enableForPullRequests-@fieldId').prop('checked')
}, function(data, status){
@@ -61,6 +64,14 @@
$('#field-@fieldId').show();
}
});
$('#fieldType-@fieldId').change(function(){
if($(this).val() == 'enum') {
$('#constraints-@fieldId').show();
} else {
$('#constraints-@fieldId').hide();
}
});
});
</script>
}

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

@@ -204,4 +204,100 @@ class ApiIntegrationTest extends AnyFunSuite {
}
}
test("issue labels") {
Using.resource(new TestingGitBucketServer(19999)) { server =>
val github = server.client("root", "root")
val repo = github.createRepository("issue_label_test").autoInit(true).create()
val issue = repo.createIssue("test").create()
// Initial label state
{
val labels = repo.getIssue(issue.getNumber).getLabels
assert(labels.size() == 0)
}
// Add labels
{
issue.addLabels("bug", "duplicate")
val labels = repo.getIssue(issue.getNumber).getLabels
assert(labels.size() == 2)
val i = labels.iterator()
val label1 = i.next()
assert(label1.getName == "bug")
assert(label1.getColor == "fc2929")
assert(label1.getUrl == "http://localhost:19999/api/v3/repos/root/issue_label_test/labels/bug")
val label2 = i.next()
assert(label2.getName == "duplicate")
assert(label2.getColor == "cccccc")
assert(label2.getUrl == "http://localhost:19999/api/v3/repos/root/issue_label_test/labels/duplicate")
}
// Remove a label
{
issue.removeLabel("duplicate")
val labels = repo.getIssue(issue.getNumber).getLabels
assert(labels.size() == 1)
val i = labels.iterator()
val label1 = i.next()
assert(label1.getName == "bug")
assert(label1.getColor == "fc2929")
assert(label1.getUrl == "http://localhost:19999/api/v3/repos/root/issue_label_test/labels/bug")
}
// Replace labels (Cannot test because GHLabel.setLabels() doesn't use the replace endpoint)
// {
// issue.setLabels("enhancement", "invalid", "question")
//
// val labels = repo.getIssue(issue.getNumber).getLabels
// assert(labels.size() == 3)
//
// val i = labels.iterator()
// val label1 = i.next()
// assert(label1.getName == "enhancement")
// assert(label1.getColor == "84b6eb")
// assert(label1.getUrl == "http://localhost:19999/api/v3/repos/root/issue_label_test/labels/enhancement")
//
// val label2 = i.next()
// assert(label2.getName == "invalid")
// assert(label2.getColor == "e6e6e6")
// assert(label2.getUrl == "http://localhost:19999/api/v3/repos/root/issue_label_test/labels/invalid")
//
// val label3 = i.next()
// assert(label3.getName == "question")
// assert(label3.getColor == "cc317c")
// assert(label3.getUrl == "http://localhost:19999/api/v3/repos/root/issue_label_test/labels/question")
// }
}
}
test("Git refs APIs") {
Using.resource(new TestingGitBucketServer(19999)) { server =>
val github = server.client("root", "root")
val repo = github.createRepository("git_refs_test").autoInit(true).create()
val sha1 = repo.getBranch("master").getSHA1
val refs1 = repo.listRefs().toList
assert(refs1.size() == 1)
assert(refs1.get(0).getRef == "refs/heads/master")
assert(refs1.get(0).getObject.getSha == sha1)
val ref = repo.createRef("refs/heads/testref", sha1)
assert(ref.getRef == "refs/heads/testref")
assert(ref.getObject.getSha == sha1)
val refs2 = repo.listRefs().toList
assert(refs2.size() == 2)
assert(refs2.get(0).getRef == "refs/heads/master")
assert(refs2.get(0).getObject.getSha == sha1)
assert(refs2.get(1).getRef == "refs/heads/testref")
assert(refs2.get(1).getObject.getSha == sha1)
}
}
}