mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-04 20:45:58 +01:00
Compare commits
196 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1c05eb961 | ||
|
|
b25f97a96f | ||
|
|
1c0f99bd64 | ||
|
|
800d48d6d2 | ||
|
|
02a8540875 | ||
|
|
048cdfc050 | ||
|
|
13f40d2b59 | ||
|
|
fe2920a08f | ||
|
|
f59beded21 | ||
|
|
c75119badf | ||
|
|
7b1292a9af | ||
|
|
9ae26800c8 | ||
|
|
1eb8c83061 | ||
|
|
a0069fde57 | ||
|
|
ca5b121272 | ||
|
|
6f4c081f10 | ||
|
|
227ee12e4c | ||
|
|
fc1cfe3f55 | ||
|
|
76fa5fb474 | ||
|
|
3c847bf957 | ||
|
|
88427b893c | ||
|
|
737b0a9bdf | ||
|
|
c8a7ef2fdb | ||
|
|
6d6331bbf3 | ||
|
|
d1f2a72f06 | ||
|
|
576daa0669 | ||
|
|
934d1df991 | ||
|
|
aa7db68e68 | ||
|
|
a1bb667ec4 | ||
|
|
90ae489d35 | ||
|
|
5bbd4f533f | ||
|
|
283baaed57 | ||
|
|
c1e2191120 | ||
|
|
28b0ea7f88 | ||
|
|
95acb8593a | ||
|
|
17d682f83b | ||
|
|
952c916e33 | ||
|
|
3793a51e3f | ||
|
|
c8b6eb1bd9 | ||
|
|
e46f1b0efc | ||
|
|
395d6e4292 | ||
|
|
770b49cc55 | ||
|
|
95cd6b6e90 | ||
|
|
1f79ed95c2 | ||
|
|
9aef90d214 | ||
|
|
f4d1c72f08 | ||
|
|
5d5dcad32c | ||
|
|
0d195a2e90 | ||
|
|
eb76db5f2c | ||
|
|
9cc5d0ea17 | ||
|
|
1d98308a21 | ||
|
|
1ceace5539 | ||
|
|
f13a473b4e | ||
|
|
4e7c10c0dc | ||
|
|
6db34cbb6b | ||
|
|
10205a8f9b | ||
|
|
d6df35f072 | ||
|
|
ab10b77c50 | ||
|
|
fb34b0909e | ||
|
|
1a869f47e0 | ||
|
|
d9aebbda62 | ||
|
|
987407909e | ||
|
|
ba9c780602 | ||
|
|
ea5834f236 | ||
|
|
c3400f1091 | ||
|
|
7bd4d0970e | ||
|
|
4a9303d7a7 | ||
|
|
5f0cacd7c1 | ||
|
|
f075132878 | ||
|
|
b72556c007 | ||
|
|
47489d9cb1 | ||
|
|
2ee70dc1b2 | ||
|
|
3400b9a0ab | ||
|
|
ad054d2f80 | ||
|
|
b0c2e5588c | ||
|
|
1fe379111c | ||
|
|
2180e31d13 | ||
|
|
275772ad00 | ||
|
|
e80da63515 | ||
|
|
71cce5b470 | ||
|
|
bb188ec948 | ||
|
|
281522fc88 | ||
|
|
a045fc6ae4 | ||
|
|
8e8e794574 | ||
|
|
735e425984 | ||
|
|
5f47b126e3 | ||
|
|
33d82beb72 | ||
|
|
3de5d806b5 | ||
|
|
8eb522fb38 | ||
|
|
370e4339f3 | ||
|
|
5b0eb7ece5 | ||
|
|
18434854d8 | ||
|
|
d3f57bdb45 | ||
|
|
37734ce26b | ||
|
|
b6cf080822 | ||
|
|
bbc817d86d | ||
|
|
5e88f3f787 | ||
|
|
f64d4843f3 | ||
|
|
bcb3450e2b | ||
|
|
c607045b7c | ||
|
|
f8e9093273 | ||
|
|
40c06417e5 | ||
|
|
c3c5535022 | ||
|
|
b7fc76d932 | ||
|
|
c8d666baba | ||
|
|
a64741011c | ||
|
|
ae9ee4779f | ||
|
|
5fd2d61861 | ||
|
|
939c9156ad | ||
|
|
d17aed2357 | ||
|
|
13382b47d1 | ||
|
|
5e5a1ea5a8 | ||
|
|
cf6d1ea137 | ||
|
|
f735e4a133 | ||
|
|
86b67863f8 | ||
|
|
718582af44 | ||
|
|
23024cacaa | ||
|
|
f62cf409eb | ||
|
|
47845dfe1b | ||
|
|
b7bb6b0787 | ||
|
|
ea41786f8c | ||
|
|
962ae2130e | ||
|
|
90ea05f2a1 | ||
|
|
f8bda516d6 | ||
|
|
378c031ecb | ||
|
|
9a5db80dea | ||
|
|
992eb0ceda | ||
|
|
39e1ac2398 | ||
|
|
d1c77de5a0 | ||
|
|
3f8069638c | ||
|
|
d62fc1185c | ||
|
|
768706e1d1 | ||
|
|
8cc9771237 | ||
|
|
8df30ef01b | ||
|
|
dd2e5bfedf | ||
|
|
e3c7eb092f | ||
|
|
5b3c3e2e7c | ||
|
|
0e04925b6b | ||
|
|
9a127256f3 | ||
|
|
1033122fec | ||
|
|
847f96d537 | ||
|
|
70f40846bb | ||
|
|
3a540aa660 | ||
|
|
1adc9b3223 | ||
|
|
0309496df6 | ||
|
|
f83ecac7ae | ||
|
|
cd4d75e35e | ||
|
|
eb61bc50d6 | ||
|
|
4bbb22f73b | ||
|
|
fcb374c5c2 | ||
|
|
a03d1c97c2 | ||
|
|
2d58b7f2d7 | ||
|
|
332a1b4b0b | ||
|
|
6bd58b0c45 | ||
|
|
fb175df851 | ||
|
|
b41aad92f2 | ||
|
|
aabae2ef7f | ||
|
|
0c3d1fd86d | ||
|
|
adba849ec5 | ||
|
|
8539486c6e | ||
|
|
86f4b41beb | ||
|
|
aa54eff3d6 | ||
|
|
27ab21c9a7 | ||
|
|
557ed827d0 | ||
|
|
9cc466a727 | ||
|
|
9a9be12324 | ||
|
|
8e91b9f0b5 | ||
|
|
2862ceb5ad | ||
|
|
d157426d66 | ||
|
|
58635674cb | ||
|
|
f6a048e0f7 | ||
|
|
c4dc1d7334 | ||
|
|
efd5a64749 | ||
|
|
13800a7023 | ||
|
|
43d19d7d52 | ||
|
|
8a8278906a | ||
|
|
d15b3fb2f6 | ||
|
|
bcd92916ca | ||
|
|
810cbda123 | ||
|
|
fee7cebdf1 | ||
|
|
28105d6d3a | ||
|
|
1673832607 | ||
|
|
298e43e612 | ||
|
|
00b88d6b6e | ||
|
|
0a12b82b48 | ||
|
|
7ef74ac3ee | ||
|
|
5853691844 | ||
|
|
3a8b93d44a | ||
|
|
806a5aecef | ||
|
|
44d2918dee | ||
|
|
64f7db6585 | ||
|
|
fb0cd272ce | ||
|
|
239c7371a8 | ||
|
|
981b228a88 | ||
|
|
0f70e5b1d6 | ||
|
|
fd30facd8f |
35
CHANGELOG.md
35
CHANGELOG.md
@@ -1,11 +1,46 @@
|
||||
# Changelog
|
||||
All changes to the project will be documented in this file.
|
||||
|
||||
### 4.21.2 - 27 Jan 2018
|
||||
- Bugfix
|
||||
|
||||
### 4.21.1 - 27 Jan 2018
|
||||
- Bugfix
|
||||
|
||||
### 4.21.0 - 27 Jan 2018
|
||||
- Release page
|
||||
- OpenID Connect support
|
||||
- New database viewer
|
||||
- Submodule links to web page
|
||||
- Clarify close/reopen button
|
||||
|
||||
## 4.20.0 - 23 Dec 2017
|
||||
- Squash and rebase merge strategy for pull requests
|
||||
- Quick pull request creation
|
||||
- Download patch from the diff view
|
||||
- Fork and create repository are proceeded asynchronously
|
||||
- Create new repository by copying existing git repository
|
||||
- Hide overflowed repository names in the sidebar
|
||||
- Support CreateEvent web hook
|
||||
- Display conflicting files if pull request can't be merged
|
||||
|
||||
## 4.19.3 - 7 Dec 2017
|
||||
- Fix file uploading bug
|
||||
- Fix reply comment form behavior in the diff view
|
||||
|
||||
## 4.19.2 - 3 Dec 2017
|
||||
- Fix routing bug in `CompositeScalatraFilter`
|
||||
- Resolve id attribute collision in the web hook editing form
|
||||
|
||||
## 4.19.1 - 2 Dec 2017
|
||||
- Update gitbucket-notifications-plugin because it had a version compatibility issue
|
||||
|
||||
## 4.19.0 - 2 Dec 2017
|
||||
- [gitbucket-maven-repository-plugin](https://github.com/takezoe/gitbucket-maven-repository-plugin) is available
|
||||
- Upgrade to Scalatra 2.6
|
||||
- Improve layout of the system settings page
|
||||
- New extension point (`sshCommandProvider`)
|
||||
- Dropped [gitbucket-pages-plugin](https://github.com/gitbucket/gitbucket-pages-plugin) from bundled plugins temporary because we couldn't complete update for Scalatra 2.6 before this release.
|
||||
|
||||
## 4.18.0 - 14 Oct 2017
|
||||
- Form to reply to review comment
|
||||
|
||||
12
README.md
12
README.md
@@ -68,12 +68,12 @@ Support
|
||||
- If you can't find same question and report, send it to [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.19.0 - 2 Dec 2017
|
||||
What's New in 4.22.x
|
||||
-------------
|
||||
|
||||
- [gitbucket-maven-repository-plugin](https://github.com/takezoe/gitbucket-maven-repository-plugin) is available
|
||||
- Upgrade to Scalatra 2.6
|
||||
- Improve layout of the system settings page
|
||||
- New extension point (`sshCommandProvider`)
|
||||
### 4.22.0 - 3 Mar 2018
|
||||
- Pull request merge strategy settings
|
||||
- Create repository with an empty commit
|
||||
- Improve database viewer
|
||||
- Update maven-repository-plugin
|
||||
|
||||
See the [change log](CHANGELOG.md) for all of the updates.
|
||||
|
||||
65
build.sbt
65
build.sbt
@@ -1,9 +1,9 @@
|
||||
import com.typesafe.sbt.license.{LicenseInfo, DepModuleInfo}
|
||||
import com.typesafe.sbt.license.{DepModuleInfo, LicenseInfo}
|
||||
import com.typesafe.sbt.pgp.PgpKeys._
|
||||
|
||||
val Organization = "io.github.gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val GitBucketVersion = "4.19.0"
|
||||
val GitBucketVersion = "4.22.0"
|
||||
val ScalatraVersion = "2.6.1"
|
||||
val JettyVersion = "9.4.7.v20170914"
|
||||
|
||||
@@ -26,43 +26,45 @@ resolvers ++= Seq(
|
||||
"amateras-snapshot" at "http://amateras.sourceforge.jp/mvn-snapshot/"
|
||||
)
|
||||
libraryDependencies ++= Seq(
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.9.0.201710071750-r",
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.9.0.201710071750-r",
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.10.0.201712302008-r",
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.10.0.201712302008-r",
|
||||
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
||||
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
|
||||
"org.scalatra" %% "scalatra-forms" % ScalatraVersion,
|
||||
"org.json4s" %% "json4s-jackson" % "3.5.1",
|
||||
"commons-io" % "commons-io" % "2.5",
|
||||
"org.json4s" %% "json4s-jackson" % "3.5.3",
|
||||
"commons-io" % "commons-io" % "2.6",
|
||||
"io.github.gitbucket" % "solidbase" % "1.0.2",
|
||||
"io.github.gitbucket" % "markedj" % "1.0.15",
|
||||
"org.apache.commons" % "commons-compress" % "1.13",
|
||||
"org.apache.commons" % "commons-email" % "1.4",
|
||||
"org.apache.httpcomponents" % "httpclient" % "4.5.3",
|
||||
"org.apache.sshd" % "apache-sshd" % "1.4.0" exclude("org.slf4j","slf4j-jdk14"),
|
||||
"org.apache.tika" % "tika-core" % "1.14",
|
||||
"org.apache.commons" % "commons-compress" % "1.15",
|
||||
"org.apache.commons" % "commons-email" % "1.5",
|
||||
"org.apache.httpcomponents" % "httpclient" % "4.5.4",
|
||||
"org.apache.sshd" % "apache-sshd" % "1.6.0" exclude("org.slf4j","slf4j-jdk14"),
|
||||
"org.apache.tika" % "tika-core" % "1.17",
|
||||
"com.github.takezoe" %% "blocking-slick-32" % "0.0.10",
|
||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||
"com.h2database" % "h2" % "1.4.195",
|
||||
"org.mariadb.jdbc" % "mariadb-java-client" % "2.1.2",
|
||||
"org.postgresql" % "postgresql" % "42.0.0",
|
||||
"com.h2database" % "h2" % "1.4.196",
|
||||
"org.mariadb.jdbc" % "mariadb-java-client" % "2.2.2",
|
||||
"org.postgresql" % "postgresql" % "42.1.4",
|
||||
"ch.qos.logback" % "logback-classic" % "1.2.3",
|
||||
"com.zaxxer" % "HikariCP" % "2.6.1",
|
||||
"com.typesafe" % "config" % "1.3.1",
|
||||
"com.typesafe.akka" %% "akka-actor" % "2.5.0",
|
||||
"com.zaxxer" % "HikariCP" % "2.7.4",
|
||||
"com.typesafe" % "config" % "1.3.2",
|
||||
"com.typesafe.akka" %% "akka-actor" % "2.5.8",
|
||||
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.0.0",
|
||||
"com.github.bkromhout" % "java-diff-utils" % "2.1.1",
|
||||
"org.cache2k" % "cache2k-all" % "1.0.0.CR1",
|
||||
"com.enragedginger" %% "akka-quartz-scheduler" % "1.6.0-akka-2.4.x" exclude("c3p0","c3p0"),
|
||||
"org.cache2k" % "cache2k-all" % "1.0.1.Final",
|
||||
"com.enragedginger" %% "akka-quartz-scheduler" % "1.6.1-akka-2.5.x" exclude("c3p0","c3p0"),
|
||||
"net.coobird" % "thumbnailator" % "0.4.8",
|
||||
"com.github.zafarkhaja" % "java-semver" % "0.9.0",
|
||||
"com.nimbusds" % "oauth2-oidc-sdk" % "5.45",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
|
||||
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
|
||||
"junit" % "junit" % "4.12" % "test",
|
||||
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
|
||||
"org.mockito" % "mockito-core" % "2.7.22" % "test",
|
||||
"com.wix" % "wix-embedded-mysql" % "2.1.4" % "test",
|
||||
"ru.yandex.qatools.embed" % "postgresql-embedded" % "2.0" % "test",
|
||||
"net.i2p.crypto" % "eddsa" % "0.1.0"
|
||||
"org.mockito" % "mockito-core" % "2.13.0" % "test",
|
||||
"com.wix" % "wix-embedded-mysql" % "3.0.0" % "test",
|
||||
"ru.yandex.qatools.embed" % "postgresql-embedded" % "2.6" % "test",
|
||||
"net.i2p.crypto" % "eddsa" % "0.2.0",
|
||||
"is.tagomor.woothee" % "woothee-java" % "1.7.0"
|
||||
)
|
||||
|
||||
// Compiler settings
|
||||
@@ -96,7 +98,13 @@ assemblyMergeStrategy in assembly := {
|
||||
//jrebel.webLinks += (target in webappPrepare).value
|
||||
//jrebel.enabled := System.getenv().get("JREBEL") != null
|
||||
javaOptions in Jetty ++= Option(System.getenv().get("JREBEL")).toSeq.flatMap { path =>
|
||||
Seq("-noverify", "-XX:+UseConcMarkSweepGC", "-XX:+CMSClassUnloadingEnabled", s"-javaagent:${path}")
|
||||
if (path.endsWith(".jar")) {
|
||||
// Legacy JRebel agent
|
||||
Seq("-noverify", "-XX:+UseConcMarkSweepGC", "-XX:+CMSClassUnloadingEnabled", s"-javaagent:${path}")
|
||||
} else {
|
||||
// New JRebel agent
|
||||
Seq(s"-agentpath:${path}")
|
||||
}
|
||||
}
|
||||
|
||||
// Exclude a war file from published artifacts
|
||||
@@ -121,8 +129,8 @@ libraryDependencies ++= Seq(
|
||||
|
||||
val executableKey = TaskKey[File]("executable")
|
||||
executableKey := {
|
||||
import java.util.jar.{ Manifest => JarManifest }
|
||||
import java.util.jar.Attributes.{ Name => AttrName }
|
||||
import java.util.jar.Attributes.{Name => AttrName}
|
||||
import java.util.jar.{Manifest => JarManifest}
|
||||
|
||||
val workDir = Keys.target.value / "executable"
|
||||
val warName = Keys.name.value + ".war"
|
||||
@@ -160,10 +168,9 @@ executableKey := {
|
||||
IO copyFile(Keys.baseDirectory.value / "plugins.json", pluginsDir / "plugins.json")
|
||||
|
||||
val json = IO read(Keys.baseDirectory.value / "plugins.json")
|
||||
PluginsJson.parse(json).foreach { case (plugin, version, file) =>
|
||||
val url = s"https://github.com/gitbucket/${plugin}/releases/download/${version}/${file}"
|
||||
PluginsJson.getUrls(json).foreach { url =>
|
||||
log info s"Download: ${url}"
|
||||
IO transfer(new java.net.URL(url).openStream, pluginsDir / file)
|
||||
IO transfer(new java.net.URL(url).openStream, pluginsDir / url.substring(url.lastIndexOf("/") + 1))
|
||||
}
|
||||
|
||||
// zip it up
|
||||
|
||||
21
contrib/linux/redhat/selinux/gitbucket.te
Normal file
21
contrib/linux/redhat/selinux/gitbucket.te
Normal file
@@ -0,0 +1,21 @@
|
||||
module gitbucket 1.0;
|
||||
|
||||
require {
|
||||
type smtp_port_t;
|
||||
type tomcat_t;
|
||||
type tomcat_var_lib_t;
|
||||
type unreserved_port_t;
|
||||
|
||||
class file { execute };
|
||||
class tcp_socket { name_bind };
|
||||
class tcp_socket { name_connect };
|
||||
}
|
||||
|
||||
# allow tomcat to send emails
|
||||
allow tomcat_t smtp_port_t:tcp_socket { name_connect };
|
||||
|
||||
# allow file executes, required during repo creation
|
||||
allow tomcat_t tomcat_var_lib_t:file { execute };
|
||||
|
||||
# allow tomcat to serve repositories via SSH
|
||||
allow tomcat_t unreserved_port_t:tcp_socket { name_bind };
|
||||
32
contrib/linux/redhat/selinux/readme.md
Normal file
32
contrib/linux/redhat/selinux/readme.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Red Hat Enterprise Linux / CentOS SELinux policy module for GitBucket
|
||||
|
||||
One way to run GitBucket on Enterprise Linux is under Tomcat. Since EL 7.4, Tomcat is no longer unconfined.
|
||||
Thus since 7.4, Enterprise Linux blocks certain operations that are required for GitBucket to work properly:
|
||||
|
||||
* Tomcat is not allowed to connect to SMTP ports, which is required to send email notifications.
|
||||
* Tomcat is not allowed to execute files, which is required for creating repositories.
|
||||
* Tomcat is not allowed to act as a server on unreserved ports, which is required for serving repositories via SSH.
|
||||
|
||||
To mitigate this, you can use the SELinux policy module provided as `gitbucket.te`. You can deploy the module with the
|
||||
attached script, e.g.:
|
||||
|
||||
~~~
|
||||
./sedeploy.sh gitbucket
|
||||
~~~
|
||||
|
||||
You most likely also need to fix file contexts on your system. Assuming a new, default Tomcat installation on 7.4, you
|
||||
can do so by issuing the following commands:
|
||||
|
||||
~~~
|
||||
GITBUCKET_HOME='/usr/share/tomcat/.gitbucket'
|
||||
mkdir -p ${GITBUCKET_HOME}
|
||||
chown tomcat.tomcat ${GITBUCKET_HOME}
|
||||
semanage fcontext -a -t tomcat_var_lib_t "${GITBUCKET_HOME}(/.*)?"
|
||||
restorecon -rv ${GITBUCKET_HOME}
|
||||
|
||||
JAVA_CONF='/usr/share/tomcat/.java'
|
||||
mkdir -p ${JAVA_CONF}
|
||||
chown tomcat.tomcat ${JAVA_CONF}
|
||||
semanage fcontext -a -t tomcat_cache_t "${JAVA_CONF}(/.*)?"
|
||||
restorecon -rv ${JAVA_CONF}
|
||||
~~~
|
||||
14
contrib/linux/redhat/selinux/sedeploy.sh
Executable file
14
contrib/linux/redhat/selinux/sedeploy.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
MODULE=${1}
|
||||
|
||||
# this will create a .mod file
|
||||
checkmodule -M -m -o ${MODULE}.mod ${MODULE}.te
|
||||
|
||||
# this will create a compiled semodule
|
||||
semodule_package -m ${MODULE}.mod -o ${MODULE}.pp
|
||||
|
||||
# this will install the module
|
||||
semodule -i ${MODULE}.pp
|
||||
@@ -6,17 +6,22 @@ The details are saved at `ISSUE_COMMENT` table.
|
||||
To determine if it was any operation, you see the `ACTION` column.
|
||||
And in the case of some actions, `CONTENT` column value contains additional information.
|
||||
|
||||
|ACTION |CONTENT |
|
||||
|---------------|-----------------|
|
||||
|comment |comment |
|
||||
|close_comment |comment |
|
||||
|reopen_comment |comment |
|
||||
|close |"Close" |
|
||||
|reopen |"Reopen" |
|
||||
|commit |comment commitId |
|
||||
|merge |comment |
|
||||
|delete_branch |branchName |
|
||||
|refer |issueId:title |
|
||||
|ACTION |CONTENT |
|
||||
|----------------|----------------------|
|
||||
|comment |comment |
|
||||
|close_comment |comment |
|
||||
|reopen_comment |comment |
|
||||
|close |"Close" |
|
||||
|reopen |"Reopen" |
|
||||
|commit |comment commitId |
|
||||
|merge |comment |
|
||||
|delete_branch |branchName |
|
||||
|refer |issueId:title |
|
||||
|add_label |labelName |
|
||||
|delete_label |labelName |
|
||||
|change_priority |oldPriority:priority |
|
||||
|change_milestone|oldMilestone:milestone|
|
||||
|assign |oldAssigned:assigned |
|
||||
|
||||
### comment
|
||||
|
||||
@@ -54,3 +59,23 @@ Therefore, this comment is not displayed, and not counted as a comment.
|
||||
|
||||
This value is saved when other issue or issue comment contains reference to the issue like `#issueId`.
|
||||
At the same time, store id and title of the referrer issue as `id:title`.
|
||||
|
||||
### add_label
|
||||
|
||||
This value is saved when users have added the label.
|
||||
|
||||
### delete_label
|
||||
|
||||
This value is saved when users have deleted the label.
|
||||
|
||||
### change_priority
|
||||
|
||||
This value is saved when users have changed the priority.
|
||||
|
||||
### change_milestone
|
||||
|
||||
This value is saved when users have changed the milestone.
|
||||
|
||||
### assign
|
||||
|
||||
This value is saved when users have assign issue/PR to user or remove the assign.
|
||||
|
||||
@@ -28,17 +28,16 @@ You don't need to integrate with your IDE, since we're using sbt to do the servl
|
||||
Fortunately, the gitbucket project is already set up to use JRebel.
|
||||
You only need to tell jvm where to find the jrebel jar.
|
||||
|
||||
To do so, edit your shell resource file (usually `~/.bash_profile` on Mac, and `~/.bashrc` on Linux), and add the following line:
|
||||
To do so, edit your shell resource file (usually `~/.bash_profile` on Mac, and `~/.bashrc` on Linux) and set the environment variable `JREBEL`.
|
||||
For example, if you unzipped your JRebel download in your home directory, you would use:
|
||||
|
||||
```bash
|
||||
export JREBEL=/path/to/jrebel/legacy/jrebel.jar
|
||||
export JREBEL=~/jrebel/legacy/jrebel.jar # legacy agent
|
||||
export JREBEL=~/jrebel/lib/libjrebel64.dylib # new agent
|
||||
```
|
||||
|
||||
For example, if you unzipped your JRebel download in your home directory, you whould use:
|
||||
|
||||
```bash
|
||||
export JREBEL=~/jrebel/legacy/jrebel.jar
|
||||
```
|
||||
You can choose the legacy JRebel agent or the new one.
|
||||
See [the document](https://zeroturnaround.com/software/jrebel/jrebel7-agent-upgrade-cli/) for details.
|
||||
|
||||
Now reload your shell:
|
||||
|
||||
|
||||
25
plugins.json
25
plugins.json
@@ -5,9 +5,9 @@
|
||||
"description": "Provides notifications feature on GitBucket.",
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.3.0",
|
||||
"range": ">=4.17.0",
|
||||
"file": "gitbucket-notifications-plugin_2.12-1.3.0.jar"
|
||||
"version": "1.4.0",
|
||||
"range": ">=4.19.0",
|
||||
"url": "https://github.com/gitbucket/gitbucket-notifications-plugin/releases/download/1.4.0/gitbucket-notifications-plugin_2.12-1.4.0.jar"
|
||||
}
|
||||
],
|
||||
"default": true
|
||||
@@ -20,7 +20,7 @@
|
||||
{
|
||||
"version": "4.5.0",
|
||||
"range": ">=4.18.0",
|
||||
"file": "gitbucket-emoji-plugin_2.12-4.5.0.jar"
|
||||
"url": "https://github.com/gitbucket/gitbucket-emoji-plugin/releases/download/4.5.0/gitbucket-emoji-plugin_2.12-4.5.0.jar"
|
||||
}
|
||||
],
|
||||
"default": false
|
||||
@@ -31,9 +31,22 @@
|
||||
"description": "Provides Gist feature on GitBucket.",
|
||||
"versions": [
|
||||
{
|
||||
"version": "4.11.0",
|
||||
"version": "4.12.0",
|
||||
"range": ">=4.21.0",
|
||||
"url": "https://github.com/gitbucket/gitbucket-gist-plugin/releases/download/4.12.0/gitbucket-gist-plugin-assembly-4.12.0.jar"
|
||||
}
|
||||
],
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": "pages",
|
||||
"name": "Pages Plugin",
|
||||
"description": "Project pages for gitbucket",
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.6.0",
|
||||
"range": ">=4.19.0",
|
||||
"file": "gitbucket-gist-plugin-assembly-4.11.0.jar"
|
||||
"url": "https://github.com/gitbucket/gitbucket-pages-plugin/releases/download/v1.6.0/gitbucket-pages-plugin_2.12-1.6.0.jar"
|
||||
}
|
||||
],
|
||||
"default": false
|
||||
|
||||
@@ -3,17 +3,12 @@ import scala.collection.JavaConverters._
|
||||
|
||||
object PluginsJson {
|
||||
|
||||
def parse(json: String): Seq[(String, String, String)] = {
|
||||
def getUrls(json: String): Seq[String] = {
|
||||
val value = Json.parse(json)
|
||||
value.asArray.values.asScala.map { plugin =>
|
||||
val pluginObject = plugin.asObject
|
||||
val pluginName = "gitbucket-" + pluginObject.get("id").asString + "-plugin"
|
||||
|
||||
val latestVersionObject = pluginObject.get("versions").asArray.asScala.head.asObject
|
||||
val file = latestVersionObject.get("file").asString
|
||||
val version = latestVersionObject.get("version").asString
|
||||
|
||||
(pluginName, version, file)
|
||||
latestVersionObject.get("url").asString
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
sbt.version=1.0.4
|
||||
sbt.version=1.1.1
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
|
||||
|
||||
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.3.12")
|
||||
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.3.13")
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.5")
|
||||
//addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "4.0.0")
|
||||
//addSbtPlugin("fi.gekkio.sbtplugins" % "sbt-jrebel-plugin" % "0.10.0")
|
||||
addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.1")
|
||||
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0")
|
||||
addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC13")
|
||||
addSbtCoursier
|
||||
addSbtPlugin("com.typesafe.sbt" % "sbt-license-report" % "1.2.0")
|
||||
|
||||
@@ -1 +1 @@
|
||||
addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC11")
|
||||
addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0")
|
||||
|
||||
39
src/main/resources/update/gitbucket-core_4.21.xml
Normal file
39
src/main/resources/update/gitbucket-core_4.21.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<changeSet>
|
||||
<createTable tableName="RELEASE">
|
||||
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||
<column name="TAG" type="varchar(100)" nullable="false"/>
|
||||
<column name="NAME" type="varchar(100)" nullable="false"/>
|
||||
<column name="AUTHOR" type="varchar(100)" nullable="false"/>
|
||||
<column name="CONTENT" type="text" nullable="true"/>
|
||||
<column name="REGISTERED_DATE" type="datetime" nullable="false"/>
|
||||
<column name="UPDATED_DATE" type="datetime" nullable="false"/>
|
||||
</createTable>
|
||||
|
||||
<addPrimaryKey constraintName="IDX_RELEASE_PK" tableName="RELEASE" columnNames="USER_NAME, REPOSITORY_NAME, TAG"/>
|
||||
<addForeignKeyConstraint constraintName="IDX_RELEASE_FK0" baseTableName="RELEASE" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
|
||||
|
||||
<createTable tableName="RELEASE_ASSET">
|
||||
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||
<column name="TAG" type="varchar(100)" nullable="false"/>
|
||||
<column name="RELEASE_ASSET_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
|
||||
<column name="FILE_NAME" type="varchar(260)" nullable="false"/>
|
||||
<column name="LABEL" type="varchar(100)" nullable="true"/>
|
||||
<column name="SIZE" type="bigint" nullable="false"/>
|
||||
<column name="UPLOADER" type="varchar(100)" nullable="false"/>
|
||||
<column name="REGISTERED_DATE" type="datetime" nullable="false"/>
|
||||
<column name="UPDATED_DATE" type="datetime" nullable="false"/>
|
||||
</createTable>
|
||||
<addPrimaryKey constraintName="IDX_RELEASE_ASSET_PK" tableName="RELEASE_ASSET" columnNames="USER_NAME, REPOSITORY_NAME, TAG, FILE_NAME"/>
|
||||
<addForeignKeyConstraint constraintName="IDX_RELEASE_ASSET_FK1" baseTableName="RELEASE_ASSET" baseColumnNames="USER_NAME, REPOSITORY_NAME, TAG" referencedTableName="RELEASE" referencedColumnNames="USER_NAME, REPOSITORY_NAME, TAG"/>
|
||||
|
||||
<createTable tableName="ACCOUNT_FEDERATION">
|
||||
<column name="ISSUER" type="varchar(100)" nullable="false"/>
|
||||
<column name="SUBJECT" type="varchar(100)" nullable="false"/>
|
||||
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||
</createTable>
|
||||
<addPrimaryKey constraintName="IDX_ACCOUNT_FEDERATION_PK" tableName="ACCOUNT_FEDERATION" columnNames="ISSUER, SUBJECT"/>
|
||||
<addForeignKeyConstraint constraintName="IDX_ACCOUNT_FEDERATION_FK0" baseTableName="ACCOUNT_FEDERATION" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
|
||||
</changeSet>
|
||||
7
src/main/resources/update/gitbucket-core_4.22.xml
Normal file
7
src/main/resources/update/gitbucket-core_4.22.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<changeSet>
|
||||
<addColumn tableName="REPOSITORY">
|
||||
<column name="MERGE_OPTIONS" type="varchar(200)" nullable="false" defaultValue="merge-commit,squash,rebase"/>
|
||||
<column name="DEFAULT_MERGE_OPTION" type="varchar(100)" nullable="false" defaultValue="merge-commit"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
@@ -2,7 +2,7 @@
|
||||
import java.util.EnumSet
|
||||
import javax.servlet._
|
||||
|
||||
import gitbucket.core.controller._
|
||||
import gitbucket.core.controller.{ReleaseController, _}
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.service.SystemSettingsService
|
||||
import gitbucket.core.servlet._
|
||||
@@ -47,6 +47,7 @@ class ScalatraBootstrap extends LifeCycle with SystemSettingsService {
|
||||
filter.mount(new MilestonesController, "/*")
|
||||
filter.mount(new IssuesController, "/*")
|
||||
filter.mount(new PullRequestsController, "/*")
|
||||
filter.mount(new ReleaseController, "/*")
|
||||
filter.mount(new RepositorySettingsController, "/*")
|
||||
|
||||
context.addFilter("compositeScalatraFilter", filter)
|
||||
|
||||
@@ -44,5 +44,17 @@ object GitBucketCoreModule extends Module("gitbucket-core",
|
||||
new Version("4.16.0"),
|
||||
new Version("4.17.0"),
|
||||
new Version("4.18.0"),
|
||||
new Version("4.19.0")
|
||||
new Version("4.19.0"),
|
||||
new Version("4.19.1"),
|
||||
new Version("4.19.2"),
|
||||
new Version("4.19.3"),
|
||||
new Version("4.20.0"),
|
||||
new Version("4.21.0",
|
||||
new LiquibaseMigration("update/gitbucket-core_4.21.xml")
|
||||
),
|
||||
new Version("4.21.1"),
|
||||
new Version("4.21.2"),
|
||||
new Version("4.22.0",
|
||||
new LiquibaseMigration("update/gitbucket-core_4.22.xml")
|
||||
)
|
||||
)
|
||||
|
||||
@@ -35,23 +35,23 @@ case class ApiCommit(
|
||||
|
||||
object ApiCommit{
|
||||
def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo, urlIsHtmlUrl: Boolean = false): ApiCommit = {
|
||||
val diffs = JGitUtil.getDiffs(git, commit.id, false)
|
||||
val diffs = JGitUtil.getDiffs(git, None, commit.id, false, false)
|
||||
ApiCommit(
|
||||
id = commit.id,
|
||||
message = commit.fullMessage,
|
||||
timestamp = commit.commitTime,
|
||||
added = diffs._1.collect {
|
||||
added = diffs.collect {
|
||||
case x if x.changeType == DiffEntry.ChangeType.ADD => x.newPath
|
||||
},
|
||||
removed = diffs._1.collect {
|
||||
removed = diffs.collect {
|
||||
case x if x.changeType == DiffEntry.ChangeType.DELETE => x.oldPath
|
||||
},
|
||||
modified = diffs._1.collect {
|
||||
modified = diffs.collect {
|
||||
case x if x.changeType != DiffEntry.ChangeType.ADD && x.changeType != DiffEntry.ChangeType.DELETE => x.newPath
|
||||
},
|
||||
author = ApiPersonIdent.author(commit),
|
||||
committer = ApiPersonIdent.committer(commit)
|
||||
)(repositoryName, urlIsHtmlUrl)
|
||||
}
|
||||
def forPushPayload(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = apply(git, repositoryName, commit, true)
|
||||
def forWebhookPayload(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = apply(git, repositoryName, commit, true)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.util.Date
|
||||
*/
|
||||
case class ApiPullRequest(
|
||||
number: Int,
|
||||
state: String,
|
||||
updated_at: Date,
|
||||
created_at: Date,
|
||||
head: ApiPullRequest.Commit,
|
||||
@@ -44,6 +45,7 @@ object ApiPullRequest{
|
||||
): ApiPullRequest =
|
||||
ApiPullRequest(
|
||||
number = issue.issueId,
|
||||
state = if (issue.closed) "closed" else "open",
|
||||
updated_at = issue.updatedDate,
|
||||
created_at = issue.registeredDate,
|
||||
head = Commit(
|
||||
|
||||
@@ -51,7 +51,7 @@ object ApiRepository{
|
||||
def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository =
|
||||
this(repositoryInfo.repository, ApiUser(owner))
|
||||
|
||||
def forPushPayload(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
|
||||
def forWebhookPayload(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
|
||||
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount, urlIsHtmlUrl=true)
|
||||
|
||||
def forDummyPayload(owner: ApiUser): ApiRepository =
|
||||
|
||||
@@ -2,7 +2,7 @@ package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.account.html
|
||||
import gitbucket.core.helper
|
||||
import gitbucket.core.model.{AccountWebHook, GroupMember, RepositoryWebHook, RepositoryWebHookEvent, Role, WebHook, WebHookContentType}
|
||||
import gitbucket.core.model.{AccountWebHook, GroupMember, RepositoryWebHook, Role, WebHook, WebHookContentType}
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.service.WebHookService._
|
||||
@@ -12,7 +12,6 @@ import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.StringUtil._
|
||||
import gitbucket.core.util._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.scalatra.i18n.Messages
|
||||
import org.scalatra.BadRequest
|
||||
import org.scalatra.forms._
|
||||
@@ -87,15 +86,16 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
"clearImage" -> trim(label("Clear image" ,boolean()))
|
||||
)(EditGroupForm.apply)
|
||||
|
||||
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
|
||||
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, initOption: String, sourceUrl: Option[String])
|
||||
case class ForkRepositoryForm(owner: String, name: String)
|
||||
|
||||
val newRepositoryForm = mapping(
|
||||
"owner" -> trim(label("Owner" , text(required, maxlength(100), identifier, existsAccount))),
|
||||
"name" -> trim(label("Repository name", text(required, maxlength(100), repository, uniqueRepository))),
|
||||
"description" -> trim(label("Description" , optional(text()))),
|
||||
"isPrivate" -> trim(label("Repository Type", boolean())),
|
||||
"createReadme" -> trim(label("Create README" , boolean()))
|
||||
"owner" -> trim(label("Owner", text(required, maxlength(100), identifier, existsAccount))),
|
||||
"name" -> trim(label("Repository name", text(required, maxlength(100), repository, uniqueRepository))),
|
||||
"description" -> trim(label("Description", optional(text()))),
|
||||
"isPrivate" -> trim(label("Repository Type", boolean())),
|
||||
"initOption" -> trim(label("Initialize option", text(required))),
|
||||
"sourceUrl" -> trim(label("Source URL", optionalRequired(_.value("initOption") == "COPY", text())))
|
||||
)(RepositoryCreationForm.apply)
|
||||
|
||||
val forkRepositoryForm = mapping(
|
||||
@@ -461,7 +461,6 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
get("/:groupName/_editgroup")(managersOnly {
|
||||
defining(params("groupName")){ groupName =>
|
||||
// TODO Don't use Option.get
|
||||
getAccountByUserName(groupName, true).map { account =>
|
||||
html.editgroup(account, getGroupMembers(groupName), flash.get("info"))
|
||||
} getOrElse NotFound()
|
||||
@@ -528,11 +527,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
post("/new", newRepositoryForm)(usersOnly { form =>
|
||||
LockUtil.lock(s"${form.owner}/${form.name}"){
|
||||
if(getRepository(form.owner, form.name).isEmpty){
|
||||
// Create the repository
|
||||
createRepository(context.loginAccount.get, form.owner, form.name, form.description, form.isPrivate, form.createReadme)
|
||||
|
||||
// Call hooks
|
||||
PluginRegistry().getRepositoryHooks.foreach(_.created(form.owner, form.name))
|
||||
createRepository(context.loginAccount.get, form.owner, form.name, form.description, form.isPrivate, form.initOption, form.sourceUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,66 +561,15 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
val loginUserName = loginAccount.userName
|
||||
val accountName = form.accountName
|
||||
|
||||
LockUtil.lock(s"${accountName}/${repository.name}"){
|
||||
if(getRepository(accountName, repository.name).isDefined ||
|
||||
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){
|
||||
// redirect to the repository if repository already exists
|
||||
redirect(s"/${accountName}/${repository.name}")
|
||||
} else {
|
||||
// Insert to the database at first
|
||||
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
|
||||
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
|
||||
|
||||
insertRepository(
|
||||
repositoryName = repository.name,
|
||||
userName = accountName,
|
||||
description = repository.repository.description,
|
||||
isPrivate = repository.repository.isPrivate,
|
||||
originRepositoryName = Some(originRepositoryName),
|
||||
originUserName = Some(originUserName),
|
||||
parentRepositoryName = Some(repository.name),
|
||||
parentUserName = Some(repository.owner)
|
||||
)
|
||||
|
||||
// Set default collaborators for the private fork
|
||||
if(repository.repository.isPrivate){
|
||||
// Copy collaborators from the source repository
|
||||
getCollaborators(repository.owner, repository.name).foreach { case (collaborator, _) =>
|
||||
addCollaborator(accountName, repository.name, collaborator.collaboratorName, collaborator.role)
|
||||
}
|
||||
// Register an owner of the source repository as a collaborator
|
||||
addCollaborator(accountName, repository.name, repository.owner, Role.ADMIN.name)
|
||||
}
|
||||
|
||||
// Insert default labels
|
||||
insertDefaultLabels(accountName, repository.name)
|
||||
// Insert default priorities
|
||||
insertDefaultPriorities(accountName, repository.name)
|
||||
|
||||
// clone repository actually
|
||||
JGitUtil.cloneRepository(
|
||||
getRepositoryDir(repository.owner, repository.name),
|
||||
FileUtil.deleteIfExists(getRepositoryDir(accountName, repository.name)))
|
||||
|
||||
// Create Wiki repository
|
||||
JGitUtil.cloneRepository(getWikiRepositoryDir(repository.owner, repository.name),
|
||||
FileUtil.deleteIfExists(getWikiRepositoryDir(accountName, repository.name)))
|
||||
|
||||
// Copy LFS files
|
||||
val lfsDir = getLfsDir(repository.owner, repository.name)
|
||||
if(lfsDir.exists){
|
||||
FileUtils.copyDirectory(lfsDir, FileUtil.deleteIfExists(getLfsDir(accountName, repository.name)))
|
||||
}
|
||||
|
||||
// Record activity
|
||||
recordForkActivity(repository.owner, repository.name, loginUserName, accountName)
|
||||
|
||||
// Call hooks
|
||||
PluginRegistry().getRepositoryHooks.foreach(_.forked(repository.owner, accountName, repository.name))
|
||||
|
||||
// redirect to the repository
|
||||
redirect(s"/${accountName}/${repository.name}")
|
||||
}
|
||||
if (getRepository(accountName, repository.name).isDefined ||
|
||||
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))) {
|
||||
// redirect to the repository if repository already exists
|
||||
redirect(s"/${accountName}/${repository.name}")
|
||||
} else {
|
||||
// fork repository asynchronously
|
||||
forkRepository(accountName, repository, loginUserName)
|
||||
// redirect to the repository
|
||||
redirect(s"/${accountName}/${repository.name}")
|
||||
}
|
||||
} else BadRequest()
|
||||
})
|
||||
|
||||
@@ -16,6 +16,8 @@ import org.eclipse.jgit.revwalk.RevWalk
|
||||
import org.scalatra.{Created, NoContent, UnprocessableEntity}
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration.Duration
|
||||
|
||||
class ApiController extends ApiControllerBase
|
||||
with RepositoryService
|
||||
@@ -143,12 +145,13 @@ trait ApiControllerBase extends ControllerBase {
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repo/contents/*")(referrersOnly { repository =>
|
||||
def getFileInfo(git: Git, revision: String, pathStr: String): Option[FileInfo] = {
|
||||
val path = new java.io.File(pathStr)
|
||||
val dirName = path.getParent match {
|
||||
case null => "."
|
||||
case s => s
|
||||
val (dirName, fileName) = pathStr.lastIndexOf('/') match {
|
||||
case -1 =>
|
||||
(".", pathStr)
|
||||
case n =>
|
||||
(pathStr.take(n), pathStr.drop(n + 1))
|
||||
}
|
||||
getFileList(git, revision, dirName).find(f => f.name.equals(path.getName))
|
||||
getFileList(git, revision, dirName).find(f => f.name.equals(fileName))
|
||||
}
|
||||
|
||||
val path = multiParams("splat").head match {
|
||||
@@ -201,13 +204,24 @@ trait ApiControllerBase extends ControllerBase {
|
||||
/*
|
||||
* https://developer.github.com/v3/git/refs/#get-a-reference
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repo/git/*") (referrersOnly { repository =>
|
||||
get("/api/v3/repos/:owner/:repo/git/refs/*") (referrersOnly { repository =>
|
||||
val revstr = multiParams("splat").head
|
||||
using(Git.open(getRepositoryDir(params("owner"), params("repo")))) { git =>
|
||||
//JsonFormat( (revstr, git.getRepository().resolve(revstr)) )
|
||||
// getRef is deprecated by jgit-4.2. use exactRef() or findRef()
|
||||
val sha = git.getRepository().exactRef(revstr).getObjectId().name()
|
||||
JsonFormat(ApiRef(revstr, ApiObject(sha)))
|
||||
val ref = git.getRepository().findRef(revstr)
|
||||
|
||||
if(ref != null){
|
||||
val sha = ref.getObjectId().name()
|
||||
JsonFormat(ApiRef(revstr, ApiObject(sha)))
|
||||
|
||||
} else {
|
||||
val refs = git.getRepository().getAllRefs().asScala
|
||||
.collect { case (str, ref) if str.startsWith("refs/" + revstr) => ref }
|
||||
|
||||
JsonFormat(refs.map { ref =>
|
||||
val sha = ref.getObjectId().name()
|
||||
ApiRef(revstr, ApiObject(sha))
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -249,7 +263,8 @@ trait ApiControllerBase extends ControllerBase {
|
||||
} yield {
|
||||
LockUtil.lock(s"${owner}/${data.name}") {
|
||||
if(getRepository(owner, data.name).isEmpty){
|
||||
createRepository(context.loginAccount.get, owner, data.name, data.description, data.`private`, data.auto_init)
|
||||
val f = createRepository(context.loginAccount.get, owner, data.name, data.description, data.`private`, data.auto_init)
|
||||
Await.result(f, Duration.Inf)
|
||||
val repository = getRepository(owner, data.name).get
|
||||
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
|
||||
} else {
|
||||
@@ -273,7 +288,8 @@ trait ApiControllerBase extends ControllerBase {
|
||||
} yield {
|
||||
LockUtil.lock(s"${groupName}/${data.name}") {
|
||||
if(getRepository(groupName, data.name).isEmpty){
|
||||
createRepository(context.loginAccount.get, groupName, data.name, data.description, data.`private`, data.auto_init)
|
||||
val f = createRepository(context.loginAccount.get, groupName, data.name, data.description, data.`private`, data.auto_init)
|
||||
Await.result(f, Duration.Inf)
|
||||
val repository = getRepository(groupName, data.name).get
|
||||
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get)))
|
||||
} else {
|
||||
@@ -651,7 +667,7 @@ trait ApiControllerBase extends ControllerBase {
|
||||
JsonFormat(ApiCommits(
|
||||
repositoryName = RepositoryName(repository),
|
||||
commitInfo = commitInfo,
|
||||
diffs = JGitUtil.getDiffs(git, commitInfo.parents.head, commitInfo.id, false, true),
|
||||
diffs = JGitUtil.getDiffs(git, Some(commitInfo.parents.head), commitInfo.id, false, true),
|
||||
author = getAccount(commitInfo.authorName, commitInfo.authorEmailAddress),
|
||||
committer = getAccount(commitInfo.committerName, commitInfo.committerEmailAddress),
|
||||
commentCount = getCommitComment(repository.owner, repository.name, sha).size
|
||||
|
||||
@@ -4,7 +4,7 @@ import java.io.FileInputStream
|
||||
|
||||
import gitbucket.core.api.ApiError
|
||||
import gitbucket.core.model.Account
|
||||
import gitbucket.core.service.{AccountService, SystemSettingsService,RepositoryService}
|
||||
import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService}
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.Implicits._
|
||||
@@ -17,9 +17,10 @@ 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
|
||||
import net.coobird.thumbnailator.Thumbnails
|
||||
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.ObjectId
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
@@ -43,25 +44,11 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
}
|
||||
|
||||
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try {
|
||||
val httpRequest = request.asInstanceOf[HttpServletRequest]
|
||||
val httpResponse = response.asInstanceOf[HttpServletResponse]
|
||||
val context = request.getServletContext.getContextPath
|
||||
val path = httpRequest.getRequestURI.substring(context.length)
|
||||
val httpRequest = request.asInstanceOf[HttpServletRequest]
|
||||
val context = request.getServletContext.getContextPath
|
||||
val path = httpRequest.getRequestURI.substring(context.length)
|
||||
|
||||
if(path.startsWith("/console/")){
|
||||
val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
|
||||
val baseUrl = this.baseUrl(httpRequest)
|
||||
if(account == null){
|
||||
// Redirect to login form
|
||||
httpResponse.sendRedirect(baseUrl + "/signin?redirect=" + StringUtil.urlEncode(path))
|
||||
} else if(account.isAdmin){
|
||||
// H2 Console (administrators only)
|
||||
chain.doFilter(request, response)
|
||||
} else {
|
||||
// Redirect to dashboard
|
||||
httpResponse.sendRedirect(baseUrl + "/")
|
||||
}
|
||||
} else if(path.startsWith("/git/") || path.startsWith("/git-lfs/")){
|
||||
if(path.startsWith("/git/") || path.startsWith("/git-lfs/")){
|
||||
// Git repository
|
||||
chain.doFilter(request, response)
|
||||
} else {
|
||||
@@ -127,12 +114,24 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
org.scalatra.NotFound(gitbucket.core.html.error("Not Found"))
|
||||
}
|
||||
|
||||
private def isBrowser(userAgent: String): Boolean = {
|
||||
if(userAgent == null || userAgent.isEmpty){
|
||||
false
|
||||
} else {
|
||||
val data = Classifier.parse(userAgent)
|
||||
val category = data.get("category")
|
||||
category == "pc" || category == "smartphone" || category == "mobilephone"
|
||||
}
|
||||
}
|
||||
|
||||
protected def Unauthorized()(implicit context: Context) =
|
||||
if(request.hasAttribute(Keys.Request.Ajax)){
|
||||
org.scalatra.Unauthorized()
|
||||
} else if(request.hasAttribute(Keys.Request.APIv3)){
|
||||
contentType = formats("json")
|
||||
org.scalatra.Unauthorized(ApiError("Requires authentication"))
|
||||
} else if(!isBrowser(request.getHeader("USER-AGENT"))){
|
||||
org.scalatra.Unauthorized()
|
||||
} else {
|
||||
if(context.loginAccount.isDefined){
|
||||
org.scalatra.Unauthorized(redirect("/"))
|
||||
|
||||
@@ -8,7 +8,7 @@ import gitbucket.core.service.IssuesService._
|
||||
|
||||
class DashboardController extends DashboardControllerBase
|
||||
with IssuesService with PullRequestService with RepositoryService with AccountService with CommitsService
|
||||
with UsersAuthenticator
|
||||
with LabelsService with PrioritiesService with MilestonesService with UsersAuthenticator
|
||||
|
||||
trait DashboardControllerBase extends ControllerBase {
|
||||
self: IssuesService with PullRequestService with RepositoryService with AccountService
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.model.Account
|
||||
import gitbucket.core.service.{AccountService, RepositoryService}
|
||||
import gitbucket.core.service.{AccountService, RepositoryService, ReleaseService}
|
||||
import gitbucket.core.servlet.Database
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
@@ -19,14 +19,13 @@ import org.apache.commons.io.{FileUtils, IOUtils}
|
||||
*
|
||||
* This servlet saves uploaded file.
|
||||
*/
|
||||
class FileUploadController extends ScalatraServlet with FileUploadSupport with RepositoryService with AccountService {
|
||||
class FileUploadController extends ScalatraServlet
|
||||
with FileUploadSupport
|
||||
with RepositoryService
|
||||
with AccountService
|
||||
with ReleaseService{
|
||||
|
||||
val maxFileSize = if (System.getProperty("gitbucket.maxFileSize") != null)
|
||||
System.getProperty("gitbucket.maxFileSize").toLong
|
||||
else
|
||||
3 * 1024 * 1024
|
||||
|
||||
configureMultipartHandling(MultipartConfig(maxFileSize = Some(maxFileSize)))
|
||||
configureMultipartHandling(MultipartConfig(maxFileSize = Some(FileUtil.MaxFileSize)))
|
||||
|
||||
post("/image"){
|
||||
execute({ (file, fileId) =>
|
||||
@@ -89,6 +88,20 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
|
||||
} getOrElse BadRequest()
|
||||
}
|
||||
|
||||
post("/release/:owner/:repository/:tag"){
|
||||
session.get(Keys.Session.LoginAccount).collect { case _: Account =>
|
||||
val owner = params("owner")
|
||||
val repository = params("repository")
|
||||
val tag = params("tag")
|
||||
execute({ (file, fileId) =>
|
||||
FileUtils.writeByteArrayToFile(
|
||||
new java.io.File(getReleaseFilesDir(owner, repository), tag + "/" + fileId),
|
||||
file.get
|
||||
)
|
||||
}, _ => true)
|
||||
}.getOrElse(BadRequest())
|
||||
}
|
||||
|
||||
post("/import") {
|
||||
import JDBCUtil._
|
||||
session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account if loginAccount.isAdmin =>
|
||||
@@ -116,6 +129,7 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
|
||||
case Some(file) if(mimeTypeChcker(file.name)) =>
|
||||
defining(FileUtil.generateFileId){ fileId =>
|
||||
f(file, fileId)
|
||||
contentType = "text/plain"
|
||||
Ok(fileId)
|
||||
}
|
||||
case _ => BadRequest()
|
||||
|
||||
@@ -1,23 +1,42 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import java.net.URI
|
||||
|
||||
import com.nimbusds.oauth2.sdk.id.State
|
||||
import com.nimbusds.openid.connect.sdk.Nonce
|
||||
import gitbucket.core.helper.xml
|
||||
import gitbucket.core.model.Account
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
import gitbucket.core.util.{Keys, LDAPUtil, ReferrerAuthenticator, UsersAuthenticator}
|
||||
import org.scalatra.forms._
|
||||
import org.scalatra.Ok
|
||||
import org.scalatra.forms._
|
||||
|
||||
|
||||
class IndexController extends IndexControllerBase
|
||||
with RepositoryService with ActivityService with AccountService with RepositorySearchService with IssuesService
|
||||
with UsersAuthenticator with ReferrerAuthenticator
|
||||
with RepositoryService
|
||||
with ActivityService
|
||||
with AccountService
|
||||
with RepositorySearchService
|
||||
with IssuesService
|
||||
with LabelsService
|
||||
with MilestonesService
|
||||
with PrioritiesService
|
||||
with UsersAuthenticator
|
||||
with ReferrerAuthenticator
|
||||
with AccountFederationService
|
||||
with OpenIDConnectService
|
||||
|
||||
|
||||
trait IndexControllerBase extends ControllerBase {
|
||||
self: RepositoryService with ActivityService with AccountService with RepositorySearchService
|
||||
with UsersAuthenticator with ReferrerAuthenticator =>
|
||||
self: RepositoryService
|
||||
with ActivityService
|
||||
with AccountService
|
||||
with RepositorySearchService
|
||||
with UsersAuthenticator
|
||||
with ReferrerAuthenticator
|
||||
with OpenIDConnectService =>
|
||||
|
||||
case class SignInForm(userName: String, password: String, hash: Option[String])
|
||||
|
||||
@@ -35,6 +54,7 @@ trait IndexControllerBase extends ControllerBase {
|
||||
//
|
||||
// case class SearchForm(query: String, owner: String, repository: String)
|
||||
|
||||
case class OidcContext(state: State, nonce: Nonce, redirectBackURI: String)
|
||||
|
||||
get("/"){
|
||||
context.loginAccount.map { account =>
|
||||
@@ -55,13 +75,59 @@ trait IndexControllerBase extends ControllerBase {
|
||||
|
||||
post("/signin", signinForm){ form =>
|
||||
authenticate(context.settings, form.userName, form.password) match {
|
||||
case Some(account) => signin(account, form.hash)
|
||||
case None => {
|
||||
case Some(account) =>
|
||||
flash.get(Keys.Flash.Redirect) match {
|
||||
case Some(redirectUrl: String) => signin(account, redirectUrl + form.hash.getOrElse(""))
|
||||
case _ => signin(account)
|
||||
}
|
||||
case None =>
|
||||
flash += "userName" -> form.userName
|
||||
flash += "password" -> form.password
|
||||
flash += "error" -> "Sorry, your Username and/or Password is incorrect. Please try again."
|
||||
redirect("/signin")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate an OpenID Connect authentication request.
|
||||
*/
|
||||
post("/signin/oidc") {
|
||||
context.settings.oidc.map { oidc =>
|
||||
val redirectURI = new URI(s"$baseUrl/signin/oidc")
|
||||
val authenticationRequest = createOIDCAuthenticationRequest(oidc.issuer, oidc.clientID, redirectURI)
|
||||
val redirectBackURI = flash.get(Keys.Flash.Redirect) match {
|
||||
case Some(redirectBackURI: String) => redirectBackURI + params.getOrElse("hash", "")
|
||||
case _ => "/"
|
||||
}
|
||||
session.setAttribute(Keys.Session.OidcContext, OidcContext(authenticationRequest.getState, authenticationRequest.getNonce, redirectBackURI))
|
||||
redirect(authenticationRequest.toURI.toString)
|
||||
} getOrElse {
|
||||
NotFound()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an OpenID Connect authentication response.
|
||||
*/
|
||||
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, redirectURI, context.state, context.nonce, oidc) map { account =>
|
||||
signin(account, context.redirectBackURI)
|
||||
} orElse {
|
||||
flash += "error" -> "Sorry, authentication failed. Please try again."
|
||||
session.invalidate()
|
||||
redirect("/signin")
|
||||
}
|
||||
case _ =>
|
||||
flash += "error" -> "Sorry, something wrong. Please try again."
|
||||
session.invalidate()
|
||||
redirect("/signin")
|
||||
}
|
||||
} getOrElse {
|
||||
NotFound()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,9 +151,9 @@ trait IndexControllerBase extends ControllerBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set account information into HttpSession and redirect.
|
||||
*/
|
||||
private def signin(account: Account, hash: Option[String]) = {
|
||||
* Set account information into HttpSession and redirect.
|
||||
*/
|
||||
private def signin(account: Account, redirectUrl: String = "/") = {
|
||||
session.setAttribute(Keys.Session.LoginAccount, account)
|
||||
updateLastLoginDate(account.userName)
|
||||
|
||||
@@ -95,14 +161,10 @@ trait IndexControllerBase extends ControllerBase {
|
||||
redirect("/" + account.userName + "/_edit")
|
||||
}
|
||||
|
||||
flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl =>
|
||||
if(redirectUrl.stripSuffix("/") == request.getContextPath){
|
||||
redirect("/")
|
||||
} else {
|
||||
redirect(redirectUrl + hash.getOrElse(""))
|
||||
}
|
||||
}.getOrElse {
|
||||
if (redirectUrl.stripSuffix("/") == request.getContextPath) {
|
||||
redirect("/")
|
||||
} else {
|
||||
redirect(redirectUrl)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -271,25 +271,25 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/:id/label/new")(writableUsersOnly { repository =>
|
||||
defining(params("id").toInt){ issueId =>
|
||||
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
||||
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt, true)
|
||||
html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
||||
}
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/:id/label/delete")(writableUsersOnly { repository =>
|
||||
defining(params("id").toInt){ issueId =>
|
||||
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
||||
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt, true)
|
||||
html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
||||
}
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/:id/assign")(writableUsersOnly { repository =>
|
||||
updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
|
||||
updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"), true)
|
||||
Ok("updated")
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { repository =>
|
||||
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
|
||||
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"), true)
|
||||
milestoneId("milestoneId").map { milestoneId =>
|
||||
getMilestonesWithIssueCount(repository.owner, repository.name)
|
||||
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
|
||||
@@ -299,7 +299,8 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/:id/priority")(writableUsersOnly { repository =>
|
||||
updatePriorityId(repository.owner, repository.name, params("id").toInt, priorityId("priorityId"))
|
||||
val priority = priorityId("priorityId")
|
||||
updatePriorityId(repository.owner, repository.name, params("id").toInt, priority, true)
|
||||
Ok("updated")
|
||||
})
|
||||
|
||||
@@ -325,7 +326,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
params("value").toIntOpt.map{ labelId =>
|
||||
executeBatch(repository) { issueId =>
|
||||
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
|
||||
registerIssueLabel(repository.owner, repository.name, issueId, labelId)
|
||||
registerIssueLabel(repository.owner, repository.name, issueId, labelId, true)
|
||||
}
|
||||
}
|
||||
} getOrElse NotFound()
|
||||
@@ -334,7 +335,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository =>
|
||||
defining(assignedUserName("value")){ value =>
|
||||
executeBatch(repository) {
|
||||
updateAssignedUserName(repository.owner, repository.name, _, value)
|
||||
updateAssignedUserName(repository.owner, repository.name, _, value, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -342,7 +343,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
post("/:owner/:repository/issues/batchedit/milestone")(writableUsersOnly { repository =>
|
||||
defining(milestoneId("value")){ value =>
|
||||
executeBatch(repository) {
|
||||
updateMilestoneId(repository.owner, repository.name, _, value)
|
||||
updateMilestoneId(repository.owner, repository.name, _, value, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -350,7 +351,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
post("/:owner/:repository/issues/batchedit/priority")(writableUsersOnly { repository =>
|
||||
defining(priorityId("value")){ value =>
|
||||
executeBatch(repository) {
|
||||
updatePriorityId(repository.owner, repository.name, _, value)
|
||||
updatePriorityId(repository.owner, repository.name, _, value, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.issues.labels.html
|
||||
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
|
||||
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService, MilestonesService, PrioritiesService}
|
||||
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
@@ -10,8 +10,9 @@ import org.scalatra.i18n.Messages
|
||||
import org.scalatra.Ok
|
||||
|
||||
class LabelsController extends LabelsControllerBase
|
||||
with LabelsService with IssuesService with RepositoryService with AccountService
|
||||
with ReferrerAuthenticator with WritableUsersAuthenticator
|
||||
with IssuesService with RepositoryService with AccountService
|
||||
with LabelsService with PrioritiesService with MilestonesService
|
||||
with ReferrerAuthenticator with WritableUsersAuthenticator
|
||||
|
||||
trait LabelsControllerBase extends ControllerBase {
|
||||
self: LabelsService with IssuesService with RepositoryService
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.issues.priorities.html
|
||||
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, PrioritiesService}
|
||||
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService, MilestonesService, PrioritiesService}
|
||||
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
@@ -10,8 +10,9 @@ import org.scalatra.i18n.Messages
|
||||
import org.scalatra.Ok
|
||||
|
||||
class PrioritiesController extends PrioritiesControllerBase
|
||||
with PrioritiesService with IssuesService with RepositoryService with AccountService
|
||||
with ReferrerAuthenticator with WritableUsersAuthenticator
|
||||
with IssuesService with RepositoryService with AccountService
|
||||
with LabelsService with PrioritiesService with MilestonesService
|
||||
with ReferrerAuthenticator with WritableUsersAuthenticator
|
||||
|
||||
trait PrioritiesControllerBase extends ControllerBase {
|
||||
self: PrioritiesService with IssuesService with RepositoryService
|
||||
|
||||
@@ -16,6 +16,8 @@ import gitbucket.core.util._
|
||||
import org.scalatra.forms._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.PersonIdent
|
||||
import org.eclipse.jgit.revwalk.RevWalk
|
||||
import org.scalatra.BadRequest
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
@@ -50,7 +52,8 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
)(PullRequestForm.apply)
|
||||
|
||||
val mergeForm = mapping(
|
||||
"message" -> trim(label("Message", text(required)))
|
||||
"message" -> trim(label("Message", text(required))),
|
||||
"strategy" -> trim(label("Strategy", text(required)))
|
||||
)(MergeForm.apply)
|
||||
|
||||
case class PullRequestForm(
|
||||
@@ -69,7 +72,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
labelNames: Option[String]
|
||||
)
|
||||
|
||||
case class MergeForm(message: String)
|
||||
case class MergeForm(message: String, strategy: String)
|
||||
|
||||
get("/:owner/:repository/pulls")(referrersOnly { repository =>
|
||||
val q = request.getParameter("q")
|
||||
@@ -115,13 +118,13 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||
val hasConflict = LockUtil.lock(s"${owner}/${name}"){
|
||||
val conflictMessage = LockUtil.lock(s"${owner}/${name}"){
|
||||
checkConflict(owner, name, pullreq.branch, issueId)
|
||||
}
|
||||
val hasMergePermission = hasDeveloperRole(owner, name, context.loginAccount)
|
||||
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch)
|
||||
val mergeStatus = PullRequestService.MergeStatus(
|
||||
hasConflict = hasConflict,
|
||||
conflictMessage = conflictMessage,
|
||||
commitStatues = getCommitStatues(owner, name, pullreq.commitIdTo),
|
||||
branchProtection = branchProtection,
|
||||
branchIsOutOfDate = JGitUtil.getShaByRef(owner, name, pullreq.branch) != Some(pullreq.commitIdFrom),
|
||||
@@ -246,51 +249,69 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
params("id").toIntOpt.flatMap { issueId =>
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
LockUtil.lock(s"${owner}/${name}"){
|
||||
getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
|
||||
using(Git.open(getRepositoryDir(owner, name))) { git =>
|
||||
// mark issue as merged and close.
|
||||
val loginAccount = context.loginAccount.get
|
||||
val commentId = createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
|
||||
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
|
||||
updateClosed(owner, name, issueId, true)
|
||||
if(repository.repository.options.mergeOptions.split(",").contains(form.strategy)){
|
||||
LockUtil.lock(s"${owner}/${name}"){
|
||||
getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
|
||||
using(Git.open(getRepositoryDir(owner, name))) { git =>
|
||||
// mark issue as merged and close.
|
||||
val loginAccount = context.loginAccount.get
|
||||
val commentId = createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
|
||||
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
|
||||
updateClosed(owner, name, issueId, true)
|
||||
|
||||
// record activity
|
||||
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
|
||||
// record activity
|
||||
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
|
||||
|
||||
// merge git repository
|
||||
mergePullRequest(git, pullreq.branch, issueId,
|
||||
s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" + form.message,
|
||||
new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
|
||||
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
|
||||
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
|
||||
|
||||
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
|
||||
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
|
||||
val revCommits = using(new RevWalk( git.getRepository )){ revWalk =>
|
||||
commits.flatten.map { commit =>
|
||||
revWalk.parseCommit(git.getRepository.resolve(commit.id))
|
||||
}
|
||||
}.reverse
|
||||
|
||||
// close issue by content of pull request
|
||||
val defaultBranch = getRepository(owner, name).get.repository.defaultBranch
|
||||
if(pullreq.branch == defaultBranch){
|
||||
commits.flatten.foreach { commit =>
|
||||
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
|
||||
// merge git repository
|
||||
form.strategy match {
|
||||
case "merge-commit" =>
|
||||
mergePullRequest(git, pullreq.branch, issueId,
|
||||
s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" + form.message,
|
||||
new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
|
||||
case "rebase" =>
|
||||
rebasePullRequest(git, pullreq.branch, issueId, revCommits,
|
||||
new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
|
||||
case "squash" =>
|
||||
squashPullRequest(git, pullreq.branch, issueId,
|
||||
s"${issue.title} (#${issueId})\n\n" + form.message,
|
||||
new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
|
||||
}
|
||||
closeIssuesFromMessage(issue.title + " " + issue.content.getOrElse(""), loginAccount.userName, owner, name)
|
||||
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
|
||||
|
||||
// close issue by content of pull request
|
||||
val defaultBranch = getRepository(owner, name).get.repository.defaultBranch
|
||||
if(pullreq.branch == defaultBranch){
|
||||
commits.flatten.foreach { commit =>
|
||||
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
|
||||
}
|
||||
closeIssuesFromMessage(issue.title + " " + issue.content.getOrElse(""), loginAccount.userName, owner, name)
|
||||
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
|
||||
}
|
||||
|
||||
updatePullRequests(owner, name, pullreq.branch)
|
||||
|
||||
// call web hook
|
||||
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||
|
||||
// call hooks
|
||||
PluginRegistry().getPullRequestHooks.foreach{ h =>
|
||||
h.addedComment(commentId, form.message, issue, repository)
|
||||
h.merged(issue, repository)
|
||||
}
|
||||
|
||||
redirect(s"/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
|
||||
updatePullRequests(owner, name, pullreq.branch)
|
||||
|
||||
// call web hook
|
||||
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||
|
||||
// call hooks
|
||||
PluginRegistry().getPullRequestHooks.foreach{ h =>
|
||||
h.addedComment(commentId, form.message, issue, repository)
|
||||
h.merged(issue, repository)
|
||||
}
|
||||
|
||||
redirect(s"/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else Some(BadRequest())
|
||||
} getOrElse NotFound()
|
||||
})
|
||||
|
||||
@@ -333,7 +354,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
Some(forkedRepository.name)
|
||||
} else if(forkedRepository.repository.originUserName.isEmpty){
|
||||
// when ForkedRepository is the original repository
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_.userName == originOwner).map(_.repositoryName)
|
||||
} else if(Some(originOwner) == forkedRepository.repository.originUserName){
|
||||
// Original repository
|
||||
forkedRepository.repository.originRepositoryName
|
||||
@@ -351,16 +372,20 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||
){ case (oldGit, newGit) =>
|
||||
val (oldId, newId) =
|
||||
if(originRepository.branchList.contains(originId) && forkedRepository.branchList.contains(forkedId)){
|
||||
// Branch name
|
||||
val rootId = JGitUtil.getForkedCommitId(oldGit, newGit,
|
||||
originRepository.owner, originRepository.name, originId,
|
||||
forkedRepository.owner, forkedRepository.name, forkedId)
|
||||
if(originRepository.branchList.contains(originId)){
|
||||
val forkedId2 = forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.id }.getOrElse(forkedId)
|
||||
|
||||
val originId2 = JGitUtil.getForkedCommitId(oldGit, newGit,
|
||||
originRepository.owner, originRepository.name, originId,
|
||||
forkedRepository.owner, forkedRepository.name, forkedId2)
|
||||
|
||||
(Option(oldGit.getRepository.resolve(originId2)), Option(newGit.getRepository.resolve(forkedId2)))
|
||||
|
||||
(Option(oldGit.getRepository.resolve(rootId)), Option(newGit.getRepository.resolve(forkedId)))
|
||||
} else {
|
||||
// Commit id
|
||||
(Option(oldGit.getRepository.resolve(originId)), Option(newGit.getRepository.resolve(forkedId)))
|
||||
val originId2 = originRepository.tags.collectFirst { case x if x.name == originId => x.id }.getOrElse(originId)
|
||||
val forkedId2 = forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.id }.getOrElse(forkedId)
|
||||
|
||||
(Option(oldGit.getRepository.resolve(originId2)), Option(newGit.getRepository.resolve(forkedId2)))
|
||||
}
|
||||
|
||||
(oldId, newId) match {
|
||||
@@ -381,9 +406,12 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
commits,
|
||||
diffs,
|
||||
((forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
|
||||
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
||||
}).filter { case (owner, name) => hasGuestRole(owner, name, context.loginAccount) },
|
||||
case (Some(userName), Some(repositoryName)) => getRepository(userName, repositoryName) match {
|
||||
case Some(x) => x.repository :: getForkedRepositories(userName, repositoryName)
|
||||
case None => getForkedRepositories(userName, repositoryName)
|
||||
}
|
||||
case _ => forkedRepository.repository :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
||||
}).map { repository => (repository.userName, repository.repositoryName) },
|
||||
commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList,
|
||||
originId,
|
||||
forkedId,
|
||||
@@ -419,7 +447,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
Some(forkedRepository.name)
|
||||
} else {
|
||||
forkedRepository.repository.originRepositoryName.orElse {
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_.userName == originOwner).map(_.repositoryName)
|
||||
}
|
||||
};
|
||||
originRepository <- getRepository(originOwner, originRepositoryName)
|
||||
@@ -434,7 +462,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
checkConflict(originRepository.owner, originRepository.name, originBranch,
|
||||
forkedRepository.owner, forkedRepository.name, forkedBranch)
|
||||
}
|
||||
html.mergecheck(conflict)
|
||||
html.mergecheck(conflict.isDefined)
|
||||
}
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
@@ -499,6 +527,35 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/pulls/proposals")(readableUsersOnly { repository =>
|
||||
val branches = JGitUtil.getBranches(
|
||||
owner = repository.owner,
|
||||
name = repository.name,
|
||||
defaultBranch = repository.repository.defaultBranch,
|
||||
origin = repository.repository.originUserName.isEmpty
|
||||
)
|
||||
.filter(x => x.mergeInfo.map(_.ahead).getOrElse(0) > 0 && x.mergeInfo.map(_.behind).getOrElse(0) == 0)
|
||||
.sortBy(br => (br.mergeInfo.isEmpty, br.commitTime))
|
||||
.map(_.name)
|
||||
.reverse
|
||||
|
||||
val targetRepository = (for {
|
||||
parentUserName <- repository.repository.parentUserName
|
||||
parentRepoName <- repository.repository.parentRepositoryName
|
||||
parentRepository <- getRepository(parentUserName, parentRepoName)
|
||||
} yield {
|
||||
parentRepository
|
||||
}).getOrElse {
|
||||
repository
|
||||
}
|
||||
|
||||
val proposedBranches = branches.filter { branch =>
|
||||
getPullRequestsByRequest(repository.owner, repository.name, branch, None).isEmpty
|
||||
}
|
||||
|
||||
html.proposals(proposedBranches, targetRepository, repository)
|
||||
})
|
||||
|
||||
/**
|
||||
* Parses branch identifier and extracts owner and branch name as tuple.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import java.io.File
|
||||
|
||||
import gitbucket.core.service.{AccountService, ActivityService, ReleaseService, RepositoryService}
|
||||
import gitbucket.core.util.{FileUtil, ReadableUsersAuthenticator, ReferrerAuthenticator, WritableUsersAuthenticator}
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import org.scalatra.forms._
|
||||
import gitbucket.core.releases.html
|
||||
import org.apache.commons.io.FileUtils
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
class ReleaseController extends ReleaseControllerBase
|
||||
with RepositoryService
|
||||
with AccountService
|
||||
with ReleaseService
|
||||
with ActivityService
|
||||
with ReadableUsersAuthenticator
|
||||
with ReferrerAuthenticator
|
||||
with WritableUsersAuthenticator
|
||||
|
||||
trait ReleaseControllerBase extends ControllerBase {
|
||||
self: RepositoryService
|
||||
with AccountService
|
||||
with ReleaseService
|
||||
with ReadableUsersAuthenticator
|
||||
with ReferrerAuthenticator
|
||||
with WritableUsersAuthenticator
|
||||
with ActivityService =>
|
||||
|
||||
case class ReleaseForm(
|
||||
name: String,
|
||||
content: Option[String]
|
||||
)
|
||||
|
||||
val releaseForm = mapping(
|
||||
"name" -> trim(text(required)),
|
||||
"content" -> trim(optional(text()))
|
||||
)(ReleaseForm.apply)
|
||||
|
||||
get("/:owner/:repository/releases")(referrersOnly {repository =>
|
||||
val releases = getReleases(repository.owner, repository.name)
|
||||
val assets = getReleaseAssetsMap(repository.owner, repository.name)
|
||||
|
||||
html.list(
|
||||
repository,
|
||||
repository.tags.reverse.map { tag =>
|
||||
(tag, releases.find(_.tag == tag.name).map { release => (release, assets(release)) })
|
||||
},
|
||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
|
||||
})
|
||||
|
||||
get("/:owner/:repository/releases/:tag")(referrersOnly { repository =>
|
||||
val tag = params("tag")
|
||||
getRelease(repository.owner, repository.name, tag).map { release =>
|
||||
html.release(release, getReleaseAssets(repository.owner, repository.name, tag), hasDeveloperRole(repository.owner, repository.name, context.loginAccount), repository)
|
||||
}.getOrElse(NotFound())
|
||||
})
|
||||
|
||||
get("/:owner/:repository/releases/:tag/assets/:fileId")(referrersOnly {repository =>
|
||||
val tag = params("tag")
|
||||
val fileId = params("fileId")
|
||||
(for {
|
||||
_ <- repository.tags.find(_.name == tag)
|
||||
_ <- getRelease(repository.owner, repository.name, tag)
|
||||
asset <- getReleaseAsset(repository.owner, repository.name, tag, fileId)
|
||||
} yield {
|
||||
response.setHeader("Content-Disposition", s"attachment; filename=${asset.label}")
|
||||
RawData(
|
||||
FileUtil.getMimeType(asset.label),
|
||||
new File(getReleaseFilesDir(repository.owner, repository.name), tag + "/" + fileId)
|
||||
)
|
||||
}).getOrElse(NotFound())
|
||||
})
|
||||
|
||||
get("/:owner/:repository/releases/:tag/create")(writableUsersOnly {repository =>
|
||||
html.form(repository, params("tag"), None)
|
||||
})
|
||||
|
||||
post("/:owner/:repository/releases/:tag/create", releaseForm)(writableUsersOnly { (form, repository) =>
|
||||
val tag = params("tag")
|
||||
val loginAccount = context.loginAccount.get
|
||||
|
||||
// Insert into RELEASE
|
||||
createRelease(repository.owner, repository.name, form.name, form.content, tag, loginAccount)
|
||||
|
||||
// Insert into RELEASE_ASSET
|
||||
request.getParameterNames.asScala.filter(_.startsWith("file:")).foreach { paramName =>
|
||||
val Array(_, fileId) = paramName.split(":")
|
||||
val fileName = params(paramName)
|
||||
val size = new java.io.File(getReleaseFilesDir(repository.owner, repository.name), tag + "/" + fileId).length
|
||||
|
||||
createReleaseAsset(repository.owner, repository.name, tag, fileId, fileName, size, loginAccount)
|
||||
}
|
||||
|
||||
recordReleaseActivity(repository.owner, repository.name, loginAccount.userName, form.name)
|
||||
|
||||
redirect(s"/${repository.owner}/${repository.name}/releases/${tag}")
|
||||
})
|
||||
|
||||
get("/:owner/:repository/releases/:tag/edit")(writableUsersOnly {repository =>
|
||||
val tag = params("tag")
|
||||
|
||||
getRelease(repository.owner, repository.name, tag).map { release =>
|
||||
html.form(repository, release.tag, Some(release, getReleaseAssets(repository.owner, repository.name, tag)))
|
||||
}.getOrElse(NotFound())
|
||||
})
|
||||
|
||||
post("/:owner/:repository/releases/:tag/edit", releaseForm)(writableUsersOnly { (form, repository) =>
|
||||
val tag = params("tag")
|
||||
val loginAccount = context.loginAccount.get
|
||||
|
||||
getRelease(repository.owner, repository.name, tag).map { release =>
|
||||
// Update RELEASE
|
||||
updateRelease(repository.owner, repository.name, tag, form.name, form.content)
|
||||
|
||||
// Delete and Insert RELEASE_ASSET
|
||||
val assets = getReleaseAssets(repository.owner, repository.name, tag)
|
||||
deleteReleaseAssets(repository.owner, repository.name, tag)
|
||||
|
||||
val fileIds = request.getParameterNames.asScala.filter(_.startsWith("file:")).map { paramName =>
|
||||
val Array(_, fileId) = paramName.split(":")
|
||||
val fileName = params(paramName)
|
||||
val size = new java.io.File(getReleaseFilesDir(repository.owner, repository.name), release.tag + "/" + fileId).length
|
||||
|
||||
createReleaseAsset(repository.owner, repository.name, tag, fileId, fileName, size, loginAccount)
|
||||
fileId
|
||||
}
|
||||
|
||||
assets.foreach { asset =>
|
||||
if(!fileIds.contains(asset.fileName)){
|
||||
val file = new java.io.File(getReleaseFilesDir(repository.owner, repository.name), release.tag + "/" + asset.fileName)
|
||||
FileUtils.forceDelete(file)
|
||||
}
|
||||
}
|
||||
|
||||
redirect(s"/${release.userName}/${release.repositoryName}/releases/${tag}")
|
||||
}.getOrElse(NotFound())
|
||||
})
|
||||
|
||||
post("/:owner/:repository/releases/:tag/delete")(writableUsersOnly { repository =>
|
||||
val tag = params("tag")
|
||||
getRelease(repository.owner, repository.name, tag).foreach { release =>
|
||||
FileUtils.deleteDirectory(new File(getReleaseFilesDir(repository.owner, repository.name), release.tag))
|
||||
}
|
||||
deleteRelease(repository.owner, repository.name, tag)
|
||||
redirect(s"/${repository.owner}/${repository.name}/releases")
|
||||
})
|
||||
|
||||
}
|
||||
@@ -39,19 +39,27 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
externalIssuesUrl: Option[String],
|
||||
wikiOption: String,
|
||||
externalWikiUrl: Option[String],
|
||||
allowFork: Boolean
|
||||
allowFork: Boolean,
|
||||
mergeOptions: Seq[String],
|
||||
defaultMergeOption: String
|
||||
)
|
||||
|
||||
val optionsForm = mapping(
|
||||
"repositoryName" -> trim(label("Repository Name" , text(required, maxlength(100), repository, renameRepositoryName))),
|
||||
"description" -> trim(label("Description" , optional(text()))),
|
||||
"isPrivate" -> trim(label("Repository Type" , boolean())),
|
||||
"issuesOption" -> trim(label("Issues Option" , text(required, featureOption))),
|
||||
"externalIssuesUrl" -> trim(label("External Issues URL", optional(text(maxlength(200))))),
|
||||
"wikiOption" -> trim(label("Wiki Option" , text(required, featureOption))),
|
||||
"externalWikiUrl" -> trim(label("External Wiki URL" , optional(text(maxlength(200))))),
|
||||
"allowFork" -> trim(label("Allow Forking" , boolean()))
|
||||
)(OptionsForm.apply)
|
||||
"repositoryName" -> trim(label("Repository Name" , text(required, maxlength(100), repository, renameRepositoryName))),
|
||||
"description" -> trim(label("Description" , optional(text()))),
|
||||
"isPrivate" -> trim(label("Repository Type" , boolean())),
|
||||
"issuesOption" -> trim(label("Issues Option" , text(required, featureOption))),
|
||||
"externalIssuesUrl" -> trim(label("External Issues URL", optional(text(maxlength(200))))),
|
||||
"wikiOption" -> trim(label("Wiki Option" , text(required, featureOption))),
|
||||
"externalWikiUrl" -> trim(label("External Wiki URL" , optional(text(maxlength(200))))),
|
||||
"allowFork" -> trim(label("Allow Forking" , boolean())),
|
||||
"mergeOptions" -> mergeOptions,
|
||||
"defaultMergeOption" -> trim(label("Default merge strategy", text(required)))
|
||||
)(OptionsForm.apply).verifying { form =>
|
||||
if(!form.mergeOptions.contains(form.defaultMergeOption)){
|
||||
Seq("defaultMergeOption" -> s"This merge strategy isn't enabled.")
|
||||
} else Nil
|
||||
}
|
||||
|
||||
// for default branch
|
||||
case class DefaultBranchForm(defaultBranch: String)
|
||||
@@ -118,7 +126,9 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
form.externalIssuesUrl,
|
||||
form.wikiOption,
|
||||
form.externalWikiUrl,
|
||||
form.allowFork
|
||||
form.allowFork,
|
||||
form.mergeOptions,
|
||||
form.defaultMergeOption
|
||||
)
|
||||
// Change repository name
|
||||
if(repository.name != form.repositoryName){
|
||||
@@ -142,6 +152,9 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
FileUtils.moveDirectory(dir, getRepositoryFilesDir(repository.owner, form.repositoryName))
|
||||
}
|
||||
}
|
||||
// Delete parent directory
|
||||
FileUtil.deleteDirectoryIfEmpty(getRepositoryFilesDir(repository.owner, repository.name))
|
||||
|
||||
// Call hooks
|
||||
PluginRegistry().getRepositoryHooks.foreach(_.renamed(repository.owner, repository.name, form.repositoryName))
|
||||
}
|
||||
@@ -179,7 +192,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
} else {
|
||||
val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch))
|
||||
val lastWeeks = getRecentStatuesContexts(repository.owner, repository.name,
|
||||
Date.from(LocalDateTime.now.minusWeeks(1).toInstant(ZoneOffset.of("UTC")))).toSet
|
||||
Date.from(LocalDateTime.now.minusWeeks(1).toInstant(ZoneOffset.UTC))).toSet
|
||||
val knownContexts = (lastWeeks ++ protection.status.contexts).toSeq.sortBy(identity)
|
||||
html.branchprotection(repository, branch, protection, knownContexts, flash.get("info"))
|
||||
}
|
||||
@@ -496,4 +509,21 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def mergeOptions = new ValueType[Seq[String]]{
|
||||
override def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[String] = {
|
||||
params.get("mergeOptions").getOrElse(Nil)
|
||||
}
|
||||
override def validate(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] = {
|
||||
val mergeOptions = params.get("mergeOptions").getOrElse(Nil)
|
||||
if(mergeOptions.isEmpty){
|
||||
Seq("mergeOptions" -> "At least one option must be enabled.")
|
||||
} else if(!mergeOptions.forall(x => Seq("merge-commit", "squash", "rebase").contains(x))){
|
||||
Seq("mergeOptions" -> "mergeOptions are invalid.")
|
||||
} else {
|
||||
Nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,12 +25,14 @@ import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder}
|
||||
import org.eclipse.jgit.errors.MissingObjectException
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
|
||||
import org.json4s.jackson.Serialization
|
||||
import org.scalatra._
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
|
||||
class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
||||
with LabelsService with MilestonesService with PrioritiesService
|
||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with PullRequestService with CommitStatusService
|
||||
with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService
|
||||
|
||||
@@ -148,14 +150,32 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
* Displays the file list of the repository root and the default branch.
|
||||
*/
|
||||
get("/:owner/:repository") {
|
||||
params.get("go-get") match {
|
||||
case Some("1") => defining(request.paths){ paths =>
|
||||
getRepository(paths(0), paths(1)).map(gitbucket.core.html.goget(_))getOrElse NotFound()
|
||||
val owner = params("owner")
|
||||
val repository = params("repository")
|
||||
|
||||
if (RepositoryCreationService.isCreating(owner, repository)) {
|
||||
gitbucket.core.repo.html.creating(owner, repository)
|
||||
} else {
|
||||
params.get("go-get") match {
|
||||
case Some("1") => defining(request.paths) { paths =>
|
||||
getRepository(owner, repository).map(gitbucket.core.html.goget(_)) getOrElse NotFound()
|
||||
}
|
||||
case _ => referrersOnly(fileList(_))
|
||||
}
|
||||
case _ => referrersOnly(fileList(_))
|
||||
}
|
||||
}
|
||||
|
||||
ajaxGet("/:owner/:repository/creating") {
|
||||
val owner = params("owner")
|
||||
val repository = params("repository")
|
||||
contentType = formats("json")
|
||||
val creating = RepositoryCreationService.isCreating(owner, repository)
|
||||
Serialization.write(Map(
|
||||
"creating" -> creating,
|
||||
"error" -> (if(creating) None else RepositoryCreationService.getCreationError(owner, repository))
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the file list of the specified path and branch.
|
||||
*/
|
||||
@@ -321,7 +341,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
commit = form.commit
|
||||
)
|
||||
|
||||
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
|
||||
redirect(s"/${repository.owner}/${repository.name}/blob/${urlEncode(form.branch)}/${
|
||||
if (form.path.length == 0) urlEncode(form.newFileName) else s"${form.path}/${urlEncode(form.newFileName)}"
|
||||
}")
|
||||
})
|
||||
@@ -382,13 +402,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
private def isLfsFile(git: Git, objectId: ObjectId): Boolean = {
|
||||
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
|
||||
if(loader.isLarge){
|
||||
false
|
||||
} else {
|
||||
new String(loader.getCachedBytes, "UTF-8").startsWith("version https://git-lfs.github.com/spec/v1")
|
||||
}
|
||||
}.getOrElse(false)
|
||||
JGitUtil.getObjectLoaderFromId(git, objectId)(JGitUtil.isLfsPointer).getOrElse(false)
|
||||
}
|
||||
|
||||
get("/:owner/:repository/blame/*"){
|
||||
@@ -403,7 +417,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
contentType = formats("json")
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
val last = git.log.add(git.getRepository.resolve(id)).addPath(path).setMaxCount(1).call.iterator.next.name
|
||||
Map(
|
||||
Serialization.write(Map(
|
||||
"root" -> s"${context.baseUrl}/${repository.owner}/${repository.name}",
|
||||
"id" -> id,
|
||||
"path" -> path,
|
||||
@@ -418,8 +432,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
"prevPath" -> blame.prevPath,
|
||||
"commited" -> blame.commitTime.getTime,
|
||||
"message" -> blame.message,
|
||||
"lines" -> blame.lines)
|
||||
})
|
||||
"lines" -> blame.lines
|
||||
)
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -432,14 +447,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
try {
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))) { revCommit =>
|
||||
JGitUtil.getDiffs(git, id, true) match {
|
||||
case (diffs, oldCommitId) =>
|
||||
html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
||||
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
|
||||
JGitUtil.getTagsOfCommit(git, revCommit.getName),
|
||||
getCommitComments(repository.owner, repository.name, id, true),
|
||||
repository, diffs, oldCommitId, hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
|
||||
}
|
||||
val diffs = JGitUtil.getDiffs(git, None, id, true, false)
|
||||
val oldCommitId = JGitUtil.getParentCommitId(git, id)
|
||||
|
||||
html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
||||
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
|
||||
JGitUtil.getTagsOfCommit(git, revCommit.getName),
|
||||
getCommitComments(repository.owner, repository.name, id, true),
|
||||
repository, diffs, oldCommitId, hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -447,6 +462,31 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/patch/:id")(referrersOnly { repository =>
|
||||
try {
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
val diff = JGitUtil.getPatch(git, None, params("id"))
|
||||
contentType = formats("txt")
|
||||
diff
|
||||
}
|
||||
} catch {
|
||||
case e:MissingObjectException => NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/patch/*...*")(referrersOnly { repository =>
|
||||
try {
|
||||
val Seq(fromId, toId) = multiParams("splat")
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
val diff = JGitUtil.getPatch(git, Some(fromId), toId)
|
||||
contentType = formats("txt")
|
||||
diff
|
||||
}
|
||||
} catch {
|
||||
case e: MissingObjectException => NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||
val id = params("id")
|
||||
createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, form.content,
|
||||
@@ -589,8 +629,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
/**
|
||||
* Displays tags.
|
||||
*/
|
||||
get("/:owner/:repository/tags")(referrersOnly {
|
||||
html.tags(_)
|
||||
get("/:owner/:repository/tags")(referrersOnly { repository =>
|
||||
redirect(s"${repository.owner}/${repository.name}/releases")
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -614,7 +654,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
repository.repository.originRepositoryName.getOrElse(repository.name)),
|
||||
getForkedRepositories(
|
||||
repository.repository.originUserName.getOrElse(repository.owner),
|
||||
repository.repository.originRepositoryName.getOrElse(repository.name)),
|
||||
repository.repository.originRepositoryName.getOrElse(repository.name)
|
||||
).map { repository => (repository.userName, repository.repositoryName) },
|
||||
context.loginAccount match {
|
||||
case None => List()
|
||||
case account: Option[Account] => getGroupsByUserName(account.get.userName)
|
||||
@@ -802,7 +843,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit =>
|
||||
val lastModifiedCommit = if(path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path)
|
||||
// get files
|
||||
val files = JGitUtil.getFileList(git, revision, path)
|
||||
val files = JGitUtil.getFileList(git, revision, path, context.settings.baseUrl)
|
||||
val parentPath = if (path == ".") Nil else path.split("/").toList
|
||||
// process README.md or README.markdown
|
||||
val readme = files.find { file =>
|
||||
|
||||
@@ -2,27 +2,33 @@ package gitbucket.core.controller
|
||||
|
||||
import java.io.FileInputStream
|
||||
|
||||
import gitbucket.core.admin.html
|
||||
import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService}
|
||||
import gitbucket.core.util.{AdminAuthenticator, Mailer}
|
||||
import gitbucket.core.ssh.SshServer
|
||||
import gitbucket.core.plugin.{PluginInfoBase, PluginRegistry, PluginRepository}
|
||||
import SystemSettingsService._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.StringUtil._
|
||||
import org.scalatra.forms._
|
||||
import org.apache.commons.io.{FileUtils, IOUtils}
|
||||
import org.scalatra.i18n.Messages
|
||||
import com.github.zafarkhaja.semver.{Version => Semver}
|
||||
import gitbucket.core.GitBucketCoreModule
|
||||
import scala.collection.JavaConverters._
|
||||
import gitbucket.core.admin.html
|
||||
import gitbucket.core.plugin.{PluginInfoBase, PluginRegistry, PluginRepository}
|
||||
import gitbucket.core.service.SystemSettingsService._
|
||||
import gitbucket.core.service.{AccountService, RepositoryService}
|
||||
import gitbucket.core.ssh.SshServer
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.StringUtil._
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
import gitbucket.core.util.{AdminAuthenticator, Mailer}
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.json4s.jackson.Serialization
|
||||
import org.scalatra._
|
||||
import org.scalatra.forms._
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
class SystemSettingsController extends SystemSettingsControllerBase
|
||||
with AccountService with RepositoryService with AdminAuthenticator
|
||||
|
||||
case class Table(name: String, columns: Seq[Column])
|
||||
case class Column(name: String, primaryKey: Boolean)
|
||||
|
||||
trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
self: AccountService with RepositoryService with AdminAuthenticator =>
|
||||
|
||||
@@ -64,6 +70,13 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
|
||||
"keystore" -> trim(label("Keystore", optional(text())))
|
||||
)(Ldap.apply)),
|
||||
"oidcAuthentication" -> trim(label("OIDC", boolean())),
|
||||
"oidc" -> optionalIfNotChecked("oidcAuthentication", mapping(
|
||||
"issuer" -> trim(label("Issuer", text(required))),
|
||||
"clientID" -> trim(label("Client ID", text(required))),
|
||||
"clientSecret" -> trim(label("Client secret", text(required))),
|
||||
"jwsAlgorithm" -> trim(label("Signature algorithm", optional(text())))
|
||||
)(OIDC.apply)),
|
||||
"skinName" -> trim(label("AdminLTE skin name", text(required)))
|
||||
)(SystemSettings.apply).verifying { settings =>
|
||||
Vector(
|
||||
@@ -152,8 +165,73 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
)(EditGroupForm.apply)
|
||||
|
||||
|
||||
get("/admin/dbviewer")(adminOnly {
|
||||
val conn = request2Session(request).conn
|
||||
val meta = conn.getMetaData
|
||||
val tables = ListBuffer[Table]()
|
||||
using(meta.getTables(null, "%", "%", Array("TABLE", "VIEW"))){ rs =>
|
||||
while(rs.next()){
|
||||
val tableName = rs.getString("TABLE_NAME")
|
||||
|
||||
val pkColumns = ListBuffer[String]()
|
||||
using(meta.getPrimaryKeys(null, null, tableName)){ rs =>
|
||||
while(rs.next()){
|
||||
pkColumns += rs.getString("COLUMN_NAME").toUpperCase
|
||||
}
|
||||
}
|
||||
|
||||
val columns = ListBuffer[Column]()
|
||||
using(meta.getColumns(null, "%", tableName, "%")){ rs =>
|
||||
while(rs.next()){
|
||||
val columnName = rs.getString("COLUMN_NAME").toUpperCase
|
||||
columns += Column(columnName, pkColumns.contains(columnName))
|
||||
}
|
||||
}
|
||||
|
||||
tables += Table(tableName.toUpperCase, columns)
|
||||
}
|
||||
}
|
||||
html.dbviewer(tables)
|
||||
})
|
||||
|
||||
post("/admin/dbviewer/_query")(adminOnly {
|
||||
contentType = formats("json")
|
||||
params.get("query").collectFirst { case query if query.trim.nonEmpty =>
|
||||
val trimmedQuery = query.trim
|
||||
if(trimmedQuery.nonEmpty){
|
||||
try {
|
||||
val conn = request2Session(request).conn
|
||||
using(conn.prepareStatement(query)){ stmt =>
|
||||
if(trimmedQuery.toUpperCase.startsWith("SELECT")){
|
||||
using(stmt.executeQuery()){ rs =>
|
||||
val meta = rs.getMetaData
|
||||
val columns = for(i <- 1 to meta.getColumnCount) yield {
|
||||
meta.getColumnName(i)
|
||||
}
|
||||
val result = ListBuffer[Map[String, String]]()
|
||||
while(rs.next()){
|
||||
val row = columns.map { columnName =>
|
||||
columnName -> Option(rs.getObject(columnName)).map(_.toString).getOrElse("<NULL>")
|
||||
}.toMap
|
||||
result += row
|
||||
}
|
||||
Ok(Serialization.write(Map("type" -> "query", "columns" -> columns, "rows" -> result)))
|
||||
}
|
||||
} else {
|
||||
val rows = stmt.executeUpdate()
|
||||
Ok(Serialization.write(Map("type" -> "update", "rows" -> rows)))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
Ok(Serialization.write(Map("type" -> "error", "message" -> e.toString)))
|
||||
}
|
||||
}
|
||||
} getOrElse Ok(Serialization.write(Map("type" -> "error", "message" -> "query is empty")))
|
||||
})
|
||||
|
||||
get("/admin/system")(adminOnly {
|
||||
html.system(flash.get("info"))
|
||||
html.settings(flash.get("info"))
|
||||
})
|
||||
|
||||
post("/admin/system", form)(adminOnly { form =>
|
||||
@@ -260,12 +338,13 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
get("/admin/users")(adminOnly {
|
||||
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
|
||||
val users = getAllUsers(includeRemoved)
|
||||
val includeGroups = params.get("includeGroups").map(_.toBoolean).getOrElse(false)
|
||||
val users = getAllUsers(includeRemoved, includeGroups)
|
||||
val members = users.collect { case account if(account.isGroupAccount) =>
|
||||
account.userName -> getGroupMembers(account.userName).map(_.userName)
|
||||
}.toMap
|
||||
|
||||
html.userlist(users, members, includeRemoved)
|
||||
html.userlist(users, members, includeRemoved, includeGroups)
|
||||
})
|
||||
|
||||
get("/admin/users/_newuser")(adminOnly {
|
||||
|
||||
@@ -76,7 +76,7 @@ trait WikiControllerBase extends ControllerBase {
|
||||
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||
|
||||
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true, false).filter(_.newPath == pageName + ".md"), repository,
|
||||
html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, Some(from), to, true, false).filter(_.newPath == pageName + ".md"), repository,
|
||||
isEditable(repository), flash.get("info"))
|
||||
}
|
||||
})
|
||||
@@ -85,7 +85,7 @@ trait WikiControllerBase extends ControllerBase {
|
||||
val Array(from, to) = params("commitId").split("\\.\\.\\.")
|
||||
|
||||
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true, false), repository,
|
||||
html.compare(None, from, to, JGitUtil.getDiffs(git, Some(from), to, true, false), repository,
|
||||
isEditable(repository), flash.get("info"))
|
||||
}
|
||||
})
|
||||
@@ -219,10 +219,13 @@ trait WikiControllerBase extends ControllerBase {
|
||||
|
||||
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
|
||||
val path = multiParams("splat").head
|
||||
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve("master"))
|
||||
|
||||
getFileContent(repository.owner, repository.name, path).map { bytes =>
|
||||
RawData(FileUtil.getContentType(path, bytes), bytes)
|
||||
} getOrElse NotFound()
|
||||
getPathObjectId(git, path, revCommit).map { objectId =>
|
||||
responseRawFile(git, objectId, path, repository)
|
||||
} getOrElse NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
private def unique: Constraint = new Constraint(){
|
||||
|
||||
19
src/main/scala/gitbucket/core/model/AccountFederation.scala
Normal file
19
src/main/scala/gitbucket/core/model/AccountFederation.scala
Normal file
@@ -0,0 +1,19 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
trait AccountFederationComponent { self: Profile =>
|
||||
import profile.api._
|
||||
|
||||
lazy val AccountFederations = TableQuery[AccountFederations]
|
||||
|
||||
class AccountFederations(tag: Tag) extends Table[AccountFederation](tag, "ACCOUNT_FEDERATION") {
|
||||
val issuer = column[String]("ISSUER")
|
||||
val subject = column[String]("SUBJECT")
|
||||
val userName = column[String]("USER_NAME")
|
||||
def * = (issuer, subject, userName) <> (AccountFederation.tupled, AccountFederation.unapply)
|
||||
|
||||
def byPrimaryKey(issuer: String, subject: String): Rep[Boolean] =
|
||||
(this.issuer === issuer.bind) && (this.subject === subject.bind)
|
||||
}
|
||||
}
|
||||
|
||||
case class AccountFederation(issuer: String, subject: String, userName: String)
|
||||
@@ -1,7 +1,7 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
import gitbucket.core.util.DatabaseConfig
|
||||
import com.github.takezoe.slick.blocking.BlockingJdbcProfile
|
||||
import gitbucket.core.util.DatabaseConfig
|
||||
|
||||
trait Profile {
|
||||
val profile: BlockingJdbcProfile
|
||||
@@ -61,7 +61,10 @@ trait CoreProfile extends ProfileProvider with Profile
|
||||
with RepositoryWebHookEventComponent
|
||||
with AccountWebHookComponent
|
||||
with AccountWebHookEventComponent
|
||||
with AccountFederationComponent
|
||||
with ProtectedBranchComponent
|
||||
with DeployKeyComponent
|
||||
with ReleaseComponent
|
||||
with ReleaseAssetComponent
|
||||
|
||||
object Profile extends CoreProfile
|
||||
|
||||
34
src/main/scala/gitbucket/core/model/Release.scala
Normal file
34
src/main/scala/gitbucket/core/model/Release.scala
Normal file
@@ -0,0 +1,34 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
trait ReleaseComponent extends TemplateComponent {
|
||||
self: Profile =>
|
||||
|
||||
import profile.api._
|
||||
import self._
|
||||
|
||||
lazy val Releases = TableQuery[Releases]
|
||||
|
||||
class Releases(tag_ : Tag) extends Table[Release](tag_, "RELEASE") with BasicTemplate {
|
||||
val name = column[String]("NAME")
|
||||
val tag = column[String]("TAG")
|
||||
val author = column[String]("AUTHOR")
|
||||
val content = column[Option[String]]("CONTENT")
|
||||
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||
|
||||
def * = (userName, repositoryName, name, tag, author, content, registeredDate, updatedDate) <> (Release.tupled, Release.unapply)
|
||||
def byPrimaryKey(owner: String, repository: String, tag: String) = byTag(owner, repository, tag)
|
||||
def byTag(owner: String, repository: String, tag: String) = byRepository(owner, repository) && (this.tag === tag.bind)
|
||||
}
|
||||
}
|
||||
|
||||
case class Release(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
name: String,
|
||||
tag: String,
|
||||
author: String,
|
||||
content: Option[String],
|
||||
registeredDate: java.util.Date,
|
||||
updatedDate: java.util.Date
|
||||
)
|
||||
40
src/main/scala/gitbucket/core/model/ReleasesAsset.scala
Normal file
40
src/main/scala/gitbucket/core/model/ReleasesAsset.scala
Normal file
@@ -0,0 +1,40 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
import java.util.Date
|
||||
|
||||
trait ReleaseAssetComponent extends TemplateComponent {
|
||||
self: Profile =>
|
||||
|
||||
import profile.api._
|
||||
import self._
|
||||
|
||||
lazy val ReleaseAssets = TableQuery[ReleaseAssets]
|
||||
|
||||
class ReleaseAssets(tag_ : Tag) extends Table[ReleaseAsset](tag_, "RELEASE_ASSET") with BasicTemplate {
|
||||
val tag = column[String]("TAG")
|
||||
val releaseAssetId = column[Int]("RELEASE_ASSET_ID", O AutoInc)
|
||||
val fileName = column[String]("FILE_NAME")
|
||||
val label = column[String]("LABEL")
|
||||
val size = column[Long]("SIZE")
|
||||
val uploader = column[String]("UPLOADER")
|
||||
val registeredDate = column[Date]("REGISTERED_DATE")
|
||||
val updatedDate = column[Date]("UPDATED_DATE")
|
||||
|
||||
def * = (userName, repositoryName, tag, releaseAssetId, fileName, label, size, uploader, registeredDate, updatedDate) <> (ReleaseAsset.tupled, ReleaseAsset.unapply)
|
||||
def byPrimaryKey(owner: String, repository: String, tag: String, fileName: String) = byTag(owner, repository, tag) && (this.fileName === fileName.bind)
|
||||
def byTag(owner: String, repository: String, tag: String) = byRepository(owner, repository) && (this.tag === tag.bind)
|
||||
}
|
||||
}
|
||||
|
||||
case class ReleaseAsset(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
tag: String,
|
||||
releaseAssetId: Int = 0,
|
||||
fileName: String,
|
||||
label: String,
|
||||
size: Long,
|
||||
uploader: String,
|
||||
registeredDate: Date,
|
||||
updatedDate: Date
|
||||
)
|
||||
@@ -22,11 +22,13 @@ trait RepositoryComponent extends TemplateComponent { self: Profile =>
|
||||
val wikiOption = column[String]("WIKI_OPTION")
|
||||
val externalWikiUrl = column[String]("EXTERNAL_WIKI_URL")
|
||||
val allowFork = column[Boolean]("ALLOW_FORK")
|
||||
val mergeOptions = column[String]("MERGE_OPTIONS")
|
||||
val defaultMergeOption = column[String]("DEFAULT_MERGE_OPTION")
|
||||
|
||||
def * = (
|
||||
(userName, repositoryName, isPrivate, description.?, defaultBranch,
|
||||
registeredDate, updatedDate, lastActivityDate, originUserName.?, originRepositoryName.?, parentUserName.?, parentRepositoryName.?),
|
||||
(issuesOption, externalIssuesUrl.?, wikiOption, externalWikiUrl.?, allowFork)
|
||||
(issuesOption, externalIssuesUrl.?, wikiOption, externalWikiUrl.?, allowFork, mergeOptions, defaultMergeOption)
|
||||
).shaped <> (
|
||||
{ case (repository, options) =>
|
||||
Repository(
|
||||
@@ -88,5 +90,7 @@ case class RepositoryOptions(
|
||||
externalIssuesUrl: Option[String],
|
||||
wikiOption: String,
|
||||
externalWikiUrl: Option[String],
|
||||
allowFork: Boolean
|
||||
allowFork: Boolean,
|
||||
mergeOptions: String,
|
||||
defaultMergeOption: String
|
||||
)
|
||||
|
||||
@@ -35,7 +35,9 @@ case class PluginMetadata(
|
||||
|
||||
case class VersionDef(
|
||||
version: String,
|
||||
file: String,
|
||||
url: String,
|
||||
range: String
|
||||
)
|
||||
){
|
||||
lazy val file = url.substring(url.lastIndexOf("/") + 1)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.model.Profile.{AccountFederations, Accounts}
|
||||
import gitbucket.core.model.{Account, AccountFederation}
|
||||
import gitbucket.core.util.SyntaxSugars.~
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
trait AccountFederationService {
|
||||
self: AccountService =>
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[AccountFederationService])
|
||||
|
||||
/**
|
||||
* Get or create a user account federated with OIDC or SAML IdP.
|
||||
*
|
||||
* @param issuer Issuer
|
||||
* @param subject Subject
|
||||
* @param mailAddress Mail address
|
||||
* @param preferredUserName Username (if this is none, username will be generated from the mail address)
|
||||
* @param fullName Fullname (defaults to username)
|
||||
* @return Account
|
||||
*/
|
||||
def getOrCreateFederatedUser(issuer: String,
|
||||
subject: String,
|
||||
mailAddress: String,
|
||||
preferredUserName: Option[String],
|
||||
fullName: Option[String])(implicit s: Session): Option[Account] =
|
||||
getAccountByFederation(issuer, subject) match {
|
||||
case Some(account) if !account.isRemoved =>
|
||||
Some(account)
|
||||
case Some(account) =>
|
||||
logger.info(s"Federated user found but disabled: userName=${account.userName}, isRemoved=${account.isRemoved}")
|
||||
None
|
||||
case None =>
|
||||
findAvailableUserName(preferredUserName, mailAddress) flatMap { userName =>
|
||||
createAccount(userName, "[DUMMY]", fullName.getOrElse(userName), mailAddress, isAdmin = false, None, None)
|
||||
createAccountFederation(issuer, subject, userName)
|
||||
getAccountByUserName(userName)
|
||||
}
|
||||
}
|
||||
|
||||
private def extractSafeStringForUserName(s: String) = """^[a-zA-Z0-9][a-zA-Z0-9\-_.]*""".r.findPrefixOf(s)
|
||||
|
||||
/**
|
||||
* Find an available username from the preferred username or mail address.
|
||||
*
|
||||
* @param mailAddress Mail address
|
||||
* @param preferredUserName Username
|
||||
* @return Available username
|
||||
*/
|
||||
def findAvailableUserName(preferredUserName: Option[String], mailAddress: String)(implicit s: Session): Option[String] = {
|
||||
preferredUserName.flatMap(n => extractSafeStringForUserName(n)).orElse(extractSafeStringForUserName(mailAddress)) match {
|
||||
case Some(safeUserName) =>
|
||||
getAccountByUserName(safeUserName, includeRemoved = true) match {
|
||||
case None => Some(safeUserName)
|
||||
case Some(_) =>
|
||||
logger.info(s"User ($safeUserName) already exists. preferredUserName=$preferredUserName, mailAddress=$mailAddress")
|
||||
None
|
||||
}
|
||||
case None =>
|
||||
logger.info(s"Could not extract username from preferredUserName=$preferredUserName, mailAddress=$mailAddress")
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
def getAccountByFederation(issuer: String, subject: String)(implicit s: Session): Option[Account] =
|
||||
AccountFederations.filter(_.byPrimaryKey(issuer, subject))
|
||||
.join(Accounts).on { case af ~ ac => af.userName === ac.userName }
|
||||
.map { case _ ~ ac => ac }
|
||||
.firstOption
|
||||
|
||||
def createAccountFederation(issuer: String, subject: String, userName: String)(implicit s: Session): Unit =
|
||||
AccountFederations insert AccountFederation(issuer, subject, userName)
|
||||
}
|
||||
|
||||
object AccountFederationService extends AccountFederationService with AccountService
|
||||
@@ -96,12 +96,14 @@ trait AccountService {
|
||||
def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] =
|
||||
Accounts filter(t => (t.mailAddress.toLowerCase === mailAddress.toLowerCase.bind) && (t.removed === false.bind, !includeRemoved)) firstOption
|
||||
|
||||
def getAllUsers(includeRemoved: Boolean = true)(implicit s: Session): List[Account] =
|
||||
if(includeRemoved){
|
||||
Accounts sortBy(_.userName) list
|
||||
} else {
|
||||
Accounts filter (_.removed === false.bind) sortBy(_.userName) list
|
||||
}
|
||||
def getAllUsers(includeRemoved: Boolean = true, includeGroups: Boolean = true)(implicit s: Session): List[Account] =
|
||||
{
|
||||
Accounts filter { t =>
|
||||
(1.bind === 1.bind) &&
|
||||
(t.groupAccount === false.bind, !includeGroups) &&
|
||||
(t.removed === false.bind, !includeRemoved)
|
||||
} sortBy(_.userName) list
|
||||
}
|
||||
|
||||
def isLastAdministrator(account: Account)(implicit s: Session): Boolean = {
|
||||
if(account.isAdmin){
|
||||
|
||||
@@ -190,6 +190,13 @@ trait ActivityService {
|
||||
Some(message),
|
||||
currentDate)
|
||||
|
||||
def recordReleaseActivity(userName: String, repositoryName: String, activityUserName: String, name: String)(implicit s: Session): Unit =
|
||||
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||
"release",
|
||||
s"[user:${activityUserName}] released ${name} at [repo:${userName}/${repositoryName}]",
|
||||
None,
|
||||
currentDate)
|
||||
|
||||
private def cut(value: String, length: Int): String =
|
||||
if(value.length > length) value.substring(0, length) + "..." else value
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import gitbucket.core.util.JGitUtil.CommitInfo
|
||||
import gitbucket.core.util.StringUtil._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.model.{Issue, PullRequest, IssueComment, IssueLabel, Label, Account, Repository, CommitState, Role}
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.model.Profile.profile._
|
||||
@@ -11,7 +12,7 @@ import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.model.Profile.dateColumnType
|
||||
|
||||
trait IssuesService {
|
||||
self: AccountService with RepositoryService =>
|
||||
self: AccountService with RepositoryService with LabelsService with PrioritiesService with MilestonesService =>
|
||||
import IssuesService._
|
||||
|
||||
def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) =
|
||||
@@ -321,11 +322,35 @@ trait IssuesService {
|
||||
} get
|
||||
}
|
||||
|
||||
def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session): Int = {
|
||||
def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int, insertComment: Boolean = false)(implicit context: Context, s: Session): Int = {
|
||||
if (insertComment) {
|
||||
IssueComments insert IssueComment(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
issueId = issueId,
|
||||
action = "add_label",
|
||||
commentedUserName = context.loginAccount.map(_.userName).getOrElse("Unknown user"),
|
||||
content = getLabel(owner, repository, labelId).map(_.labelName).getOrElse("Unknown label"),
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate
|
||||
)
|
||||
}
|
||||
IssueLabels insert IssueLabel(owner, repository, issueId, labelId)
|
||||
}
|
||||
|
||||
def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session): Int = {
|
||||
def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int, insertComment: Boolean = false)(implicit context: Context, s: Session): Int = {
|
||||
if (insertComment) {
|
||||
IssueComments insert IssueComment(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
issueId = issueId,
|
||||
action = "delete_label",
|
||||
commentedUserName = context.loginAccount.map(_.userName).getOrElse("Unknown user"),
|
||||
content = getLabel(owner, repository, labelId).map(_.labelName).getOrElse("Unknown label"),
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate
|
||||
)
|
||||
}
|
||||
IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
|
||||
}
|
||||
|
||||
@@ -350,15 +375,57 @@ trait IssuesService {
|
||||
.update(title, content, currentDate)
|
||||
}
|
||||
|
||||
def updateAssignedUserName(owner: String, repository: String, issueId: Int, assignedUserName: Option[String])(implicit s: Session): Int = {
|
||||
def updateAssignedUserName(owner: String, repository: String, issueId: Int, assignedUserName: Option[String], insertComment: Boolean = false)(implicit context: Context, s: Session): Int = {
|
||||
if (insertComment) {
|
||||
val oldAssigned = getIssue(owner, repository, s"${issueId}").get.assignedUserName.getOrElse("Not assigned")
|
||||
val assigned = assignedUserName.getOrElse("Not assigned")
|
||||
IssueComments insert IssueComment(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
issueId = issueId,
|
||||
action = "assign",
|
||||
commentedUserName = context.loginAccount.map(_.userName).getOrElse("Unknown user"),
|
||||
content = s"${oldAssigned}:${assigned}",
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate
|
||||
)
|
||||
}
|
||||
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(t => (t.assignedUserName?, t.updatedDate)).update(assignedUserName, currentDate)
|
||||
}
|
||||
|
||||
def updateMilestoneId(owner: String, repository: String, issueId: Int, milestoneId: Option[Int])(implicit s: Session): Int = {
|
||||
def updateMilestoneId(owner: String, repository: String, issueId: Int, milestoneId: Option[Int], insertComment: Boolean = false)(implicit context: Context, s: Session): Int = {
|
||||
if (insertComment) {
|
||||
val oldMilestoneName = getIssue(owner, repository, s"${issueId}").get.milestoneId.map(getMilestone(owner, repository, _).map(_.title).getOrElse("Unknown milestone")).getOrElse("No milestone")
|
||||
val milestoneName = milestoneId.map(getMilestone(owner, repository, _).map(_.title).getOrElse("Unknown milestone")).getOrElse("No milestone")
|
||||
IssueComments insert IssueComment(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
issueId = issueId,
|
||||
action = "change_milestone",
|
||||
commentedUserName = context.loginAccount.map(_.userName).getOrElse("Unknown user"),
|
||||
content = s"${oldMilestoneName}:${milestoneName}",
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate
|
||||
)
|
||||
}
|
||||
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(t => (t.milestoneId?, t.updatedDate)).update(milestoneId, currentDate)
|
||||
}
|
||||
|
||||
def updatePriorityId(owner: String, repository: String, issueId: Int, priorityId: Option[Int])(implicit s: Session): Int = {
|
||||
def updatePriorityId(owner: String, repository: String, issueId: Int, priorityId: Option[Int], insertComment: Boolean = false)(implicit context: Context, s: Session): Int = {
|
||||
if (insertComment) {
|
||||
val oldPriorityName = getIssue(owner, repository, s"${issueId}").get.priorityId.map(getPriority(owner, repository, _).map(_.priorityName).getOrElse("Unknown priority")).getOrElse("No priority")
|
||||
val priorityName = priorityId.map(getPriority(owner, repository, _).map(_.priorityName).getOrElse("Unknown priority")).getOrElse("No priority")
|
||||
IssueComments insert IssueComment(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
issueId = issueId,
|
||||
action = "change_priority",
|
||||
commentedUserName = context.loginAccount.map(_.userName).getOrElse("Unknown user"),
|
||||
content = s"${oldPriorityName}:${priorityName}",
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate
|
||||
)
|
||||
}
|
||||
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(t => (t.priorityId?, t.updatedDate)).update(priorityId, currentDate)
|
||||
}
|
||||
|
||||
@@ -471,7 +538,14 @@ trait IssuesService {
|
||||
|
||||
def getAssignableUserNames(owner: String, repository: String)(implicit s: Session): List[String] = {
|
||||
(getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER)) :::
|
||||
(if (getAccountByUserName(owner).get.isGroupAccount) getGroupMembers(owner).map(_.userName) else List(owner))).distinct.sorted
|
||||
(getAccountByUserName(owner) match {
|
||||
case Some(x) if x.isGroupAccount =>
|
||||
getGroupMembers(owner).map(_.userName)
|
||||
case Some(_) =>
|
||||
List(owner)
|
||||
case None =>
|
||||
Nil
|
||||
})).distinct.sorted
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,40 +3,55 @@ package gitbucket.core.service
|
||||
import gitbucket.core.model.Account
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
|
||||
import org.eclipse.jgit.merge.MergeStrategy
|
||||
import org.eclipse.jgit.merge.{MergeStrategy, Merger, RecursiveMerger}
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.transport.RefSpec
|
||||
import org.eclipse.jgit.errors.NoMergeBaseException
|
||||
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent, Repository}
|
||||
import org.eclipse.jgit.revwalk.RevWalk
|
||||
import org.eclipse.jgit.lib.{CommitBuilder, ObjectId, PersonIdent, Repository}
|
||||
import org.eclipse.jgit.revwalk.{RevCommit, RevWalk}
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
trait MergeService {
|
||||
import MergeService._
|
||||
|
||||
/**
|
||||
* Checks whether conflict will be caused in merging within pull request.
|
||||
* Returns true if conflict will be caused.
|
||||
*/
|
||||
def checkConflict(userName: String, repositoryName: String, branch: String, issueId: Int): Boolean = {
|
||||
def checkConflict(userName: String, repositoryName: String, branch: String, issueId: Int): Option[String] = {
|
||||
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
|
||||
MergeCacheInfo(git, branch, issueId).checkConflict()
|
||||
new MergeCacheInfo(git, branch, issueId).checkConflict()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether conflict will be caused in merging within pull request.
|
||||
* only cache check.
|
||||
* Returns Some(true) if conflict will be caused.
|
||||
* Returns None if cache has not created yet.
|
||||
*/
|
||||
def checkConflictCache(userName: String, repositoryName: String, branch: String, issueId: Int): Option[Boolean] = {
|
||||
def checkConflictCache(userName: String, repositoryName: String, branch: String, issueId: Int): Option[Option[String]] = {
|
||||
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
|
||||
MergeCacheInfo(git, branch, issueId).checkConflictCache()
|
||||
new MergeCacheInfo(git, branch, issueId).checkConflictCache()
|
||||
}
|
||||
}
|
||||
/** merge pull request */
|
||||
def mergePullRequest(git:Git, branch: String, issueId: Int, message:String, committer:PersonIdent): Unit = {
|
||||
MergeCacheInfo(git, branch, issueId).merge(message, committer)
|
||||
|
||||
/** merge the pull request with a merge commit */
|
||||
def mergePullRequest(git: Git, branch: String, issueId: Int, message: String, committer: PersonIdent): Unit = {
|
||||
new MergeCacheInfo(git, branch, issueId).merge(message, committer)
|
||||
}
|
||||
|
||||
/** rebase to the head of the pull request branch */
|
||||
def rebasePullRequest(git: Git, branch: String, issueId: Int, commits: Seq[RevCommit], committer: PersonIdent): Unit = {
|
||||
new MergeCacheInfo(git, branch, issueId).rebase(committer, commits)
|
||||
}
|
||||
|
||||
/** squash commits in the pull request and append it */
|
||||
def squashPullRequest(git: Git, branch: String, issueId: Int, message: String, committer: PersonIdent): Unit = {
|
||||
new MergeCacheInfo(git, branch, issueId).squash(message, committer)
|
||||
}
|
||||
|
||||
/** fetch remote branch to my repository refs/pull/{issueId}/head */
|
||||
def fetchAsPullRequest(userName: String, repositoryName: String, requestUserName: String, requestRepositoryName: String, requestBranch:String, issueId:Int){
|
||||
using(Git.open(getRepositoryDir(userName, repositoryName))){ git =>
|
||||
@@ -46,11 +61,12 @@ trait MergeService {
|
||||
.call
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
|
||||
*/
|
||||
def tryMergeRemote(localUserName: String, localRepositoryName: String, localBranch: String,
|
||||
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String): Option[(ObjectId, ObjectId, ObjectId)] = {
|
||||
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String): Either[String, (ObjectId, ObjectId, ObjectId)] = {
|
||||
using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git =>
|
||||
val remoteRefName = s"refs/heads/${remoteBranch}"
|
||||
val tmpRefName = s"refs/remote-temp/${remoteUserName}/${remoteRepositoryName}/${remoteBranch}"
|
||||
@@ -67,12 +83,12 @@ trait MergeService {
|
||||
val mergeTip = git.getRepository.resolve(tmpRefName)
|
||||
try {
|
||||
if(merger.merge(mergeBaseTip, mergeTip)){
|
||||
Some((merger.getResultTreeId, mergeBaseTip, mergeTip))
|
||||
Right((merger.getResultTreeId, mergeBaseTip, mergeTip))
|
||||
} else {
|
||||
None
|
||||
Left(createConflictMessage(mergeTip, mergeBaseTip, merger))
|
||||
}
|
||||
} catch {
|
||||
case e: NoMergeBaseException => None
|
||||
case e: NoMergeBaseException => Left(e.toString)
|
||||
}
|
||||
} finally {
|
||||
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
|
||||
@@ -81,30 +97,33 @@ trait MergeService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
|
||||
* Checks whether conflict will be caused in merging. Returns `Some(errorMessage)` if conflict will be caused.
|
||||
*/
|
||||
def checkConflict(userName: String, repositoryName: String, branch: String,
|
||||
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean =
|
||||
tryMergeRemote(userName, repositoryName, branch, requestUserName, requestRepositoryName, requestBranch).isEmpty
|
||||
requestUserName: String, requestRepositoryName: String, requestBranch: String): Option[String] =
|
||||
tryMergeRemote(userName, repositoryName, branch, requestUserName, requestRepositoryName, requestBranch).left.toOption
|
||||
|
||||
def pullRemote(localUserName: String, localRepositoryName: String, localBranch: String,
|
||||
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String,
|
||||
loginAccount: Account, message: String): Option[ObjectId] = {
|
||||
tryMergeRemote(localUserName, localRepositoryName, localBranch, remoteUserName, remoteRepositoryName, remoteBranch).map{ case (newTreeId, oldBaseId, oldHeadId) =>
|
||||
tryMergeRemote(localUserName, localRepositoryName, localBranch, remoteUserName, remoteRepositoryName, remoteBranch).map { case (newTreeId, oldBaseId, oldHeadId) =>
|
||||
using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git =>
|
||||
val committer = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
|
||||
val newCommit = Util.createMergeCommit(git.getRepository, newTreeId, committer, message, Seq(oldBaseId, oldHeadId))
|
||||
Util.updateRefs(git.getRepository, s"refs/heads/${localBranch}", newCommit, false, committer, Some("merge"))
|
||||
}
|
||||
oldBaseId
|
||||
}
|
||||
}.toOption
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object MergeService{
|
||||
|
||||
object Util{
|
||||
// return treeId
|
||||
// return merge commit id
|
||||
def createMergeCommit(repository: Repository, treeId: ObjectId, committer: PersonIdent, message: String, parents: Seq[ObjectId]): ObjectId = {
|
||||
val mergeCommit = new CommitBuilder()
|
||||
mergeCommit.setTreeId(treeId)
|
||||
@@ -113,14 +132,14 @@ object MergeService{
|
||||
mergeCommit.setCommitter(committer)
|
||||
mergeCommit.setMessage(message)
|
||||
// insertObject and got mergeCommit Object Id
|
||||
val inserter = repository.newObjectInserter
|
||||
val mergeCommitId = inserter.insert(mergeCommit)
|
||||
inserter.flush()
|
||||
inserter.close()
|
||||
mergeCommitId
|
||||
using(repository.newObjectInserter){ inserter =>
|
||||
val mergeCommitId = inserter.insert(mergeCommit)
|
||||
inserter.flush()
|
||||
mergeCommitId
|
||||
}
|
||||
}
|
||||
def updateRefs(repository: Repository, ref: String, newObjectId: ObjectId, force: Boolean, committer: PersonIdent, refLogMessage: Option[String] = None):Unit = {
|
||||
// update refs
|
||||
|
||||
def updateRefs(repository: Repository, ref: String, newObjectId: ObjectId, force: Boolean, committer: PersonIdent, refLogMessage: Option[String] = None): Unit = {
|
||||
val refUpdate = repository.updateRef(ref)
|
||||
refUpdate.setNewObjectId(newObjectId)
|
||||
refUpdate.setForceUpdate(force)
|
||||
@@ -129,33 +148,41 @@ object MergeService{
|
||||
refUpdate.update()
|
||||
}
|
||||
}
|
||||
case class MergeCacheInfo(git:Git, branch:String, issueId:Int){
|
||||
val repository = git.getRepository
|
||||
val mergedBranchName = s"refs/pull/${issueId}/merge"
|
||||
val conflictedBranchName = s"refs/pull/${issueId}/conflict"
|
||||
|
||||
class MergeCacheInfo(git: Git, branch: String, issueId: Int){
|
||||
|
||||
private val repository = git.getRepository
|
||||
|
||||
private val mergedBranchName = s"refs/pull/${issueId}/merge"
|
||||
private val conflictedBranchName = s"refs/pull/${issueId}/conflict"
|
||||
|
||||
lazy val mergeBaseTip = repository.resolve(s"refs/heads/${branch}")
|
||||
lazy val mergeTip = repository.resolve(s"refs/pull/${issueId}/head")
|
||||
def checkConflictCache(): Option[Boolean] = {
|
||||
Option(repository.resolve(mergedBranchName)).flatMap{ merged =>
|
||||
lazy val mergeTip = repository.resolve(s"refs/pull/${issueId}/head")
|
||||
|
||||
def checkConflictCache(): Option[Option[String]] = {
|
||||
Option(repository.resolve(mergedBranchName)).flatMap { merged =>
|
||||
if(parseCommit(merged).getParents().toSet == Set( mergeBaseTip, mergeTip )){
|
||||
// merged branch exists
|
||||
Some(false)
|
||||
Some(None)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}.orElse(Option(repository.resolve(conflictedBranchName)).flatMap{ conflicted =>
|
||||
if(parseCommit(conflicted).getParents().toSet == Set( mergeBaseTip, mergeTip )){
|
||||
val commit = parseCommit(conflicted)
|
||||
if(commit.getParents().toSet == Set( mergeBaseTip, mergeTip )){
|
||||
// conflict branch exists
|
||||
Some(true)
|
||||
Some(Some(commit.getFullMessage))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
def checkConflict():Boolean ={
|
||||
|
||||
def checkConflict(): Option[String] ={
|
||||
checkConflictCache.getOrElse(checkConflictForce)
|
||||
}
|
||||
def checkConflictForce():Boolean ={
|
||||
|
||||
def checkConflictForce(): Option[String] ={
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(repository, true)
|
||||
val conflicted = try {
|
||||
!merger.merge(mergeBaseTip, mergeTip)
|
||||
@@ -164,35 +191,114 @@ object MergeService{
|
||||
}
|
||||
val mergeTipCommit = using(new RevWalk( repository ))(_.parseCommit( mergeTip ))
|
||||
val committer = mergeTipCommit.getCommitterIdent
|
||||
def updateBranch(treeId:ObjectId, message:String, branchName:String){
|
||||
|
||||
def _updateBranch(treeId: ObjectId, message: String, branchName: String){
|
||||
// creates merge commit
|
||||
val mergeCommitId = createMergeCommit(treeId, committer, message)
|
||||
Util.updateRefs(repository, branchName, mergeCommitId, true, committer)
|
||||
}
|
||||
|
||||
if(!conflicted){
|
||||
updateBranch(merger.getResultTreeId, s"Merge ${mergeTip.name} into ${mergeBaseTip.name}", mergedBranchName)
|
||||
_updateBranch(merger.getResultTreeId, s"Merge ${mergeTip.name} into ${mergeBaseTip.name}", mergedBranchName)
|
||||
git.branchDelete().setForce(true).setBranchNames(conflictedBranchName).call()
|
||||
None
|
||||
} else {
|
||||
updateBranch(mergeTipCommit.getTree().getId(), s"can't merge ${mergeTip.name} into ${mergeBaseTip.name}", conflictedBranchName)
|
||||
val message = createConflictMessage(mergeTip, mergeBaseTip, merger)
|
||||
_updateBranch(mergeTipCommit.getTree().getId(), message, conflictedBranchName)
|
||||
git.branchDelete().setForce(true).setBranchNames(mergedBranchName).call()
|
||||
Some(message)
|
||||
}
|
||||
conflicted
|
||||
}
|
||||
|
||||
// update branch from cache
|
||||
def merge(message:String, committer:PersonIdent) = {
|
||||
if(checkConflict()){
|
||||
def merge(message: String, committer: PersonIdent) = {
|
||||
if(checkConflict().isDefined){
|
||||
throw new RuntimeException("This pull request can't merge automatically.")
|
||||
}
|
||||
val mergeResultCommit = parseCommit( Option(repository.resolve(mergedBranchName)).getOrElse(throw new RuntimeException(s"not found branch ${mergedBranchName}")) )
|
||||
val mergeResultCommit = parseCommit(Option(repository.resolve(mergedBranchName)).getOrElse {
|
||||
throw new RuntimeException(s"Not found branch ${mergedBranchName}")
|
||||
})
|
||||
// creates merge commit
|
||||
val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message)
|
||||
// update refs
|
||||
Util.updateRefs(repository, s"refs/heads/${branch}", mergeCommitId, false, committer, Some("merged"))
|
||||
}
|
||||
|
||||
def rebase(committer: PersonIdent, commits: Seq[RevCommit]): Unit = {
|
||||
if(checkConflict().isDefined){
|
||||
throw new RuntimeException("This pull request can't merge automatically.")
|
||||
}
|
||||
|
||||
def _cloneCommit(commit: RevCommit, parentId: ObjectId, baseId: ObjectId): CommitBuilder = {
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(repository, true)
|
||||
merger.merge(commit.toObjectId, baseId)
|
||||
|
||||
val newCommit = new CommitBuilder()
|
||||
newCommit.setTreeId(merger.getResultTreeId)
|
||||
newCommit.addParentId(parentId)
|
||||
newCommit.setAuthor(commit.getAuthorIdent)
|
||||
newCommit.setCommitter(committer)
|
||||
newCommit.setMessage(commit.getFullMessage)
|
||||
newCommit
|
||||
}
|
||||
|
||||
val mergeBaseTipCommit = using(new RevWalk( repository ))(_.parseCommit( mergeBaseTip ))
|
||||
var previousId = mergeBaseTipCommit.getId
|
||||
|
||||
using(repository.newObjectInserter){ inserter =>
|
||||
commits.foreach { commit =>
|
||||
val nextCommit = _cloneCommit(commit, previousId, mergeBaseTipCommit.getId)
|
||||
previousId = inserter.insert(nextCommit)
|
||||
}
|
||||
inserter.flush()
|
||||
}
|
||||
|
||||
Util.updateRefs(repository, s"refs/heads/${branch}", previousId, false, committer, Some("rebased"))
|
||||
}
|
||||
|
||||
def squash(message: String, committer: PersonIdent): Unit = {
|
||||
if(checkConflict().isDefined){
|
||||
throw new RuntimeException("This pull request can't merge automatically.")
|
||||
}
|
||||
|
||||
val mergeBaseTipCommit = using(new RevWalk( repository ))(_.parseCommit(mergeBaseTip))
|
||||
val mergeBranchHeadCommit = using(new RevWalk( repository ))(_.parseCommit(repository.resolve(mergedBranchName)))
|
||||
|
||||
// Create squash commit
|
||||
val mergeCommit = new CommitBuilder()
|
||||
mergeCommit.setTreeId(mergeBranchHeadCommit.getTree.getId)
|
||||
mergeCommit.setParentId(mergeBaseTipCommit)
|
||||
mergeCommit.setAuthor(mergeBranchHeadCommit.getAuthorIdent)
|
||||
mergeCommit.setCommitter(committer)
|
||||
mergeCommit.setMessage(message)
|
||||
|
||||
// insertObject and got squash commit Object Id
|
||||
val newCommitId = using(repository.newObjectInserter){ inserter =>
|
||||
val newCommitId = inserter.insert(mergeCommit)
|
||||
inserter.flush()
|
||||
newCommitId
|
||||
}
|
||||
|
||||
Util.updateRefs(repository, mergedBranchName, newCommitId, true, committer)
|
||||
|
||||
// rebase to squash commit
|
||||
Util.updateRefs(repository, s"refs/heads/${branch}", repository.resolve(mergedBranchName), false, committer, Some("squashed"))
|
||||
}
|
||||
|
||||
// return treeId
|
||||
private def createMergeCommit(treeId: ObjectId, committer: PersonIdent, message: String) =
|
||||
Util.createMergeCommit(repository, treeId, committer, message, Seq[ObjectId](mergeBaseTip, mergeTip))
|
||||
|
||||
private def parseCommit(id:ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id))
|
||||
private def parseCommit(id: ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id))
|
||||
|
||||
}
|
||||
|
||||
private def createConflictMessage(mergeTip: ObjectId, mergeBaseTip: ObjectId, merger: Merger): String = {
|
||||
val mergeResults = merger.asInstanceOf[RecursiveMerger].getMergeResults
|
||||
|
||||
s"Can't merge ${mergeTip.name} into ${mergeBaseTip.name}\n\n" +
|
||||
"Conflicting files:\n" +
|
||||
mergeResults.asScala.map { case (key, _) => "- " + key + "\n" }.mkString
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
191
src/main/scala/gitbucket/core/service/OpenIDConnectService.scala
Normal file
191
src/main/scala/gitbucket/core/service/OpenIDConnectService.scala
Normal file
@@ -0,0 +1,191 @@
|
||||
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.oauth2.sdk._
|
||||
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic
|
||||
import com.nimbusds.oauth2.sdk.id.{ClientID, Issuer, State}
|
||||
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet
|
||||
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata
|
||||
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator
|
||||
import com.nimbusds.openid.connect.sdk.{AuthenticationErrorResponse, _}
|
||||
import gitbucket.core.model.Account
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import scala.collection.JavaConverters.{asScalaSet, mapAsJavaMap}
|
||||
|
||||
/**
|
||||
* Service class for the OpenID Connect authentication.
|
||||
*/
|
||||
trait OpenIDConnectService {
|
||||
self: AccountFederationService =>
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[OpenIDConnectService])
|
||||
|
||||
private val JWK_REQUEST_TIMEOUT = 5000
|
||||
|
||||
private val OIDC_SCOPE = new Scope(
|
||||
OIDCScopeValue.OPENID,
|
||||
OIDCScopeValue.EMAIL,
|
||||
OIDCScopeValue.PROFILE)
|
||||
|
||||
/**
|
||||
* Obtain the OIDC metadata from discovery and create an authentication request.
|
||||
*
|
||||
* @param issuer Issuer, used to construct the discovery endpoint URL, e.g. https://accounts.google.com
|
||||
* @param clientID Client ID (given by the issuer)
|
||||
* @param redirectURI Redirect URI
|
||||
* @return Authentication request
|
||||
*/
|
||||
def createOIDCAuthenticationRequest(issuer: Issuer,
|
||||
clientID: ClientID,
|
||||
redirectURI: URI): AuthenticationRequest = {
|
||||
val metadata = OIDCProviderMetadata.resolve(issuer)
|
||||
new AuthenticationRequest(
|
||||
metadata.getAuthorizationEndpointURI,
|
||||
new ResponseType(ResponseType.Value.CODE),
|
||||
OIDC_SCOPE,
|
||||
clientID,
|
||||
redirectURI,
|
||||
new State(),
|
||||
new Nonce())
|
||||
}
|
||||
|
||||
/**
|
||||
* Proceed the OpenID Connect authentication.
|
||||
*
|
||||
* @param params Query parameters of the authentication response
|
||||
* @param redirectURI Redirect URI
|
||||
* @param state State saved in the session
|
||||
* @param nonce Nonce saved in the session
|
||||
* @param oidc OIDC settings
|
||||
* @return ID token
|
||||
*/
|
||||
def authenticate(params: Map[String, String],
|
||||
redirectURI: URI,
|
||||
state: State,
|
||||
nonce: Nonce,
|
||||
oidc: SystemSettingsService.OIDC)(implicit s: Session): Option[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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the authentication response.
|
||||
*
|
||||
* @param params Query parameters of the authentication response
|
||||
* @param state State saved in the session
|
||||
* @param redirectURI Redirect URI
|
||||
* @return Authentication response
|
||||
*/
|
||||
def validateOIDCAuthenticationResponse(params: Map[String, String], state: State, redirectURI: URI): Option[AuthenticationSuccessResponse] =
|
||||
try {
|
||||
AuthenticationResponseParser.parse(redirectURI, mapAsJavaMap(params)) match {
|
||||
case response: AuthenticationSuccessResponse =>
|
||||
if (response.getState == state) {
|
||||
Some(response)
|
||||
} else {
|
||||
logger.info(s"OIDC authentication state did not match: response(${response.getState}) != session($state)")
|
||||
None
|
||||
}
|
||||
case response: AuthenticationErrorResponse =>
|
||||
logger.info(s"OIDC authentication response has error: ${response.getErrorObject}")
|
||||
None
|
||||
}
|
||||
} catch {
|
||||
case e: ParseException =>
|
||||
logger.info(s"OIDC authentication response has error: $e")
|
||||
None
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the ID token from the OpenID Provider.
|
||||
*
|
||||
* @param authorizationCode Authorization code in the query string
|
||||
* @param nonce Nonce
|
||||
* @param redirectURI Redirect URI
|
||||
* @param oidc OIDC settings
|
||||
* @return Token response
|
||||
*/
|
||||
def obtainOIDCToken(authorizationCode: AuthorizationCode,
|
||||
nonce: Nonce,
|
||||
redirectURI: URI,
|
||||
oidc: SystemSettingsService.OIDC): Option[IDTokenClaimsSet] = {
|
||||
val metadata = OIDCProviderMetadata.resolve(oidc.issuer)
|
||||
val tokenRequest = new TokenRequest(metadata.getTokenEndpointURI,
|
||||
new ClientSecretBasic(oidc.clientID, oidc.clientSecret),
|
||||
new AuthorizationCodeGrant(authorizationCode, redirectURI),
|
||||
OIDC_SCOPE)
|
||||
val httpResponse = tokenRequest.toHTTPRequest.send()
|
||||
try {
|
||||
OIDCTokenResponseParser.parse(httpResponse) match {
|
||||
case response: OIDCTokenResponse =>
|
||||
validateOIDCTokenResponse(response, metadata, nonce, oidc)
|
||||
case response: TokenErrorResponse =>
|
||||
logger.info(s"OIDC token response has error: ${response.getErrorObject.toJSONObject}")
|
||||
None
|
||||
}
|
||||
} catch {
|
||||
case e: ParseException =>
|
||||
logger.info(s"OIDC token response has error: $e")
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the token response.
|
||||
*
|
||||
* @param response Token response
|
||||
* @param metadata OpenID Provider metadata
|
||||
* @param nonce Nonce
|
||||
* @return Claims
|
||||
*/
|
||||
def validateOIDCTokenResponse(response: OIDCTokenResponse,
|
||||
metadata: OIDCProviderMetadata,
|
||||
nonce: Nonce,
|
||||
oidc: SystemSettingsService.OIDC): Option[IDTokenClaimsSet] =
|
||||
Option(response.getOIDCTokens.getIDToken) match {
|
||||
case Some(jwt) =>
|
||||
val validator = oidc.jwsAlgorithm map { jwsAlgorithm =>
|
||||
new IDTokenValidator(metadata.getIssuer, oidc.clientID, jwsAlgorithm, metadata.getJWKSetURI.toURL,
|
||||
new DefaultResourceRetriever(JWK_REQUEST_TIMEOUT, JWK_REQUEST_TIMEOUT))
|
||||
} getOrElse {
|
||||
new IDTokenValidator(metadata.getIssuer, oidc.clientID)
|
||||
}
|
||||
try {
|
||||
Some(validator.validate(jwt, nonce))
|
||||
} catch {
|
||||
case e@(_: BadJOSEException | _: JOSEException) =>
|
||||
logger.info(s"OIDC ID token has error: $e")
|
||||
None
|
||||
}
|
||||
case None =>
|
||||
logger.info(s"OIDC token response does not have a valid ID token: ${response.toJSONObject}")
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
object OpenIDConnectService {
|
||||
/**
|
||||
* All signature algorithms.
|
||||
*/
|
||||
val JWS_ALGORITHMS: Map[String, Set[JWSAlgorithm]] = Seq(
|
||||
"HMAC" -> Family.HMAC_SHA,
|
||||
"RSA" -> Family.RSA,
|
||||
"ECDSA" -> Family.EC,
|
||||
"EdDSA" -> Family.ED
|
||||
).toMap.map { case (name, family) => (name, asScalaSet(family).toSet) }
|
||||
}
|
||||
@@ -79,7 +79,7 @@ trait PullRequestService { self: IssuesService with CommitsService =>
|
||||
commitIdFrom,
|
||||
commitIdTo)
|
||||
|
||||
def getPullRequestsByRequest(userName: String, repositoryName: String, branch: String, closed: Boolean)
|
||||
def getPullRequestsByRequest(userName: String, repositoryName: String, branch: String, closed: Option[Boolean])
|
||||
(implicit s: Session): List[PullRequest] =
|
||||
PullRequests
|
||||
.join(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
|
||||
@@ -87,7 +87,7 @@ trait PullRequestService { self: IssuesService with CommitsService =>
|
||||
(t1.requestUserName === userName.bind) &&
|
||||
(t1.requestRepositoryName === repositoryName.bind) &&
|
||||
(t1.requestBranch === branch.bind) &&
|
||||
(t2.closed === closed.bind)
|
||||
(t2.closed === closed.get.bind, closed.isDefined)
|
||||
}
|
||||
.map { case (t1, t2) => t1 }
|
||||
.list
|
||||
@@ -118,7 +118,7 @@ trait PullRequestService { self: IssuesService with CommitsService =>
|
||||
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
|
||||
*/
|
||||
def updatePullRequests(owner: String, repository: String, branch: String)(implicit s: Session): Unit =
|
||||
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
|
||||
getPullRequestsByRequest(owner, repository, branch, Some(false)).foreach { pullreq =>
|
||||
if(Repositories.filter(_.byRepository(pullreq.userName, pullreq.repositoryName)).exists.run){
|
||||
// Update the git repository
|
||||
val (commitIdTo, commitIdFrom) = JGitUtil.updatePullRequest(
|
||||
@@ -230,7 +230,7 @@ trait PullRequestService { self: IssuesService with CommitsService =>
|
||||
helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
|
||||
}
|
||||
|
||||
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true, false)
|
||||
val diffs = JGitUtil.getDiffs(newGit, Some(oldId.getName), newId.getName, true, false)
|
||||
|
||||
(commits, diffs)
|
||||
}
|
||||
@@ -244,8 +244,8 @@ object PullRequestService {
|
||||
case class PullRequestCount(userName: String, count: Int)
|
||||
|
||||
case class MergeStatus(
|
||||
hasConflict: Boolean,
|
||||
commitStatues:List[CommitStatus],
|
||||
conflictMessage: Option[String],
|
||||
commitStatues: List[CommitStatus],
|
||||
branchProtection: ProtectedBranchService.ProtectedBranchInfo,
|
||||
branchIsOutOfDate: Boolean,
|
||||
hasUpdatePermission: Boolean,
|
||||
@@ -253,12 +253,13 @@ object PullRequestService {
|
||||
hasMergePermission: Boolean,
|
||||
commitIdTo: String){
|
||||
|
||||
val hasConflict = conflictMessage.isDefined
|
||||
val statuses: List[CommitStatus] =
|
||||
commitStatues ++ (branchProtection.contexts.toSet -- commitStatues.map(_.context).toSet).map(CommitStatus.pending(branchProtection.owner, branchProtection.repository, _))
|
||||
val hasRequiredStatusProblem = needStatusCheck && branchProtection.contexts.exists(context => statuses.find(_.context == context).map(_.state) != Some(CommitState.SUCCESS))
|
||||
val hasProblem = hasRequiredStatusProblem || hasConflict || (statuses.nonEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
|
||||
val canUpdate = branchIsOutOfDate && !hasConflict
|
||||
val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
|
||||
val canUpdate = branchIsOutOfDate && !hasConflict
|
||||
val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
|
||||
lazy val commitStateSummary:(CommitState, String) = {
|
||||
val stateMap = statuses.groupBy(_.state)
|
||||
val state = CommitState.combine(stateMap.keySet)
|
||||
|
||||
87
src/main/scala/gitbucket/core/service/ReleaseService.scala
Normal file
87
src/main/scala/gitbucket/core/service/ReleaseService.scala
Normal file
@@ -0,0 +1,87 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.model.{Account, Release, ReleaseAsset}
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.model.Profile.dateColumnType
|
||||
|
||||
trait ReleaseService {
|
||||
self: AccountService with RepositoryService =>
|
||||
|
||||
def createReleaseAsset(owner: String, repository: String, tag: String, fileName: String, label: String, size: Long, loginAccount: Account)(implicit s: Session): Unit = {
|
||||
ReleaseAssets insert ReleaseAsset(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
tag = tag,
|
||||
fileName = fileName,
|
||||
label = label,
|
||||
size = size,
|
||||
uploader = loginAccount.userName,
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate
|
||||
)
|
||||
}
|
||||
|
||||
def getReleaseAssets(owner: String, repository: String, tag: String)(implicit s: Session): Seq[ReleaseAsset] = {
|
||||
ReleaseAssets.filter(x => x.byTag(owner, repository, tag)).list
|
||||
}
|
||||
|
||||
def getReleaseAssetsMap(owner: String, repository: String)(implicit s: Session): Map[Release, Seq[ReleaseAsset]] = {
|
||||
val releases = getReleases(owner, repository)
|
||||
releases.map(rel => (rel -> getReleaseAssets(owner, repository, rel.tag))).toMap
|
||||
}
|
||||
|
||||
def getReleaseAsset(owner: String, repository: String, tag: String, fileId: String)(implicit s: Session): Option[ReleaseAsset] = {
|
||||
ReleaseAssets.filter(x => x.byPrimaryKey(owner, repository, tag, fileId)) firstOption
|
||||
}
|
||||
|
||||
def deleteReleaseAssets(owner: String, repository: String, tag: String)(implicit s: Session): Unit = {
|
||||
ReleaseAssets.filter(x => x.byTag(owner, repository, tag)) delete
|
||||
}
|
||||
|
||||
def createRelease(owner: String, repository: String, name: String, content: Option[String], tag: String,
|
||||
loginAccount: Account)(implicit context: Context, s: Session): Int = {
|
||||
Releases insert Release(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
name = name,
|
||||
tag = tag,
|
||||
author = loginAccount.userName,
|
||||
content = content,
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate
|
||||
)
|
||||
}
|
||||
|
||||
def getReleases(owner: String, repository: String)(implicit s: Session): Seq[Release] = {
|
||||
Releases.filter(x => x.byRepository(owner, repository)).list
|
||||
}
|
||||
|
||||
def getRelease(owner: String, repository: String, tag: String)(implicit s: Session): Option[Release] = {
|
||||
//Releases filter (_.byPrimaryKey(owner, repository, releaseId)) firstOption
|
||||
Releases filter (_.byTag(owner, repository, tag)) firstOption
|
||||
}
|
||||
|
||||
// def getReleaseByTag(owner: String, repository: String, tag: String)(implicit s: Session): Option[Release] = {
|
||||
// Releases filter (_.byTag(owner, repository, tag)) firstOption
|
||||
// }
|
||||
//
|
||||
// def getRelease(owner: String, repository: String, releaseId: String)(implicit s: Session): Option[Release] = {
|
||||
// if (isInteger(releaseId))
|
||||
// getRelease(owner, repository, releaseId.toInt)
|
||||
// else None
|
||||
// }
|
||||
|
||||
def updateRelease(owner: String, repository: String, tag: String, title: String, content: Option[String])(implicit s: Session): Int = {
|
||||
Releases
|
||||
.filter (_.byPrimaryKey(owner, repository, tag))
|
||||
.map { t => (t.name, t.content, t.updatedDate) }
|
||||
.update (title, content, currentDate)
|
||||
}
|
||||
|
||||
def deleteRelease(owner: String, repository: String, tag: String)(implicit s: Session): Unit = {
|
||||
deleteReleaseAssets(owner, repository, tag)
|
||||
Releases filter (_.byPrimaryKey(owner, repository, tag)) delete
|
||||
}
|
||||
}
|
||||
@@ -1,71 +1,209 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.JGitUtil
|
||||
import gitbucket.core.model.Account
|
||||
import gitbucket.core.util.{FileUtil, JGitUtil, LockUtil}
|
||||
import gitbucket.core.model.{Account, Role}
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.servlet.Database
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.dircache.DirCache
|
||||
import org.eclipse.jgit.lib.{FileMode, Constants}
|
||||
import org.eclipse.jgit.lib.{Constants, FileMode}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.Future
|
||||
|
||||
object RepositoryCreationService {
|
||||
|
||||
private val Creating = new ConcurrentHashMap[String, Option[String]]()
|
||||
|
||||
def isCreating(owner: String, repository: String): Boolean = {
|
||||
Option(Creating.get(s"${owner}/${repository}")).map(_.isEmpty).getOrElse(false)
|
||||
}
|
||||
|
||||
def startCreation(owner: String, repository: String): Unit = {
|
||||
Creating.put(s"${owner}/${repository}", None)
|
||||
}
|
||||
|
||||
def endCreation(owner: String, repository: String, error: Option[String]): Unit = {
|
||||
error match {
|
||||
case None => Creating.remove(s"${owner}/${repository}")
|
||||
case Some(error) => Creating.put(s"${owner}/${repository}", Some(error))
|
||||
}
|
||||
}
|
||||
|
||||
def getCreationError(owner: String, repository: String): Option[String] = {
|
||||
Option(Creating.remove(s"${owner}/${repository}")).getOrElse(None)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
trait RepositoryCreationService {
|
||||
self: AccountService with RepositoryService with LabelsService with WikiService with ActivityService with PrioritiesService =>
|
||||
|
||||
def createRepository(loginAccount: Account, owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
|
||||
(implicit s: Session) {
|
||||
val ownerAccount = getAccountByUserName(owner).get
|
||||
val loginUserName = loginAccount.userName
|
||||
def createRepository(loginAccount: Account, owner: String, name: String, description: Option[String],
|
||||
isPrivate: Boolean, createReadme: Boolean): Future[Unit] = {
|
||||
createRepository(loginAccount, owner, name, description, isPrivate, if (createReadme) "README" else "EMPTY", None)
|
||||
}
|
||||
|
||||
// Insert to the database at first
|
||||
insertRepository(name, owner, description, isPrivate)
|
||||
def createRepository(loginAccount: Account, owner: String, name: String, description: Option[String],
|
||||
isPrivate: Boolean, initOption: String, sourceUrl: Option[String]): Future[Unit] = Future {
|
||||
RepositoryCreationService.startCreation(owner, name)
|
||||
try {
|
||||
Database() withTransaction { implicit session =>
|
||||
val ownerAccount = getAccountByUserName(owner).get
|
||||
val loginUserName = loginAccount.userName
|
||||
|
||||
// // Add collaborators for group repository
|
||||
// if(ownerAccount.isGroupAccount){
|
||||
// getGroupMembers(owner).foreach { member =>
|
||||
// addCollaborator(owner, name, member.userName)
|
||||
// }
|
||||
// }
|
||||
val copyRepositoryDir = if (initOption == "COPY") {
|
||||
sourceUrl.flatMap { url =>
|
||||
val dir = Files.createTempDirectory(s"gitbucket-${owner}-${name}").toFile
|
||||
Git.cloneRepository().setBare(true).setURI(url).setDirectory(dir).setCloneAllBranches(true).call()
|
||||
Some(dir)
|
||||
}
|
||||
} else None
|
||||
|
||||
// Insert default labels
|
||||
insertDefaultLabels(owner, name)
|
||||
|
||||
// Insert default priorities
|
||||
insertDefaultPriorities(owner, name)
|
||||
// Insert to the database at first
|
||||
insertRepository(name, owner, description, isPrivate)
|
||||
|
||||
// Create the actual repository
|
||||
val gitdir = getRepositoryDir(owner, name)
|
||||
JGitUtil.initRepository(gitdir)
|
||||
// // Add collaborators for group repository
|
||||
// if(ownerAccount.isGroupAccount){
|
||||
// getGroupMembers(owner).foreach { member =>
|
||||
// addCollaborator(owner, name, member.userName)
|
||||
// }
|
||||
// }
|
||||
|
||||
if(createReadme){
|
||||
using(Git.open(gitdir)){ git =>
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||
val content = if(description.nonEmpty){
|
||||
name + "\n" +
|
||||
"===============\n" +
|
||||
"\n" +
|
||||
description.get
|
||||
} else {
|
||||
name + "\n" +
|
||||
"===============\n"
|
||||
// Insert default labels
|
||||
insertDefaultLabels(owner, name)
|
||||
|
||||
// Insert default priorities
|
||||
insertDefaultPriorities(owner, name)
|
||||
|
||||
// Create the actual repository
|
||||
val gitdir = getRepositoryDir(owner, name)
|
||||
JGitUtil.initRepository(gitdir)
|
||||
|
||||
if (initOption == "README" || initOption == "EMPTY_COMMIT") {
|
||||
using(Git.open(gitdir)) { git =>
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||
|
||||
if (initOption == "README") {
|
||||
val content = if (description.nonEmpty) {
|
||||
name + "\n" +
|
||||
"===============\n" +
|
||||
"\n" +
|
||||
description.get
|
||||
} else {
|
||||
name + "\n" +
|
||||
"===============\n"
|
||||
}
|
||||
|
||||
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
|
||||
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||
}
|
||||
builder.finish()
|
||||
|
||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
|
||||
Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
|
||||
}
|
||||
}
|
||||
|
||||
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
|
||||
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||
builder.finish()
|
||||
copyRepositoryDir.foreach { dir =>
|
||||
try {
|
||||
using(Git.open(dir)) { git =>
|
||||
git.push().setRemote(gitdir.toURI.toString).setPushAll().setPushTags().call()
|
||||
}
|
||||
} finally {
|
||||
FileUtils.deleteQuietly(dir)
|
||||
}
|
||||
}
|
||||
|
||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
|
||||
Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
|
||||
// Create Wiki repository
|
||||
createWikiRepository(loginAccount, owner, name)
|
||||
|
||||
// Record activity
|
||||
recordCreateRepositoryActivity(owner, name, loginUserName)
|
||||
|
||||
// Call hooks
|
||||
PluginRegistry().getRepositoryHooks.foreach(_.created(owner, name))
|
||||
}
|
||||
|
||||
RepositoryCreationService.endCreation(owner, name, None)
|
||||
|
||||
} catch {
|
||||
case ex: Exception => RepositoryCreationService.endCreation(owner, name, Some(ex.toString))
|
||||
}
|
||||
}
|
||||
|
||||
// Create Wiki repository
|
||||
createWikiRepository(loginAccount, owner, name)
|
||||
def forkRepository(accountName: String, repository: RepositoryInfo, loginUserName: String): Future[Unit] = Future {
|
||||
RepositoryCreationService.startCreation(accountName, repository.name)
|
||||
try {
|
||||
LockUtil.lock(s"${accountName}/${repository.name}") {
|
||||
Database() withTransaction { implicit session =>
|
||||
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
|
||||
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
|
||||
|
||||
// Record activity
|
||||
recordCreateRepositoryActivity(owner, name, loginUserName)
|
||||
insertRepository(
|
||||
repositoryName = repository.name,
|
||||
userName = accountName,
|
||||
description = repository.repository.description,
|
||||
isPrivate = repository.repository.isPrivate,
|
||||
originRepositoryName = Some(originRepositoryName),
|
||||
originUserName = Some(originUserName),
|
||||
parentRepositoryName = Some(repository.name),
|
||||
parentUserName = Some(repository.owner)
|
||||
)
|
||||
|
||||
// Set default collaborators for the private fork
|
||||
if (repository.repository.isPrivate) {
|
||||
// Copy collaborators from the source repository
|
||||
getCollaborators(repository.owner, repository.name).foreach { case (collaborator, _) =>
|
||||
addCollaborator(accountName, repository.name, collaborator.collaboratorName, collaborator.role)
|
||||
}
|
||||
// Register an owner of the source repository as a collaborator
|
||||
addCollaborator(accountName, repository.name, repository.owner, Role.ADMIN.name)
|
||||
}
|
||||
|
||||
// Insert default labels
|
||||
insertDefaultLabels(accountName, repository.name)
|
||||
// Insert default priorities
|
||||
insertDefaultPriorities(accountName, repository.name)
|
||||
|
||||
// clone repository actually
|
||||
JGitUtil.cloneRepository(
|
||||
getRepositoryDir(repository.owner, repository.name),
|
||||
FileUtil.deleteIfExists(getRepositoryDir(accountName, repository.name)))
|
||||
|
||||
// Create Wiki repository
|
||||
JGitUtil.cloneRepository(getWikiRepositoryDir(repository.owner, repository.name),
|
||||
FileUtil.deleteIfExists(getWikiRepositoryDir(accountName, repository.name)))
|
||||
|
||||
// Copy LFS files
|
||||
val lfsDir = getLfsDir(repository.owner, repository.name)
|
||||
if (lfsDir.exists) {
|
||||
FileUtils.copyDirectory(lfsDir, FileUtil.deleteIfExists(getLfsDir(accountName, repository.name)))
|
||||
}
|
||||
|
||||
// Record activity
|
||||
recordForkActivity(repository.owner, repository.name, loginUserName, accountName)
|
||||
|
||||
// Call hooks
|
||||
PluginRegistry().getRepositoryHooks.foreach(_.forked(repository.owner, accountName, repository.name))
|
||||
|
||||
RepositoryCreationService.endCreation(accountName, repository.name, None)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
case ex: Exception => RepositoryCreationService.endCreation(accountName, repository.name, Some(ex.toString))
|
||||
}
|
||||
}
|
||||
|
||||
def insertDefaultLabels(userName: String, repositoryName: String)(implicit s: Session): Unit = {
|
||||
|
||||
@@ -3,7 +3,7 @@ package gitbucket.core.service
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
import gitbucket.core.model.{Account, Collaborator, Repository, RepositoryOptions, Role}
|
||||
import gitbucket.core.model.{Account, Collaborator, Repository, RepositoryOptions, Role, Release}
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.model.Profile.dateColumnType
|
||||
@@ -46,7 +46,9 @@ trait RepositoryService { self: AccountService =>
|
||||
externalIssuesUrl = None,
|
||||
wikiOption = "PUBLIC", // TODO DISABLE for the forked repository?
|
||||
externalWikiUrl = None,
|
||||
allowFork = true
|
||||
allowFork = true,
|
||||
mergeOptions = "merge-commit,squash,rebase",
|
||||
defaultMergeOption = "merge-commit"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -75,6 +77,8 @@ trait RepositoryService { self: AccountService =>
|
||||
val protectedBranches = ProtectedBranches .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val protectedBranchContexts = ProtectedBranchContexts.filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val deployKeys = DeployKeys .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val releases = Releases .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val releaseAssets = ReleaseAssets .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
|
||||
Repositories.filter { t =>
|
||||
(t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind)
|
||||
@@ -95,9 +99,9 @@ trait RepositoryService { self: AccountService =>
|
||||
|
||||
RepositoryWebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
RepositoryWebHookEvents.insertAll(webHookEvents .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
Priorities .insertAll(priorities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
|
||||
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
Priorities .insertAll(priorities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
|
||||
|
||||
val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
|
||||
val newPriorities = Priorities.filter(_.byRepository(newUserName, newRepositoryName)).list
|
||||
@@ -120,6 +124,8 @@ trait RepositoryService { self: AccountService =>
|
||||
ProtectedBranches .insertAll(protectedBranches.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
ProtectedBranchContexts.insertAll(protectedBranchContexts.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
DeployKeys .insertAll(deployKeys .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
Releases .insertAll(releases .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
ReleaseAssets .insertAll(releaseAssets .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
|
||||
// Update source repository of pull requests
|
||||
PullRequests.filter { t =>
|
||||
@@ -159,21 +165,23 @@ trait RepositoryService { self: AccountService =>
|
||||
}
|
||||
|
||||
def deleteRepository(userName: String, repositoryName: String)(implicit s: Session): Unit = {
|
||||
Activities .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Collaborators .filter(_.byRepository(userName, repositoryName)).delete
|
||||
CommitComments.filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Labels .filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
|
||||
PullRequests .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Issues .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Priorities .filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueId .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Milestones .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Activities .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Collaborators .filter(_.byRepository(userName, repositoryName)).delete
|
||||
CommitComments .filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Labels .filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
|
||||
PullRequests .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Issues .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Priorities .filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueId .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Milestones .filter(_.byRepository(userName, repositoryName)).delete
|
||||
RepositoryWebHooks .filter(_.byRepository(userName, repositoryName)).delete
|
||||
RepositoryWebHookEvents .filter(_.byRepository(userName, repositoryName)).delete
|
||||
DeployKeys .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Repositories .filter(_.byRepository(userName, repositoryName)).delete
|
||||
DeployKeys .filter(_.byRepository(userName, repositoryName)).delete
|
||||
ReleaseAssets .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Releases .filter(_.byRepository(userName, repositoryName)).delete
|
||||
Repositories .filter(_.byRepository(userName, repositoryName)).delete
|
||||
|
||||
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME
|
||||
Repositories
|
||||
@@ -356,14 +364,36 @@ trait RepositoryService { self: AccountService =>
|
||||
/**
|
||||
* Save repository options.
|
||||
*/
|
||||
def saveRepositoryOptions(userName: String, repositoryName: String,
|
||||
description: Option[String], isPrivate: Boolean,
|
||||
issuesOption: String, externalIssuesUrl: Option[String],
|
||||
wikiOption: String, externalWikiUrl: Option[String],
|
||||
allowFork: Boolean)(implicit s: Session): Unit =
|
||||
def saveRepositoryOptions(userName: String, repositoryName: String, description: Option[String], isPrivate: Boolean,
|
||||
issuesOption: String, externalIssuesUrl: Option[String], wikiOption: String, externalWikiUrl: Option[String],
|
||||
allowFork: Boolean, mergeOptions: Seq[String], defaultMergeOption: String)(implicit s: Session): Unit = {
|
||||
|
||||
Repositories.filter(_.byRepository(userName, repositoryName))
|
||||
.map { r => (r.description.?, r.isPrivate, r.issuesOption, r.externalIssuesUrl.?, r.wikiOption, r.externalWikiUrl.?, r.allowFork, r.updatedDate) }
|
||||
.update (description, isPrivate, issuesOption, externalIssuesUrl, wikiOption, externalWikiUrl, allowFork, currentDate)
|
||||
.map { r => (
|
||||
r.description.?,
|
||||
r.isPrivate,
|
||||
r.issuesOption,
|
||||
r.externalIssuesUrl.?,
|
||||
r.wikiOption,
|
||||
r.externalWikiUrl.?,
|
||||
r.allowFork,
|
||||
r.mergeOptions,
|
||||
r.defaultMergeOption,
|
||||
r.updatedDate
|
||||
) }
|
||||
.update (
|
||||
description,
|
||||
isPrivate,
|
||||
issuesOption,
|
||||
externalIssuesUrl,
|
||||
wikiOption,
|
||||
externalWikiUrl,
|
||||
allowFork,
|
||||
mergeOptions.mkString(","),
|
||||
defaultMergeOption,
|
||||
currentDate
|
||||
)
|
||||
}
|
||||
|
||||
def saveRepositoryDefaultBranch(userName: String, repositoryName: String,
|
||||
defaultBranch: String)(implicit s: Session): Unit =
|
||||
@@ -390,7 +420,7 @@ trait RepositoryService { self: AccountService =>
|
||||
Collaborators
|
||||
.join(Accounts).on(_.collaboratorName === _.userName)
|
||||
.filter { case (t1, t2) => t1.byRepository(userName, repositoryName) }
|
||||
.map { case (t1, t2) => (t1, t2.groupAccount) }
|
||||
.map { case (t1, t2) => (t1, t2.groupAccount) }
|
||||
.sortBy { case (t1, t2) => t1.collaboratorName }
|
||||
.list
|
||||
|
||||
@@ -402,13 +432,13 @@ trait RepositoryService { self: AccountService =>
|
||||
val q1 = Collaborators
|
||||
.join(Accounts).on { case (t1, t2) => (t1.collaboratorName === t2.userName) && (t2.groupAccount === false.bind) }
|
||||
.filter { case (t1, t2) => t1.byRepository(userName, repositoryName) }
|
||||
.map { case (t1, t2) => (t1.collaboratorName, t1.role) }
|
||||
.map { case (t1, t2) => (t1.collaboratorName, t1.role) }
|
||||
|
||||
val q2 = Collaborators
|
||||
.join(Accounts).on { case (t1, t2) => (t1.collaboratorName === t2.userName) && (t2.groupAccount === true.bind) }
|
||||
.join(GroupMembers).on { case ((t1, t2), t3) => t2.userName === t3.groupName }
|
||||
.filter { case ((t1, t2), t3) => t1.byRepository(userName, repositoryName) }
|
||||
.map { case ((t1, t2), t3) => (t3.userName, t1.role) }
|
||||
.map { case ((t1, t2), t3) => (t3.userName, t1.role) }
|
||||
|
||||
q1.union(q2).list.filter { x => filter.isEmpty || filter.exists(_.name == x._2) }.map(_._1)
|
||||
}
|
||||
@@ -443,17 +473,31 @@ trait RepositoryService { self: AccountService =>
|
||||
}
|
||||
}
|
||||
|
||||
def isReadable(repository: Repository, loginAccount: Option[Account])(implicit s: Session): Boolean = {
|
||||
if(!repository.isPrivate){
|
||||
true
|
||||
} else {
|
||||
loginAccount match {
|
||||
case Some(x) if(x.isAdmin) => true
|
||||
case Some(x) if(repository.userName == x.userName) => true
|
||||
case Some(x) if(getGroupMembers(repository.userName).exists(_.userName == x.userName)) => true
|
||||
case Some(x) if(getCollaboratorUserNames(repository.userName, repository.repositoryName).contains(x.userName)) => true
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int =
|
||||
Query(Repositories.filter { t =>
|
||||
(t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)
|
||||
}.length).first
|
||||
|
||||
|
||||
def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[(String, String)] =
|
||||
def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[Repository] =
|
||||
Repositories.filter { t =>
|
||||
(t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)
|
||||
}
|
||||
.sortBy(_.userName asc).map(t => t.userName -> t.repositoryName).list
|
||||
.sortBy(_.userName asc).list//.map(t => t.userName -> t.repositoryName).list
|
||||
|
||||
private val templateExtensions = Seq("md", "markdown")
|
||||
|
||||
@@ -500,7 +544,7 @@ object RepositoryService {
|
||||
this(repo.owner, repo.name, model, issueCount, pullCount, forkedCount, repo.branchList, repo.tags, managers)
|
||||
|
||||
/**
|
||||
* Creates instance without issue count and pull request count.
|
||||
* Creates instance without issue and pull request count.
|
||||
*/
|
||||
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) =
|
||||
this(repo.owner, repo.name, model, 0, 0, forkedCount, repo.branchList, repo.tags, managers)
|
||||
@@ -525,5 +569,4 @@ object RepositoryService {
|
||||
context.settings.sshAddress.map { x => s"ssh://${x.genericUser}@${x.host}:${x.port}/${owner}/${name}.git" }
|
||||
} else None
|
||||
def openRepoUrl(openUrl: String)(implicit context: Context): String = s"github-${context.platform}://openRepo/${openUrl}"
|
||||
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import Implicits.request2Session
|
||||
* It may be called many times in one request, so each method stores
|
||||
* its result into the cache which available during a request.
|
||||
*/
|
||||
trait RequestCache extends SystemSettingsService with AccountService with IssuesService with RepositoryService {
|
||||
trait RequestCache extends SystemSettingsService with AccountService with IssuesService with RepositoryService
|
||||
with LabelsService with MilestonesService with PrioritiesService {
|
||||
|
||||
private implicit def context2Session(implicit context: Context): Session =
|
||||
request2Session(context.request)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.util.Implicits._
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm
|
||||
import com.nimbusds.oauth2.sdk.auth.Secret
|
||||
import com.nimbusds.oauth2.sdk.id.{ClientID, Issuer}
|
||||
import gitbucket.core.service.SystemSettingsService._
|
||||
import gitbucket.core.util.ConfigUtil._
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
import SystemSettingsService._
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
trait SystemSettingsService {
|
||||
|
||||
@@ -54,6 +57,15 @@ trait SystemSettingsService {
|
||||
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
|
||||
}
|
||||
}
|
||||
props.setProperty(OidcAuthentication, settings.oidcAuthentication.toString)
|
||||
if (settings.oidcAuthentication) {
|
||||
settings.oidc.map { oidc =>
|
||||
props.setProperty(OidcIssuer, oidc.issuer.getValue)
|
||||
props.setProperty(OidcClientId, oidc.clientID.getValue)
|
||||
props.setProperty(OidcClientSecret, oidc.clientSecret.getValue)
|
||||
oidc.jwsAlgorithm.map { x => props.setProperty(OidcJwsAlgorithm, x.getName) }
|
||||
}
|
||||
}
|
||||
props.setProperty(SkinName, settings.skinName.toString)
|
||||
using(new java.io.FileOutputStream(GitBucketConf)){ out =>
|
||||
props.store(out, null)
|
||||
@@ -113,6 +125,17 @@ trait SystemSettingsService {
|
||||
} else {
|
||||
None
|
||||
},
|
||||
getValue(props, OidcAuthentication, false),
|
||||
if (getValue(props, OidcAuthentication, false)) {
|
||||
Some(OIDC(
|
||||
getValue(props, OidcIssuer, ""),
|
||||
getValue(props, OidcClientId, ""),
|
||||
getValue(props, OidcClientSecret, ""),
|
||||
getOptionValue(props, OidcJwsAlgorithm, None)
|
||||
))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
getValue(props, SkinName, "skin-blue")
|
||||
)
|
||||
}
|
||||
@@ -139,6 +162,8 @@ object SystemSettingsService {
|
||||
smtp: Option[Smtp],
|
||||
ldapAuthentication: Boolean,
|
||||
ldap: Option[Ldap],
|
||||
oidcAuthentication: Boolean,
|
||||
oidc: Option[OIDC],
|
||||
skinName: String){
|
||||
|
||||
def baseUrl(request: HttpServletRequest): String = baseUrl.fold {
|
||||
@@ -166,6 +191,16 @@ object SystemSettingsService {
|
||||
ssl: Option[Boolean],
|
||||
keystore: Option[String])
|
||||
|
||||
case class OIDC(
|
||||
issuer: Issuer,
|
||||
clientID: ClientID,
|
||||
clientSecret: Secret,
|
||||
jwsAlgorithm: Option[JWSAlgorithm])
|
||||
object OIDC {
|
||||
def apply(issuer: String, clientID: String, clientSecret: String, jwsAlgorithm: Option[String]): OIDC =
|
||||
new OIDC(new Issuer(issuer), new ClientID(clientID), new Secret(clientSecret), jwsAlgorithm.map(JWSAlgorithm.parse))
|
||||
}
|
||||
|
||||
case class Smtp(
|
||||
host: String,
|
||||
port: Option[Int],
|
||||
@@ -221,6 +256,11 @@ object SystemSettingsService {
|
||||
private val LdapTls = "ldap.tls"
|
||||
private val LdapSsl = "ldap.ssl"
|
||||
private val LdapKeystore = "ldap.keystore"
|
||||
private val OidcAuthentication = "oidc_authentication"
|
||||
private val OidcIssuer = "oidc.issuer"
|
||||
private val OidcClientId = "oidc.client_id"
|
||||
private val OidcClientSecret = "oidc.client_secret"
|
||||
private val OidcJwsAlgorithm = "oidc.jws_algorithm"
|
||||
private val SkinName = "skinName"
|
||||
|
||||
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = {
|
||||
|
||||
@@ -362,6 +362,35 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
|
||||
object WebHookService {
|
||||
trait WebHookPayload
|
||||
|
||||
// https://developer.github.com/v3/activity/events/types/#createevent
|
||||
case class WebHookCreatePayload(
|
||||
sender: ApiUser,
|
||||
description: String,
|
||||
ref: String,
|
||||
ref_type: String,
|
||||
master_branch: String,
|
||||
repository: ApiRepository
|
||||
) extends FieldSerializable with WebHookPayload {
|
||||
val pusher_type = "user"
|
||||
}
|
||||
|
||||
object WebHookCreatePayload {
|
||||
|
||||
def apply(git: Git, sender: Account, refName: String, repositoryInfo: RepositoryInfo,
|
||||
commits: List[CommitInfo], repositoryOwner: Account,
|
||||
ref: String, refType: String): WebHookCreatePayload =
|
||||
WebHookCreatePayload(
|
||||
sender = ApiUser(sender),
|
||||
ref = ref,
|
||||
ref_type = refType,
|
||||
description = repositoryInfo.repository.description.getOrElse(""),
|
||||
master_branch = repositoryInfo.repository.defaultBranch,
|
||||
repository = ApiRepository.forWebhookPayload(
|
||||
repositoryInfo,
|
||||
owner= ApiUser(repositoryOwner))
|
||||
)
|
||||
}
|
||||
|
||||
// https://developer.github.com/v3/activity/events/types/#pushevent
|
||||
case class WebHookPushPayload(
|
||||
pusher: ApiPusher,
|
||||
@@ -391,8 +420,8 @@ object WebHookService {
|
||||
ref = refName,
|
||||
before = ObjectId.toString(oldId),
|
||||
after = ObjectId.toString(newId),
|
||||
commits = commits.map{ commit => ApiCommit.forPushPayload(git, RepositoryName(repositoryInfo), commit) },
|
||||
repository = ApiRepository.forPushPayload(
|
||||
commits = commits.map{ commit => ApiCommit.forWebhookPayload(git, RepositoryName(repositoryInfo), commit) },
|
||||
repository = ApiRepository.forWebhookPayload(
|
||||
repositoryInfo,
|
||||
owner= ApiUser(repositoryOwner))
|
||||
)
|
||||
|
||||
@@ -75,22 +75,6 @@ trait WikiService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of the specified file.
|
||||
*/
|
||||
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
if(!JGitUtil.isEmpty(git)){
|
||||
val index = path.lastIndexOf('/')
|
||||
val parentPath = if(index < 0) "." else path.substring(0, index)
|
||||
val fileName = if(index < 0) path else path.substring(index + 1)
|
||||
|
||||
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
|
||||
git.getRepository.open(file.id).getBytes
|
||||
}
|
||||
} else None
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of wiki page names.
|
||||
*/
|
||||
|
||||
@@ -28,20 +28,30 @@ class CompositeScalatraFilter extends Filter {
|
||||
}
|
||||
|
||||
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = {
|
||||
val requestUri = request.asInstanceOf[HttpServletRequest].getRequestURI
|
||||
val contextPath = request.getServletContext.getContextPath
|
||||
val requestPath = request.asInstanceOf[HttpServletRequest].getRequestURI.substring(contextPath.length)
|
||||
val checkPath = if(requestPath.endsWith("/")){
|
||||
requestPath
|
||||
} else {
|
||||
requestPath + "/"
|
||||
}
|
||||
|
||||
filters
|
||||
.filter { case (_, path) =>
|
||||
val start = path.replaceFirst("/\\*$", "/")
|
||||
(requestUri + "/").startsWith(start)
|
||||
}
|
||||
.foreach { case (filter, _) =>
|
||||
val mockChain = new MockFilterChain()
|
||||
filter.doFilter(request, response, mockChain)
|
||||
if(mockChain.continue == false){
|
||||
return ()
|
||||
if(!checkPath.startsWith("/upload/") && !checkPath.startsWith("/git/") && !checkPath.startsWith("/git-lfs/") &&
|
||||
!checkPath.startsWith("/plugin-assets/")){
|
||||
filters
|
||||
.filter { case (_, path) =>
|
||||
val start = path.replaceFirst("/\\*$", "/")
|
||||
checkPath.startsWith(start)
|
||||
}
|
||||
}
|
||||
.foreach { case (filter, _) =>
|
||||
val mockChain = new MockFilterChain()
|
||||
filter.doFilter(request, response, mockChain)
|
||||
if(mockChain.continue == false){
|
||||
return ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
chain.doFilter(request, response)
|
||||
}
|
||||
@@ -56,8 +66,8 @@ class MockFilterChain extends FilterChain {
|
||||
}
|
||||
}
|
||||
|
||||
class FilterChainFilter(chain: FilterChain) extends Filter {
|
||||
override def init(filterConfig: FilterConfig): Unit = ()
|
||||
override def destroy(): Unit = ()
|
||||
override def doFilter(request: ServletRequest, response: ServletResponse, mockChain: FilterChain) = chain.doFilter(request, response)
|
||||
}
|
||||
//class FilterChainFilter(chain: FilterChain) extends Filter {
|
||||
// override def init(filterConfig: FilterConfig): Unit = ()
|
||||
// override def destroy(): Unit = ()
|
||||
// override def doFilter(request: ServletRequest, response: ServletResponse, mockChain: FilterChain) = chain.doFilter(request, response)
|
||||
//}
|
||||
|
||||
@@ -185,7 +185,8 @@ import scala.collection.JavaConverters._
|
||||
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String, sshUrl: Option[String])
|
||||
extends PostReceiveHook with PreReceiveHook
|
||||
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService
|
||||
with WebHookPullRequestService with CommitsService {
|
||||
with LabelsService with PrioritiesService with MilestonesService
|
||||
with WebHookPullRequestService with CommitsService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
|
||||
private var existIds: Seq[String] = Nil
|
||||
@@ -306,6 +307,18 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
newId = command.getNewId(), oldId = command.getOldId())
|
||||
}
|
||||
}
|
||||
if (command.getType == ReceiveCommand.Type.CREATE) {
|
||||
callWebHookOf(owner, repository, WebHook.Create) {
|
||||
for {
|
||||
pusherAccount <- getAccountByUserName(pusher)
|
||||
ownerAccount <- getAccountByUserName(owner)
|
||||
} yield {
|
||||
val refType = if (refName(1) == "tags") "tag" else "branch"
|
||||
WebHookCreatePayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount,
|
||||
ref = branchName, refType = refType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// call post-commit hook
|
||||
PluginRegistry().getReceiveHooks.foreach(_.postReceive(owner, repository, receivePack, command, pusher))
|
||||
@@ -347,8 +360,8 @@ class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
commitIds.map { case (oldCommitId, newCommitId) =>
|
||||
val commits = using(Git.open(Directory.getWikiRepositoryDir(owner, repository))) { git =>
|
||||
JGitUtil.getCommitLog(git, oldCommitId, newCommitId).flatMap { commit =>
|
||||
val diffs = JGitUtil.getDiffs(git, commit.id, false)
|
||||
diffs._1.collect { case diff if diff.newPath.toLowerCase.endsWith(".md") =>
|
||||
val diffs = JGitUtil.getDiffs(git, None, commit.id, false, false)
|
||||
diffs.collect { case diff if diff.newPath.toLowerCase.endsWith(".md") =>
|
||||
val action = if(diff.changeType == ChangeType.ADD) "created" else "edited"
|
||||
val fileName = diff.newPath
|
||||
(action, fileName, commit.id)
|
||||
|
||||
@@ -21,7 +21,8 @@ class PluginControllerFilter extends Filter {
|
||||
}
|
||||
|
||||
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = {
|
||||
val requestUri = request.asInstanceOf[HttpServletRequest].getRequestURI
|
||||
val contextPath = request.getServletContext.getContextPath
|
||||
val requestUri = request.asInstanceOf[HttpServletRequest].getRequestURI.substring(contextPath.length)
|
||||
|
||||
PluginRegistry().getControllers()
|
||||
.filter { case (_, path) =>
|
||||
|
||||
@@ -23,7 +23,7 @@ class TransactionFilter extends Filter {
|
||||
|
||||
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
|
||||
val servletPath = req.asInstanceOf[HttpServletRequest].getServletPath()
|
||||
if(servletPath.startsWith("/assets/") || servletPath == "/console" || servletPath == "/git" || servletPath == "/git-lfs"){
|
||||
if(servletPath.startsWith("/assets/") || servletPath == "/git" || servletPath == "/git-lfs"){
|
||||
// assets and git-lfs don't need transaction
|
||||
chain.doFilter(req, res)
|
||||
} else {
|
||||
|
||||
@@ -97,16 +97,10 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService with A
|
||||
{
|
||||
defining(request.paths){ paths =>
|
||||
getRepository(paths(0), paths(1)).map { repository =>
|
||||
if(!repository.repository.isPrivate){
|
||||
if(isReadable(repository.repository, context.loginAccount)){
|
||||
action(repository)
|
||||
} else {
|
||||
context.loginAccount match {
|
||||
case Some(x) if(x.isAdmin) => action(repository)
|
||||
case Some(x) if(paths(0) == x.userName) => action(repository)
|
||||
case Some(x) if(getGroupMembers(repository.owner).exists(_.userName == x.userName)) => action(repository)
|
||||
case Some(x) if(getCollaboratorUserNames(paths(0), paths(1)).contains(x.userName)) => action(repository)
|
||||
case _ => Unauthorized()
|
||||
}
|
||||
Unauthorized()
|
||||
}
|
||||
} getOrElse NotFound()
|
||||
}
|
||||
|
||||
@@ -54,6 +54,12 @@ object Directory {
|
||||
def getAttachedDir(owner: String, repository: String): File =
|
||||
new File(getRepositoryFilesDir(owner, repository), "comments")
|
||||
|
||||
/**
|
||||
* Directory for released files
|
||||
*/
|
||||
def getReleaseFilesDir(owner: String, repository: String): File =
|
||||
new File(getRepositoryFilesDir(owner, repository), "releases")
|
||||
|
||||
/**
|
||||
* Directory for files which are attached to issue.
|
||||
*/
|
||||
|
||||
@@ -76,4 +76,9 @@ object FileUtil {
|
||||
file
|
||||
}
|
||||
|
||||
lazy val MaxFileSize = if (System.getProperty("gitbucket.maxFileSize") != null)
|
||||
System.getProperty("gitbucket.maxFileSize").toLong
|
||||
else
|
||||
3 * 1024 * 1024
|
||||
|
||||
}
|
||||
|
||||
@@ -22,10 +22,11 @@ import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.function.Consumer
|
||||
|
||||
import org.cache2k.{Cache2kBuilder, CacheEntry}
|
||||
import org.cache2k.Cache2kBuilder
|
||||
import org.eclipse.jgit.api.errors.{InvalidRefNameException, JGitInternalException, NoHeadException, RefAlreadyExistsException}
|
||||
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter}
|
||||
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter, RawTextComparator}
|
||||
import org.eclipse.jgit.dircache.DirCacheEntry
|
||||
import org.eclipse.jgit.util.io.DisabledOutputStream
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
@@ -150,9 +151,10 @@ object JGitUtil {
|
||||
*
|
||||
* @param name the module name
|
||||
* @param path the path in the repository
|
||||
* @param url the repository url of this module
|
||||
* @param repositoryUrl the repository url of this module
|
||||
* @param viewerUrl the repository viewer url of this module
|
||||
*/
|
||||
case class SubmoduleInfo(name: String, path: String, url: String)
|
||||
case class SubmoduleInfo(name: String, path: String, repositoryUrl: String, viewerUrl: String)
|
||||
|
||||
case class BranchMergeInfo(ahead: Int, behind: Int, isMerged: Boolean)
|
||||
|
||||
@@ -188,11 +190,9 @@ object JGitUtil {
|
||||
val dir = git.getRepository.getDirectory
|
||||
val keyPrefix = dir.getAbsolutePath + "@"
|
||||
|
||||
cache.forEach(new Consumer[CacheEntry[String, Int]] {
|
||||
override def accept(entry: CacheEntry[String, Int]): Unit = {
|
||||
if(entry.getKey.startsWith(keyPrefix)){
|
||||
cache.remove(entry.getKey)
|
||||
}
|
||||
cache.keys.forEach(key => {
|
||||
if (key.startsWith(keyPrefix)) {
|
||||
cache.remove(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -253,9 +253,10 @@ object JGitUtil {
|
||||
* @param git the Git object
|
||||
* @param revision the branch name or commit id
|
||||
* @param path the directory path (optional)
|
||||
* @param baseUrl the base url of GitBucket instance. This parameter is used to generate links of submodules (optional)
|
||||
* @return HTML of the file list
|
||||
*/
|
||||
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
|
||||
def getFileList(git: Git, revision: String, path: String = ".", baseUrl: Option[String] = None): List[FileInfo] = {
|
||||
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||
val objectId = git.getRepository.resolve(revision)
|
||||
if(objectId == null) return Nil
|
||||
@@ -341,7 +342,7 @@ object JGitUtil {
|
||||
useTreeWalk(revCommit){ treeWalk =>
|
||||
while (treeWalk.next()) {
|
||||
val linkUrl = if (treeWalk.getFileMode(0) == FileMode.GITLINK) {
|
||||
getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url)
|
||||
getSubmodules(git, revCommit.getTree, baseUrl).find(_.path == treeWalk.getPathString).map(_.viewerUrl)
|
||||
} else None
|
||||
fileList +:= (treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getNameString, treeWalk.getPathString, linkUrl)
|
||||
}
|
||||
@@ -518,93 +519,49 @@ object JGitUtil {
|
||||
}.toMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tuple of diff of the given commit and parent commit ids.
|
||||
* DiffInfos returned from this method don't include the patch property.
|
||||
*/
|
||||
def getDiffs(git: Git, id: String, fetchContent: Boolean): (List[DiffInfo], Option[String]) = {
|
||||
@scala.annotation.tailrec
|
||||
def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[RevCommit]): List[RevCommit] =
|
||||
i.hasNext match {
|
||||
case true if(logs.size < 2) => getCommitLog(i, logs :+ i.next)
|
||||
case _ => logs
|
||||
}
|
||||
def getPatch(git: Git, from: Option[String], to: String): String = {
|
||||
val out = new ByteArrayOutputStream()
|
||||
val df = new DiffFormatter(out)
|
||||
df.setRepository(git.getRepository)
|
||||
df.setDiffComparator(RawTextComparator.DEFAULT)
|
||||
df.setDetectRenames(true)
|
||||
df.format(getDiffEntries(git, from, to).head)
|
||||
new String(out.toByteArray, "UTF-8")
|
||||
}
|
||||
|
||||
private def getDiffEntries(git: Git, from: Option[String], to: String): Seq[DiffEntry] = {
|
||||
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id)))
|
||||
val commits = getCommitLog(revWalk.iterator, Nil)
|
||||
val revCommit = commits(0)
|
||||
val df = new DiffFormatter(DisabledOutputStream.INSTANCE)
|
||||
df.setRepository(git.getRepository)
|
||||
|
||||
if(commits.length >= 2){
|
||||
// not initial commit
|
||||
val oldCommit = if(revCommit.getParentCount >= 2) {
|
||||
// merge commit
|
||||
revCommit.getParents.head
|
||||
} else {
|
||||
commits(1)
|
||||
}
|
||||
(getDiffs(git, oldCommit.getName, id, fetchContent, false), Some(oldCommit.getName))
|
||||
|
||||
} else {
|
||||
// initial commit
|
||||
using(new TreeWalk(git.getRepository)){ treeWalk =>
|
||||
treeWalk.setRecursive(true)
|
||||
treeWalk.addTree(revCommit.getTree)
|
||||
val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
|
||||
while(treeWalk.next){
|
||||
val newIsImage = FileUtil.isImage(treeWalk.getPathString)
|
||||
buffer.append((if(!fetchContent){
|
||||
DiffInfo(
|
||||
changeType = ChangeType.ADD,
|
||||
oldPath = "",
|
||||
newPath = treeWalk.getPathString,
|
||||
oldContent = None,
|
||||
newContent = None,
|
||||
oldIsImage = false,
|
||||
newIsImage = newIsImage,
|
||||
oldObjectId = None,
|
||||
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
|
||||
oldMode = treeWalk.getFileMode(0).toString,
|
||||
newMode = treeWalk.getFileMode(0).toString,
|
||||
tooLarge = false,
|
||||
patch = None
|
||||
)
|
||||
} else {
|
||||
DiffInfo(
|
||||
changeType = ChangeType.ADD,
|
||||
oldPath = "",
|
||||
newPath = treeWalk.getPathString,
|
||||
oldContent = None,
|
||||
newContent = JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray),
|
||||
oldIsImage = false,
|
||||
newIsImage = newIsImage,
|
||||
oldObjectId = None,
|
||||
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
|
||||
oldMode = treeWalk.getFileMode(0).toString,
|
||||
newMode = treeWalk.getFileMode(0).toString,
|
||||
tooLarge = false,
|
||||
patch = None
|
||||
)
|
||||
}))
|
||||
val toCommit = revWalk.parseCommit(git.getRepository.resolve(to))
|
||||
from match {
|
||||
case None => {
|
||||
toCommit.getParentCount match {
|
||||
case 0 => df.scan(new EmptyTreeIterator(), new CanonicalTreeParser(null, git.getRepository.newObjectReader(), toCommit.getTree)).asScala
|
||||
case _ => df.scan(toCommit.getParent(0), toCommit.getTree).asScala
|
||||
}
|
||||
(buffer.toList, None)
|
||||
}
|
||||
case Some(from) => {
|
||||
val fromCommit = revWalk.parseCommit(git.getRepository.resolve(from))
|
||||
df.scan(fromCommit.getTree, toCommit.getTree).asScala
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getDiffs(git: Git, from: String, to: String, fetchContent: Boolean, makePatch: Boolean): List[DiffInfo] = {
|
||||
val reader = git.getRepository.newObjectReader
|
||||
val oldTreeIter = new CanonicalTreeParser
|
||||
oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
|
||||
def getParentCommitId(git: Git, id: String): Option[String] = {
|
||||
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||
val commit = revWalk.parseCommit(git.getRepository.resolve(id))
|
||||
commit.getParentCount match {
|
||||
case 0 => None
|
||||
case _ => Some(commit.getParent(0).getName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val newTreeIter = new CanonicalTreeParser
|
||||
newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
git.getRepository.getConfig.setString("diff", null, "renames", "copies")
|
||||
|
||||
val diffs = git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala
|
||||
def getDiffs(git: Git, from: Option[String], to: String, fetchContent: Boolean, makePatch: Boolean): List[DiffInfo] = {
|
||||
val diffs = getDiffEntries(git, from, to)
|
||||
diffs.map { diff =>
|
||||
if(diffs.size > 100){
|
||||
DiffInfo(
|
||||
@@ -639,7 +596,7 @@ object JGitUtil {
|
||||
oldMode = diff.getOldMode.toString,
|
||||
newMode = diff.getNewMode.toString,
|
||||
tooLarge = false,
|
||||
patch = (if(makePatch) Some(makePatchFromDiffEntry(git, diff)) else None)
|
||||
patch = (if(makePatch) Some(makePatchFromDiffEntry(git, diff)) else None) // TODO use DiffFormatter
|
||||
)
|
||||
} else {
|
||||
DiffInfo(
|
||||
@@ -655,7 +612,7 @@ object JGitUtil {
|
||||
oldMode = diff.getOldMode.toString,
|
||||
newMode = diff.getNewMode.toString,
|
||||
tooLarge = false,
|
||||
patch = (if(makePatch) Some(makePatchFromDiffEntry(git, diff)) else None)
|
||||
patch = (if(makePatch) Some(makePatchFromDiffEntry(git, diff)) else None) // TODO use DiffFormatter
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -775,7 +732,7 @@ object JGitUtil {
|
||||
/**
|
||||
* Read submodule information from .gitmodules
|
||||
*/
|
||||
def getSubmodules(git: Git, tree: RevTree): List[SubmoduleInfo] = {
|
||||
def getSubmodules(git: Git, tree: RevTree, baseUrl: Option[String]): List[SubmoduleInfo] = {
|
||||
val repository = git.getRepository
|
||||
getContentFromPath(git, tree, ".gitmodules", true).map { bytes =>
|
||||
(try {
|
||||
@@ -783,7 +740,7 @@ object JGitUtil {
|
||||
config.getSubsections("submodule").asScala.map { module =>
|
||||
val path = config.getString("submodule", module, "path")
|
||||
val url = config.getString("submodule", module, "url")
|
||||
SubmoduleInfo(module, path, url)
|
||||
SubmoduleInfo(module, path, url, StringUtil.getRepositoryViewerUrl(url, baseUrl))
|
||||
}
|
||||
} catch {
|
||||
case e: ConfigInvalidException => {
|
||||
@@ -847,17 +804,22 @@ object JGitUtil {
|
||||
}
|
||||
}
|
||||
|
||||
def isLfsPointer(loader: ObjectLoader): Boolean = {
|
||||
!loader.isLarge && new String(loader.getBytes(), "UTF-8").startsWith("version https://git-lfs.github.com/spec/v1")
|
||||
}
|
||||
|
||||
def getContentInfo(git: Git, path: String, objectId: ObjectId): ContentInfo = {
|
||||
// Viewer
|
||||
using(git.getRepository.getObjectDatabase){ db =>
|
||||
val loader = db.open(objectId)
|
||||
val isLfs = isLfsPointer(loader)
|
||||
val large = FileUtil.isLarge(loader.getSize)
|
||||
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
|
||||
val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None
|
||||
val size = Some(getContentSize(loader))
|
||||
|
||||
if(viewer == "other"){
|
||||
if(bytes.isDefined && FileUtil.isText(bytes.get)){
|
||||
if(!isLfs && bytes.isDefined && FileUtil.isText(bytes.get)){
|
||||
// text
|
||||
ContentInfo("text", size, Some(StringUtil.convertFromByteArray(bytes.get)), Some(StringUtil.detectEncoding(bytes.get)))
|
||||
} else {
|
||||
@@ -1024,7 +986,7 @@ object JGitUtil {
|
||||
val blame = blamer.call()
|
||||
var blameMap = Map[String, JGitUtil.BlameInfo]()
|
||||
var idLine = List[(String, Int)]()
|
||||
val commits = 0.to(blame.getResultContents().size() - 1).map{ i =>
|
||||
val commits = 0.to(blame.getResultContents().size() - 1).map { i =>
|
||||
val c = blame.getSourceCommit(i)
|
||||
if(!blameMap.contains(c.name)){
|
||||
blameMap += c.name -> JGitUtil.BlameInfo(
|
||||
|
||||
@@ -25,6 +25,11 @@ object Keys {
|
||||
*/
|
||||
val DashboardPulls = "dashboard/pulls"
|
||||
|
||||
/**
|
||||
* Session key for the OpenID Connect authentication.
|
||||
*/
|
||||
val OidcContext = "oidcContext"
|
||||
|
||||
/**
|
||||
* Generate session key for the issue search condition.
|
||||
*/
|
||||
|
||||
@@ -123,17 +123,22 @@ object StringUtil {
|
||||
"(?i)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(?!\\w)".r
|
||||
.findAllIn(message).matchData.map(_.group(1)).toSeq.distinct
|
||||
|
||||
private val GitBucketUrlPattern = "^(https?://.+)/git/(.+?)/(.+?)\\.git$".r
|
||||
private val GitHubUrlPattern = "^https://(.+@)?github\\.com/(.+?)/(.+?)\\.git$".r
|
||||
private val BitBucketUrlPattern = "^https?://(.+@)?bitbucket\\.org/(.+?)/(.+?)\\.git$".r
|
||||
private val GitLabUrlPattern = "^https?://(.+@)?gitlab\\.com/(.+?)/(.+?)\\.git$".r
|
||||
|
||||
def getRepositoryViewerUrl(gitRepositoryUrl: String, baseUrl: Option[String]): String = {
|
||||
def removeUserName(baseUrl: String): String = baseUrl.replaceFirst("(https?://).+@", "$1")
|
||||
|
||||
gitRepositoryUrl match {
|
||||
case GitBucketUrlPattern(base, user, repository) if baseUrl.map(removeUserName(base).startsWith).getOrElse(false)
|
||||
=> s"${removeUserName(base)}/$user/$repository"
|
||||
case GitHubUrlPattern (_, user, repository) => s"https://github.com/$user/$repository"
|
||||
case BitBucketUrlPattern(_, user, repository) => s"https://bitbucket.org/$user/$repository"
|
||||
case GitLabUrlPattern (_, user, repository) => s"https://gitlab.com/$user/$repository"
|
||||
case _ => gitRepositoryUrl
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Encode search string for LIKE condition.
|
||||
// * This method has been copied from Slick's SqlUtilsComponent.
|
||||
// */
|
||||
// def likeEncode(s: String) = {
|
||||
// val b = new StringBuilder
|
||||
// for(c <- s) c match {
|
||||
// case '%' | '_' | '^' => b append '^' append c
|
||||
// case _ => b append c
|
||||
// }
|
||||
// b.toString
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="@context.path/@account.userName/_personalToken" validate="true">
|
||||
<form method="POST" action="@context.path/@account.userName/_personalToken" validate="true" autocomplete="off">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading strong">Generate new token</div>
|
||||
<div class="panel-body">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@gitbucket.core.helper.html.information(info)
|
||||
@gitbucket.core.helper.html.error(error)
|
||||
@if(LDAPUtil.isDummyMailAddress(account)){<div class="alert alert-danger">Please register your mail address.</div>}
|
||||
<form action="@helpers.url(account.userName)/_edit" method="POST" validate="true">
|
||||
<form action="@helpers.url(account.userName)/_edit" method="POST" validate="true" autocomplete="off">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading strong">Profile</div>
|
||||
<div class="panel-body">
|
||||
@@ -55,13 +55,21 @@
|
||||
<div class="pull-right">
|
||||
<a href="@context.path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
|
||||
</div>
|
||||
<input type="submit" class="btn btn-success" value="Save"/>
|
||||
<input type="submit" class="btn btn-success" value="Save" id="save"/>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#save').click(function(){
|
||||
if($('#password').val() != ''){
|
||||
return confirm('Are you sure you want to change password?');
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
$('#delete').click(function(){
|
||||
return confirm('Once you delete your account, there is no going back.\nAre you sure?');
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@gitbucket.core.account.html.menu("profile", account.userName, true){
|
||||
@gitbucket.core.helper.html.information(info)
|
||||
<h2>Edit group</h2>
|
||||
<form id="form" method="post" action="@context.path/@account.userName/_editgroup" validate="true">
|
||||
<form id="form" method="post" action="@context.path/@account.userName/_editgroup" validate="true" autocomplete="off">
|
||||
@gitbucket.core.account.html.groupform(Some(account), members, false)
|
||||
<fieldset class="border-top">
|
||||
<div class="pull-right">
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
</div>
|
||||
<div style="padding-left: 10px; padding-right: 10px;">
|
||||
@account.description.map{ description =>
|
||||
<p style="color: white;">@description</p>
|
||||
<p style="color: #999">@description</p>
|
||||
}
|
||||
@if(account.url.isDefined){
|
||||
<p style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<i class="octicon octicon-home"></i> <a href="@account.url">@account.url</a>
|
||||
</p>
|
||||
}
|
||||
<p style="color: white;">
|
||||
<p style="color: #999">
|
||||
<i class="octicon octicon-clock"></i> Joined on @helpers.date(account.registeredDate)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
||||
<p class="muted">
|
||||
A repository contains all the files for your project including the revision history.
|
||||
</p>
|
||||
<form id="form" method="post" action="@context.path/new" validate="true">
|
||||
<form id="form" method="post" action="@context.path/new" validate="true" autocomplete="off">
|
||||
<fieldset class="border-top form-group">
|
||||
<dl style="float: left;">
|
||||
<dt>Owner</dt>
|
||||
@@ -39,7 +39,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<label for="description" class="strong">Description (optional):</label>
|
||||
<input type="text" name="description" id="description" class="form-control" style="width: 95%;"/>
|
||||
<input type="text" name="description" id="description" class="form-control" />
|
||||
</fieldset>
|
||||
<fieldset class="border-top">
|
||||
<label class="radio">
|
||||
@@ -58,11 +58,34 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset class="border-top">
|
||||
<label for="createReadme" class="checkbox">
|
||||
<input type="checkbox" name="createReadme" id="createReadme"/>
|
||||
<label class="radio">
|
||||
<input type="radio" name="initOption" value="EMPTY" checked/>
|
||||
<span class="strong">Create an empty repository</span>
|
||||
<div class="normal muted">
|
||||
Create an empty repository. You have to initialize by yourself initially.
|
||||
</div>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="initOption" value="EMPTY_COMMIT"/>
|
||||
<span class="strong">Initialize this repository with an empty commit</span>
|
||||
<div class="normal muted">
|
||||
Create an empty repository with empty commit. You can clone the repository immediately.
|
||||
</div>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="initOption" value="README"/>
|
||||
<span class="strong">Initialize this repository with a README</span>
|
||||
<div class="normal muted">
|
||||
This will let you immediately clone the repository to your computer. Skip this step if you’re importing an existing repository.
|
||||
Create a repository which has README.md. You can clone the repository immediately.
|
||||
</div>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="initOption" value="COPY"/>
|
||||
<span class="strong">Copy existing git repository</span>
|
||||
<div class="normal muted">
|
||||
Create new repository from existing git repository.
|
||||
<input type="text" class="form-control" name="sourceUrl" id="sourceUrl" disabled placeholder="Source git repository URL..."/>
|
||||
<span id="error-sourceUrl" class="error"></span>
|
||||
</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
@@ -83,4 +106,8 @@ $('#owner-dropdown a').click(function(){
|
||||
|
||||
$('#owner-dropdown span.strong').html($(this).find('span').html());
|
||||
});
|
||||
|
||||
$('input[name=initOption]').click(function () {
|
||||
$('#sourceUrl').prop('disabled', $('input[name=initOption]:checked').val() != 'COPY');
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="content-wrapper main-center">
|
||||
<div class="content body">
|
||||
<h2>Create your account</h2>
|
||||
<form action="@context.path/register" method="POST" validate="true">
|
||||
<form action="@context.path/register" method="POST" validate="true" autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<fieldset>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="@context.path/@account.userName/_ssh" validate="true">
|
||||
<form method="POST" action="@context.path/@account.userName/_ssh" validate="true" autocomplete="off">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading strong">Add a SSH Key</div>
|
||||
<div class="panel-body">
|
||||
|
||||
111
src/main/twirl/gitbucket/core/admin/dbviewer.scala.html
Normal file
111
src/main/twirl/gitbucket/core/admin/dbviewer.scala.html
Normal file
@@ -0,0 +1,111 @@
|
||||
@(tables: Seq[gitbucket.core.controller.Table])(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main("Database viewer") {
|
||||
@gitbucket.core.admin.html.menu("dbviewer") {
|
||||
<div class="container">
|
||||
<div class="col-md-3">
|
||||
<div id="table-tree">
|
||||
<ul>
|
||||
@tables.map { table =>
|
||||
<li data-jstree='{"icon":"@context.path/assets/common/images/table.gif"}' data-table="@table.name">@table.name
|
||||
<ul>
|
||||
@table.columns.map { column =>
|
||||
<li data-jstree='{"icon":"@context.path/assets/common/images/column.gif"}' data-table="@table.name" data-column="@column.name">
|
||||
@column.name @if(column.primaryKey){ (PK) }
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div id="editor" style="width: 100%; height: 300px;"></div>
|
||||
<div class="block">
|
||||
<input type="button" value="Run query" id="run-query" class="btn btn-success">
|
||||
<input type="button" value="Clear" id="clear-query" class="btn btn-default">
|
||||
<label for="autorun">
|
||||
<input type="checkbox" id="autorun" name="autorun"/>
|
||||
Auto Run Query
|
||||
</label>
|
||||
</div>
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<script src="@helpers.assets("/vendors/ace/ace.js")" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="@helpers.assets("/vendors/vakata-jstree-3.3.4/jstree.min.js")" type="text/javascript" charset="utf-8"></script>
|
||||
<link rel="stylesheet" href="@helpers.assets("/vendors/vakata-jstree-3.3.4/themes/default/style.min.css")" />
|
||||
<script>
|
||||
$(function(){
|
||||
$('#editor').text($('#initial').val());
|
||||
var editor = ace.edit("editor");
|
||||
editor.setTheme("ace/theme/monokai");
|
||||
editor.getSession().setMode("ace/mode/sql");
|
||||
|
||||
$('#table-tree').jstree();
|
||||
|
||||
$('#table-tree').on('select_node.jstree', function(e, data){
|
||||
//var selectedNodes = $('#table-tree').jstree('get_selected', true);
|
||||
if(editor.getValue().trim() == '' || $('#autorun').is(':checked')){
|
||||
if(data.node.data['column']){
|
||||
editor.setValue('SELECT `' + data.node.data['column'] + '` FROM `' + data.node.data['table'] + '`');
|
||||
} else if(data.node.data['table']){
|
||||
editor.setValue('SELECT * FROM `' + data.node.data['table']+ '`');
|
||||
}
|
||||
if($('#autorun').is(':checked')){
|
||||
$('#run-query').click();
|
||||
}
|
||||
} else {
|
||||
if(data.node.data['column']){
|
||||
editor.getSession().insert(editor.getCursorPosition(), data.node.data['column']);
|
||||
} else if(data.node.data['table']){
|
||||
editor.getSession().insert(editor.getCursorPosition(), data.node.data['table']);
|
||||
}
|
||||
}
|
||||
editor.focus();
|
||||
});
|
||||
|
||||
$('#clear-query').click(function(){
|
||||
editor.setValue('');
|
||||
});
|
||||
|
||||
$('#run-query').click(function(){
|
||||
var selectedText = editor.getSession().doc.getTextRange(editor.selection.getRange()).trim();
|
||||
|
||||
$.post('@context.path/admin/dbviewer/_query', { query: selectedText == '' ? editor.getValue() : selectedText },
|
||||
function(data){
|
||||
if(data.type == "query"){
|
||||
var table = $('<table class="table table-bordered table-hover table-scroll">');
|
||||
|
||||
var header = $('<tr>');
|
||||
$.each(data.columns, function(i, column){
|
||||
header.append($('<th>').text(column));
|
||||
});
|
||||
table.append($('<thead>').append(header));
|
||||
|
||||
var body = $('<tbody>');
|
||||
$.each(data.rows, function(i, rs){
|
||||
var row = $('<tr>');
|
||||
$.each(data.columns, function(i, column){
|
||||
row.append($('<td>').text(rs[column]));
|
||||
});
|
||||
body.append(row);
|
||||
});
|
||||
|
||||
table.append(body);
|
||||
$('#result').empty().append(table);
|
||||
|
||||
} else if(data.type == "update"){
|
||||
$('#result').empty().append($('<span>').text('Updated ' + data.rows + ' rows.'));
|
||||
|
||||
} else if(data.type == "error"){
|
||||
$('#result').empty().append($('<span class="error">').text(data.message));
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -25,10 +25,10 @@
|
||||
<span>Data export / import</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item-hover">
|
||||
<a href="@context.path/console/login.jsp" target="_blank">
|
||||
<li class="menu-item-hover @if(active=="dbviewer"){active}">
|
||||
<a href="@context.path/admin/dbviewer">
|
||||
<i class="menu-icon octicon octicon-database"></i>
|
||||
<span>H2 console</span>
|
||||
<span>Database viewer</span>
|
||||
</a>
|
||||
</li>
|
||||
@gitbucket.core.plugin.PluginRegistry().getSystemSettingMenus.map { menu =>
|
||||
|
||||
47
src/main/twirl/gitbucket/core/admin/settings.scala.html
Normal file
47
src/main/twirl/gitbucket/core/admin/settings.scala.html
Normal file
@@ -0,0 +1,47 @@
|
||||
@(info: Option[Any])(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.service.OpenIDConnectService
|
||||
@import gitbucket.core.util.DatabaseConfig
|
||||
@gitbucket.core.html.main("System settings"){
|
||||
@gitbucket.core.admin.html.menu("system"){
|
||||
@gitbucket.core.helper.html.information(info)
|
||||
<form action="@context.path/admin/system" method="POST" validate="true" class="form-horizontal" autocomplete="off">
|
||||
<ul class="nav nav-tabs fill-width" id="pullreq-tab">
|
||||
<li><a href="#system">System settings</a></li>
|
||||
<li><a href="#authentication">Authentication</a></li>
|
||||
</ul>
|
||||
<div class="tab-content fill-width" style="padding-top: 20px;">
|
||||
<div class="tab-pane" id="system">
|
||||
@settings_system(info)
|
||||
</div>
|
||||
<div class="tab-pane" id="authentication">
|
||||
@settings_authentication(info)
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="align-right" style="margin-top: 20px;">
|
||||
<input type="submit" class="btn btn-success" value="Apply changes"/>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
// Determine active tab from hash
|
||||
if(location.hash == '#authentication'){
|
||||
$('li:has(a[href="#authentication"])').addClass('active');
|
||||
$('div#authentication').addClass('active');
|
||||
} else {
|
||||
$('li:has(a[href="#system"])').addClass('active');
|
||||
$('div#system').addClass('active');
|
||||
}
|
||||
// Set hash when tab is clicked
|
||||
$('ul.nav-tabs li a').click(function(e){
|
||||
location.href = $(e.delegateTarget).attr("href");
|
||||
});
|
||||
|
||||
$('#pullreq-tab a').click(function (e) {
|
||||
e.preventDefault();
|
||||
$(this).tab('show');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,160 @@
|
||||
@(info: Option[Any])(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.service.OpenIDConnectService
|
||||
<!--====================================================================-->
|
||||
<!-- LDAP -->
|
||||
<!--====================================================================-->
|
||||
<fieldset>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(context.settings.ldap){ checked} />
|
||||
LDAP
|
||||
</label>
|
||||
</fieldset>
|
||||
<div class="ldap">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapHost">LDAP host</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapHost" name="ldap.host" class="form-control" value="@context.settings.ldap.map(_.host)"/>
|
||||
<span id="error-ldap_host" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapPort">LDAP port</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapPort" name="ldap.port" class="form-control input-mini" value="@context.settings.ldap.map(_.port)"/>
|
||||
<span id="error-ldap_port" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapBindDN">Bind DN</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapBindDN" name="ldap.bindDN" class="form-control" value="@context.settings.ldap.map(_.bindDN)"/>
|
||||
<span id="error-ldap_bindDN" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapBindPassword">Bind password</label>
|
||||
<div class="col-md-10">
|
||||
<input type="password" id="ldapBindPassword" name="ldap.bindPassword" class="form-control" value="@context.settings.ldap.map(_.bindPassword)"/>
|
||||
<span id="error-ldap_bindPassword" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapBaseDN">Base DN</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapBaseDN" name="ldap.baseDN" class="form-control" value="@context.settings.ldap.map(_.baseDN)"/>
|
||||
<span id="error-ldap_baseDN" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapUserNameAttribute">User name attribute</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapUserNameAttribute" name="ldap.userNameAttribute" class="form-control" value="@context.settings.ldap.map(_.userNameAttribute)"/>
|
||||
<span id="error-ldap_userNameAttribute" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapAdditionalFilterCondition">Additional filter condition</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapAdditionalFilterCondition" name="ldap.additionalFilterCondition" class="form-control" value="@context.settings.ldap.map(_.additionalFilterCondition)"/>
|
||||
<span id="error-ldap_additionalFilterCondition" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapFullNameAttribute">Full name attribute</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapFullNameAttribute" name="ldap.fullNameAttribute" class="form-control" value="@context.settings.ldap.map(_.fullNameAttribute)"/>
|
||||
<span id="error-ldap_fullNameAttribute" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapMailAttribute">Mail address attribute</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapMailAttribute" name="ldap.mailAttribute" class="form-control" value="@context.settings.ldap.map(_.mailAttribute)"/>
|
||||
<span id="error-ldap_mailAttribute" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2">Enable TLS</label>
|
||||
<div class="col-md-10">
|
||||
<input type="checkbox" name="ldap.tls"@if(context.settings.ldap.flatMap(_.tls).getOrElse(false)){ checked}/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2">Enable SSL</label>
|
||||
<div class="col-md-10">
|
||||
<input type="checkbox" name="ldap.ssl"@if(context.settings.ldap.flatMap(_.ssl).getOrElse(false)){ checked}/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapBindDN">Keystore</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapKeystore" name="ldap.keystore" class="form-control" value="@context.settings.ldap.map(_.keystore)"/>
|
||||
<span id="error-ldap_keystore" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--====================================================================-->
|
||||
<!-- OpenID Connect -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<fieldset>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="oidcAuthentication" name="oidcAuthentication"@if(context.settings.oidc){ checked} />
|
||||
OpenID Connect
|
||||
</label>
|
||||
</fieldset>
|
||||
<div class="oidc">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="oidcIssuer">Issuer</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="oidcIssuer" name="oidc.issuer" class="form-control" value="@context.settings.oidc.map(_.issuer.getValue)"/>
|
||||
<span id="error-oidc_issuer" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="oidcClientID">Client ID</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="oidcClientID" name="oidc.clientID" class="form-control" value="@context.settings.oidc.map(_.clientID.getValue)"/>
|
||||
<span id="error-oidc_clientID" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="oidcClientID">Client secret</label>
|
||||
<div class="col-md-10">
|
||||
<input type="password" id="oidcClientSecret" name="oidc.clientSecret" class="form-control" value="@context.settings.oidc.map(_.clientSecret.getValue)"/>
|
||||
<span id="error-oidc_clientSecret" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="oidcJwsAlgorithm">Expected signature</label>
|
||||
<div class="col-md-10">
|
||||
<select id="oidcJwsAlgorithm" name="oidc.jwsAlgorithm" class="form-control">
|
||||
<option value="" @if(context.settings.oidc.flatMap(_.jwsAlgorithm) == None){selected}>
|
||||
No signature
|
||||
</option>
|
||||
@OpenIDConnectService.JWS_ALGORITHMS.map { case (family, algorithms) =>
|
||||
<optgroup label="@family">
|
||||
@algorithms.map { algorithm =>
|
||||
<option value="@algorithm.getName" @if(context.settings.oidc.flatMap(_.jwsAlgorithm) == Some(algorithm)){selected}>
|
||||
@algorithm.getName
|
||||
</option>
|
||||
}
|
||||
</optgroup>
|
||||
}
|
||||
</select>
|
||||
<span class="muted">Choose the expected signature algorithm of the token response. Most IdP provides RS256 or HS256.</span>
|
||||
<span id="error-oidc_jwsAlgorithm" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
$('#ldapAuthentication').change(function(){
|
||||
$('.ldap input').prop('disabled', !$(this).prop('checked'));
|
||||
}).change();
|
||||
|
||||
$('#oidcAuthentication').change(function(){
|
||||
$('.oidc input, .oidc select').prop('disabled', !$(this).prop('checked'));
|
||||
}).change();
|
||||
});
|
||||
</script>
|
||||
329
src/main/twirl/gitbucket/core/admin/settings_system.scala.html
Normal file
329
src/main/twirl/gitbucket/core/admin/settings_system.scala.html
Normal file
@@ -0,0 +1,329 @@
|
||||
@(info: Option[Any])(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.util.DatabaseConfig
|
||||
<!--====================================================================-->
|
||||
<!-- System properties -->
|
||||
<!--====================================================================-->
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>Property</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>GITBUCKET_HOME</td>
|
||||
<td>@gitbucket.core.util.Directory.GitBucketHome</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DATABASE_URL</td>
|
||||
@if(DatabaseConfig.url.startsWith("jdbc:h2:")) {
|
||||
<td class="danger">
|
||||
<p>@gitbucket.core.util.DatabaseConfig.url</p>
|
||||
<p>
|
||||
Your GitBucket is running on embedded H2 database.
|
||||
Recommend to <a href="https://github.com/gitbucket/gitbucket/wiki/External-database-configuration">configure to use external database</a> if you would like to use GitBucket for important purpose.
|
||||
</p>
|
||||
</td>
|
||||
}else{
|
||||
<td>@gitbucket.core.util.DatabaseConfig.url</td>
|
||||
}
|
||||
</tr>
|
||||
</table>
|
||||
<!--====================================================================-->
|
||||
<!-- Base URL -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><span class="strong">Base URL</span> (e.g. <code>http://example.com/gitbucket</code>)</label>
|
||||
<fieldset>
|
||||
<div class="controls">
|
||||
<input type="text" name="baseUrl" id="baseUrl" class="form-control" value="@context.settings.baseUrl"/>
|
||||
<span id="error-baseUrl" class="error"></span>
|
||||
</div>
|
||||
</fieldset>
|
||||
<p class="muted">
|
||||
The base URL is used for redirect, notification email, git repository URL box and more.
|
||||
If the base URL is empty, GitBucket generates URL from the request information.
|
||||
You can use this property to adjust to URL differences between the reverse proxy and GitBucket.
|
||||
</p>
|
||||
<!--====================================================================-->
|
||||
<!-- Information -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><span class="strong">Information</span> (HTML is available)</label>
|
||||
<fieldset>
|
||||
<textarea name="information" class="form-control" style="height: 100px;">@context.settings.information</textarea>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- AdminLTE SkinName -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">
|
||||
AdminLTE skin name
|
||||
</label>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="skinName">Skin name</label>
|
||||
<div class="col-md-10">
|
||||
<select id="skinName" name="skinName" class="form-control">
|
||||
<optgroup label="Dark">
|
||||
@Seq(
|
||||
("skin-black", "Black"),
|
||||
("skin-blue", "Blue"),
|
||||
("skin-green", "Green"),
|
||||
("skin-purple", "Purple"),
|
||||
("skin-red", "Red"),
|
||||
("skin-yellow", "Yellow"),
|
||||
).map{ skin =>
|
||||
<option value="@skin._1"@if(skin._1 == context.settings.skinName){ selected=""}>@skin._2</option>
|
||||
}
|
||||
</optgroup>
|
||||
<optgroup label="Light">
|
||||
@Seq(
|
||||
("skin-black-light", "Light black"),
|
||||
("skin-blue-light", "Light blue"),
|
||||
("skin-green-light", "Light green"),
|
||||
("skin-purple-light", "Light purple"),
|
||||
("skin-red-light", "Light red"),
|
||||
("skin-yellow-light", "Light yellow"),
|
||||
).map{ skin =>
|
||||
<option value="@skin._1"@if(skin._1 == context.settings.skinName){ selected=""} >@skin._2</option>
|
||||
}
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!--====================================================================-->
|
||||
<!-- Account registration -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">Account registration</label>
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="allowAccountRegistration" value="true"@if(context.settings.allowAccountRegistration){ checked}>
|
||||
<span class="strong">Allow</span> <span class="normal">- Users can create accounts by themselves.</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="allowAccountRegistration" value="false"@if(!context.settings.allowAccountRegistration){ checked}>
|
||||
<span class="strong">Deny</span> <span class="normal">- Only administrators can create accounts.</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<hr>
|
||||
<label class="strong">Default permissions when creating a new repository</label>
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="isCreateRepoOptionPublic" value="true"@if(context.settings.isCreateRepoOptionPublic){ checked}>
|
||||
<span class="strong">Public</span> <span class="normal">- All users and guests can read the repository.</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="isCreateRepoOptionPublic" value="false"@if(!context.settings.isCreateRepoOptionPublic){ checked}>
|
||||
<span class="strong">Private</span> <span class="normal">- Only collaborators can read the repository.</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Anonymous access -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">Anonymous access</label>
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="allowAnonymousAccess" value="true"@if(context.settings.allowAnonymousAccess){ checked}>
|
||||
<span class="strong">Allow</span> <span class="normal">- Anyone can view public repositories and user/group profiles.</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="allowAnonymousAccess" value="false"@if(!context.settings.allowAnonymousAccess){ checked}>
|
||||
<span class="strong">Deny</span> <span class="normal">- Users must authenticate before viewing any information.</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Activity -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><span class="strong">Limit of activity logs</span> (Unlimited if it is not specified or zero)</label>
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="activityLogLimit">Limit</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="activityLogLimit" name="activityLogLimit" class="form-control input-mini" value="@context.settings.activityLogLimit"/>
|
||||
<span id="error-activityLogLimit" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Services -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">Services</label>
|
||||
<fieldset>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="gravatar"@if(context.settings.gravatar){ checked}/>
|
||||
Use Gravatar for profile images
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- SSH access -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">SSH access</label>
|
||||
<fieldset>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="ssh" name="ssh"@if(context.settings.ssh){ checked}/>
|
||||
Enable SSH access to git repository
|
||||
<span class="muted normal">(Both SSH host and Base URL are required if SSH access is enabled)</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<div class="ssh">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="sshHost">SSH host</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="sshHost" name="sshHost" class="form-control" value="@context.settings.sshHost"/>
|
||||
<span id="error-sshHost" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="sshPort">SSH port</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="sshPort" name="sshPort" class="form-control" value="@context.settings.sshPort"/>
|
||||
<span id="error-sshPort" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--====================================================================-->
|
||||
<!-- Communication email -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">Communication</label>
|
||||
<fieldset>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="useSMTP" name="useSMTP" @if(context.settings.useSMTP){ checked}/>
|
||||
SMTP
|
||||
<span class="muted normal">(Enable notification as well as SMTP configuration if you want to send notification email too)</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<div class="useSMTP">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="smtpHost">SMTP host</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="smtpHost" name="smtp.host" class="form-control" value="@context.settings.smtp.map(_.host)"/>
|
||||
<span id="error-smtp_host" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="smtpPort">SMTP port</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="smtpPort" name="smtp.port" class="form-control input-mini" value="@context.settings.smtp.map(_.port)"/>
|
||||
<span id="error-smtp_port" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="smtpUser">SMTP user</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="smtpUser" name="smtp.user" class="form-control" value="@context.settings.smtp.map(_.user)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="smtpPassword">SMTP password</label>
|
||||
<div class="col-md-10">
|
||||
<input type="password" id="smtpPassword" name="smtp.password" class="form-control" value="@context.settings.smtp.map(_.password)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="smtpSsl">Enable SSL</label>
|
||||
<div class="col-md-10">
|
||||
<input type="checkbox" id="smtpSsl" name="smtp.ssl"@if(context.settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="smtpStarttls">Enable STARTTLS</label>
|
||||
<div class="col-md-10">
|
||||
<input type="checkbox" id="smtpStarttls" name="smtp.starttls"@if(context.settings.smtp.flatMap(_.starttls).getOrElse(false)){ checked}/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="fromAddress">FROM address</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="fromAddress" name="smtp.fromAddress" class="form-control" value="@context.settings.smtp.map(_.fromAddress)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="fromName">FROM name</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="fromName" name="smtp.fromName" class="form-control" value="@context.settings.smtp.map(_.fromName)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
Send test mail to:
|
||||
<input type="text" id="testAddress" size="30"/>
|
||||
<input type="button" id="sendTestMail" value="Send"/>
|
||||
</div>
|
||||
</div>
|
||||
<!--====================================================================-->
|
||||
<!-- Notification email -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">Notifications</label>
|
||||
<fieldset>
|
||||
<label class="checkbox" for="notification">
|
||||
<input type="checkbox" id="notification" name="notification"@if(context.settings.notification){ checked}/>
|
||||
Send notifications
|
||||
</label>
|
||||
</fieldset>
|
||||
<script>
|
||||
$(function(){
|
||||
$('#skinName').change(function(evt) {
|
||||
var that = $(evt.target);
|
||||
var themeCss = $('link[rel="stylesheet"][href*="skin-"]');
|
||||
var oldVal = new RegExp('(skin-.*?).min.css').exec(themeCss.attr('href'))[1];
|
||||
themeCss.attr('href', themeCss.attr('href').replace(oldVal, that.val()));
|
||||
$(document.body).removeClass(oldVal).addClass(that.val());
|
||||
});
|
||||
|
||||
$('#sendTestMail').click(function(){
|
||||
var host = $('#smtpHost' ).val();
|
||||
var port = $('#smtpPort' ).val();
|
||||
var user = $('#smtpUser' ).val();
|
||||
var password = $('#smtpPassword').val();
|
||||
var ssl = $('#smtpSsl' ).prop('checked');
|
||||
var starttls = $('#smtpStarttls').prop('checked');
|
||||
var fromAddress = $('#fromAddress' ).val();
|
||||
var fromName = $('#fromName' ).val();
|
||||
var testAddress = $('#testAddress' ).val();
|
||||
|
||||
if(host == ''){
|
||||
alert('SMTP Host is required.');
|
||||
$('#smtpHost').focus();
|
||||
} else if(testAddress == ''){
|
||||
alert('Destination is required.');
|
||||
$('#testAddress').focus();
|
||||
} else {
|
||||
$.post('@context.path/admin/system/sendmail', {
|
||||
'smtp.host': host,
|
||||
'smtp.port': port,
|
||||
'smtp.user': user,
|
||||
'smtp.password': password,
|
||||
'smtp.ssl': ssl,
|
||||
'smtp.starttls': starttls,
|
||||
'smtp.fromAddress': fromAddress,
|
||||
'smtp.fromName': fromName,
|
||||
'testAddress': testAddress
|
||||
}, function(data, status){
|
||||
if(data != ''){
|
||||
alert(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$('#ssh').change(function(){
|
||||
$('.ssh input').prop('disabled', !$(this).prop('checked'));
|
||||
}).change();
|
||||
|
||||
$('#useSMTP').change(function(){
|
||||
$('.useSMTP input').prop('disabled', !$(this).prop('checked'));
|
||||
|
||||
// With only SMTP in current version, notification cannot be enabled if no communication channel exists
|
||||
$('#notification').prop('disabled', !$(this).prop('checked'));
|
||||
|
||||
if (!$(this).prop('checked')) {
|
||||
// With only SMTP in current version, if SMTP is unchecked then we disable notification
|
||||
$('#notification').prop('checked', false);
|
||||
}
|
||||
}).change();
|
||||
});
|
||||
</script>
|
||||
@@ -1,460 +0,0 @@
|
||||
@(info: Option[Any])(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.util.DatabaseConfig
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main("System settings"){
|
||||
@gitbucket.core.admin.html.menu("system"){
|
||||
@gitbucket.core.helper.html.information(info)
|
||||
<form action="@context.path/admin/system" method="POST" validate="true" class="form-horizontal">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading strong">System settings</div>
|
||||
<div class="panel-body">
|
||||
<!--====================================================================-->
|
||||
<!-- System properties -->
|
||||
<!--====================================================================-->
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>Property</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>GITBUCKET_HOME</td>
|
||||
<td>@gitbucket.core.util.Directory.GitBucketHome</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DATABASE_URL</td>
|
||||
@if(DatabaseConfig.url.startsWith("jdbc:h2:")) {
|
||||
<td class="danger">
|
||||
<p>@gitbucket.core.util.DatabaseConfig.url</p>
|
||||
<p>
|
||||
Your GitBucket is running on embedded H2 database.
|
||||
Recommend to <a href="https://github.com/gitbucket/gitbucket/wiki/External-database-configuration">configure to use external database</a> if you would like to use GitBucket for important purpose.
|
||||
</p>
|
||||
</td>
|
||||
}else{
|
||||
<td>@gitbucket.core.util.DatabaseConfig.url</td>
|
||||
}
|
||||
</tr>
|
||||
</table>
|
||||
<!--====================================================================-->
|
||||
<!-- Base URL -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><span class="strong">Base URL</span> (e.g. <code>http://example.com/gitbucket</code>)</label>
|
||||
<fieldset>
|
||||
<div class="controls">
|
||||
<input type="text" name="baseUrl" id="baseUrl" class="form-control" value="@context.settings.baseUrl"/>
|
||||
<span id="error-baseUrl" class="error"></span>
|
||||
</div>
|
||||
</fieldset>
|
||||
<p class="muted">
|
||||
The base URL is used for redirect, notification email, git repository URL box and more.
|
||||
If the base URL is empty, GitBucket generates URL from the request information.
|
||||
You can use this property to adjust to URL differences between the reverse proxy and GitBucket.
|
||||
</p>
|
||||
<!--====================================================================-->
|
||||
<!-- Information -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><span class="strong">Information</span> (HTML is available)</label>
|
||||
<fieldset>
|
||||
<textarea name="information" class="form-control" style="height: 100px;">@context.settings.information</textarea>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- AdminLTE SkinName -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">
|
||||
AdminLTE skin name
|
||||
</label>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="skinName">Skin name</label>
|
||||
<div class="col-md-10">
|
||||
<select id="skinName" name="skinName" class="form-control">
|
||||
<optgroup label="Dark">
|
||||
@Seq(
|
||||
("skin-black", "Black"),
|
||||
("skin-blue", "Blue"),
|
||||
("skin-green", "Green"),
|
||||
("skin-purple", "Purple"),
|
||||
("skin-red", "Red"),
|
||||
("skin-yellow", "Yellow"),
|
||||
).map{ skin =>
|
||||
<option value="@skin._1"@if(skin._1 == context.settings.skinName){ selected=""}>@skin._2</option>
|
||||
}
|
||||
</optgroup>
|
||||
<optgroup label="Light">
|
||||
@Seq(
|
||||
("skin-black-light", "Light black"),
|
||||
("skin-blue-light", "Light blue"),
|
||||
("skin-green-light", "Light green"),
|
||||
("skin-purple-light", "Light purple"),
|
||||
("skin-red-light", "Light red"),
|
||||
("skin-yellow-light", "Light yellow"),
|
||||
).map{ skin =>
|
||||
<option value="@skin._1"@if(skin._1 == context.settings.skinName){ selected=""} >@skin._2</option>
|
||||
}
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!--====================================================================-->
|
||||
<!-- Account registration -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">Account registration</label>
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="allowAccountRegistration" value="true"@if(context.settings.allowAccountRegistration){ checked}>
|
||||
<span class="strong">Allow</span> <span class="normal">- Users can create accounts by themselves.</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="allowAccountRegistration" value="false"@if(!context.settings.allowAccountRegistration){ checked}>
|
||||
<span class="strong">Deny</span> <span class="normal">- Only administrators can create accounts.</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<hr>
|
||||
<label class="strong">Default permissions when creating a new repository</label>
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="isCreateRepoOptionPublic" value="true"@if(context.settings.isCreateRepoOptionPublic){ checked}>
|
||||
<span class="strong">Public</span> <span class="normal">- All users and guests can read the repository.</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="isCreateRepoOptionPublic" value="false"@if(!context.settings.isCreateRepoOptionPublic){ checked}>
|
||||
<span class="strong">Private</span> <span class="normal">- Only collaborators can read the repository.</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Anonymous access -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">Anonymous access</label>
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="allowAnonymousAccess" value="true"@if(context.settings.allowAnonymousAccess){ checked}>
|
||||
<span class="strong">Allow</span> <span class="normal">- Anyone can view public repositories and user/group profiles.</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="allowAnonymousAccess" value="false"@if(!context.settings.allowAnonymousAccess){ checked}>
|
||||
<span class="strong">Deny</span> <span class="normal">- Users must authenticate before viewing any information.</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Activity -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><span class="strong">Limit of activity logs</span> (Unlimited if it is not specified or zero)</label>
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="activityLogLimit">Limit</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="activityLogLimit" name="activityLogLimit" class="form-control input-mini" value="@context.settings.activityLogLimit"/>
|
||||
<span id="error-activityLogLimit" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Services -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">Services</label>
|
||||
<fieldset>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="gravatar"@if(context.settings.gravatar){ checked}/>
|
||||
Use Gravatar for profile images
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- SSH access -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">SSH access</label>
|
||||
<fieldset>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="ssh" name="ssh"@if(context.settings.ssh){ checked}/>
|
||||
Enable SSH access to git repository
|
||||
<span class="muted normal">(Both SSH host and Base URL are required if SSH access is enabled)</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<div class="ssh">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="sshHost">SSH host</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="sshHost" name="sshHost" class="form-control" value="@context.settings.sshHost"/>
|
||||
<span id="error-sshHost" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="sshPort">SSH port</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="sshPort" name="sshPort" class="form-control" value="@context.settings.sshPort"/>
|
||||
<span id="error-sshPort" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--====================================================================-->
|
||||
<!-- Authentication -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">Authentication</label>
|
||||
<fieldset>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(context.settings.ldap){ checked} />
|
||||
LDAP
|
||||
</label>
|
||||
</fieldset>
|
||||
<div class="ldap">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapHost">LDAP host</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapHost" name="ldap.host" class="form-control" value="@context.settings.ldap.map(_.host)"/>
|
||||
<span id="error-ldap_host" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapPort">LDAP port</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapPort" name="ldap.port" class="form-control input-mini" value="@context.settings.ldap.map(_.port)"/>
|
||||
<span id="error-ldap_port" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapBindDN">Bind DN</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapBindDN" name="ldap.bindDN" class="form-control" value="@context.settings.ldap.map(_.bindDN)"/>
|
||||
<span id="error-ldap_bindDN" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapBindPassword">Bind password</label>
|
||||
<div class="col-md-10">
|
||||
<input type="password" id="ldapBindPassword" name="ldap.bindPassword" class="form-control" value="@context.settings.ldap.map(_.bindPassword)"/>
|
||||
<span id="error-ldap_bindPassword" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapBaseDN">Base DN</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapBaseDN" name="ldap.baseDN" class="form-control" value="@context.settings.ldap.map(_.baseDN)"/>
|
||||
<span id="error-ldap_baseDN" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapUserNameAttribute">User name attribute</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapUserNameAttribute" name="ldap.userNameAttribute" class="form-control" value="@context.settings.ldap.map(_.userNameAttribute)"/>
|
||||
<span id="error-ldap_userNameAttribute" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapAdditionalFilterCondition">Additional filter condition</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapAdditionalFilterCondition" name="ldap.additionalFilterCondition" class="form-control" value="@context.settings.ldap.map(_.additionalFilterCondition)"/>
|
||||
<span id="error-ldap_additionalFilterCondition" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapFullNameAttribute">Full name attribute</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapFullNameAttribute" name="ldap.fullNameAttribute" class="form-control" value="@context.settings.ldap.map(_.fullNameAttribute)"/>
|
||||
<span id="error-ldap_fullNameAttribute" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapMailAttribute">Mail address attribute</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapMailAttribute" name="ldap.mailAttribute" class="form-control" value="@context.settings.ldap.map(_.mailAttribute)"/>
|
||||
<span id="error-ldap_mailAttribute" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2">Enable TLS</label>
|
||||
<div class="col-md-10">
|
||||
<input type="checkbox" name="ldap.tls"@if(context.settings.ldap.flatMap(_.tls).getOrElse(false)){ checked}/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2">Enable SSL</label>
|
||||
<div class="col-md-10">
|
||||
<input type="checkbox" name="ldap.ssl"@if(context.settings.ldap.flatMap(_.ssl).getOrElse(false)){ checked}/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="ldapBindDN">Keystore</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="ldapKeystore" name="ldap.keystore" class="form-control" value="@context.settings.ldap.map(_.keystore)"/>
|
||||
<span id="error-ldap_keystore" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--====================================================================-->
|
||||
<!-- Notification email -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">Notifications</label>
|
||||
<fieldset>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="notification" name="notification"@if(context.settings.notification){ checked}/>
|
||||
Send notifications
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Communication email -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">Communication</label>
|
||||
<fieldset>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="useSMTP" name="useSMTP" @if(context.settings.useSMTP){ checked}/>
|
||||
SMTP
|
||||
<span class="muted normal">(Enable notification as well as SMTP configuration if you want to send notification email too)</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<div class="useSMTP">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="smtpHost">SMTP host</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="smtpHost" name="smtp.host" class="form-control" value="@context.settings.smtp.map(_.host)"/>
|
||||
<span id="error-smtp_host" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="smtpPort">SMTP port</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="smtpPort" name="smtp.port" class="form-control input-mini" value="@context.settings.smtp.map(_.port)"/>
|
||||
<span id="error-smtp_port" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="smtpUser">SMTP user</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="smtpUser" name="smtp.user" class="form-control" value="@context.settings.smtp.map(_.user)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="smtpPassword">SMTP password</label>
|
||||
<div class="col-md-10">
|
||||
<input type="password" id="smtpPassword" name="smtp.password" class="form-control" value="@context.settings.smtp.map(_.password)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="smtpSsl">Enable SSL</label>
|
||||
<div class="col-md-10">
|
||||
<input type="checkbox" id="smtpSsl" name="smtp.ssl"@if(context.settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="smtpStarttls">Enable STARTTLS</label>
|
||||
<div class="col-md-10">
|
||||
<input type="checkbox" id="smtpStarttls" name="smtp.starttls"@if(context.settings.smtp.flatMap(_.starttls).getOrElse(false)){ checked}/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="fromAddress">FROM address</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="fromAddress" name="smtp.fromAddress" class="form-control" value="@context.settings.smtp.map(_.fromAddress)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="fromName">FROM name</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="fromName" name="smtp.fromName" class="form-control" value="@context.settings.smtp.map(_.fromName)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
Send test mail to:
|
||||
<input type="text" id="testAddress" size="30"/>
|
||||
<input type="button" id="sendTestMail" value="Send"/>
|
||||
</div>
|
||||
</div>
|
||||
@*
|
||||
<!--====================================================================-->
|
||||
<!-- GitLFS -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label class="strong">
|
||||
GitLFS <span class="muted normal">(Enter the LFS server url to enable GitLFS support)</span>
|
||||
</label>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="smtpHost">LFS server url</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="lfsServerUrl" name="lfs.serverUrl" class="form-control" value="@context.settings.lfs.serverUrl"/>
|
||||
<span id="error-lfs_serverUrl" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
*@
|
||||
</div>
|
||||
</div>
|
||||
<div class="align-right" style="margin-top: 20px;">
|
||||
<input type="submit" class="btn btn-success" value="Apply changes"/>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#skinName').change(function(evt) {
|
||||
var that = $(evt.target);
|
||||
var themeCss = $('link[rel="stylesheet"][href*="skin-"]');
|
||||
var oldVal = new RegExp('(skin-.*?).min.css').exec(themeCss.attr('href'))[1];
|
||||
themeCss.attr('href', themeCss.attr('href').replace(oldVal, that.val()));
|
||||
$(document.body).removeClass(oldVal).addClass(that.val());
|
||||
});
|
||||
|
||||
$('#sendTestMail').click(function(){
|
||||
var host = $('#smtpHost' ).val();
|
||||
var port = $('#smtpPort' ).val();
|
||||
var user = $('#smtpUser' ).val();
|
||||
var password = $('#smtpPassword').val();
|
||||
var ssl = $('#smtpSsl' ).prop('checked');
|
||||
var starttls = $('#smtpStarttls').prop('checked');
|
||||
var fromAddress = $('#fromAddress' ).val();
|
||||
var fromName = $('#fromName' ).val();
|
||||
var testAddress = $('#testAddress' ).val();
|
||||
|
||||
if(host == ''){
|
||||
alert('SMTP Host is required.');
|
||||
$('#smtpHost').focus();
|
||||
} else if(testAddress == ''){
|
||||
alert('Destination is required.');
|
||||
$('#testAddress').focus();
|
||||
} else {
|
||||
$.post('@context.path/admin/system/sendmail', {
|
||||
'smtp.host': host,
|
||||
'smtp.port': port,
|
||||
'smtp.user': user,
|
||||
'smtp.password': password,
|
||||
'smtp.ssl': ssl,
|
||||
'smtp.starttls': starttls,
|
||||
'smtp.fromAddress': fromAddress,
|
||||
'smtp.fromName': fromName,
|
||||
'testAddress': testAddress
|
||||
}, function(data, status){
|
||||
if(data != ''){
|
||||
alert(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$('#ssh').change(function(){
|
||||
$('.ssh input').prop('disabled', !$(this).prop('checked'));
|
||||
}).change();
|
||||
|
||||
$('#useSMTP').change(function(){
|
||||
$('.useSMTP input').prop('disabled', !$(this).prop('checked'));
|
||||
|
||||
// With only SMTP in current version, notification cannot be enabled if no communication channel exists
|
||||
$('#notification').prop('disabled', !$(this).prop('checked'));
|
||||
|
||||
if (!$(this).prop('checked')) {
|
||||
// With only SMTP in current version, if SMTP is unchecked then we disable notification
|
||||
$('#notification').prop('checked', false);
|
||||
}
|
||||
}).change();
|
||||
|
||||
$('#ldapAuthentication').change(function(){
|
||||
$('.ldap input').prop('disabled', !$(this).prop('checked'));
|
||||
}).change();
|
||||
});
|
||||
</script>
|
||||
@@ -2,7 +2,7 @@
|
||||
@gitbucket.core.html.main(if(account.isEmpty) "New user" else "Update user"){
|
||||
@gitbucket.core.admin.html.menu("users"){
|
||||
@gitbucket.core.helper.html.error(error)
|
||||
<form method="POST" action="@if(account.isEmpty){@context.path/admin/users/_newuser} else {@context.path/admin/users/@account.get.userName/_edituser}" validate="true">
|
||||
<form method="POST" validate="true" autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<fieldset class="form-group">
|
||||
@@ -82,9 +82,24 @@
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="border-top">
|
||||
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create user} else {Update user}"/>
|
||||
@if(account.isEmpty){
|
||||
<input type="submit" class="btn btn-success" value="Create user" formaction="@context.path/admin/users/_newuser"/>
|
||||
} else {
|
||||
<input type="submit" class="btn btn-success" value="Update user" formaction="@context.path/admin/users/@account.get.userName/_edituser" id="update"/>
|
||||
}
|
||||
<a href="@context.path/admin/users" class="btn btn-default">Cancel</a>
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#update').click(function(){
|
||||
if($('#password').val() != ''){
|
||||
return confirm('Are you sure you want to change password of this user?');
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main(if(account.isEmpty) "New group" else "Update group"){
|
||||
@gitbucket.core.admin.html.menu("users"){
|
||||
<form id="form" method="post" action="@context.path/admin/users/@(account.map(x => s"${x.userName}/_editgroup").getOrElse("_newgroup"))" validate="true">
|
||||
@gitbucket.core.account.html.groupform(account, members, true
|
||||
)
|
||||
<form id="form" method="post" action="@context.path/admin/users/@(account.map(x => s"${x.userName}/_editgroup").getOrElse("_newgroup"))" validate="true" autocomplete="off">
|
||||
@gitbucket.core.account.html.groupform(account, members, true)
|
||||
<fieldset class="border-top">
|
||||
@if(account.isDefined){
|
||||
<div class="pull-right">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@(users: List[gitbucket.core.model.Account], members: Map[String, List[String]], includeRemoved: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||
@(users: List[gitbucket.core.model.Account], members: Map[String, List[String]], includeRemoved: Boolean, includeGroups: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main("Manage Users"){
|
||||
@gitbucket.core.admin.html.menu("users"){
|
||||
@@ -10,6 +10,10 @@
|
||||
<input type="checkbox" id="includeRemoved" name="includeRemoved" @if(includeRemoved){checked}/>
|
||||
Include removed users
|
||||
</label>
|
||||
<label for="includeGroups">
|
||||
<input type="checkbox" id="includeGroups" name="includeGroups" @if(includeGroups){checked}/>
|
||||
Include group accounts
|
||||
</label>
|
||||
<table class="table table-bordered table-hover">
|
||||
@users.map { account =>
|
||||
<tr>
|
||||
@@ -63,8 +67,9 @@
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#includeRemoved').click(function(){
|
||||
location.href = '@context.path/admin/users?includeRemoved=' + this.checked;
|
||||
$('#includeRemoved,#includeGroups').click(function(){
|
||||
location.href = '@context.path/admin/users?includeRemoved=' + $('#includeRemoved').prop('checked')
|
||||
+ '&includeGroups=' + $('#includeGroups').prop('checked');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
case "reopen_issue" => detailActivity(activity, "issue-reopened")
|
||||
case "open_pullreq" => detailActivity(activity, "git-pull-request")
|
||||
case "merge_pullreq" => detailActivity(activity, "git-merge")
|
||||
case "release" => detailActivity(activity, "package")
|
||||
case "create_repository" => simpleActivity(activity, "repo")
|
||||
case "create_branch" => simpleActivity(activity, "git-branch")
|
||||
case "delete_branch" => simpleActivity(activity, "circle-slash")
|
||||
|
||||
@@ -65,7 +65,7 @@ $(function(){
|
||||
}
|
||||
@dropzone(clickable: Boolean, textareaId: Option[String]) = {
|
||||
url: '@context.path/upload/file/@repository.owner/@repository.name',
|
||||
maxFilesize: 10,
|
||||
maxFilesize: @{FileUtil.MaxFileSize / 1024 / 1024},
|
||||
clickable: @clickable,
|
||||
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
|
||||
success: function(file, id) {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
@if(hasWritePermission) {
|
||||
<li id="create-branch" style="display: none;">
|
||||
<a><form action="@helpers.url(repository)/branches" method="post" style="margin: 0;">
|
||||
<span class="new-branch-name">Create branch: <span class="new-branch"></span></span>
|
||||
<span class="strong">Create branch: <span class="new-branch"></span></span>
|
||||
<br><span style="padding-left: 17px;">from '@branch'</span>
|
||||
<input type="hidden" name="new">
|
||||
<input type="hidden" name="from" value="@branch">
|
||||
|
||||
@@ -10,9 +10,15 @@
|
||||
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
||||
@if(showIndex){
|
||||
<div class="pull-right" style="margin-bottom: 10px;">
|
||||
@if(oldCommitId.isEmpty && newCommitId.isDefined) {
|
||||
<a href="@helpers.url(repository)/patch/@newCommitId" class="btn btn-default">Patch</a>
|
||||
}
|
||||
@if(oldCommitId.isDefined && newCommitId.isDefined) {
|
||||
<a href="@helpers.url(repository)/patch/@oldCommitId...@newCommitId" class="btn btn-default">Patch</a>
|
||||
}
|
||||
<div class="btn-group" data-toggle="buttons">
|
||||
<input type="button" id="btn-unified" class="btn btn-default btn-small active" value="Unified">
|
||||
<input type="button" id="btn-split" class="btn btn-default btn-small" value="Split">
|
||||
<input type="button" id="btn-unified" class="btn btn-default active" value="Unified">
|
||||
<input type="button" id="btn-split" class="btn btn-default" value="Split">
|
||||
</div>
|
||||
</div>
|
||||
Showing <a href="javascript:void(0);" id="toggle-file-list">@diffs.size changed @helpers.plural(diffs.size, "file")</a>
|
||||
@@ -232,7 +238,6 @@ $(function(){
|
||||
var $this = $(this);
|
||||
var $tr = $this.closest('tr');
|
||||
var $check = $this.closest('table:not(.diff)').find('.toggle-notes');
|
||||
//var url = '';
|
||||
if (!$check.prop('checked')) {
|
||||
$check.prop('checked', true).trigger('change');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@(account: Option[gitbucket.core.model.Account])(implicit context: gitbucket.core.controller.Context)
|
||||
<div id="avatar" class="muted">
|
||||
@if(account.nonEmpty && account.get.image.nonEmpty){
|
||||
<img src="@context.path/@account.get.userName/_avatar" style="with: 120px; height: 120px;"/>
|
||||
<img src="@context.path/@account.get.userName/_avatar" style="width: 120px; height: 120px;"/>
|
||||
} else {
|
||||
<div id="clickable">Upload Image</div>
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@import gitbucket.core.view.helpers
|
||||
@if(isEditable){
|
||||
<hr/><br/>
|
||||
<form method="POST" validate="true">
|
||||
<form method="POST" validate="true" autocomplete="off">
|
||||
<div class="panel panel-default issue-comment-box">
|
||||
<div class="panel-body">
|
||||
@gitbucket.core.helper.html.preview(
|
||||
@@ -22,12 +22,23 @@
|
||||
elastic = true,
|
||||
tabIndex = 1
|
||||
)
|
||||
<div class="text-right">
|
||||
<input type="hidden" name="issueId" value="@issue.issueId"/>
|
||||
@if((reopenable || !issue.closed) && (isManageable || issue.openedUserName == context.loginAccount.get.userName)){
|
||||
<input type="submit" class="btn btn-default" tabindex="3" formaction="@helpers.url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
|
||||
}
|
||||
<input type="submit" class="btn btn-success" tabindex="2" formaction="@helpers.url(repository)/issue_comments/new" value="Comment"/>
|
||||
<div class="text-right">
|
||||
<input type="hidden" name="issueId" value="@issue.issueId"/>
|
||||
@if((reopenable || !issue.closed) && (isManageable || issue.openedUserName == context.loginAccount.get.userName)){
|
||||
<input type="hidden" id="action" name="action" value="comment"/>
|
||||
<div class="btn-group dropup">
|
||||
<input type="submit" class="btn btn-success" tabindex="2" formaction="@helpers.url(repository)/issue_comments/new" value="Comment" id="submit-button"/>
|
||||
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a id="menu-comment">Comment</a></li>
|
||||
<li><a id="menu-x-and-comment">@{if(issue.closed) "Reopen" else "Close"} and comment</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
} else {
|
||||
<input type="submit" class="btn btn-success" tabindex="2" formaction="@helpers.url(repository)/issue_comments/new" value="Comment"/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,8 +46,13 @@
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#action').click(function(){
|
||||
$('<input type="hidden">').attr('name', 'action').val($(this).val().toLowerCase()).appendTo('form');
|
||||
$('#menu-comment').click(function(){
|
||||
$('#submit-button').attr('value', 'Comment').attr('formaction', '@helpers.url(repository)/issue_comments/new');
|
||||
$('#action').val('comment');
|
||||
});
|
||||
$('#menu-x-and-comment').click(function(){
|
||||
$('#submit-button').attr('value', '@{if(issue.closed) "Reopen" else "Close"} and comment').attr('formaction', '@helpers.url(repository)/issue_comments/state');
|
||||
$('#action').val('@{if(issue.closed) "reopen" else "close"}');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -5,6 +5,43 @@
|
||||
pullreq: Option[gitbucket.core.model.PullRequest] = None)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@import gitbucket.core.model.CommitComment
|
||||
@issueOrPullRequest()={ @if(issue.exists(_.isPullRequest))( "pull request" )else( "issue" ) }
|
||||
@showFormatedComment(comment: gitbucket.core.model.IssueComment)={
|
||||
<div class="panel panel-default issue-comment-box" id="comment-@comment.commentId">
|
||||
<div class="panel-heading">
|
||||
@helpers.avatar(comment.commentedUserName, 20)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
<span class="muted">
|
||||
@if(comment.action == "comment"){
|
||||
commented
|
||||
} else {
|
||||
referenced the @issueOrPullRequest()
|
||||
}
|
||||
<a href="#comment-@comment.commentId">
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</a>
|
||||
</span>
|
||||
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer"
|
||||
&& (isManageable || context.loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
|
||||
<span class="pull-right">
|
||||
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-pencil" aria-label="Edit"></i></a>
|
||||
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-x" aria-label="Remove"></i></a>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="panel-body issue-content markdown-body" id="commentContent-@comment.commentId">
|
||||
@helpers.markdown(
|
||||
markdown = comment.content,
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = isManageable
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if(issue.isDefined){
|
||||
<div class="panel panel-default issue-comment-box">
|
||||
<div class="panel-heading">
|
||||
@@ -30,142 +67,170 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@issueOrPullRequest()={ @if(issue.isDefined && issue.get.isPullRequest)( "pull request" )else( "issue" ) }
|
||||
|
||||
@comments.map {
|
||||
case comment: gitbucket.core.model.IssueComment => {
|
||||
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"
|
||||
&& comment.action != "commit" && comment.action != "refer"){
|
||||
<div class="panel panel-default issue-comment-box" id="comment-@comment.commentId">
|
||||
<div class="panel-heading">
|
||||
@helpers.avatar(comment.commentedUserName, 20)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
<span class="muted">
|
||||
@if(comment.action == "comment"){
|
||||
commented
|
||||
} else {
|
||||
referenced the @issueOrPullRequest()
|
||||
}
|
||||
<a href="@helpers.url(repository)/issues/@issue.get.issueId#comment-@comment.commentId">
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</a>
|
||||
</span>
|
||||
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer"
|
||||
&& (isManageable || context.loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
|
||||
<span class="pull-right">
|
||||
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-pencil" aria-label="Edit"></i></a>
|
||||
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-x" aria-label="Remove"></i></a>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="panel-body issue-content markdown-body" id="commentContent-@comment.commentId">
|
||||
@helpers.markdown(
|
||||
markdown = comment.content,
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = isManageable
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if(comment.action == "commit"){
|
||||
@defining({
|
||||
val (content, id) = " ([a-f0-9]{40})$".r.findFirstMatchIn(comment.content)
|
||||
@comment.action match {
|
||||
case "commit" => {
|
||||
@defining({
|
||||
val (content, id) = " ([a-f0-9]{40})$".r.findFirstMatchIn(comment.content)
|
||||
.map(m => (m.before.toString -> Some(m.group(1))))
|
||||
.getOrElse(comment.content -> None)
|
||||
val head = content.take(100).takeWhile(_ != '\n')
|
||||
(id, head, if(head == content){ None }else{ Some(content.drop(head.length).dropWhile(_ == '\n')) }.filter(_.nonEmpty))
|
||||
}){ case (commitId, head, rest) =>
|
||||
<div class="discussion-item discussion-item-commit">
|
||||
val head = content.take(100).takeWhile(_ != '\n')
|
||||
(id, head, if(head == content){ None }else{ Some(content.drop(head.length).dropWhile(_ == '\n')) }.filter(_.nonEmpty))
|
||||
}){ case (commitId, head, rest) =>
|
||||
<div class="discussion-item discussion-item-commit">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-bookmark"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
added a commit that referenced this @issueOrPullRequest()
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
<div style="discussion-item-content">
|
||||
@commitId.map{ id =>
|
||||
<span class="pull-right"><a href="@context.path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></span>
|
||||
}
|
||||
<span class="discussion-item-content-head"><i class="octicon octicon-git-commit"></i></span>
|
||||
@helpers.link(head, repository)
|
||||
@rest.map{ content =>
|
||||
<a href="javascript:void(0)" class="omit" onclick="$(this.parentNode).find('.discussion-item-content-text').toggle()">...</a>
|
||||
<pre class="reset discussion-item-content-text" style="display:none">@helpers.link(content, repository)</pre>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
case "refer" => {
|
||||
<div class="discussion-item discussion-item-refer">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-bookmark"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
added a commit that referenced this @issueOrPullRequest()
|
||||
referenced the @issueOrPullRequest()
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
<div style="discussion-item-content">
|
||||
@commitId.map{ id =>
|
||||
<span class="pull-right"><a href="@context.path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></span>
|
||||
}
|
||||
<span class="discussion-item-content-head"><i class="octicon octicon-git-commit"></i></span>
|
||||
@helpers.link(head, repository)
|
||||
@rest.map{ content =>
|
||||
<a href="javascript:void(0)" class="omit" onclick="$(this.parentNode).find('.discussion-item-content-text').toggle()">...</a>
|
||||
<pre class="reset discussion-item-content-text" style="display:none">@helpers.link(content, repository)</pre>
|
||||
@defining(comment.content.split(":")){ case Array(issueId, rest @ _*) =>
|
||||
<strong>@helpers.issueLink(repository, issueId.toInt): @rest.mkString(":")</strong>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if(comment.action == "refer"){
|
||||
<div class="discussion-item discussion-item-refer">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-bookmark"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
referenced the @issueOrPullRequest()
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
case "merge" => {
|
||||
@showFormatedComment(comment)
|
||||
<div class="discussion-item discussion-item-merge">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-git-merge"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
merged commit
|
||||
<code>@pullreq.map(_.commitIdTo.substring(0, 7))</code> into
|
||||
@if(pullreq.get.requestUserName == repository.owner){
|
||||
<code>@pullreq.map(_.branch)</code> from <code>@pullreq.map(_.requestBranch)</code>
|
||||
} else {
|
||||
<code>@pullreq.map(_.userName):@pullreq.map(_.branch)</code> from <code>@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</code>
|
||||
}
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
<div style="discussion-item-content">
|
||||
@defining(comment.content.split(":")){ case Array(issueId, rest @ _*) =>
|
||||
<strong>@helpers.issueLink(repository, issueId.toInt): @rest.mkString(":")</strong>
|
||||
}
|
||||
}
|
||||
case "close" | "close_comment" => {
|
||||
@if(comment.action == "close_comment"){
|
||||
@showFormatedComment(comment)
|
||||
}
|
||||
<div class="discussion-item discussion-item-close">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-circle-slash"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
closed this @issueOrPullRequest()
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if(comment.action == "merge"){
|
||||
<div class="discussion-item discussion-item-merge">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-git-merge"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
merged commit
|
||||
<code>@pullreq.map(_.commitIdTo.substring(0, 7))</code> into
|
||||
@if(pullreq.get.requestUserName == repository.owner){
|
||||
<code>@pullreq.map(_.branch)</code> from <code>@pullreq.map(_.requestBranch)</code>
|
||||
} else {
|
||||
<code>@pullreq.map(_.userName):@pullreq.map(_.branch)</code> from <code>@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</code>
|
||||
}
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
}
|
||||
case "reopen" | "reopen_comment" => {
|
||||
@if(comment.action == "reopen_comment"){
|
||||
@showFormatedComment(comment)
|
||||
}
|
||||
<div class="discussion-item discussion-item-reopen">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-primitive-dot"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
reopened the @issueOrPullRequest()
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if(comment.action == "close" || comment.action == "close_comment"){
|
||||
<div class="discussion-item discussion-item-close">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-circle-slash"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
closed this @issueOrPullRequest()
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
}
|
||||
case "delete_branch" => {
|
||||
<div class="discussion-item discussion-item-delete_branch">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-git-branch"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
deleted the <code>@pullreq.map(_.requestBranch)</code> branch
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if(comment.action == "reopen" || comment.action == "reopen_comment"){
|
||||
<div class="discussion-item discussion-item-reopen">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-primitive-dot"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
reopened the @issueOrPullRequest()
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
}
|
||||
case "add_label" => {
|
||||
<div class="discussion-item discussion-item-add-label">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-tag"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
add the <code>@comment.content</code> label
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if(comment.action == "delete_branch"){
|
||||
<div class="discussion-item discussion-item-delete_branch">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-git-branch"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
deleted the <code>@pullreq.map(_.requestBranch)</code> branch
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
}
|
||||
case "delete_label" => {
|
||||
<div class="discussion-item discussion-item-delete-label">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-tag"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
removed the <code>@comment.content</code> label
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
case "change_priority" => {
|
||||
<div class="discussion-item discussion-item-change-priority">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-flame"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
change priority from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
case "change_milestone" => {
|
||||
<div class="discussion-item discussion-item-change-milestone">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-milestone"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
change milestone from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
case "assign" => {
|
||||
<div class="discussion-item discussion-item-assign">
|
||||
<div class="discussion-item-header">
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-person"></i></span>
|
||||
@helpers.avatar(comment.commentedUserName, 16)
|
||||
@helpers.user(comment.commentedUserName, styleClass="username strong")
|
||||
change assignee from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
|
||||
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
case _ => {
|
||||
@showFormatedComment(comment)
|
||||
}
|
||||
}
|
||||
}
|
||||
case comment: CommitComment => {
|
||||
@@ -186,11 +251,7 @@ $(function(){
|
||||
$content = $('#issueContent');
|
||||
}
|
||||
|
||||
$.get(url,
|
||||
{
|
||||
dataType : 'html'
|
||||
},
|
||||
function(data){
|
||||
$.get(url, { dataType : 'html' }, function(data){
|
||||
$content.empty().html(data);
|
||||
});
|
||||
return false;
|
||||
@@ -198,8 +259,7 @@ $(function(){
|
||||
$('.issue-comment-box i.octicon-x').click(function(){
|
||||
if(confirm('Are you sure you want to delete this?')) {
|
||||
var id = $(this).closest('a').data('comment-id');
|
||||
$.post('@helpers.url(repository)/issue_comments/delete/' + id,
|
||||
function(data){
|
||||
$.post('@helpers.url(repository)/issue_comments/delete/' + id, function(data){
|
||||
if(data > 0) {
|
||||
$('#comment-' + id).remove();
|
||||
}
|
||||
@@ -213,22 +273,24 @@ $(function(){
|
||||
var url = '@helpers.url(repository)/commit_comments/_data/' + id;
|
||||
var $content = $('.commit-commentContent-' + id, $(this).closest('.commit-comment-box'));
|
||||
|
||||
$.get(url,
|
||||
{
|
||||
dataType : 'html'
|
||||
},
|
||||
function(data){
|
||||
$.get(url, { dataType : 'html' }, function(data){
|
||||
$content.empty().html(data);
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$(document).on('click', '.commit-comment-box i.octicon-x', function(){
|
||||
if(confirm('Are you sure you want to delete this?')) {
|
||||
var id = $(this).closest('a').data('comment-id');
|
||||
$.post('@helpers.url(repository)/commit_comments/delete/' + id,
|
||||
function(data){
|
||||
if(data > 0) {
|
||||
$('.commit-comment-' + id).closest('.not-diff').remove();
|
||||
var comment = $('.commit-comment-' + id).closest('.not-diff');
|
||||
if(comment.prev('.not-diff').length == 0){
|
||||
comment.next('.not-diff').find('.reply-comment').remove();
|
||||
}
|
||||
comment.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -260,10 +322,7 @@ $(function(){
|
||||
var $commentContent = $(ev.target).parents('div[class*=commit-commentContent-]'),
|
||||
commentId = $commentContent.attr('class').match(/commit-commentContent-.+/)[0].replace(/commit-commentContent-/, ''),
|
||||
checkboxes = $commentContent.find(':checkbox');
|
||||
$.get('@helpers.url(repository)/commit_comments/_data/' + commentId,
|
||||
{
|
||||
dataType : 'html'
|
||||
},
|
||||
$.get('@helpers.url(repository)/commit_comments/_data/' + commentId, { dataType : 'html' },
|
||||
function(responseContent){
|
||||
$.ajax({
|
||||
url: '@helpers.url(repository)/commit_comments/edit/' + commentId,
|
||||
@@ -283,10 +342,7 @@ $(function(){
|
||||
@if(issue.isDefined){
|
||||
$('#issueContent').on('click', ':checkbox', function(ev){
|
||||
var checkboxes = $('#issueContent :checkbox');
|
||||
$.get('@helpers.url(repository)/issues/_data/@issue.get.issueId',
|
||||
{
|
||||
dataType : 'html'
|
||||
},
|
||||
$.get('@helpers.url(repository)/issues/_data/@issue.get.issueId', { dataType : 'html' },
|
||||
function(responseContent){
|
||||
$.ajax({
|
||||
url: '@helpers.url(repository)/issues/edit/@issue.get.issueId',
|
||||
@@ -304,10 +360,7 @@ $(function(){
|
||||
var $commentContent = $(ev.target).parents('div[id^=commentContent-]'),
|
||||
commentId = $commentContent.attr('id').replace(/commentContent-/, ''),
|
||||
checkboxes = $commentContent.find(':checkbox');
|
||||
$.get('@helpers.url(repository)/issue_comments/_data/' + commentId,
|
||||
{
|
||||
dataType : 'html'
|
||||
},
|
||||
$.get('@helpers.url(repository)/issue_comments/_data/' + commentId, { dataType : 'html' },
|
||||
function(responseContent){
|
||||
$.ajax({
|
||||
url: '@helpers.url(repository)/issue_comments/edit/' + commentId,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@gitbucket.core.html.menu("issues", repository){
|
||||
<form action="@helpers.url(repository)/issues/new" method="POST" validate="true" class="form-group">
|
||||
<form action="@helpers.url(repository)/issues/new" method="POST" validate="true" class="form-group" autocomplete="off">
|
||||
<div class="row-fluid">
|
||||
<div class="col-md-9">
|
||||
<span id="error-title" class="error"></span>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@import gitbucket.core.view.helpers
|
||||
@defining(label.map(_.labelId).getOrElse("new")){ labelId =>
|
||||
<div id="edit-label-area-@labelId">
|
||||
<form class="form-inline">
|
||||
<form class="form-inline" autocomplete="off">
|
||||
<input type="text" id="labelName-@labelId" style="width: 300px; float: left; margin-right: 4px;" class="form-control input-sm" value="@label.map(_.labelName)"@if(labelId == "new"){ placeholder="New label name"}/>
|
||||
<div id="label-color-@labelId" class="input-group color bscp" data-color="#@label.map(_.color).getOrElse("888888")" data-color-format="hex" style="width: 100px; float: left;">
|
||||
<input type="text" class="form-control input-sm" id="labelColor-@labelId" value="#@label.map(_.color).getOrElse("888888")" style="width: 100px;">
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<a href="@condition.copy(state = "closed").toURL">Closed <span class="badge">@closedCount</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
<form method="GET" action="@helpers.url(repository)/search" id="search-filter-form" class="form-inline pull-right">
|
||||
<form method="GET" action="@helpers.url(repository)/search" id="search-filter-form" class="form-inline pull-right" autocomplete="off">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="q" placeholder="Search..."/>
|
||||
<input type="hidden" name="type" value="issue"/>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="muted">Create a new milestone to help organize your issues and pull requests.</div>
|
||||
}
|
||||
<hr style="margin-top: 12px; margin-bottom: 18px;" class="fill-width"/>
|
||||
<form method="POST" action="@helpers.url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true">
|
||||
<form method="POST" action="@helpers.url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true" autocomplete="off">
|
||||
<fieldset class="form-group">
|
||||
<input type="text" id="title" name="title" class="form-control" style="width: 500px;" value="@milestone.map(_.title)" placeholder="Title"/>
|
||||
<span id="error-title" class="error"></span>
|
||||
@@ -29,11 +29,9 @@
|
||||
<input type="submit" class="btn btn-success" value="Create milestone"/>
|
||||
} else {
|
||||
@if(milestone.get.closedDate.isDefined){
|
||||
<input type="button" class="btn btn-default" value="Open" id="open"
|
||||
onclick="location.href='@helpers.url(repository)/issues/milestones/@milestone.get.milestoneId/close';"/>
|
||||
<input type="button" class="btn btn-default" value="Open" id="open" onclick="location.href='@helpers.url(repository)/issues/milestones/@milestone.get.milestoneId/close';"/>
|
||||
} else {
|
||||
<input type="button" class="btn btn-default" value="Close" id="close"
|
||||
onclick="location.href='@helpers.url(repository)/issues/milestones/@milestone.get.milestoneId/open';"/>
|
||||
<input type="button" class="btn btn-default" value="Close" id="close" onclick="location.href='@helpers.url(repository)/issues/milestones/@milestone.get.milestoneId/open';"/>
|
||||
}
|
||||
<input type="submit" class="btn btn-success" value="Update milestone"/>
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@import gitbucket.core.view.helpers
|
||||
@defining(priority.map(_.priorityId).getOrElse("new")){ priorityId =>
|
||||
<div id="edit-priority-area-@priorityId">
|
||||
<form class="form-inline">
|
||||
<form class="form-inline" autocomplete="off">
|
||||
<input type="text" id="priorityName-@priorityId" style="width: 200px; float: left; margin-right: 4px;" class="form-control input-sm" value="@priority.map(_.priorityName)"@if(priorityId == "new"){ placeholder="New priority name"}/>
|
||||
<div id="priority-color-@priorityId" class="input-group color bscp" data-color="#@priority.map(_.color).getOrElse("888888")" data-color-format="hex" style="width: 100px; float: left;">
|
||||
<input type="text" class="form-control input-sm" id="priorityColor-@priorityId" value="#@priority.map(_.color).getOrElse("888888")" style="width: 100px;">
|
||||
|
||||
@@ -121,9 +121,11 @@
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
@*
|
||||
$('#search').submit(function(){
|
||||
return $.trim($(this).find('input[name=query]').val()) != '';
|
||||
});
|
||||
*@
|
||||
@if(body.toString.contains("main-sidebar")){
|
||||
$(".sidebar-toggle").on('click', function(e){
|
||||
$.post('@context.path/sidebar-collapse', { collapse: !$('body').hasClass('sidebar-collapse') });
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
@menuitem("", "files", "Files", "code")
|
||||
@if(repository.branchList.nonEmpty) {
|
||||
@menuitem("/branches", "branches", "Branches", "git-branch", repository.branchList.length)
|
||||
@menuitem("/tags", "tags", "Tags", "tag", repository.tags.length)
|
||||
}
|
||||
@menuitem("/releases", "releases", "Releases", "tag", repository.tags.length)
|
||||
@if(repository.repository.options.issuesOption != "DISABLE") {
|
||||
@menuitem("/issues", "issues", "Issues", "issue-opened", repository.issueCount)
|
||||
@menuitem("/pulls", "pulls", "Pull requests", "git-pull-request", repository.pullCount)
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
Only those with write access to this repository can merge pull requests.
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<hr>
|
||||
@status.conflictMessage.map { message => @helpers.markdown(message, originRepository, false, true, false) }
|
||||
</div>
|
||||
} else {
|
||||
@if(status.branchIsOutOfDate){
|
||||
@if(status.hasUpdatePermission){
|
||||
@@ -139,8 +143,48 @@
|
||||
<span id="error-message" class="error"></span>
|
||||
<textarea name="message" style="height: 80px; margin-top: 8px; margin-bottom: 8px;" class="form-control">@issue.title</textarea>
|
||||
<div>
|
||||
<input type="button" class="btn btn-default" value="Cancel" id="cancel-merge-pull-request"/>
|
||||
<input type="submit" class="btn btn-success" value="Confirm merge"/>
|
||||
<div class="btn-group">
|
||||
<button id="merge-strategy-btn" class="dropdown-toggle btn btn-default" data-toggle="dropdown">
|
||||
<span class="strong">
|
||||
@(Map(
|
||||
"merge-commit" -> "Merge commit",
|
||||
"squash" -> "Squash",
|
||||
"rebase" -> "Rebase"
|
||||
)(originRepository.repository.options.defaultMergeOption))
|
||||
</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
@defining(originRepository.repository.options.mergeOptions.split(",")){ mergeOptions =>
|
||||
@if(mergeOptions.contains("merge-commit")){
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="merge-strategy" data-value="merge-commit">
|
||||
<strong>Merge commit</strong><br>These commits will be added to the base branch via a merge commit.
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
@if(mergeOptions.contains("squash")){
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="merge-strategy" data-value="squash">
|
||||
<strong>Squash</strong><br>These commits will be combined into one commit in the base branch.
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
@if(mergeOptions.contains("rebase")){
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="merge-strategy" data-value="rebase">
|
||||
<strong>Rebase</strong><br>These commits will be rebased and added to the base branch.
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="button" class="btn btn-default" value="Cancel" id="cancel-merge-pull-request"/>
|
||||
<input type="submit" class="btn btn-success" value="Confirm merge"/>
|
||||
<input type="hidden" name="strategy" value="@originRepository.repository.options.defaultMergeOption"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -194,5 +238,10 @@ $(function(){
|
||||
$('#merge-command-copy-1').attr('data-clipboard-text', $('#merge-command').text());
|
||||
});
|
||||
}
|
||||
|
||||
$('.merge-strategy').click(function(){
|
||||
$('button#merge-strategy-btn > span.strong').text($(this).find('strong').text());
|
||||
$('input[name=strategy]').val($(this).data('value'));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
13
src/main/twirl/gitbucket/core/pulls/proposals.scala.html
Normal file
13
src/main/twirl/gitbucket/core/pulls/proposals.scala.html
Normal file
@@ -0,0 +1,13 @@
|
||||
@(branches: Seq[String],
|
||||
parent: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@if(branches.nonEmpty){
|
||||
@branches.map { branch =>
|
||||
<div class="box-content" style="line-height: 20pt; margin-bottom: 6px; padding: 10px 6px 10px 10px; background-color: #fff9ea">
|
||||
<strong><i class="menu-icon octicon octicon-git-branch"></i><span class="muted">@branch</span></strong>
|
||||
<a class="pull-right btn btn-success" style="position: relative; top: -4px;"
|
||||
href="@helpers.url(repository)/compare/@{parent.owner}:@{parent.repository.defaultBranch}...@{repository.owner}:@{branch}">Compare & pull request</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
78
src/main/twirl/gitbucket/core/releases/form.scala.html
Normal file
78
src/main/twirl/gitbucket/core/releases/form.scala.html
Normal file
@@ -0,0 +1,78 @@
|
||||
@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
tag: String,
|
||||
release: Option[(gitbucket.core.model.Release, Seq[gitbucket.core.model.ReleaseAsset])])(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main(s"New Release - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@gitbucket.core.html.menu("releases", repository){
|
||||
<form method="POST" validate="true" class="form-group" autocomplete="off">
|
||||
<div class="row-fluid">
|
||||
<div class="co`l-md-12">
|
||||
@if(release.isEmpty){
|
||||
<h3>New release for @tag</h3>
|
||||
} else {
|
||||
<h3>Update release for @tag</h3>
|
||||
}
|
||||
<span id="error-name" class="error"></span>
|
||||
<input type="text" id="release-name" name="name" class="form-control" value="@release.map { case (release, _) => @release.name }.getOrElse(tag)" placeholder="Title" style="margin-bottom: 6px;" autofocus/>
|
||||
@gitbucket.core.helper.html.preview(
|
||||
repository = repository,
|
||||
content = release.flatMap { case (release, _) => release.content }.getOrElse(""),
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = true,
|
||||
completionContext = "releases",
|
||||
style = "height: 200px; max-height: 500px;",
|
||||
elastic = true,
|
||||
placeholder = "Describe this release"
|
||||
)
|
||||
<ul id="assets-list" class="collaborator">
|
||||
@release.map { case (release, assets) =>
|
||||
@assets.map { asset =>
|
||||
<li>
|
||||
<a href="@context.baseUrl/@repository.owner/@repository.name/_release/@helpers.encodeRefName(tag)/@asset.fileName"><i class="octicon octicon-file"></i>@asset.label</a>
|
||||
<a href="#" class="remove pull-right" style="padding-top: 0px;">(remove)</a>
|
||||
<input type="hidden" name="file:@asset.fileName" value="@asset.label"/>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
<div style="border: 1px dashed #ccc; color: gray; background-color: #eee; padding: 4px;">
|
||||
<div id="drop" class="clickable">Attach release files by dragging & dropping, or selecting them.</div>
|
||||
</div>
|
||||
<div class="align-right" style="margin-top: 12px;">
|
||||
@if(release.isEmpty){
|
||||
<input type="submit" class="btn btn-success" value="Submit new release" formaction="@helpers.url(repository)/releases/@helpers.encodeRefName(tag)/create"/>
|
||||
} else {
|
||||
<input type="submit" class="btn btn-success" value="Update release" formaction="@helpers.url(repository)/releases/@helpers.encodeRefName(tag)/edit"/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$(document).on('click', '.remove', function(){
|
||||
$(this).parent().remove();
|
||||
});
|
||||
|
||||
$("#drop").dropzone({
|
||||
maxFilesize: @{gitbucket.core.util.FileUtil.MaxFileSize / 1024 / 1024},
|
||||
url: '@context.path/upload/release/@repository.owner/@repository.name/@helpers.encodeRefName(tag)',
|
||||
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
|
||||
success: function(file, id) {
|
||||
var attach =
|
||||
'<li><a href="@context.baseUrl/@repository.owner/@repository.name/_release/@helpers.encodeRefName(tag)/' + id + '">' +
|
||||
'<i class="octicon octicon-file"></i>' + escapeHtml(file.name) + '</a>' +
|
||||
'<a href="#" class="remove pull-right" style="padding-top: 0px;">(remove)</a>' +
|
||||
'<input type="hidden" name="file:' + id + '" value="' + escapeHtml(file.name) + '"/>'
|
||||
'</li>';
|
||||
$('#assets-list').append(attach);
|
||||
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
64
src/main/twirl/gitbucket/core/releases/list.scala.html
Normal file
64
src/main/twirl/gitbucket/core/releases/list.scala.html
Normal file
@@ -0,0 +1,64 @@
|
||||
@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
releases: Seq[(gitbucket.core.util.JGitUtil.TagInfo, Option[(gitbucket.core.model.Release, Seq[gitbucket.core.model.ReleaseAsset])])],
|
||||
hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main("Releases" + s" - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@gitbucket.core.html.menu("releases", repository){
|
||||
<table class="table table-bordered table-releases">
|
||||
<thead>
|
||||
<tr><th>@releases.length releases</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@releases.map { case (tag, release) =>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="col-md-2 text-right">
|
||||
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName(tag.name)" class="strong"><i class="octicon octicon-tag"></i>@tag.name</a><br>
|
||||
<a href="@helpers.url(repository)/commit/@tag.id" class="monospace muted"><i class="octicon octicon-git-commit"></i>@tag.id.substring(0, 7)</a><br>
|
||||
<span class="muted">@gitbucket.core.helper.html.datetimeago(tag.time)</span>
|
||||
</div>
|
||||
<div class="col-md-10" style="border-left: 1px solid #eee">
|
||||
<div class="release-note markdown-body">
|
||||
@release.map { case (release, assets) =>
|
||||
<h3><a href="@helpers.url(repository)/releases/@release.tag">@release.name</a></h3>
|
||||
<p class="muted">
|
||||
@helpers.avatar(release.author, 20) @helpers.user(release.author, styleClass="username") released this @gitbucket.core.helper.html.datetimeago(release.registeredDate)
|
||||
</p>
|
||||
@helpers.markdown(
|
||||
markdown = release.content getOrElse "No description provided.",
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = hasWritePermission
|
||||
)
|
||||
}.getOrElse {
|
||||
@if(hasWritePermission){
|
||||
<div class="pull-right">
|
||||
<a class="btn btn-success" href="@helpers.url(repository)/releases/@{helpers.encodeRefName(tag.name)}/create" id="edit">Create release</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<h4>Downloads</h4>
|
||||
<ul style="list-style: none; padding-left: 8px;">
|
||||
@release.map { case (release, assets) =>
|
||||
@assets.map { asset =>
|
||||
<li>
|
||||
<i class="octicon octicon-file"></i><a href="@helpers.url(repository)/releases/@release.tag/assets/@asset.fileName">@asset.label</a>
|
||||
<span class="label label-default">@helpers.readableSize(Some(asset.size))</span>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
<li><a href="@helpers.url(repository)/archive/@{helpers.encodeRefName(tag.name)}.zip"><i class="octicon octicon-file-zip"></i>Source code (zip)</a></li>
|
||||
<li><a href="@helpers.url(repository)/archive/@{helpers.encodeRefName(tag.name)}.tar.gz"><i class="octicon octicon-file-zip"></i>Source code (tar.gz)</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
}
|
||||
65
src/main/twirl/gitbucket/core/releases/release.scala.html
Normal file
65
src/main/twirl/gitbucket/core/releases/release.scala.html
Normal file
@@ -0,0 +1,65 @@
|
||||
@(release: gitbucket.core.model.Release,
|
||||
assets: Seq[gitbucket.core.model.ReleaseAsset],
|
||||
hasWritePermission: Boolean,
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main(s"Release ${release.name} - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@gitbucket.core.html.menu("releases", repository){
|
||||
<div class="row">
|
||||
<div class="col-md-2 text-right">
|
||||
@defining(repository.tags.find(_.name == release.tag)){ tag =>
|
||||
@tag.map { tag =>
|
||||
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName(tag.name)" class="strong"><i class="octicon octicon-tag"></i>@tag.name</a><br>
|
||||
<a href="@helpers.url(repository)/commit/@tag.id" class="monospace muted"><i class="octicon octicon-git-commit"></i>@tag.id.substring(0, 7)</a><br>
|
||||
<span class="muted">@gitbucket.core.helper.html.datetimeago(tag.time)</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-10" style="border-left: 1px solid #eee">
|
||||
<div class="markdown-body">
|
||||
<h3>
|
||||
@release.name
|
||||
@if(hasWritePermission){
|
||||
<div class="pull-right">
|
||||
<form method="POST" action="@helpers.url(repository)/releases/@release.tag/delete" id="delete-form">
|
||||
<a class="btn btn-default" href="@helpers.url(repository)/releases/@release.tag/edit" id="edit">Edit</a>
|
||||
<input type="submit" class="btn btn-danger" value="Delete">
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</h3>
|
||||
<p class="muted">
|
||||
@helpers.avatar(release.author, 20) @helpers.user(release.author, styleClass="username") released this @gitbucket.core.helper.html.datetimeago(release.registeredDate)
|
||||
</p>
|
||||
@helpers.markdown(
|
||||
markdown = release.content getOrElse "No description provided.",
|
||||
repository = repository,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = hasWritePermission
|
||||
)
|
||||
<h4>Downloads</h4>
|
||||
<ul style="list-style: none; padding-left: 8px;" id="attachedFiles">
|
||||
@assets.map{ asset =>
|
||||
<li>
|
||||
<i class="octicon octicon-file"></i><a href="@helpers.url(repository)/releases/@release.tag/assets/@asset.fileName">@asset.label</a>
|
||||
<span class="label label-default">@helpers.readableSize(Some(asset.size))</span>
|
||||
</li>
|
||||
}
|
||||
<li><a href="@helpers.url(repository)/archive/@{helpers.encodeRefName(release.tag)}.zip"><i class="octicon octicon-file-zip"></i>Source code (zip)</a></li>
|
||||
<li><a href="@helpers.url(repository)/archive/@{helpers.encodeRefName(release.tag)}.tar.gz"><i class="octicon octicon-file-zip"></i>Source code (tar.gz)</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#delete-form').submit(function(){
|
||||
return confirm('Are you sure you want to delete this release?');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user