Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df69f88186 | ||
|
|
8d1323f354 | ||
|
|
2f598b618b | ||
|
|
baf0b0b92c | ||
|
|
27a75250a6 | ||
|
|
15f60402a5 | ||
|
|
41c6fc90b3 | ||
|
|
34356b04a8 | ||
|
|
2ca02b6539 | ||
|
|
cd0c71dffb | ||
|
|
a59120fe19 | ||
|
|
fdc35f48ed | ||
|
|
bae9b7ddc3 | ||
|
|
3dd9b7e587 | ||
|
|
44c905bdab | ||
|
|
5214040257 | ||
|
|
7ad9f901dd | ||
|
|
f472d52954 | ||
|
|
1e752af41b | ||
|
|
3ba46c3fc6 | ||
|
|
bf83da476f | ||
|
|
6b8c4cf8d0 | ||
|
|
445329c07a | ||
|
|
8f370e19c6 | ||
|
|
736dbcfb58 | ||
|
|
c1cb7f87e0 | ||
|
|
3c14fcefc9 | ||
|
|
831badf8db | ||
|
|
5f8a6e8d24 | ||
|
|
a3981493f7 | ||
|
|
6919cf5d4d | ||
|
|
f6d1e6bdd6 | ||
|
|
a13ff89acd | ||
|
|
cd5c76279a | ||
|
|
debff5e4b8 | ||
|
|
433e207ec5 | ||
|
|
3775f6a907 | ||
|
|
10d611c0eb | ||
|
|
963bc4d672 | ||
|
|
e68a21ee30 | ||
|
|
d5c083b70f | ||
|
|
2deb9cf417 | ||
|
|
fca0cfcdc7 | ||
|
|
1466e1bdb3 | ||
|
|
dd48bc443a | ||
|
|
f455738e5f | ||
|
|
85193803cd | ||
|
|
4e90a6074a | ||
|
|
ca94fa5184 | ||
|
|
f14a7c996f | ||
|
|
989d22f4d8 | ||
|
|
400a812343 | ||
|
|
97284f1ced | ||
|
|
5e6a0d7e16 | ||
|
|
599e11245f | ||
|
|
538d714c96 | ||
|
|
953915ba2a | ||
|
|
1a2f5da055 | ||
|
|
749a469d37 | ||
|
|
c7d084321a | ||
|
|
00a61cd6cf | ||
|
|
d9c6c13c62 | ||
|
|
5260c5e889 | ||
|
|
1700f96c62 | ||
|
|
5a0f9f8bbb | ||
|
|
8fa22b4de2 | ||
|
|
d17cae16fd | ||
|
|
c4c48962cf | ||
|
|
4140e92f0b | ||
|
|
887e560a1b | ||
|
|
e2d70181e8 | ||
|
|
148c453dbc | ||
|
|
f6ee9d311d | ||
|
|
35209e43bb | ||
|
|
4a3ecf063d | ||
|
|
4c79101624 | ||
|
|
921b01661b | ||
|
|
c63301d8e6 | ||
|
|
c9ed9d2237 | ||
|
|
ca55cbe456 | ||
|
|
d4828613ee | ||
|
|
a0be02ce2f | ||
|
|
9b8016a4d5 | ||
|
|
8fdd3bfd21 | ||
|
|
695a061e3c | ||
|
|
bd50d9218e | ||
|
|
d8f13bc1ce | ||
|
|
ed84d1a3c9 | ||
|
|
3c765d879c |
3
.gitignore
vendored
@@ -2,6 +2,9 @@
|
||||
*.log
|
||||
.ensime
|
||||
.ensime_cache
|
||||
.DS_Store
|
||||
.java-version
|
||||
.tmp
|
||||
|
||||
# sbt specific
|
||||
dist/*
|
||||
|
||||
@@ -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." }
|
||||
]
|
||||
|
||||
13
CHANGELOG.md
@@ -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
|
||||
|
||||
|
||||
25
README.md
@@ -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.
|
||||
|
||||
48
build.sbt
@@ -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"
|
||||
)
|
||||
|
||||
19
doc/build.md
@@ -18,7 +18,21 @@ $ sbt ~jetty:start
|
||||
|
||||
Then access `http://localhost:8080/` in your browser. The default administrator account is `root` and password is `root`.
|
||||
|
||||
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
|
||||
---------
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
Before Width: | Height: | Size: 203 KiB After Width: | Height: | Size: 112 KiB |
@@ -1 +1 @@
|
||||
sbt.version=1.7.1
|
||||
sbt.version=1.7.2
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.5 KiB |
6
src/main/resources/update/gitbucket-core_4.39.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<changeSet>
|
||||
<addColumn tableName="CUSTOM_FIELD">
|
||||
<column name="CONSTRAINTS" type="varchar(200)" nullable="true"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
@@ -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")),
|
||||
)
|
||||
|
||||
@@ -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])
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -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">×</button>
|
||||
@Html(information)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@gitbucket.core.html.signinform(context.settings)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 949 B After Width: | Height: | Size: 85 B |
|
Before Width: | Height: | Size: 691 B After Width: | Height: | Size: 625 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 4.8 KiB |
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||