mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-09 12:15:35 +02:00
Compare commits
188 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a79f105eea | ||
|
|
b916595da3 | ||
|
|
90b63090cc | ||
|
|
345685ed7c | ||
|
|
1d9fe5770e | ||
|
|
ec307b84d3 | ||
|
|
5d7346db91 | ||
|
|
443498433d | ||
|
|
a58ce07736 | ||
|
|
1903c3990c | ||
|
|
90441c8eec | ||
|
|
601919bcc6 | ||
|
|
6903b096f5 | ||
|
|
e44fed09fa | ||
|
|
8ed0c8a170 | ||
|
|
31118ac285 | ||
|
|
c851b7582f | ||
|
|
102a02d527 | ||
|
|
1968bf871a | ||
|
|
5cd4e48173 | ||
|
|
111d212cb5 | ||
|
|
d648d34393 | ||
|
|
bbaa5b38e7 | ||
|
|
3c50a78be2 | ||
|
|
82cc1fa530 | ||
|
|
8c0581973e | ||
|
|
bfa15f5d75 | ||
|
|
57e87b581d | ||
|
|
5aa6f5bce3 | ||
|
|
9ba098a805 | ||
|
|
b2d8567c26 | ||
|
|
19d97c93ce | ||
|
|
cad2daa2f9 | ||
|
|
585f0b5769 | ||
|
|
dd23d1109b | ||
|
|
5e84221d39 | ||
|
|
faf3e6c26b | ||
|
|
969da2c63b | ||
|
|
ba61891510 | ||
|
|
a581871a89 | ||
|
|
d96e1fa503 | ||
|
|
b66812d76c | ||
|
|
ae32016856 | ||
|
|
56aec15e68 | ||
|
|
d0c8e33ec5 | ||
|
|
7ab260e688 | ||
|
|
0d2c923664 | ||
|
|
e7a47fe3a4 | ||
|
|
e454f78c5a | ||
|
|
192e4ade3e | ||
|
|
733797cb6f | ||
|
|
f0e4157a46 | ||
|
|
2ad6948bb4 | ||
|
|
dd46d649a6 | ||
|
|
bbe8a9b9e4 | ||
|
|
376b109602 | ||
|
|
1d085d52bb | ||
|
|
d1f42e0ed7 | ||
|
|
101f8598ed | ||
|
|
da62f6f8fb | ||
|
|
5750286b5d | ||
|
|
f2ca4fb64b | ||
|
|
5f6b577cbf | ||
|
|
62004c279c | ||
|
|
838c7fb991 | ||
|
|
93786f0fd6 | ||
|
|
f3514e5625 | ||
|
|
a2b0ee0c24 | ||
|
|
2bcab30529 | ||
|
|
91cda6d245 | ||
|
|
a82e579d57 | ||
|
|
94421c7a63 | ||
|
|
ea7c8e62de | ||
|
|
b84421723b | ||
|
|
9a42b93d1f | ||
|
|
e162cd956a | ||
|
|
6431d25409 | ||
|
|
70e2079c7f | ||
|
|
e8737d263a | ||
|
|
4c4b08f1b8 | ||
|
|
c7e1edf262 | ||
|
|
876bb396fd | ||
|
|
a6788f858f | ||
|
|
b263764730 | ||
|
|
1b1bd371a4 | ||
|
|
f194a08cfe | ||
|
|
1211bfc7be | ||
|
|
eab7011e0f | ||
|
|
6f30ffa865 | ||
|
|
de3026248c | ||
|
|
413e75be5a | ||
|
|
6a8ec18f9a | ||
|
|
5b1b2ef3d7 | ||
|
|
9a705c62bf | ||
|
|
b103180bf6 | ||
|
|
536a0d3fe2 | ||
|
|
356202e28a | ||
|
|
6db36e12b5 | ||
|
|
bfcd5a2855 | ||
|
|
e218b52b78 | ||
|
|
46998dc1fa | ||
|
|
977f856854 | ||
|
|
da2a7bf77d | ||
|
|
3da3a048f0 | ||
|
|
7b5b453e56 | ||
|
|
c18f95edf8 | ||
|
|
71cf043f56 | ||
|
|
a31e4b5897 | ||
|
|
1679da4abe | ||
|
|
505bc71f9a | ||
|
|
4bc057c653 | ||
|
|
8eee13d7aa | ||
|
|
8981e339b4 | ||
|
|
e1dd5dd057 | ||
|
|
cb64f8eab8 | ||
|
|
c47d50d0df | ||
|
|
1f46da2273 | ||
|
|
06fc26cd06 | ||
|
|
3a4f9b9027 | ||
|
|
f98c849c7c | ||
|
|
aa0bd5b34a | ||
|
|
b52e904ed1 | ||
|
|
70e0dcf99d | ||
|
|
56bb20dfd2 | ||
|
|
7d7d2f488d | ||
|
|
72affd67b9 | ||
|
|
0cf1f43deb | ||
|
|
8494c682a7 | ||
|
|
1af5611159 | ||
|
|
4d39f63ef7 | ||
|
|
120d1c2fff | ||
|
|
62e9c0358a | ||
|
|
5a90848c75 | ||
|
|
760d443f74 | ||
|
|
5ee0e75dfe | ||
|
|
3b4d2d6f91 | ||
|
|
dfaabeb41d | ||
|
|
ff3205b6c7 | ||
|
|
0fae2dac35 | ||
|
|
4db4fe28b4 | ||
|
|
5b87efa032 | ||
|
|
3ad609bad7 | ||
|
|
8145cba111 | ||
|
|
24b9a9a12c | ||
|
|
ee7220ebd2 | ||
|
|
8fb72fd55e | ||
|
|
a1eded2d9a | ||
|
|
7f184e1126 | ||
|
|
09aafbcce1 | ||
|
|
7f5024a746 | ||
|
|
8fec0870a8 | ||
|
|
a8d2afaff7 | ||
|
|
8fd92f1c2f | ||
|
|
419ea16ead | ||
|
|
e72d808a3c | ||
|
|
44e8c0a9be | ||
|
|
e2c39d7815 | ||
|
|
687cd54f9a | ||
|
|
911754e1dc | ||
|
|
0067cbce6f | ||
|
|
f40f8427aa | ||
|
|
98ceff2391 | ||
|
|
642a51a208 | ||
|
|
9ec7c321d8 | ||
|
|
a3c419b6f5 | ||
|
|
15c28cffa4 | ||
|
|
f4d0f16481 | ||
|
|
45535e4fdf | ||
|
|
64635c5dc6 | ||
|
|
2fd95c7f1a | ||
|
|
eb6da85183 | ||
|
|
bcc05f021c | ||
|
|
d58ed55c3a | ||
|
|
057f029c80 | ||
|
|
c9a12ff913 | ||
|
|
66bf00b5d3 | ||
|
|
7ba3ca6f15 | ||
|
|
a1bacccc09 | ||
|
|
333eeb4bad | ||
|
|
3f2935612d | ||
|
|
4c87bdd959 | ||
|
|
3543073150 | ||
|
|
e50fe604c2 | ||
|
|
63369258bd | ||
|
|
e7c3376303 | ||
|
|
86163f66ce | ||
|
|
518f0bfc28 | ||
|
|
0a759f6127 |
7
.github/CONTRIBUTING.md
vendored
Normal file
7
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Guideline for Issues
|
||||||
|
|
||||||
|
- At first, See [FAQ](https://github.com/gitbucket/gitbucket/wiki/FAQ) and check issues whether there is a same question or request in the past.
|
||||||
|
- If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
|
||||||
|
- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
|
||||||
|
- Write an issue in English. At least, write subject in English.
|
||||||
|
- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it.
|
||||||
19
.github/ISSUE_TEMPLATE.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
### Before submitting an issue to Gitbucket I have first:
|
||||||
|
|
||||||
|
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
|
||||||
|
- [] searched for similar already existing issue
|
||||||
|
- [] read the documentation and [wiki](https://github.com/gitbucket/gitbucket/wiki)
|
||||||
|
|
||||||
|
*(if you have performed all the above, remove the paragraph and continue describing the issue with template below)*
|
||||||
|
|
||||||
|
## Issue
|
||||||
|
**Impacted version**: xxxx
|
||||||
|
|
||||||
|
**Deployment mode**: *explain here how you use gitbucket : standalone app, under webcontainer (which one), with an http frontend (nginx, httpd, ...)*
|
||||||
|
|
||||||
|
**Problem description**:
|
||||||
|
- *be as explicit has you can*
|
||||||
|
- *describe the problem and its symptoms*
|
||||||
|
- *explain how to reproduce*
|
||||||
|
- *attach whatever information that can help understanding the context (screen capture, log files)*
|
||||||
|
- *do your best to use a correct english (re-read yourself)*
|
||||||
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
### Before submitting a pull-request to Gitbucket I have first:
|
||||||
|
|
||||||
|
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
|
||||||
|
- [] rebased my branch over master
|
||||||
|
- [] verified that project is compiling
|
||||||
|
- [] verified that tests are passing
|
||||||
|
- [] squashed my commits as appropriate *(keep several commits if it is relevant to understand the PR)*
|
||||||
|
- [] [marked as closed](https://help.github.com/articles/closing-issues-via-commit-messages/) all issue ID that this PR should correct
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Guideline for Issues
|
|
||||||
|
|
||||||
- If you have any question about GitBucket, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raise an issue.
|
|
||||||
- Make sure check whether there is a same question or request in the past.
|
|
||||||
- When raise a new issue, write subject in **English** at least.
|
|
||||||
- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
|
|
||||||
- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it.
|
|
||||||
33
README.md
33
README.md
@@ -1,7 +1,10 @@
|
|||||||
GitBucket [](https://gitter.im/gitbucket/gitbucket) [](https://travis-ci.org/gitbucket/gitbucket)
|
GitBucket [](https://gitter.im/gitbucket/gitbucket) [](https://travis-ci.org/gitbucket/gitbucket)
|
||||||
=========
|
=========
|
||||||
|
|
||||||
GitBucket is a GitHub clone powered by Scala which has easy installation and high extensibility.
|
GitBucket is a Git platform powered by Scala offering:
|
||||||
|
- easy installation
|
||||||
|
- high extensibility by plugins
|
||||||
|
- API compatibility with Github
|
||||||
|
|
||||||
Features
|
Features
|
||||||
--------
|
--------
|
||||||
@@ -56,10 +59,35 @@ Support
|
|||||||
- Make sure check whether there is a same question or request in the past.
|
- Make sure check whether there is a same question or request in the past.
|
||||||
- When raise a new issue, write subject in **English** at least.
|
- When raise a new issue, write subject in **English** at least.
|
||||||
- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
|
- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
|
||||||
- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it.
|
- First priority of GitBucket is easy installation and API compatibility with GitHub, so we might reject if your request is against it.
|
||||||
|
|
||||||
Release Notes
|
Release Notes
|
||||||
--------
|
--------
|
||||||
|
### 4.0 - 30 Apr 2016
|
||||||
|
|
||||||
|
- MySQL and PostgreSQL support
|
||||||
|
- Data export and import
|
||||||
|
- Migration system has been switched to [solidbase](https://github.com/gitbucket/solidbase)
|
||||||
|
|
||||||
|
### 3.14 - 30 Apr 2016
|
||||||
|
|
||||||
|
- File attachment and search for wiki pages
|
||||||
|
- New extension points to add menus
|
||||||
|
- Content-Type of webhooks has been choosable
|
||||||
|
|
||||||
|
### 3.13 - 1 Apr 2016
|
||||||
|
- Refresh user interface for wide screen
|
||||||
|
- Add `pull_request` key in list issues API for pull requests
|
||||||
|
- Add `X-Hub-Signature` security to webhooks
|
||||||
|
- Provide SHA-256 checksum for `gitbucket.war`
|
||||||
|
|
||||||
|
### 3.12 - 27 Feb 2016
|
||||||
|
- New GitHub UI
|
||||||
|
- Improve mobile view
|
||||||
|
- Improve printing style
|
||||||
|
- Individual URL for pull request tabs
|
||||||
|
- SSH host configuration is separated from HTTP base URL
|
||||||
|
|
||||||
### 3.11 - 30 Jan 2016
|
### 3.11 - 30 Jan 2016
|
||||||
- Upgrade Scalatra to 2.4
|
- Upgrade Scalatra to 2.4
|
||||||
- Sidebar and Footer for Wiki
|
- Sidebar and Footer for Wiki
|
||||||
@@ -67,6 +95,7 @@ Release Notes
|
|||||||
- Limit recent updated repositories list
|
- Limit recent updated repositories list
|
||||||
- Issue actions look-alike GitHub
|
- Issue actions look-alike GitHub
|
||||||
- Web API for labels
|
- Web API for labels
|
||||||
|
- Requires Java 8
|
||||||
|
|
||||||
### 3.10 - 30 Dec 2015
|
### 3.10 - 30 Dec 2015
|
||||||
- Move to Bootstrap3
|
- Move to Bootstrap3
|
||||||
|
|||||||
84
build.sbt
84
build.sbt
@@ -1,6 +1,6 @@
|
|||||||
val Organization = "gitbucket"
|
val Organization = "gitbucket"
|
||||||
val Name = "gitbucket"
|
val Name = "gitbucket"
|
||||||
val GitBucketVersion = "3.11.0"
|
val GitBucketVersion = "3.14.0"
|
||||||
val ScalatraVersion = "2.4.0"
|
val ScalatraVersion = "2.4.0"
|
||||||
val JettyVersion = "9.3.6.v20151106"
|
val JettyVersion = "9.3.6.v20151106"
|
||||||
|
|
||||||
@@ -10,15 +10,15 @@ sourcesInBase := false
|
|||||||
organization := Organization
|
organization := Organization
|
||||||
name := Name
|
name := Name
|
||||||
version := GitBucketVersion
|
version := GitBucketVersion
|
||||||
scalaVersion := "2.11.6"
|
scalaVersion := "2.11.8"
|
||||||
|
|
||||||
// dependency settings
|
// dependency settings
|
||||||
resolvers ++= Seq(
|
resolvers ++= Seq(
|
||||||
Classpaths.typesafeReleases,
|
Classpaths.typesafeReleases,
|
||||||
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/",
|
"sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/"
|
||||||
"amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/"
|
|
||||||
)
|
)
|
||||||
libraryDependencies ++= Seq(
|
libraryDependencies ++= Seq(
|
||||||
|
"org.scala-lang.modules" %% "scala-java8-compat" % "0.7.0",
|
||||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.1.201511131810-r",
|
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.1.201511131810-r",
|
||||||
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.1.1.201511131810-r",
|
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.1.1.201511131810-r",
|
||||||
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
||||||
@@ -26,7 +26,7 @@ libraryDependencies ++= Seq(
|
|||||||
"org.json4s" %% "json4s-jackson" % "3.3.0",
|
"org.json4s" %% "json4s-jackson" % "3.3.0",
|
||||||
"io.github.gitbucket" %% "scalatra-forms" % "1.0.0",
|
"io.github.gitbucket" %% "scalatra-forms" % "1.0.0",
|
||||||
"commons-io" % "commons-io" % "2.4",
|
"commons-io" % "commons-io" % "2.4",
|
||||||
"io.github.gitbucket" % "markedj" % "1.0.6",
|
"io.github.gitbucket" % "markedj" % "1.0.8",
|
||||||
"org.apache.commons" % "commons-compress" % "1.10",
|
"org.apache.commons" % "commons-compress" % "1.10",
|
||||||
"org.apache.commons" % "commons-email" % "1.4",
|
"org.apache.commons" % "commons-email" % "1.4",
|
||||||
"org.apache.httpcomponents" % "httpclient" % "4.5.1",
|
"org.apache.httpcomponents" % "httpclient" % "4.5.1",
|
||||||
@@ -39,20 +39,21 @@ libraryDependencies ++= Seq(
|
|||||||
"com.mchange" % "c3p0" % "0.9.5.2",
|
"com.mchange" % "c3p0" % "0.9.5.2",
|
||||||
"com.typesafe" % "config" % "1.3.0",
|
"com.typesafe" % "config" % "1.3.0",
|
||||||
"com.typesafe.akka" %% "akka-actor" % "2.3.14",
|
"com.typesafe.akka" %% "akka-actor" % "2.3.14",
|
||||||
|
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.0.0",
|
||||||
"com.enragedginger" %% "akka-quartz-scheduler" % "1.4.0-akka-2.3.x" exclude("c3p0","c3p0"),
|
"com.enragedginger" %% "akka-quartz-scheduler" % "1.4.0-akka-2.3.x" exclude("c3p0","c3p0"),
|
||||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
|
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
|
||||||
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
|
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
|
||||||
"junit" % "junit" % "4.12" % "test",
|
"junit" % "junit" % "4.12" % "test",
|
||||||
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
|
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
|
||||||
"org.specs2" %% "specs2-junit" % "3.6.6" % "test"
|
"org.scalaz" %% "scalaz-core" % "7.2.0" % "test"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Twirl settings
|
// Twirl settings
|
||||||
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._"
|
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._"
|
||||||
|
|
||||||
// Compiler settings
|
// Compiler settings
|
||||||
scalacOptions := Seq("-deprecation", "-language:postfixOps")
|
scalacOptions := Seq("-deprecation", "-language:postfixOps", "-Ybackend:GenBCode", "-Ydelambdafy:method", "-target:jvm-1.8")
|
||||||
javacOptions in compile ++= Seq("-target", "7", "-source", "7")
|
javacOptions in compile ++= Seq("-target", "8", "-source", "8")
|
||||||
javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml"
|
javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml"
|
||||||
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console")
|
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console")
|
||||||
javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test"
|
javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test"
|
||||||
@@ -82,36 +83,36 @@ jrebelSettings
|
|||||||
// Create executable war file
|
// Create executable war file
|
||||||
val executableConfig = config("executable").hide
|
val executableConfig = config("executable").hide
|
||||||
Keys.ivyConfigurations += executableConfig
|
Keys.ivyConfigurations += executableConfig
|
||||||
libraryDependencies ++= Seq(
|
libraryDependencies ++= Seq(
|
||||||
"org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable",
|
"org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable",
|
||||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable",
|
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable",
|
||||||
"org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable",
|
"org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable",
|
||||||
"org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable",
|
"org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable",
|
||||||
"org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable",
|
"org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable",
|
||||||
"org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable",
|
"org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable",
|
||||||
"org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable",
|
"org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable",
|
||||||
"org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable",
|
"org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable",
|
||||||
"org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable"
|
"org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable"
|
||||||
)
|
)
|
||||||
|
|
||||||
val executableKey = TaskKey[File]("executable")
|
val executableKey = TaskKey[File]("executable")
|
||||||
executableKey := {
|
executableKey := {
|
||||||
import org.apache.ivy.util.ChecksumHelper
|
import org.apache.ivy.util.ChecksumHelper
|
||||||
import java.util.jar.{ Manifest => JarManifest }
|
import java.util.jar.{ Manifest => JarManifest }
|
||||||
import java.util.jar.Attributes.{ Name => AttrName }
|
import java.util.jar.Attributes.{ Name => AttrName }
|
||||||
|
|
||||||
val workDir = Keys.target.value / "executable"
|
val workDir = Keys.target.value / "executable"
|
||||||
val warName = Keys.name.value + ".war"
|
val warName = Keys.name.value + ".war"
|
||||||
|
|
||||||
val log = streams.value.log
|
val log = streams.value.log
|
||||||
log info s"building executable webapp in ${workDir}"
|
log info s"building executable webapp in ${workDir}"
|
||||||
|
|
||||||
// initialize temp directory
|
// initialize temp directory
|
||||||
val temp = workDir / "webapp"
|
val temp = workDir / "webapp"
|
||||||
IO delete temp
|
IO delete temp
|
||||||
|
|
||||||
// include jetty classes
|
// include jetty classes
|
||||||
val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name)
|
val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name)
|
||||||
jettyJars foreach { jar =>
|
jettyJars foreach { jar =>
|
||||||
IO unzip (jar, temp, (name:String) =>
|
IO unzip (jar, temp, (name:String) =>
|
||||||
(name startsWith "javax/") ||
|
(name startsWith "javax/") ||
|
||||||
@@ -120,31 +121,34 @@ executableKey := {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// include original war file
|
// include original war file
|
||||||
val warFile = (Keys.`package`).value
|
val warFile = (Keys.`package`).value
|
||||||
IO unzip (warFile, temp)
|
IO unzip (warFile, temp)
|
||||||
|
|
||||||
// include launcher classes
|
// include launcher classes
|
||||||
val classDir = (Keys.classDirectory in Compile).value
|
val classDir = (Keys.classDirectory in Compile).value
|
||||||
val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */)
|
val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */)
|
||||||
launchClasses foreach { name =>
|
launchClasses foreach { name =>
|
||||||
IO copyFile (classDir / name, temp / name)
|
IO copyFile (classDir / name, temp / name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// zip it up
|
// zip it up
|
||||||
IO delete (temp / "META-INF" / "MANIFEST.MF")
|
IO delete (temp / "META-INF" / "MANIFEST.MF")
|
||||||
val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp)
|
val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp)
|
||||||
val manifest = new JarManifest
|
val manifest = new JarManifest
|
||||||
manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
|
manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
|
||||||
manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
|
manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
|
||||||
val outputFile = workDir / warName
|
val outputFile = workDir / warName
|
||||||
IO jar (contentMappings, outputFile, manifest)
|
IO jar (contentMappings, outputFile, manifest)
|
||||||
|
|
||||||
// generate checksums
|
// generate checksums
|
||||||
Seq("md5", "sha1") foreach { algorithm =>
|
Seq(
|
||||||
IO.write(
|
"md5" -> "MD5",
|
||||||
workDir / (warName + "." + algorithm),
|
"sha1" -> "SHA-1",
|
||||||
ChecksumHelper computeAsString (outputFile, algorithm)
|
"sha256" -> "SHA-256"
|
||||||
)
|
)
|
||||||
|
.foreach { case (extension, algorithm) =>
|
||||||
|
val checksumFile = workDir / (warName + "." + extension)
|
||||||
|
Checksums generate (outputFile, checksumFile, algorithm)
|
||||||
}
|
}
|
||||||
|
|
||||||
// done
|
// done
|
||||||
@@ -153,7 +157,7 @@ executableKey := {
|
|||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
Keys.artifact in (Compile, executableKey) ~= {
|
Keys.artifact in (Compile, executableKey) ~= {
|
||||||
_ copy (`type` = "war", extension = "war"))
|
_ copy (`type` = "war", extension = "war"))
|
||||||
}
|
}
|
||||||
addArtifact(Keys.artifact in (Compile, executableKey), executableKey)
|
addArtifact(Keys.artifact in (Compile, executableKey), executableKey)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -32,3 +32,11 @@ $ sbt executable
|
|||||||
```
|
```
|
||||||
|
|
||||||
at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. We release this war file as release artifact.
|
at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. We release this war file as release artifact.
|
||||||
|
|
||||||
|
Run tests spec
|
||||||
|
---------
|
||||||
|
To run the full serie of tests, run the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
sbt test
|
||||||
|
```
|
||||||
|
|||||||
@@ -6,15 +6,14 @@ Update version number
|
|||||||
|
|
||||||
Note to update version number in files below:
|
Note to update version number in files below:
|
||||||
|
|
||||||
### project/build.scala
|
### build.sbt
|
||||||
|
|
||||||
```scala
|
```scala
|
||||||
object MyBuild extends Build {
|
val Organization = "gitbucket"
|
||||||
val Organization = "gitbucket"
|
val Name = "gitbucket"
|
||||||
val Name = "gitbucket"
|
val GitBucketVersion = "3.12.0" // <---- update version!!
|
||||||
val Version = "3.3.0" // <---- update version!!
|
val ScalatraVersion = "2.4.0"
|
||||||
val ScalaVersion = "2.11.6"
|
val JettyVersion = "9.3.6.v20151106"
|
||||||
val ScalatraVersion = "2.3.1"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
|
### src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
|
||||||
@@ -26,8 +25,8 @@ object AutoUpdate {
|
|||||||
* The history of versions. A head of this sequence is the current GitBucket version.
|
* The history of versions. A head of this sequence is the current GitBucket version.
|
||||||
*/
|
*/
|
||||||
val versions = Seq(
|
val versions = Seq(
|
||||||
new Version(3, 3), // <---- add this line!!
|
new Version(3, 12), // <---- add this line!!
|
||||||
new Version(3, 2),
|
new Version(3, 11),
|
||||||
```
|
```
|
||||||
|
|
||||||
Generate release files
|
Generate release files
|
||||||
|
|||||||
34
project/Checksums.scala
Normal file
34
project/Checksums.scala
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import java.security.MessageDigest;
|
||||||
|
import scala.annotation._
|
||||||
|
import sbt._
|
||||||
|
import sbt.Using._
|
||||||
|
|
||||||
|
object Checksums {
|
||||||
|
private val bufferSize = 2048
|
||||||
|
|
||||||
|
def generate(source:File, target:File, algorithm:String):Unit =
|
||||||
|
IO write (target, compute(source, algorithm))
|
||||||
|
|
||||||
|
def compute(file:File, algorithm:String):String =
|
||||||
|
hex(raw(file, algorithm))
|
||||||
|
|
||||||
|
def raw(file:File, algorithm:String):Array[Byte] =
|
||||||
|
(Using fileInputStream file) { is =>
|
||||||
|
val md = MessageDigest getInstance algorithm
|
||||||
|
val buf = new Array[Byte](bufferSize)
|
||||||
|
md.reset()
|
||||||
|
@tailrec
|
||||||
|
def loop() {
|
||||||
|
val len = is read buf
|
||||||
|
if (len != -1) {
|
||||||
|
md update (buf, 0, len)
|
||||||
|
loop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loop()
|
||||||
|
md.digest()
|
||||||
|
}
|
||||||
|
|
||||||
|
def hex(bytes:Array[Byte]):String =
|
||||||
|
bytes map { it => "%02x" format (it.toInt & 0xff) } mkString ""
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
sbt.version=0.13.8
|
sbt.version=0.13.9
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
export GITBUCKET_VERSION=`cat ../project/build.scala | grep 'val Version' | cut -d \" -f 2`
|
export GITBUCKET_VERSION=`cat ../build.sbt | grep 'val GitBucketVersion' | cut -d \" -f 2`
|
||||||
echo "GITBUCKET_VERSION: $GITBUCKET_VERSION"
|
echo "GITBUCKET_VERSION: $GITBUCKET_VERSION"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<extension>
|
<extension>
|
||||||
<groupId>org.apache.maven.wagon</groupId>
|
<groupId>org.apache.maven.wagon</groupId>
|
||||||
<artifactId>wagon-ssh</artifactId>
|
<artifactId>wagon-ssh</artifactId>
|
||||||
<version>1.0-beta-6</version>
|
<version>2.10</version>
|
||||||
</extension>
|
</extension>
|
||||||
</extensions>
|
</extensions>
|
||||||
</build>
|
</build>
|
||||||
|
|||||||
Binary file not shown.
2
sbt.bat
2
sbt.bat
@@ -1,2 +1,2 @@
|
|||||||
set SCRIPT_DIR=%~dp0
|
set SCRIPT_DIR=%~dp0
|
||||||
java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.8.jar" %*
|
java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.9.jar" %*
|
||||||
|
|||||||
2
sbt.sh
2
sbt.sh
@@ -1,2 +1,2 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
java $JAVA_OPTS -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.8.jar "$@"
|
java $JAVA_OPTS -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.9.jar "$@"
|
||||||
|
|||||||
1
src/main/resources/update/3_13.sql
Normal file
1
src/main/resources/update/3_13.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE WEB_HOOK ADD COLUMN TOKEN VARCHAR(100);
|
||||||
3
src/main/resources/update/3_14.sql
Normal file
3
src/main/resources/update/3_14.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE WEB_HOOK ADD COLUMN CTYPE VARCHAR(10);
|
||||||
|
|
||||||
|
UPDATE WEB_HOOK SET CTYPE = 'form';
|
||||||
@@ -27,12 +27,10 @@ class ScalatraBootstrap extends LifeCycle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.mount(new IndexController, "/")
|
context.mount(new IndexController, "/")
|
||||||
context.mount(new SearchController, "/")
|
context.mount(new ApiController, "/api/v3")
|
||||||
context.mount(new FileUploadController, "/upload")
|
context.mount(new FileUploadController, "/upload")
|
||||||
|
context.mount(new SystemSettingsController, "/admin")
|
||||||
context.mount(new DashboardController, "/*")
|
context.mount(new DashboardController, "/*")
|
||||||
context.mount(new UserManagementController, "/*")
|
|
||||||
context.mount(new SystemSettingsController, "/*")
|
|
||||||
context.mount(new PluginsController, "/*")
|
|
||||||
context.mount(new AccountController, "/*")
|
context.mount(new AccountController, "/*")
|
||||||
context.mount(new RepositoryViewerController, "/*")
|
context.mount(new RepositoryViewerController, "/*")
|
||||||
context.mount(new WikiController, "/*")
|
context.mount(new WikiController, "/*")
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ case class ApiIssue(
|
|||||||
body: String)(repositoryName: RepositoryName, isPullRequest: Boolean){
|
body: String)(repositoryName: RepositoryName, isPullRequest: Boolean){
|
||||||
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
|
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
|
||||||
val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "issues" }}/${number}")
|
val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "issues" }}/${number}")
|
||||||
|
val pull_request = if (isPullRequest) {
|
||||||
|
Some(Map(
|
||||||
|
"url" -> ApiPath(s"/api/v3/repos/${repositoryName.fullName}/pulls/${number}"),
|
||||||
|
"html_url" -> ApiPath(s"/${repositoryName.fullName}/pull/${number}")
|
||||||
|
// "diff_url" -> ApiPath(s"/${repositoryName.fullName}/pull/${number}.diff"),
|
||||||
|
// "patch_url" -> ApiPath(s"/${repositoryName.fullName}/pull/${number}.patch")
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object ApiIssue{
|
object ApiIssue{
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
import gitbucket.core.account.html
|
import gitbucket.core.account.html
|
||||||
import gitbucket.core.api._
|
|
||||||
import gitbucket.core.helper
|
import gitbucket.core.helper
|
||||||
import gitbucket.core.model.GroupMember
|
import gitbucket.core.model.GroupMember
|
||||||
import gitbucket.core.service._
|
import gitbucket.core.service._
|
||||||
@@ -14,22 +13,19 @@ import gitbucket.core.util._
|
|||||||
|
|
||||||
import io.github.gitbucket.scalatra.forms._
|
import io.github.gitbucket.scalatra.forms._
|
||||||
import org.apache.commons.io.FileUtils
|
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.scalatra.i18n.Messages
|
import org.scalatra.i18n.Messages
|
||||||
|
|
||||||
|
|
||||||
class AccountController extends AccountControllerBase
|
class AccountController extends AccountControllerBase
|
||||||
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
||||||
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
|
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
|
||||||
with AccessTokenService with WebHookService
|
with AccessTokenService with WebHookService with RepositoryCreationService
|
||||||
|
|
||||||
|
|
||||||
trait AccountControllerBase extends AccountManagementControllerBase {
|
trait AccountControllerBase extends AccountManagementControllerBase {
|
||||||
self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
||||||
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
|
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
|
||||||
with AccessTokenService with WebHookService =>
|
with AccessTokenService with WebHookService with RepositoryCreationService =>
|
||||||
|
|
||||||
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
|
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
|
||||||
url: Option[String], fileId: Option[String])
|
url: Option[String], fileId: Option[String])
|
||||||
@@ -133,7 +129,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
val members = getGroupMembers(account.userName)
|
val members = getGroupMembers(account.userName)
|
||||||
gitbucket.core.account.html.repositories(account,
|
gitbucket.core.account.html.repositories(account,
|
||||||
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
|
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
|
||||||
getVisibleRepositories(context.loginAccount, context.baseUrl, Some(userName)),
|
getVisibleRepositories(context.loginAccount, Some(userName)),
|
||||||
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
|
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,25 +152,6 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/users/#get-a-single-user
|
|
||||||
*/
|
|
||||||
get("/api/v3/users/:userName") {
|
|
||||||
getAccountByUserName(params("userName")).map { account =>
|
|
||||||
JsonFormat(ApiUser(account))
|
|
||||||
} getOrElse NotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/users/#get-the-authenticated-user
|
|
||||||
*/
|
|
||||||
get("/api/v3/user") {
|
|
||||||
context.loginAccount.map { account =>
|
|
||||||
JsonFormat(ApiUser(account))
|
|
||||||
} getOrElse Unauthorized
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
get("/:userName/_edit")(oneselfOnly {
|
get("/:userName/_edit")(oneselfOnly {
|
||||||
val userName = params("userName")
|
val userName = params("userName")
|
||||||
getAccountByUserName(userName).map { x =>
|
getAccountByUserName(userName).map { x =>
|
||||||
@@ -366,8 +343,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
*/
|
*/
|
||||||
post("/new", newRepositoryForm)(usersOnly { form =>
|
post("/new", newRepositoryForm)(usersOnly { form =>
|
||||||
LockUtil.lock(s"${form.owner}/${form.name}"){
|
LockUtil.lock(s"${form.owner}/${form.name}"){
|
||||||
if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){
|
if(getRepository(form.owner, form.name).isEmpty){
|
||||||
createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme)
|
createRepository(context.loginAccount.get, form.owner, form.name, form.description, form.isPrivate, form.createReadme)
|
||||||
}
|
}
|
||||||
|
|
||||||
// redirect to the repository
|
// redirect to the repository
|
||||||
@@ -375,54 +352,6 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Create user repository
|
|
||||||
* https://developer.github.com/v3/repos/#create
|
|
||||||
*/
|
|
||||||
post("/api/v3/user/repos")(usersOnly {
|
|
||||||
val owner = context.loginAccount.get.userName
|
|
||||||
(for {
|
|
||||||
data <- extractFromJsonBody[CreateARepository] if data.isValid
|
|
||||||
} yield {
|
|
||||||
LockUtil.lock(s"${owner}/${data.name}") {
|
|
||||||
if(getRepository(owner, data.name, context.baseUrl).isEmpty){
|
|
||||||
createRepository(owner, data.name, data.description, data.`private`, data.auto_init)
|
|
||||||
val repository = getRepository(owner, data.name, context.baseUrl).get
|
|
||||||
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
|
|
||||||
} else {
|
|
||||||
ApiError(
|
|
||||||
"A repository with this name already exists on this account",
|
|
||||||
Some("https://developer.github.com/v3/repos/#create")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create group repository
|
|
||||||
* https://developer.github.com/v3/repos/#create
|
|
||||||
*/
|
|
||||||
post("/api/v3/orgs/:org/repos")(managersOnly {
|
|
||||||
val groupName = params("org")
|
|
||||||
(for {
|
|
||||||
data <- extractFromJsonBody[CreateARepository] if data.isValid
|
|
||||||
} yield {
|
|
||||||
LockUtil.lock(s"${groupName}/${data.name}") {
|
|
||||||
if(getRepository(groupName, data.name, context.baseUrl).isEmpty){
|
|
||||||
createRepository(groupName, data.name, data.description, data.`private`, data.auto_init)
|
|
||||||
val repository = getRepository(groupName, data.name, context.baseUrl).get
|
|
||||||
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get)))
|
|
||||||
} else {
|
|
||||||
ApiError(
|
|
||||||
"A repository with this name already exists for this group",
|
|
||||||
Some("https://developer.github.com/v3/repos/#create")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
|
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
|
||||||
val loginAccount = context.loginAccount.get
|
val loginAccount = context.loginAccount.get
|
||||||
val loginUserName = loginAccount.userName
|
val loginUserName = loginAccount.userName
|
||||||
@@ -447,7 +376,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
val accountName = form.accountName
|
val accountName = form.accountName
|
||||||
|
|
||||||
LockUtil.lock(s"${accountName}/${repository.name}"){
|
LockUtil.lock(s"${accountName}/${repository.name}"){
|
||||||
if(getRepository(accountName, repository.name, baseUrl).isDefined ||
|
if(getRepository(accountName, repository.name).isDefined ||
|
||||||
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){
|
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){
|
||||||
// redirect to the repository if repository already exists
|
// redirect to the repository if repository already exists
|
||||||
redirect(s"/${accountName}/${repository.name}")
|
redirect(s"/${accountName}/${repository.name}")
|
||||||
@@ -456,7 +385,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
|
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
|
||||||
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
|
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
|
||||||
|
|
||||||
createRepository(
|
insertRepository(
|
||||||
repositoryName = repository.name,
|
repositoryName = repository.name,
|
||||||
userName = accountName,
|
userName = accountName,
|
||||||
description = repository.repository.description,
|
description = repository.repository.description,
|
||||||
@@ -496,68 +425,6 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
private def createRepository(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) {
|
|
||||||
val ownerAccount = getAccountByUserName(owner).get
|
|
||||||
val loginAccount = context.loginAccount.get
|
|
||||||
val loginUserName = loginAccount.userName
|
|
||||||
|
|
||||||
// Insert to the database at first
|
|
||||||
createRepository(name, owner, description, isPrivate)
|
|
||||||
|
|
||||||
// Add collaborators for group repository
|
|
||||||
if(ownerAccount.isGroupAccount){
|
|
||||||
getGroupMembers(owner).foreach { member =>
|
|
||||||
addCollaborator(owner, name, member.userName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert default labels
|
|
||||||
insertDefaultLabels(owner, name)
|
|
||||||
|
|
||||||
// Create the actual repository
|
|
||||||
val gitdir = getRepositoryDir(owner, name)
|
|
||||||
JGitUtil.initRepository(gitdir)
|
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Wiki repository
|
|
||||||
createWikiRepository(loginAccount, owner, name)
|
|
||||||
|
|
||||||
// Record activity
|
|
||||||
recordCreateRepositoryActivity(owner, name, loginUserName)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
|
|
||||||
createLabel(userName, repositoryName, "bug", "fc2929")
|
|
||||||
createLabel(userName, repositoryName, "duplicate", "cccccc")
|
|
||||||
createLabel(userName, repositoryName, "enhancement", "84b6eb")
|
|
||||||
createLabel(userName, repositoryName, "invalid", "e6e6e6")
|
|
||||||
createLabel(userName, repositoryName, "question", "cc317c")
|
|
||||||
createLabel(userName, repositoryName, "wontfix", "ffffff")
|
|
||||||
}
|
|
||||||
|
|
||||||
private def existsAccount: Constraint = new Constraint(){
|
private def existsAccount: Constraint = new Constraint(){
|
||||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||||
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
|
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
|
||||||
|
|||||||
389
src/main/scala/gitbucket/core/controller/ApiController.scala
Normal file
389
src/main/scala/gitbucket/core/controller/ApiController.scala
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import gitbucket.core.api._
|
||||||
|
import gitbucket.core.model._
|
||||||
|
import gitbucket.core.service.IssuesService.IssueSearchCondition
|
||||||
|
import gitbucket.core.service.PullRequestService._
|
||||||
|
import gitbucket.core.service._
|
||||||
|
import gitbucket.core.util.ControlUtil._
|
||||||
|
import gitbucket.core.util.Directory._
|
||||||
|
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||||
|
import gitbucket.core.util._
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.scalatra.{NoContent, UnprocessableEntity, Created}
|
||||||
|
import scala.collection.JavaConverters._
|
||||||
|
|
||||||
|
class ApiController extends ApiControllerBase
|
||||||
|
with RepositoryService
|
||||||
|
with AccountService
|
||||||
|
with ProtectedBranchService
|
||||||
|
with IssuesService
|
||||||
|
with LabelsService
|
||||||
|
with PullRequestService
|
||||||
|
with CommitStatusService
|
||||||
|
with RepositoryCreationService
|
||||||
|
with HandleCommentService
|
||||||
|
with WebHookService
|
||||||
|
with WebHookPullRequestService
|
||||||
|
with WebHookIssueCommentService
|
||||||
|
with WikiService
|
||||||
|
with ActivityService
|
||||||
|
with OwnerAuthenticator
|
||||||
|
with UsersAuthenticator
|
||||||
|
with GroupManagerAuthenticator
|
||||||
|
with ReferrerAuthenticator
|
||||||
|
with ReadableUsersAuthenticator
|
||||||
|
with CollaboratorsAuthenticator
|
||||||
|
|
||||||
|
trait ApiControllerBase extends ControllerBase {
|
||||||
|
self: RepositoryService
|
||||||
|
with AccountService
|
||||||
|
with ProtectedBranchService
|
||||||
|
with IssuesService
|
||||||
|
with LabelsService
|
||||||
|
with PullRequestService
|
||||||
|
with CommitStatusService
|
||||||
|
with RepositoryCreationService
|
||||||
|
with HandleCommentService
|
||||||
|
with OwnerAuthenticator
|
||||||
|
with UsersAuthenticator
|
||||||
|
with GroupManagerAuthenticator
|
||||||
|
with ReferrerAuthenticator
|
||||||
|
with ReadableUsersAuthenticator
|
||||||
|
with CollaboratorsAuthenticator =>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/users/#get-a-single-user
|
||||||
|
*/
|
||||||
|
get("/api/v3/users/:userName") {
|
||||||
|
getAccountByUserName(params("userName")).map { account =>
|
||||||
|
JsonFormat(ApiUser(account))
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/users/#get-the-authenticated-user
|
||||||
|
*/
|
||||||
|
get("/api/v3/user") {
|
||||||
|
context.loginAccount.map { account =>
|
||||||
|
JsonFormat(ApiUser(account))
|
||||||
|
} getOrElse Unauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create user repository
|
||||||
|
* https://developer.github.com/v3/repos/#create
|
||||||
|
*/
|
||||||
|
post("/api/v3/user/repos")(usersOnly {
|
||||||
|
val owner = context.loginAccount.get.userName
|
||||||
|
(for {
|
||||||
|
data <- extractFromJsonBody[CreateARepository] if data.isValid
|
||||||
|
} 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 repository = getRepository(owner, data.name).get
|
||||||
|
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
|
||||||
|
} else {
|
||||||
|
ApiError(
|
||||||
|
"A repository with this name already exists on this account",
|
||||||
|
Some("https://developer.github.com/v3/repos/#create")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create group repository
|
||||||
|
* https://developer.github.com/v3/repos/#create
|
||||||
|
*/
|
||||||
|
post("/api/v3/orgs/:org/repos")(managersOnly {
|
||||||
|
val groupName = params("org")
|
||||||
|
(for {
|
||||||
|
data <- extractFromJsonBody[CreateARepository] if data.isValid
|
||||||
|
} 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 repository = getRepository(groupName, data.name).get
|
||||||
|
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get)))
|
||||||
|
} else {
|
||||||
|
ApiError(
|
||||||
|
"A repository with this name already exists for this group",
|
||||||
|
Some("https://developer.github.com/v3/repos/#create")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
|
||||||
|
patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository =>
|
||||||
|
import gitbucket.core.api._
|
||||||
|
(for{
|
||||||
|
branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined
|
||||||
|
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
|
||||||
|
} yield {
|
||||||
|
if(protection.enabled){
|
||||||
|
enableBranchProtection(repository.owner, repository.name, branch, protection.status.enforcement_level == ApiBranchProtection.Everyone, protection.status.contexts)
|
||||||
|
} else {
|
||||||
|
disableBranchProtection(repository.owner, repository.name, branch)
|
||||||
|
}
|
||||||
|
JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository)))
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://developer.github.com/v3/rate_limit/#get-your-current-rate-limit-status
|
||||||
|
* but not enabled.
|
||||||
|
*/
|
||||||
|
get("/api/v3/rate_limit"){
|
||||||
|
contentType = formats("json")
|
||||||
|
// this message is same as github enterprise...
|
||||||
|
org.scalatra.NotFound(ApiError("Rate limiting is not enabled."))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository =>
|
||||||
|
(for{
|
||||||
|
issueId <- params("id").toIntOpt
|
||||||
|
comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
|
||||||
|
} yield {
|
||||||
|
JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) })
|
||||||
|
}).getOrElse(NotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/issues/comments/#create-a-comment
|
||||||
|
*/
|
||||||
|
post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository =>
|
||||||
|
(for{
|
||||||
|
issueId <- params("id").toIntOpt
|
||||||
|
issue <- getIssue(repository.owner, repository.name, issueId.toString)
|
||||||
|
body <- extractFromJsonBody[CreateAComment].map(_.body) if ! body.isEmpty
|
||||||
|
action = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
|
||||||
|
(issue, id) <- handleComment(issue, Some(body), repository, action)
|
||||||
|
issueComment <- getComment(repository.owner, repository.name, id.toString())
|
||||||
|
} yield {
|
||||||
|
JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest))
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all labels for this repository
|
||||||
|
* https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository/labels")(referrersOnly { repository =>
|
||||||
|
JsonFormat(getLabels(repository.owner, repository.name).map { label =>
|
||||||
|
ApiLabel(label, RepositoryName(repository))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single label
|
||||||
|
* https://developer.github.com/v3/issues/labels/#get-a-single-label
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository/labels/:labelName")(referrersOnly { repository =>
|
||||||
|
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
|
||||||
|
JsonFormat(ApiLabel(label, RepositoryName(repository)))
|
||||||
|
} getOrElse NotFound()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a label
|
||||||
|
* https://developer.github.com/v3/issues/labels/#create-a-label
|
||||||
|
*/
|
||||||
|
post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository =>
|
||||||
|
(for{
|
||||||
|
data <- extractFromJsonBody[CreateALabel] if data.isValid
|
||||||
|
} yield {
|
||||||
|
LockUtil.lock(RepositoryName(repository).fullName) {
|
||||||
|
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
|
||||||
|
val labelId = createLabel(repository.owner, repository.name, data.name, data.color)
|
||||||
|
getLabel(repository.owner, repository.name, labelId).map { label =>
|
||||||
|
Created(JsonFormat(ApiLabel(label, RepositoryName(repository))))
|
||||||
|
} getOrElse NotFound()
|
||||||
|
} else {
|
||||||
|
// TODO ApiError should support errors field to enhance compatibility of GitHub API
|
||||||
|
UnprocessableEntity(ApiError(
|
||||||
|
"Validation Failed",
|
||||||
|
Some("https://developer.github.com/v3/issues/labels/#create-a-label")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) getOrElse NotFound()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a label
|
||||||
|
* https://developer.github.com/v3/issues/labels/#update-a-label
|
||||||
|
*/
|
||||||
|
patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
|
||||||
|
(for{
|
||||||
|
data <- extractFromJsonBody[CreateALabel] if data.isValid
|
||||||
|
} yield {
|
||||||
|
LockUtil.lock(RepositoryName(repository).fullName) {
|
||||||
|
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
|
||||||
|
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
|
||||||
|
updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color)
|
||||||
|
JsonFormat(ApiLabel(
|
||||||
|
getLabel(repository.owner, repository.name, label.labelId).get,
|
||||||
|
RepositoryName(repository)))
|
||||||
|
} else {
|
||||||
|
// TODO ApiError should support errors field to enhance compatibility of GitHub API
|
||||||
|
UnprocessableEntity(ApiError(
|
||||||
|
"Validation Failed",
|
||||||
|
Some("https://developer.github.com/v3/issues/labels/#create-a-label")))
|
||||||
|
}
|
||||||
|
} getOrElse NotFound()
|
||||||
|
}
|
||||||
|
}) getOrElse NotFound()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a label
|
||||||
|
* https://developer.github.com/v3/issues/labels/#delete-a-label
|
||||||
|
*/
|
||||||
|
delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
|
||||||
|
LockUtil.lock(RepositoryName(repository).fullName) {
|
||||||
|
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
|
||||||
|
deleteLabel(repository.owner, repository.name, label.labelId)
|
||||||
|
NoContent()
|
||||||
|
} getOrElse NotFound()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/pulls/#list-pull-requests
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository =>
|
||||||
|
val page = IssueSearchCondition.page(request)
|
||||||
|
// TODO: more api spec condition
|
||||||
|
val condition = IssueSearchCondition(request)
|
||||||
|
val baseOwner = getAccountByUserName(repository.owner).get
|
||||||
|
val issues:List[(Issue, Account, Int, PullRequest, Repository, Account)] = searchPullRequestByApi(condition, (page - 1) * PullRequestLimit, PullRequestLimit, repository.owner -> repository.name)
|
||||||
|
JsonFormat(issues.map{case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) =>
|
||||||
|
ApiPullRequest(
|
||||||
|
issue,
|
||||||
|
pullRequest,
|
||||||
|
ApiRepository(headRepo, ApiUser(headOwner)),
|
||||||
|
ApiRepository(repository, ApiUser(baseOwner)),
|
||||||
|
ApiUser(issueUser)) })
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/pulls/#get-a-single-pull-request
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository =>
|
||||||
|
(for{
|
||||||
|
issueId <- params("id").toIntOpt
|
||||||
|
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
|
||||||
|
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set())
|
||||||
|
baseOwner <- users.get(repository.owner)
|
||||||
|
headOwner <- users.get(pullRequest.requestUserName)
|
||||||
|
issueUser <- users.get(issue.openedUserName)
|
||||||
|
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
|
||||||
|
} yield {
|
||||||
|
JsonFormat(ApiPullRequest(
|
||||||
|
issue,
|
||||||
|
pullRequest,
|
||||||
|
ApiRepository(headRepo, ApiUser(headOwner)),
|
||||||
|
ApiRepository(repository, ApiUser(baseOwner)),
|
||||||
|
ApiUser(issueUser)))
|
||||||
|
}).getOrElse(NotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository/pulls/:id/commits")(referrersOnly { repository =>
|
||||||
|
val owner = repository.owner
|
||||||
|
val name = repository.name
|
||||||
|
params("id").toIntOpt.flatMap{ issueId =>
|
||||||
|
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||||
|
using(Git.open(getRepositoryDir(owner, name))){ git =>
|
||||||
|
val oldId = git.getRepository.resolve(pullreq.commitIdFrom)
|
||||||
|
val newId = git.getRepository.resolve(pullreq.commitIdTo)
|
||||||
|
val repoFullName = RepositoryName(repository)
|
||||||
|
val commits = git.log.addRange(oldId, newId).call.iterator.asScala.map(c => ApiCommitListItem(new CommitInfo(c), repoFullName)).toList
|
||||||
|
JsonFormat(commits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/#get
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository")(referrersOnly { repository =>
|
||||||
|
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(repository.owner).get)))
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/statuses/#create-a-status
|
||||||
|
*/
|
||||||
|
post("/api/v3/repos/:owner/:repo/statuses/:sha")(collaboratorsOnly { repository =>
|
||||||
|
(for{
|
||||||
|
ref <- params.get("sha")
|
||||||
|
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
||||||
|
data <- extractFromJsonBody[CreateAStatus] if data.isValid
|
||||||
|
creator <- context.loginAccount
|
||||||
|
state <- CommitState.valueOf(data.state)
|
||||||
|
statusId = createCommitStatus(repository.owner, repository.name, sha, data.context.getOrElse("default"),
|
||||||
|
state, data.target_url, data.description, new java.util.Date(), creator)
|
||||||
|
status <- getCommitStatus(repository.owner, repository.name, statusId)
|
||||||
|
} yield {
|
||||||
|
JsonFormat(ApiCommitStatus(status, ApiUser(creator)))
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
|
||||||
|
*
|
||||||
|
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
|
||||||
|
*/
|
||||||
|
val listStatusesRoute = get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository =>
|
||||||
|
(for{
|
||||||
|
ref <- params.get("ref")
|
||||||
|
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
||||||
|
} yield {
|
||||||
|
JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) =>
|
||||||
|
ApiCommitStatus(status, ApiUser(creator))
|
||||||
|
})
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
|
||||||
|
*
|
||||||
|
* legacy route
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repo/statuses/:ref"){
|
||||||
|
listStatusesRoute.action()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
|
||||||
|
*
|
||||||
|
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repo/commits/:ref/status")(referrersOnly { repository =>
|
||||||
|
(for{
|
||||||
|
ref <- params.get("ref")
|
||||||
|
owner <- getAccountByUserName(repository.owner)
|
||||||
|
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
||||||
|
} yield {
|
||||||
|
val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha)
|
||||||
|
JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner)))
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
|
||||||
|
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -180,7 +180,6 @@ abstract class ControllerBase extends ScalatraFilter
|
|||||||
* Context object for the current request.
|
* Context object for the current request.
|
||||||
*/
|
*/
|
||||||
case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){
|
case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){
|
||||||
|
|
||||||
val path = settings.baseUrl.getOrElse(request.getContextPath)
|
val path = settings.baseUrl.getOrElse(request.getContextPath)
|
||||||
val currentPath = request.getRequestURI.substring(request.getContextPath.length)
|
val currentPath = request.getRequestURI.substring(request.getContextPath.length)
|
||||||
val baseUrl = settings.baseUrl(request)
|
val baseUrl = settings.baseUrl(request)
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ trait DashboardControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
val userName = context.loginAccount.get.userName
|
val userName = context.loginAccount.get.userName
|
||||||
val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName)
|
val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName)
|
||||||
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
|
val userRepos = getUserRepositories(userName, true).map(repo => repo.owner -> repo.name)
|
||||||
val page = IssueSearchCondition.page(request)
|
val page = IssueSearchCondition.page(request)
|
||||||
|
|
||||||
html.issues(
|
html.issues(
|
||||||
@@ -108,7 +108,9 @@ trait DashboardControllerBase extends ControllerBase {
|
|||||||
case _ => condition.copy(author = Some(userName))
|
case _ => condition.copy(author = Some(userName))
|
||||||
},
|
},
|
||||||
filter,
|
filter,
|
||||||
getGroupNames(userName))
|
getGroupNames(userName),
|
||||||
|
getVisibleRepositories(context.loginAccount, withoutPhysicalInfo = true),
|
||||||
|
getUserRepositories(userName, withoutPhysicalInfo = true))
|
||||||
}
|
}
|
||||||
|
|
||||||
private def searchPullRequests(filter: String) = {
|
private def searchPullRequests(filter: String) = {
|
||||||
@@ -131,7 +133,9 @@ trait DashboardControllerBase extends ControllerBase {
|
|||||||
case _ => condition.copy(author = Some(userName))
|
case _ => condition.copy(author = Some(userName))
|
||||||
},
|
},
|
||||||
filter,
|
filter,
|
||||||
getGroupNames(userName))
|
getGroupNames(userName),
|
||||||
|
getVisibleRepositories(context.loginAccount, withoutPhysicalInfo = true),
|
||||||
|
getUserRepositories(userName, withoutPhysicalInfo = true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
import gitbucket.core.util.{Keys, FileUtil}
|
import gitbucket.core.model.Account
|
||||||
|
import gitbucket.core.service.{AccountService, RepositoryService}
|
||||||
|
import gitbucket.core.servlet.Database
|
||||||
|
import gitbucket.core.util._
|
||||||
import gitbucket.core.util.ControlUtil._
|
import gitbucket.core.util.ControlUtil._
|
||||||
import gitbucket.core.util.Directory._
|
import gitbucket.core.util.Directory._
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.eclipse.jgit.dircache.DirCache
|
||||||
|
import org.eclipse.jgit.lib.{FileMode, Constants}
|
||||||
import org.scalatra._
|
import org.scalatra._
|
||||||
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem}
|
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem}
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.{IOUtils, FileUtils}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides Ajax based file upload functionality.
|
* Provides Ajax based file upload functionality.
|
||||||
*
|
*
|
||||||
* This servlet saves uploaded file.
|
* This servlet saves uploaded file.
|
||||||
*/
|
*/
|
||||||
class FileUploadController extends ScalatraServlet with FileUploadSupport {
|
class FileUploadController extends ScalatraServlet with FileUploadSupport with RepositoryService with AccountService {
|
||||||
|
|
||||||
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
|
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
|
||||||
|
|
||||||
@@ -31,6 +37,54 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport {
|
|||||||
}, FileUtil.isUploadableType)
|
}, FileUtil.isUploadableType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
post("/wiki/:owner/:repository"){
|
||||||
|
// Don't accept not logged-in users
|
||||||
|
session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account =>
|
||||||
|
val owner = params("owner")
|
||||||
|
val repository = params("repository")
|
||||||
|
|
||||||
|
// Check whether logged-in user is collaborator
|
||||||
|
collaboratorsOnly(owner, repository, loginAccount){
|
||||||
|
execute({ (file, fileId) =>
|
||||||
|
val fileName = file.getName
|
||||||
|
LockUtil.lock(s"${owner}/${repository}/wiki") {
|
||||||
|
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))) { git =>
|
||||||
|
val builder = DirCache.newInCore.builder()
|
||||||
|
val inserter = git.getRepository.newObjectInserter()
|
||||||
|
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||||
|
|
||||||
|
if(headId != null){
|
||||||
|
JGitUtil.processTree(git, headId){ (path, tree) =>
|
||||||
|
if(path != fileName){
|
||||||
|
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val bytes = IOUtils.toByteArray(file.getInputStream)
|
||||||
|
builder.add(JGitUtil.createDirCacheEntry(fileName, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, bytes)))
|
||||||
|
builder.finish()
|
||||||
|
|
||||||
|
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
|
||||||
|
Constants.HEAD, loginAccount.userName, loginAccount.mailAddress, s"Uploaded ${fileName}")
|
||||||
|
|
||||||
|
fileName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, FileUtil.isImage)
|
||||||
|
}
|
||||||
|
} getOrElse BadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
private def collaboratorsOnly(owner: String, repository: String, loginAccount: Account)(action: => Any): Any = {
|
||||||
|
implicit val session = Database.getSession(request)
|
||||||
|
loginAccount match {
|
||||||
|
case x if(x.isAdmin) => action
|
||||||
|
case x if(getCollaborators(owner, repository).contains(x.userName)) => action
|
||||||
|
case _ => BadRequest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private def execute(f: (FileItem, String) => Unit, mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
|
private def execute(f: (FileItem, String) => Unit, mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
|
||||||
case Some(file) if(mimeTypeChcker(file.name)) =>
|
case Some(file) if(mimeTypeChcker(file.name)) =>
|
||||||
defining(FileUtil.generateFileId){ fileId =>
|
defining(FileUtil.generateFileId){ fileId =>
|
||||||
|
|||||||
@@ -1,36 +1,46 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
import gitbucket.core.api._
|
|
||||||
import gitbucket.core.helper.xml
|
import gitbucket.core.helper.xml
|
||||||
import gitbucket.core.html
|
|
||||||
import gitbucket.core.model.Account
|
import gitbucket.core.model.Account
|
||||||
import gitbucket.core.service.{RepositoryService, ActivityService, AccountService}
|
import gitbucket.core.service._
|
||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator}
|
import gitbucket.core.util.ControlUtil._
|
||||||
|
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator, ReferrerAuthenticator, StringUtil}
|
||||||
|
|
||||||
import io.github.gitbucket.scalatra.forms._
|
import io.github.gitbucket.scalatra.forms._
|
||||||
|
|
||||||
|
|
||||||
class IndexController extends IndexControllerBase
|
class IndexController extends IndexControllerBase
|
||||||
with RepositoryService with ActivityService with AccountService with UsersAuthenticator
|
with RepositoryService with ActivityService with AccountService with RepositorySearchService with IssuesService
|
||||||
|
with UsersAuthenticator with ReferrerAuthenticator
|
||||||
|
|
||||||
|
|
||||||
trait IndexControllerBase extends ControllerBase {
|
trait IndexControllerBase extends ControllerBase {
|
||||||
self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
|
self: RepositoryService with ActivityService with AccountService with RepositorySearchService
|
||||||
|
with UsersAuthenticator with ReferrerAuthenticator =>
|
||||||
|
|
||||||
case class SignInForm(userName: String, password: String)
|
case class SignInForm(userName: String, password: String)
|
||||||
|
|
||||||
val form = mapping(
|
val signinForm = mapping(
|
||||||
"userName" -> trim(label("Username", text(required))),
|
"userName" -> trim(label("Username", text(required))),
|
||||||
"password" -> trim(label("Password", text(required)))
|
"password" -> trim(label("Password", text(required)))
|
||||||
)(SignInForm.apply)
|
)(SignInForm.apply)
|
||||||
|
|
||||||
|
val searchForm = mapping(
|
||||||
|
"query" -> trim(text(required)),
|
||||||
|
"owner" -> trim(text(required)),
|
||||||
|
"repository" -> trim(text(required))
|
||||||
|
)(SearchForm.apply)
|
||||||
|
|
||||||
|
case class SearchForm(query: String, owner: String, repository: String)
|
||||||
|
|
||||||
|
|
||||||
get("/"){
|
get("/"){
|
||||||
val loginAccount = context.loginAccount
|
val loginAccount = context.loginAccount
|
||||||
if(loginAccount.isEmpty) {
|
if(loginAccount.isEmpty) {
|
||||||
html.index(getRecentActivities(),
|
gitbucket.core.html.index(getRecentActivities(),
|
||||||
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
|
getVisibleRepositories(loginAccount, withoutPhysicalInfo = true),
|
||||||
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
|
loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val loginUserName = loginAccount.get.userName
|
val loginUserName = loginAccount.get.userName
|
||||||
@@ -39,9 +49,9 @@ trait IndexControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
visibleOwnerSet ++= loginUserGroups
|
visibleOwnerSet ++= loginUserGroups
|
||||||
|
|
||||||
html.index(getRecentActivitiesByOwners(visibleOwnerSet),
|
gitbucket.core.html.index(getRecentActivitiesByOwners(visibleOwnerSet),
|
||||||
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
|
getVisibleRepositories(loginAccount, withoutPhysicalInfo = true),
|
||||||
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
|
loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,10 +61,10 @@ trait IndexControllerBase extends ControllerBase {
|
|||||||
if(redirect.isDefined && redirect.get.startsWith("/")){
|
if(redirect.isDefined && redirect.get.startsWith("/")){
|
||||||
flash += Keys.Flash.Redirect -> redirect.get
|
flash += Keys.Flash.Redirect -> redirect.get
|
||||||
}
|
}
|
||||||
html.signin()
|
gitbucket.core.html.signin()
|
||||||
}
|
}
|
||||||
|
|
||||||
post("/signin", form){ form =>
|
post("/signin", signinForm){ form =>
|
||||||
authenticate(context.settings, form.userName, form.password) match {
|
authenticate(context.settings, form.userName, form.password) match {
|
||||||
case Some(account) => signin(account)
|
case Some(account) => signin(account)
|
||||||
case None => redirect("/signin")
|
case None => redirect("/signin")
|
||||||
@@ -110,13 +120,40 @@ trait IndexControllerBase extends ControllerBase {
|
|||||||
getAccountByUserName(params("userName")).isDefined
|
getAccountByUserName(params("userName")).isDefined
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
// TODO Move to RepositoryViwerController?
|
||||||
* @see https://developer.github.com/v3/rate_limit/#get-your-current-rate-limit-status
|
post("/search", searchForm){ form =>
|
||||||
* but not enabled.
|
redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
|
||||||
*/
|
|
||||||
get("/api/v3/rate_limit"){
|
|
||||||
contentType = formats("json")
|
|
||||||
// this message is same as github enterprise...
|
|
||||||
org.scalatra.NotFound(ApiError("Rate limiting is not enabled."))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Move to RepositoryViwerController?
|
||||||
|
get("/:owner/:repository/search")(referrersOnly { repository =>
|
||||||
|
defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) =>
|
||||||
|
val page = try {
|
||||||
|
val i = params.getOrElse("page", "1").toInt
|
||||||
|
if(i <= 0) 1 else i
|
||||||
|
} catch {
|
||||||
|
case e: NumberFormatException => 1
|
||||||
|
}
|
||||||
|
|
||||||
|
target.toLowerCase match {
|
||||||
|
case "issue" => gitbucket.core.search.html.issues(
|
||||||
|
countFiles(repository.owner, repository.name, query),
|
||||||
|
searchIssues(repository.owner, repository.name, query),
|
||||||
|
countWikiPages(repository.owner, repository.name, query),
|
||||||
|
query, page, repository)
|
||||||
|
|
||||||
|
case "wiki" => gitbucket.core.search.html.wiki(
|
||||||
|
countFiles(repository.owner, repository.name, query),
|
||||||
|
countIssues(repository.owner, repository.name, query),
|
||||||
|
searchWikiPages(repository.owner, repository.name, query),
|
||||||
|
query, page, repository)
|
||||||
|
|
||||||
|
case _ => gitbucket.core.search.html.code(
|
||||||
|
searchFiles(repository.owner, repository.name, query),
|
||||||
|
countIssues(repository.owner, repository.name, query),
|
||||||
|
countWikiPages(repository.owner, repository.name, query),
|
||||||
|
query, page, repository)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
import gitbucket.core.api._
|
|
||||||
import gitbucket.core.issues.html
|
import gitbucket.core.issues.html
|
||||||
import gitbucket.core.model.Issue
|
|
||||||
import gitbucket.core.service.IssuesService._
|
import gitbucket.core.service.IssuesService._
|
||||||
import gitbucket.core.service._
|
import gitbucket.core.service._
|
||||||
import gitbucket.core.util.ControlUtil._
|
import gitbucket.core.util.ControlUtil._
|
||||||
@@ -16,11 +14,11 @@ import org.scalatra.Ok
|
|||||||
|
|
||||||
|
|
||||||
class IssuesController extends IssuesControllerBase
|
class IssuesController extends IssuesControllerBase
|
||||||
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService
|
||||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService
|
||||||
|
|
||||||
trait IssuesControllerBase extends ControllerBase {
|
trait IssuesControllerBase extends ControllerBase {
|
||||||
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService
|
||||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService =>
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService =>
|
||||||
|
|
||||||
case class IssueCreateForm(title: String, content: Option[String],
|
case class IssueCreateForm(title: String, content: Option[String],
|
||||||
@@ -78,18 +76,6 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository =>
|
|
||||||
(for{
|
|
||||||
issueId <- params("id").toIntOpt
|
|
||||||
comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
|
|
||||||
} yield {
|
|
||||||
JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) })
|
|
||||||
}).getOrElse(NotFound)
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
|
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
|
||||||
defining(repository.owner, repository.name){ case (owner, name) =>
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
html.create(
|
html.create(
|
||||||
@@ -128,7 +114,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
getIssue(owner, name, issueId.toString).foreach { issue =>
|
getIssue(owner, name, issueId.toString).foreach { issue =>
|
||||||
// extract references and create refer comment
|
// extract references and create refer comment
|
||||||
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
|
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)
|
||||||
|
|
||||||
// call web hooks
|
// call web hooks
|
||||||
callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)
|
callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)
|
||||||
@@ -150,7 +136,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
// update issue
|
// update issue
|
||||||
updateIssue(owner, name, issue.issueId, title, issue.content)
|
updateIssue(owner, name, issue.issueId, title, issue.content)
|
||||||
// extract references and create refer comment
|
// extract references and create refer comment
|
||||||
createReferComment(owner, name, issue.copy(title = title), title)
|
createReferComment(owner, name, issue.copy(title = title), title, context.loginAccount.get)
|
||||||
|
|
||||||
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
||||||
} else Unauthorized
|
} else Unauthorized
|
||||||
@@ -165,7 +151,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
// update issue
|
// update issue
|
||||||
updateIssue(owner, name, issue.issueId, issue.title, content)
|
updateIssue(owner, name, issue.issueId, issue.title, content)
|
||||||
// extract references and create refer comment
|
// extract references and create refer comment
|
||||||
createReferComment(owner, name, issue, content.getOrElse(""))
|
createReferComment(owner, name, issue, content.getOrElse(""), context.loginAccount.get)
|
||||||
|
|
||||||
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
||||||
} else Unauthorized
|
} else Unauthorized
|
||||||
@@ -174,30 +160,22 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
|
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
|
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
|
||||||
redirect(s"/${repository.owner}/${repository.name}/${
|
val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
|
||||||
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
handleComment(issue, Some(form.content), repository, actionOpt) map { case (issue, id) =>
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/${
|
||||||
|
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
||||||
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/issues/comments/#create-a-comment
|
|
||||||
*/
|
|
||||||
post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository =>
|
|
||||||
(for{
|
|
||||||
issueId <- params("id").toIntOpt
|
|
||||||
body <- extractFromJsonBody[CreateAComment].map(_.body) if ! body.isEmpty
|
|
||||||
(issue, id) <- handleComment(issueId, Some(body), repository)()
|
|
||||||
issueComment <- getComment(repository.owner, repository.name, id.toString())
|
|
||||||
} yield {
|
|
||||||
JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest))
|
|
||||||
}) getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
|
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
|
||||||
handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
|
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
|
||||||
redirect(s"/${repository.owner}/${repository.name}/${
|
val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
|
||||||
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
handleComment(issue, form.content, repository, actionOpt) map { case (issue, id) =>
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/${
|
||||||
|
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
||||||
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -315,8 +293,16 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
|
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
|
||||||
defining(params.get("value")){ action =>
|
defining(params.get("value")){ action =>
|
||||||
action match {
|
action match {
|
||||||
case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) }
|
case Some("open") => executeBatch(repository) { issueId =>
|
||||||
case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) }
|
getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
|
||||||
|
handleComment(issue, None, repository, Some("reopen"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Some("close") => executeBatch(repository) { issueId =>
|
||||||
|
getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
|
||||||
|
handleComment(issue, None, repository, Some("close"))
|
||||||
|
}
|
||||||
|
}
|
||||||
case _ => // TODO BadRequest
|
case _ => // TODO BadRequest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,99 +359,6 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Same method exists in PullRequestController. Should it moved to IssueService?
|
|
||||||
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
|
|
||||||
StringUtil.extractIssueId(message).foreach { issueId =>
|
|
||||||
val content = fromIssue.issueId + ":" + fromIssue.title
|
|
||||||
if(getIssue(owner, repository, issueId).isDefined){
|
|
||||||
// Not add if refer comment already exist.
|
|
||||||
if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
|
|
||||||
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, content, "refer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
|
|
||||||
*/
|
|
||||||
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
|
|
||||||
(getAction: Issue => Option[String] =
|
|
||||||
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
|
|
||||||
|
|
||||||
defining(repository.owner, repository.name){ case (owner, name) =>
|
|
||||||
val userName = context.loginAccount.get.userName
|
|
||||||
|
|
||||||
getIssue(owner, name, issueId.toString) flatMap { issue =>
|
|
||||||
val (action, recordActivity) =
|
|
||||||
getAction(issue)
|
|
||||||
.collect {
|
|
||||||
case "close" if(!issue.closed) => true ->
|
|
||||||
(Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
|
|
||||||
case "reopen" if(issue.closed) => false ->
|
|
||||||
(Some("reopen") -> Some(recordReopenIssueActivity _))
|
|
||||||
}
|
|
||||||
.map { case (closed, t) =>
|
|
||||||
updateClosed(owner, name, issueId, closed)
|
|
||||||
t
|
|
||||||
}
|
|
||||||
.getOrElse(None -> None)
|
|
||||||
|
|
||||||
val commentId = (content, action) match {
|
|
||||||
case (None, None) => None
|
|
||||||
case (None, Some(action)) => Some(createComment(owner, name, userName, issueId, action.capitalize, action))
|
|
||||||
case (Some(content), _) => Some(createComment(owner, name, userName, issueId, content, action.map(_+ "_comment").getOrElse("comment")))
|
|
||||||
}
|
|
||||||
|
|
||||||
// record comment activity if comment is entered
|
|
||||||
content foreach {
|
|
||||||
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
|
|
||||||
(owner, name, userName, issueId, _)
|
|
||||||
}
|
|
||||||
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
|
|
||||||
|
|
||||||
// extract references and create refer comment
|
|
||||||
content.map { content =>
|
|
||||||
createReferComment(owner, name, issue, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// call web hooks
|
|
||||||
action match {
|
|
||||||
case None => commentId.map{ commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) }
|
|
||||||
case Some(act) => val webHookAction = act match {
|
|
||||||
case "open" => "opened"
|
|
||||||
case "reopen" => "reopened"
|
|
||||||
case "close" => "closed"
|
|
||||||
case _ => act
|
|
||||||
}
|
|
||||||
if(issue.isPullRequest){
|
|
||||||
callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get)
|
|
||||||
} else {
|
|
||||||
callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// notifications
|
|
||||||
Notifier() match {
|
|
||||||
case f =>
|
|
||||||
content foreach {
|
|
||||||
f.toNotify(repository, issue, _){
|
|
||||||
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
|
|
||||||
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId.get}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
action foreach {
|
|
||||||
f.toNotify(repository, issue, _){
|
|
||||||
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commentId.map( issue -> _ )
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
|
private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
|
||||||
defining(repository.owner, repository.name){ case (owner, repoName) =>
|
defining(repository.owner, repository.name){ case (owner, repoName) =>
|
||||||
val page = IssueSearchCondition.page(request)
|
val page = IssueSearchCondition.page(request)
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
import gitbucket.core.api.{ApiError, CreateALabel, ApiLabel, JsonFormat}
|
|
||||||
import gitbucket.core.issues.labels.html
|
import gitbucket.core.issues.labels.html
|
||||||
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
|
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
|
||||||
import gitbucket.core.util.{LockUtil, RepositoryName, ReferrerAuthenticator, CollaboratorsAuthenticator}
|
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
|
||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
import io.github.gitbucket.scalatra.forms._
|
import io.github.gitbucket.scalatra.forms._
|
||||||
import org.scalatra.i18n.Messages
|
import org.scalatra.i18n.Messages
|
||||||
import org.scalatra.{NoContent, UnprocessableEntity, Created, Ok}
|
import org.scalatra.Ok
|
||||||
|
|
||||||
class LabelsController extends LabelsControllerBase
|
class LabelsController extends LabelsControllerBase
|
||||||
with LabelsService with IssuesService with RepositoryService with AccountService
|
with LabelsService with IssuesService with RepositoryService with AccountService
|
||||||
@@ -24,6 +23,7 @@ trait LabelsControllerBase extends ControllerBase {
|
|||||||
"labelColor" -> trim(label("Color", text(required, color)))
|
"labelColor" -> trim(label("Color", text(required, color)))
|
||||||
)(LabelForm.apply)
|
)(LabelForm.apply)
|
||||||
|
|
||||||
|
|
||||||
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
|
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
|
||||||
html.list(
|
html.list(
|
||||||
getLabels(repository.owner, repository.name),
|
getLabels(repository.owner, repository.name),
|
||||||
@@ -32,26 +32,6 @@ trait LabelsControllerBase extends ControllerBase {
|
|||||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* List all labels for this repository
|
|
||||||
* https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repository/labels")(referrersOnly { repository =>
|
|
||||||
JsonFormat(getLabels(repository.owner, repository.name).map { label =>
|
|
||||||
ApiLabel(label, RepositoryName(repository))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a single label
|
|
||||||
* https://developer.github.com/v3/issues/labels/#get-a-single-label
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repository/labels/:labelName")(referrersOnly { repository =>
|
|
||||||
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
|
|
||||||
JsonFormat(ApiLabel(label, RepositoryName(repository)))
|
|
||||||
} getOrElse NotFound()
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
|
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
|
||||||
html.edit(None, repository)
|
html.edit(None, repository)
|
||||||
})
|
})
|
||||||
@@ -66,31 +46,6 @@ trait LabelsControllerBase extends ControllerBase {
|
|||||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a label
|
|
||||||
* https://developer.github.com/v3/issues/labels/#create-a-label
|
|
||||||
*/
|
|
||||||
post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository =>
|
|
||||||
(for{
|
|
||||||
data <- extractFromJsonBody[CreateALabel] if data.isValid
|
|
||||||
} yield {
|
|
||||||
LockUtil.lock(RepositoryName(repository).fullName) {
|
|
||||||
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
|
|
||||||
val labelId = createLabel(repository.owner, repository.name, data.name, data.color)
|
|
||||||
getLabel(repository.owner, repository.name, labelId).map { label =>
|
|
||||||
Created(JsonFormat(ApiLabel(label, RepositoryName(repository))))
|
|
||||||
} getOrElse NotFound()
|
|
||||||
} else {
|
|
||||||
// TODO ApiError should support errors field to enhance compatibility of GitHub API
|
|
||||||
UnprocessableEntity(ApiError(
|
|
||||||
"Validation Failed",
|
|
||||||
Some("https://developer.github.com/v3/issues/labels/#create-a-label")
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) getOrElse NotFound()
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
|
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
|
||||||
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
|
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
|
||||||
html.edit(Some(label), repository)
|
html.edit(Some(label), repository)
|
||||||
@@ -107,50 +62,11 @@ trait LabelsControllerBase extends ControllerBase {
|
|||||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a label
|
|
||||||
* https://developer.github.com/v3/issues/labels/#update-a-label
|
|
||||||
*/
|
|
||||||
patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
|
|
||||||
(for{
|
|
||||||
data <- extractFromJsonBody[CreateALabel] if data.isValid
|
|
||||||
} yield {
|
|
||||||
LockUtil.lock(RepositoryName(repository).fullName) {
|
|
||||||
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
|
|
||||||
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
|
|
||||||
updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color)
|
|
||||||
JsonFormat(ApiLabel(
|
|
||||||
getLabel(repository.owner, repository.name, label.labelId).get,
|
|
||||||
RepositoryName(repository)))
|
|
||||||
} else {
|
|
||||||
// TODO ApiError should support errors field to enhance compatibility of GitHub API
|
|
||||||
UnprocessableEntity(ApiError(
|
|
||||||
"Validation Failed",
|
|
||||||
Some("https://developer.github.com/v3/issues/labels/#create-a-label")))
|
|
||||||
}
|
|
||||||
} getOrElse NotFound()
|
|
||||||
}
|
|
||||||
}) getOrElse NotFound()
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository =>
|
ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository =>
|
||||||
deleteLabel(repository.owner, repository.name, params("labelId").toInt)
|
deleteLabel(repository.owner, repository.name, params("labelId").toInt)
|
||||||
Ok()
|
Ok()
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a label
|
|
||||||
* https://developer.github.com/v3/issues/labels/#delete-a-label
|
|
||||||
*/
|
|
||||||
delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
|
|
||||||
LockUtil.lock(RepositoryName(repository).fullName) {
|
|
||||||
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
|
|
||||||
deleteLabel(repository.owner, repository.name, label.labelId)
|
|
||||||
NoContent()
|
|
||||||
} getOrElse NotFound()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constraint for the identifier such as user name, repository name or page name.
|
* Constraint for the identifier such as user name, repository name or page name.
|
||||||
*/
|
*/
|
||||||
@@ -169,7 +85,11 @@ trait LabelsControllerBase extends ControllerBase {
|
|||||||
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = {
|
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = {
|
||||||
val owner = params("owner")
|
val owner = params("owner")
|
||||||
val repository = params("repository")
|
val repository = params("repository")
|
||||||
getLabel(owner, repository, value).map(_ => "Name has already been taken.")
|
params.get("labelId").map { labelId =>
|
||||||
|
getLabel(owner, repository, value).filter(_.labelId != labelId.toInt).map(_ => "Name has already been taken.")
|
||||||
|
}.getOrElse {
|
||||||
|
getLabel(owner, repository, value).map(_ => "Name has already been taken.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package gitbucket.core.controller
|
|
||||||
|
|
||||||
import gitbucket.core.admin.plugins.html
|
|
||||||
import gitbucket.core.plugin.PluginRegistry
|
|
||||||
import gitbucket.core.util.AdminAuthenticator
|
|
||||||
|
|
||||||
class PluginsController extends ControllerBase with AdminAuthenticator {
|
|
||||||
get("/admin/plugins")(adminOnly {
|
|
||||||
html.plugins(PluginRegistry().getPlugins())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
import gitbucket.core.api._
|
import gitbucket.core.model.WebHook
|
||||||
import gitbucket.core.model.{Account, CommitStatus, CommitState, Repository, PullRequest, Issue, WebHook}
|
|
||||||
import gitbucket.core.pulls.html
|
import gitbucket.core.pulls.html
|
||||||
import gitbucket.core.service.CommitStatusService
|
import gitbucket.core.service.CommitStatusService
|
||||||
import gitbucket.core.service.MergeService
|
import gitbucket.core.service.MergeService
|
||||||
@@ -82,24 +81,6 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/pulls/#list-pull-requests
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository =>
|
|
||||||
val page = IssueSearchCondition.page(request)
|
|
||||||
// TODO: more api spec condition
|
|
||||||
val condition = IssueSearchCondition(request)
|
|
||||||
val baseOwner = getAccountByUserName(repository.owner).get
|
|
||||||
val issues:List[(Issue, Account, Int, PullRequest, Repository, Account)] = searchPullRequestByApi(condition, (page - 1) * PullRequestLimit, PullRequestLimit, repository.owner -> repository.name)
|
|
||||||
JsonFormat(issues.map{case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) =>
|
|
||||||
ApiPullRequest(
|
|
||||||
issue,
|
|
||||||
pullRequest,
|
|
||||||
ApiRepository(headRepo, ApiUser(headOwner)),
|
|
||||||
ApiRepository(repository, ApiUser(baseOwner)),
|
|
||||||
ApiUser(issueUser)) })
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
|
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
|
||||||
params("id").toIntOpt.flatMap{ issueId =>
|
params("id").toIntOpt.flatMap{ issueId =>
|
||||||
val owner = repository.owner
|
val owner = repository.owner
|
||||||
@@ -126,47 +107,6 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/pulls/#get-a-single-pull-request
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository =>
|
|
||||||
(for{
|
|
||||||
issueId <- params("id").toIntOpt
|
|
||||||
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
|
|
||||||
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set())
|
|
||||||
baseOwner <- users.get(repository.owner)
|
|
||||||
headOwner <- users.get(pullRequest.requestUserName)
|
|
||||||
issueUser <- users.get(issue.openedUserName)
|
|
||||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
|
|
||||||
} yield {
|
|
||||||
JsonFormat(ApiPullRequest(
|
|
||||||
issue,
|
|
||||||
pullRequest,
|
|
||||||
ApiRepository(headRepo, ApiUser(headOwner)),
|
|
||||||
ApiRepository(repository, ApiUser(baseOwner)),
|
|
||||||
ApiUser(issueUser)))
|
|
||||||
}).getOrElse(NotFound)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repository/pulls/:id/commits")(referrersOnly { repository =>
|
|
||||||
val owner = repository.owner
|
|
||||||
val name = repository.name
|
|
||||||
params("id").toIntOpt.flatMap{ issueId =>
|
|
||||||
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
|
||||||
using(Git.open(getRepositoryDir(owner, name))){ git =>
|
|
||||||
val oldId = git.getRepository.resolve(pullreq.commitIdFrom)
|
|
||||||
val newId = git.getRepository.resolve(pullreq.commitIdTo)
|
|
||||||
val repoFullName = RepositoryName(repository)
|
|
||||||
val commits = git.log.addRange(oldId, newId).call.iterator.asScala.map(c => ApiCommitListItem(new CommitInfo(c), repoFullName)).toList
|
|
||||||
JsonFormat(commits)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository =>
|
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository =>
|
||||||
params("id").toIntOpt.flatMap{ issueId =>
|
params("id").toIntOpt.flatMap{ issueId =>
|
||||||
val owner = repository.owner
|
val owner = repository.owner
|
||||||
@@ -196,7 +136,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
issue,
|
issue,
|
||||||
pullreq,
|
pullreq,
|
||||||
repository,
|
repository,
|
||||||
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get)
|
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName).get)
|
||||||
}
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
@@ -229,7 +169,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
if(branchProtection.needStatusCheck(loginAccount.userName)){
|
if(branchProtection.needStatusCheck(loginAccount.userName)){
|
||||||
flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check."
|
flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check."
|
||||||
} else {
|
} else {
|
||||||
val repository = getRepository(owner, name, context.baseUrl).get
|
val repository = getRepository(owner, name).get
|
||||||
LockUtil.lock(s"${owner}/${name}"){
|
LockUtil.lock(s"${owner}/${name}"){
|
||||||
val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){
|
val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){
|
||||||
pullreq.branch
|
pullreq.branch
|
||||||
@@ -238,7 +178,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
val existIds = using(Git.open(Directory.getRepositoryDir(owner, name))) { git => JGitUtil.getAllCommitIds(git) }.toSet
|
val existIds = using(Git.open(Directory.getRepositoryDir(owner, name))) { git => JGitUtil.getAllCommitIds(git) }.toSet
|
||||||
pullRemote(owner, name, pullreq.requestBranch, pullreq.userName, pullreq.repositoryName, pullreq.branch, loginAccount,
|
pullRemote(owner, name, pullreq.requestBranch, pullreq.userName, pullreq.repositoryName, pullreq.branch, loginAccount,
|
||||||
"Merge branch '${alias}' into ${pullreq.requestBranch}") match {
|
s"Merge branch '${alias}' into ${pullreq.requestBranch}") match {
|
||||||
case None => // conflict
|
case None => // conflict
|
||||||
flash += "error" -> s"Can't automatic merging branch '${alias}' into ${pullreq.requestBranch}."
|
flash += "error" -> s"Can't automatic merging branch '${alias}' into ${pullreq.requestBranch}."
|
||||||
case Some(oldId) =>
|
case Some(oldId) =>
|
||||||
@@ -310,7 +250,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
|
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
|
||||||
|
|
||||||
// close issue by content of pull request
|
// close issue by content of pull request
|
||||||
val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch
|
val defaultBranch = getRepository(owner, name).get.repository.defaultBranch
|
||||||
if(pullreq.branch == defaultBranch){
|
if(pullreq.branch == defaultBranch){
|
||||||
commits.flatten.foreach { commit =>
|
commits.flatten.foreach { commit =>
|
||||||
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
|
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
|
||||||
@@ -343,7 +283,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
val headBranch:Option[String] = params.get("head")
|
val headBranch:Option[String] = params.get("head")
|
||||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||||
case (Some(originUserName), Some(originRepositoryName)) => {
|
case (Some(originUserName), Some(originRepositoryName)) => {
|
||||||
getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository =>
|
getRepository(originUserName, originRepositoryName).map { originRepository =>
|
||||||
using(
|
using(
|
||||||
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
|
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
|
||||||
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||||
@@ -384,12 +324,12 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
forkedRepository.repository.originRepositoryName
|
forkedRepository.repository.originRepositoryName
|
||||||
} else {
|
} else {
|
||||||
// Sibling repository
|
// Sibling repository
|
||||||
getUserRepositories(originOwner, context.baseUrl).find { x =>
|
getUserRepositories(originOwner).find { x =>
|
||||||
x.repository.originUserName == forkedRepository.repository.originUserName &&
|
x.repository.originUserName == forkedRepository.repository.originUserName &&
|
||||||
x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName
|
x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName
|
||||||
}.map(_.repository.repositoryName)
|
}.map(_.repository.repositoryName)
|
||||||
};
|
};
|
||||||
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
|
originRepository <- getRepository(originOwner, originRepositoryName)
|
||||||
) yield {
|
) yield {
|
||||||
using(
|
using(
|
||||||
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
||||||
@@ -457,7 +397,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
|
originRepository <- getRepository(originOwner, originRepositoryName)
|
||||||
) yield {
|
) yield {
|
||||||
using(
|
using(
|
||||||
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
||||||
@@ -523,7 +463,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
getIssue(owner, name, issueId.toString) foreach { issue =>
|
getIssue(owner, name, issueId.toString) foreach { issue =>
|
||||||
// extract references and create refer comment
|
// extract references and create refer comment
|
||||||
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
|
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)
|
||||||
|
|
||||||
// notifications
|
// notifications
|
||||||
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
|
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
|
||||||
@@ -535,19 +475,6 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO Same method exists in IssueController. Should it moved to IssueService?
|
|
||||||
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
|
|
||||||
StringUtil.extractIssueId(message).foreach { issueId =>
|
|
||||||
val content = fromIssue.issueId + ":" + fromIssue.title
|
|
||||||
if(getIssue(owner, repository, issueId).isDefined){
|
|
||||||
// Not add if refer comment already exist.
|
|
||||||
if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
|
|
||||||
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, content, "refer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses branch identifier and extracts owner and branch name as tuple.
|
* Parses branch identifier and extracts owner and branch name as tuple.
|
||||||
*
|
*
|
||||||
@@ -611,14 +538,4 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
hasWritePermission(owner, repoName, context.loginAccount))
|
hasWritePermission(owner, repoName, context.loginAccount))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: same as gitbucket.core.servlet.CommitLogHook ...
|
|
||||||
private def createIssueComment(owner: String, repository: String, commit: CommitInfo) = {
|
|
||||||
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
|
|
||||||
if(getIssue(owner, repository, issueId).isDefined){
|
|
||||||
getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
|
|
||||||
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.scalatra.i18n.Messages
|
|||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import org.eclipse.jgit.lib.Constants
|
import org.eclipse.jgit.lib.Constants
|
||||||
import org.eclipse.jgit.lib.ObjectId
|
import org.eclipse.jgit.lib.ObjectId
|
||||||
|
import gitbucket.core.model.WebHookContentType
|
||||||
|
|
||||||
|
|
||||||
class RepositorySettingsController extends RepositorySettingsControllerBase
|
class RepositorySettingsController extends RepositorySettingsControllerBase
|
||||||
@@ -49,12 +50,16 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
)(CollaboratorForm.apply)
|
)(CollaboratorForm.apply)
|
||||||
|
|
||||||
// for web hook url addition
|
// for web hook url addition
|
||||||
case class WebHookForm(url: String, events: Set[WebHook.Event])
|
case class WebHookForm(url: String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])
|
||||||
|
|
||||||
def webHookForm(update:Boolean) = mapping(
|
def webHookForm(update:Boolean) = mapping(
|
||||||
"url" -> trim(label("url", text(required, webHook(update)))),
|
"url" -> trim(label("url", text(required, webHook(update)))),
|
||||||
"events" -> webhookEvents
|
"events" -> webhookEvents,
|
||||||
)(WebHookForm.apply)
|
"ctype" -> label("ctype", text()),
|
||||||
|
"token" -> optional(trim(label("token", text(maxlength(100)))))
|
||||||
|
)(
|
||||||
|
(url, events, ctype, token) => WebHookForm(url, events, WebHookContentType.valueOf(ctype), token)
|
||||||
|
)
|
||||||
|
|
||||||
// for transfer ownership
|
// for transfer ownership
|
||||||
case class TransferOwnerShipForm(newOwner: String)
|
case class TransferOwnerShipForm(newOwner: String)
|
||||||
@@ -141,22 +146,6 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
|
|
||||||
patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository =>
|
|
||||||
import gitbucket.core.api._
|
|
||||||
(for{
|
|
||||||
branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined
|
|
||||||
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
|
|
||||||
} yield {
|
|
||||||
if(protection.enabled){
|
|
||||||
enableBranchProtection(repository.owner, repository.name, branch, protection.status.enforcement_level == ApiBranchProtection.Everyone, protection.status.contexts)
|
|
||||||
} else {
|
|
||||||
disableBranchProtection(repository.owner, repository.name, branch)
|
|
||||||
}
|
|
||||||
JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository)))
|
|
||||||
}) getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the Collaborators page.
|
* Display the Collaborators page.
|
||||||
*/
|
*/
|
||||||
@@ -198,7 +187,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
* Display the web hook edit page.
|
* Display the web hook edit page.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository =>
|
get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository =>
|
||||||
val webhook = WebHook(repository.owner, repository.name, "")
|
val webhook = WebHook(repository.owner, repository.name, "", WebHookContentType.FORM, None)
|
||||||
html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true)
|
html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -206,7 +195,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
* Add the web hook URL.
|
* Add the web hook URL.
|
||||||
*/
|
*/
|
||||||
post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) =>
|
post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) =>
|
||||||
addWebHook(repository.owner, repository.name, form.url, form.events)
|
addWebHook(repository.owner, repository.name, form.url, form.events, form.ctype, form.token)
|
||||||
flash += "info" -> s"Webhook ${form.url} created"
|
flash += "info" -> s"Webhook ${form.url} created"
|
||||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||||
})
|
})
|
||||||
@@ -235,7 +224,9 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
import scala.concurrent.ExecutionContext.Implicits.global
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
|
||||||
val url = params("url")
|
val url = params("url")
|
||||||
val dummyWebHookInfo = WebHook(repository.owner, repository.name, url)
|
val token = Some(params("token"))
|
||||||
|
val ctype = WebHookContentType.valueOf(params("ctype"))
|
||||||
|
val dummyWebHookInfo = WebHook(repository.owner, repository.name, url, ctype, token)
|
||||||
val dummyPayload = {
|
val dummyPayload = {
|
||||||
val ownerAccount = getAccountByUserName(repository.owner).get
|
val ownerAccount = getAccountByUserName(repository.owner).get
|
||||||
val commits = if(repository.commitCount == 0) List.empty else git.log
|
val commits = if(repository.commitCount == 0) List.empty else git.log
|
||||||
@@ -294,7 +285,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
* Update web hook settings.
|
* Update web hook settings.
|
||||||
*/
|
*/
|
||||||
post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) =>
|
post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) =>
|
||||||
updateWebHook(repository.owner, repository.name, form.url, form.events)
|
updateWebHook(repository.owner, repository.name, form.url, form.events, form.ctype, form.token)
|
||||||
flash += "info" -> s"webhook ${form.url} updated"
|
flash += "info" -> s"webhook ${form.url} updated"
|
||||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package gitbucket.core.controller
|
|||||||
|
|
||||||
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||||
|
|
||||||
import gitbucket.core.api._
|
|
||||||
import gitbucket.core.plugin.PluginRegistry
|
import gitbucket.core.plugin.PluginRegistry
|
||||||
import gitbucket.core.repo.html
|
import gitbucket.core.repo.html
|
||||||
import gitbucket.core.helper
|
import gitbucket.core.helper
|
||||||
@@ -13,7 +12,7 @@ import gitbucket.core.util.StringUtil._
|
|||||||
import gitbucket.core.util.ControlUtil._
|
import gitbucket.core.util.ControlUtil._
|
||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
import gitbucket.core.util.Directory._
|
import gitbucket.core.util.Directory._
|
||||||
import gitbucket.core.model.{Account, CommitState, WebHook}
|
import gitbucket.core.model.{Account, WebHook}
|
||||||
import gitbucket.core.service.WebHookService._
|
import gitbucket.core.service.WebHookService._
|
||||||
import gitbucket.core.view
|
import gitbucket.core.view
|
||||||
import gitbucket.core.view.helpers
|
import gitbucket.core.view.helpers
|
||||||
@@ -122,13 +121,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
fileList(_)
|
fileList(_)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/repos/#get
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repository")(referrersOnly { repository =>
|
|
||||||
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(repository.owner).get)))
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the file list of the specified path and branch.
|
* Displays the file list of the specified path and branch.
|
||||||
*/
|
*/
|
||||||
@@ -160,65 +152,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/repos/statuses/#create-a-status
|
|
||||||
*/
|
|
||||||
post("/api/v3/repos/:owner/:repo/statuses/:sha")(collaboratorsOnly { repository =>
|
|
||||||
(for{
|
|
||||||
ref <- params.get("sha")
|
|
||||||
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
|
||||||
data <- extractFromJsonBody[CreateAStatus] if data.isValid
|
|
||||||
creator <- context.loginAccount
|
|
||||||
state <- CommitState.valueOf(data.state)
|
|
||||||
statusId = createCommitStatus(repository.owner, repository.name, sha, data.context.getOrElse("default"),
|
|
||||||
state, data.target_url, data.description, new java.util.Date(), creator)
|
|
||||||
status <- getCommitStatus(repository.owner, repository.name, statusId)
|
|
||||||
} yield {
|
|
||||||
JsonFormat(ApiCommitStatus(status, ApiUser(creator)))
|
|
||||||
}) getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
|
|
||||||
*
|
|
||||||
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
|
|
||||||
*/
|
|
||||||
val listStatusesRoute = get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository =>
|
|
||||||
(for{
|
|
||||||
ref <- params.get("ref")
|
|
||||||
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
|
||||||
} yield {
|
|
||||||
JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) =>
|
|
||||||
ApiCommitStatus(status, ApiUser(creator))
|
|
||||||
})
|
|
||||||
}) getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
|
|
||||||
*
|
|
||||||
* legacy route
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repo/statuses/:ref"){
|
|
||||||
listStatusesRoute.action()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
|
|
||||||
*
|
|
||||||
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repo/commits/:ref/status")(referrersOnly { repository =>
|
|
||||||
(for{
|
|
||||||
ref <- params.get("ref")
|
|
||||||
owner <- getAccountByUserName(repository.owner)
|
|
||||||
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
|
||||||
} yield {
|
|
||||||
val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha)
|
|
||||||
JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner)))
|
|
||||||
}) getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
|
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
|
||||||
val (branch, path) = splitPath(repository, multiParams("splat").head)
|
val (branch, path) = splitPath(repository, multiParams("splat").head)
|
||||||
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
|
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
|
||||||
@@ -560,11 +493,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
html.forked(
|
html.forked(
|
||||||
getRepository(
|
getRepository(
|
||||||
repository.repository.originUserName.getOrElse(repository.owner),
|
repository.repository.originUserName.getOrElse(repository.owner),
|
||||||
repository.repository.originRepositoryName.getOrElse(repository.name),
|
repository.repository.originRepositoryName.getOrElse(repository.name)),
|
||||||
context.baseUrl),
|
|
||||||
getForkedRepositories(
|
getForkedRepositories(
|
||||||
repository.repository.originUserName.getOrElse(repository.owner),
|
repository.repository.originUserName.getOrElse(repository.owner),
|
||||||
repository.repository.originRepositoryName.getOrElse(repository.name)),
|
repository.repository.originRepositoryName.getOrElse(repository.name)),
|
||||||
|
context.loginAccount match {
|
||||||
|
case None => List()
|
||||||
|
case account: Option[Account] => getGroupsByUserName(account.get.userName)
|
||||||
|
}, // groups of current user
|
||||||
repository)
|
repository)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -575,13 +511,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
val ref = multiParams("splat").head
|
val ref = multiParams("splat").head
|
||||||
JGitUtil.getTreeId(git, ref).map{ treeId =>
|
JGitUtil.getTreeId(git, ref).map{ treeId =>
|
||||||
html.find(ref,
|
html.find(ref, treeId, repository)
|
||||||
treeId,
|
|
||||||
repository,
|
|
||||||
context.loginAccount match {
|
|
||||||
case None => List()
|
|
||||||
case account: Option[Account] => getGroupsByUserName(account.get.userName)
|
|
||||||
})
|
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -643,10 +573,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
html.files(revision, repository,
|
html.files(revision, repository,
|
||||||
if(path == ".") Nil else path.split("/").toList, // current path
|
if(path == ".") Nil else path.split("/").toList, // current path
|
||||||
context.loginAccount match {
|
|
||||||
case None => List()
|
|
||||||
case account: Option[Account] => getGroupsByUserName(account.get.userName)
|
|
||||||
}, // groups of current user
|
|
||||||
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
|
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
|
||||||
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
||||||
getPullRequestFromBranch(repository.owner, repository.name, revstr, repository.repository.defaultBranch),
|
getPullRequestFromBranch(repository.owner, repository.name, revstr, repository.repository.defaultBranch),
|
||||||
@@ -759,8 +685,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
.setTree(revCommit.getTree)
|
.setTree(revCommit.getTree)
|
||||||
.setOutputStream(response.getOutputStream)
|
.setOutputStream(response.getOutputStream)
|
||||||
.call()
|
.call()
|
||||||
|
|
||||||
Unit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
package gitbucket.core.controller
|
|
||||||
|
|
||||||
import gitbucket.core.search.html
|
|
||||||
import gitbucket.core.service._
|
|
||||||
import gitbucket.core.util.{StringUtil, ControlUtil, ReferrerAuthenticator, Implicits}
|
|
||||||
import ControlUtil._
|
|
||||||
import Implicits._
|
|
||||||
import io.github.gitbucket.scalatra.forms._
|
|
||||||
|
|
||||||
class SearchController extends SearchControllerBase
|
|
||||||
with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator
|
|
||||||
|
|
||||||
trait SearchControllerBase extends ControllerBase { self: RepositoryService
|
|
||||||
with ActivityService with RepositorySearchService with ReferrerAuthenticator =>
|
|
||||||
|
|
||||||
val searchForm = mapping(
|
|
||||||
"query" -> trim(text(required)),
|
|
||||||
"owner" -> trim(text(required)),
|
|
||||||
"repository" -> trim(text(required))
|
|
||||||
)(SearchForm.apply)
|
|
||||||
|
|
||||||
case class SearchForm(query: String, owner: String, repository: String)
|
|
||||||
|
|
||||||
post("/search", searchForm){ form =>
|
|
||||||
redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
|
|
||||||
}
|
|
||||||
|
|
||||||
get("/:owner/:repository/search")(referrersOnly { repository =>
|
|
||||||
defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) =>
|
|
||||||
val page = try {
|
|
||||||
val i = params.getOrElse("page", "1").toInt
|
|
||||||
if(i <= 0) 1 else i
|
|
||||||
} catch {
|
|
||||||
case e: NumberFormatException => 1
|
|
||||||
}
|
|
||||||
|
|
||||||
target.toLowerCase match {
|
|
||||||
case "issue" => html.issues(
|
|
||||||
searchIssues(repository.owner, repository.name, query),
|
|
||||||
countFiles(repository.owner, repository.name, query),
|
|
||||||
query, page, repository)
|
|
||||||
|
|
||||||
case _ => html.code(
|
|
||||||
searchFiles(repository.owner, repository.name, query),
|
|
||||||
countIssues(repository.owner, repository.name, query),
|
|
||||||
query, page, repository)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,24 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
import gitbucket.core.admin.html
|
import gitbucket.core.admin.html
|
||||||
import gitbucket.core.service.{AccountService, SystemSettingsService}
|
import gitbucket.core.service.{AccountService, SystemSettingsService, RepositoryService}
|
||||||
import gitbucket.core.util.AdminAuthenticator
|
import gitbucket.core.util.AdminAuthenticator
|
||||||
import gitbucket.core.ssh.SshServer
|
import gitbucket.core.ssh.SshServer
|
||||||
|
import gitbucket.core.plugin.PluginRegistry
|
||||||
import SystemSettingsService._
|
import SystemSettingsService._
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
|
import gitbucket.core.util.ControlUtil._
|
||||||
|
import gitbucket.core.util.Directory._
|
||||||
|
import gitbucket.core.util.StringUtil._
|
||||||
import io.github.gitbucket.scalatra.forms._
|
import io.github.gitbucket.scalatra.forms._
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.scalatra.i18n.Messages
|
||||||
|
|
||||||
class SystemSettingsController extends SystemSettingsControllerBase
|
class SystemSettingsController extends SystemSettingsControllerBase
|
||||||
with AccountService with AdminAuthenticator
|
with AccountService with RepositoryService with AdminAuthenticator
|
||||||
|
|
||||||
trait SystemSettingsControllerBase extends ControllerBase {
|
trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||||
self: AccountService with AdminAuthenticator =>
|
self: AccountService with RepositoryService with AdminAuthenticator =>
|
||||||
|
|
||||||
private val form = mapping(
|
private val form = mapping(
|
||||||
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
||||||
@@ -23,6 +30,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
|||||||
"notification" -> trim(label("Notification", boolean())),
|
"notification" -> trim(label("Notification", boolean())),
|
||||||
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
|
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
|
||||||
"ssh" -> trim(label("SSH access", boolean())),
|
"ssh" -> trim(label("SSH access", boolean())),
|
||||||
|
"sshHost" -> trim(label("SSH host", optional(text()))),
|
||||||
"sshPort" -> trim(label("SSH port", optional(number()))),
|
"sshPort" -> trim(label("SSH port", optional(number()))),
|
||||||
"useSMTP" -> trim(label("SMTP", boolean())),
|
"useSMTP" -> trim(label("SMTP", boolean())),
|
||||||
"smtp" -> optionalIfNotChecked("useSMTP", mapping(
|
"smtp" -> optionalIfNotChecked("useSMTP", mapping(
|
||||||
@@ -50,9 +58,14 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
|||||||
"keystore" -> trim(label("Keystore", optional(text())))
|
"keystore" -> trim(label("Keystore", optional(text())))
|
||||||
)(Ldap.apply))
|
)(Ldap.apply))
|
||||||
)(SystemSettings.apply).verifying { settings =>
|
)(SystemSettings.apply).verifying { settings =>
|
||||||
if(settings.ssh && settings.baseUrl.isEmpty){
|
Vector(
|
||||||
Seq("baseUrl" -> "Base URL is required if SSH access is enabled.")
|
if(settings.ssh && settings.baseUrl.isEmpty){
|
||||||
} else Nil
|
Some("baseUrl" -> "Base URL is required if SSH access is enabled.")
|
||||||
|
} else None,
|
||||||
|
if(settings.ssh && settings.sshHost.isEmpty){
|
||||||
|
Some("sshHost" -> "SSH host is required if SSH access is enabled.")
|
||||||
|
} else None
|
||||||
|
).flatten
|
||||||
}
|
}
|
||||||
|
|
||||||
private val pluginForm = mapping(
|
private val pluginForm = mapping(
|
||||||
@@ -61,6 +74,61 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
case class PluginForm(pluginIds: List[String])
|
case class PluginForm(pluginIds: List[String])
|
||||||
|
|
||||||
|
|
||||||
|
case class NewUserForm(userName: String, password: String, fullName: String,
|
||||||
|
mailAddress: String, isAdmin: Boolean,
|
||||||
|
url: Option[String], fileId: Option[String])
|
||||||
|
|
||||||
|
case class EditUserForm(userName: String, password: Option[String], fullName: String,
|
||||||
|
mailAddress: String, isAdmin: Boolean, url: Option[String],
|
||||||
|
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
|
||||||
|
|
||||||
|
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||||
|
members: String)
|
||||||
|
|
||||||
|
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||||
|
members: String, clearImage: Boolean, isRemoved: Boolean)
|
||||||
|
|
||||||
|
|
||||||
|
val newUserForm = mapping(
|
||||||
|
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||||
|
"password" -> trim(label("Password" ,text(required, maxlength(20)))),
|
||||||
|
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
||||||
|
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
|
||||||
|
"isAdmin" -> trim(label("User Type" ,boolean())),
|
||||||
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" ,optional(text())))
|
||||||
|
)(NewUserForm.apply)
|
||||||
|
|
||||||
|
val editUserForm = mapping(
|
||||||
|
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))),
|
||||||
|
"password" -> trim(label("Password" ,optional(text(maxlength(20))))),
|
||||||
|
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
||||||
|
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))),
|
||||||
|
"isAdmin" -> trim(label("User Type" ,boolean())),
|
||||||
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||||
|
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||||
|
"removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
|
||||||
|
)(EditUserForm.apply)
|
||||||
|
|
||||||
|
val newGroupForm = mapping(
|
||||||
|
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||||
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||||
|
"members" -> trim(label("Members" ,text(required, members)))
|
||||||
|
)(NewGroupForm.apply)
|
||||||
|
|
||||||
|
val editGroupForm = mapping(
|
||||||
|
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
|
||||||
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||||
|
"members" -> trim(label("Members" ,text(required, members))),
|
||||||
|
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||||
|
"removed" -> trim(label("Disable" ,boolean()))
|
||||||
|
)(EditGroupForm.apply)
|
||||||
|
|
||||||
|
|
||||||
get("/admin/system")(adminOnly {
|
get("/admin/system")(adminOnly {
|
||||||
html.system(flash.get("info"))
|
html.system(flash.get("info"))
|
||||||
})
|
})
|
||||||
@@ -68,20 +136,155 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
|||||||
post("/admin/system", form)(adminOnly { form =>
|
post("/admin/system", form)(adminOnly { form =>
|
||||||
saveSystemSettings(form)
|
saveSystemSettings(form)
|
||||||
|
|
||||||
if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){
|
if (form.sshAddress != context.settings.sshAddress) {
|
||||||
SshServer.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){
|
|
||||||
SshServer.start(
|
|
||||||
form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort),
|
|
||||||
form.baseUrl.get)
|
|
||||||
} else if(!form.ssh && SshServer.isActive){
|
|
||||||
SshServer.stop()
|
SshServer.stop()
|
||||||
|
for {
|
||||||
|
sshAddress <- form.sshAddress
|
||||||
|
baseUrl <- form.baseUrl
|
||||||
|
}
|
||||||
|
SshServer.start(sshAddress, baseUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
flash += "info" -> "System settings has been updated."
|
flash += "info" -> "System settings has been updated."
|
||||||
redirect("/admin/system")
|
redirect("/admin/system")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
get("/admin/plugins")(adminOnly {
|
||||||
|
html.plugins(PluginRegistry().getPlugins())
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
get("/admin/users")(adminOnly {
|
||||||
|
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
|
||||||
|
val users = getAllUsers(includeRemoved)
|
||||||
|
val members = users.collect { case account if(account.isGroupAccount) =>
|
||||||
|
account.userName -> getGroupMembers(account.userName).map(_.userName)
|
||||||
|
}.toMap
|
||||||
|
|
||||||
|
html.userlist(users, members, includeRemoved)
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/admin/users/_newuser")(adminOnly {
|
||||||
|
html.user(None)
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
|
||||||
|
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url)
|
||||||
|
updateImage(form.userName, form.fileId, false)
|
||||||
|
redirect("/admin/users")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/admin/users/:userName/_edituser")(adminOnly {
|
||||||
|
val userName = params("userName")
|
||||||
|
html.user(getAccountByUserName(userName, true))
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
|
||||||
|
val userName = params("userName")
|
||||||
|
getAccountByUserName(userName, true).map { account =>
|
||||||
|
|
||||||
|
if(form.isRemoved){
|
||||||
|
// Remove repositories
|
||||||
|
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
||||||
|
// deleteRepository(userName, repositoryName)
|
||||||
|
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
||||||
|
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
||||||
|
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
||||||
|
// }
|
||||||
|
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
||||||
|
removeUserRelatedData(userName)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAccount(account.copy(
|
||||||
|
password = form.password.map(sha1).getOrElse(account.password),
|
||||||
|
fullName = form.fullName,
|
||||||
|
mailAddress = form.mailAddress,
|
||||||
|
isAdmin = form.isAdmin,
|
||||||
|
url = form.url,
|
||||||
|
isRemoved = form.isRemoved))
|
||||||
|
|
||||||
|
updateImage(userName, form.fileId, form.clearImage)
|
||||||
|
redirect("/admin/users")
|
||||||
|
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/admin/users/_newgroup")(adminOnly {
|
||||||
|
html.usergroup(None, Nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
|
||||||
|
createGroup(form.groupName, form.url)
|
||||||
|
updateGroupMembers(form.groupName, form.members.split(",").map {
|
||||||
|
_.split(":") match {
|
||||||
|
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
||||||
|
}
|
||||||
|
}.toList)
|
||||||
|
updateImage(form.groupName, form.fileId, false)
|
||||||
|
redirect("/admin/users")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/admin/users/:groupName/_editgroup")(adminOnly {
|
||||||
|
defining(params("groupName")){ groupName =>
|
||||||
|
html.usergroup(getAccountByUserName(groupName, true), getGroupMembers(groupName))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
|
||||||
|
defining(params("groupName"), form.members.split(",").map {
|
||||||
|
_.split(":") match {
|
||||||
|
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
||||||
|
}
|
||||||
|
}.toList){ case (groupName, members) =>
|
||||||
|
getAccountByUserName(groupName, true).map { account =>
|
||||||
|
updateGroup(groupName, form.url, form.isRemoved)
|
||||||
|
|
||||||
|
if(form.isRemoved){
|
||||||
|
// Remove from GROUP_MEMBER
|
||||||
|
updateGroupMembers(form.groupName, Nil)
|
||||||
|
// Remove repositories
|
||||||
|
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||||
|
deleteRepository(groupName, repositoryName)
|
||||||
|
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
|
||||||
|
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
|
||||||
|
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Update GROUP_MEMBER
|
||||||
|
updateGroupMembers(form.groupName, members)
|
||||||
|
// Update COLLABORATOR for group repositories
|
||||||
|
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||||
|
removeCollaborators(form.groupName, repositoryName)
|
||||||
|
members.foreach { case (userName, isManager) =>
|
||||||
|
addCollaborator(form.groupName, repositoryName, userName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateImage(form.groupName, form.fileId, form.clearImage)
|
||||||
|
redirect("/admin/users")
|
||||||
|
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
private def members: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||||
|
if(value.split(",").exists {
|
||||||
|
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
|
||||||
|
}) None else Some("Must select one manager at least.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||||
|
params.get(paramName).flatMap { userName =>
|
||||||
|
if(userName == context.loginAccount.get.userName && params.get("removed") == Some("true"))
|
||||||
|
Some("You can't disable your account yourself")
|
||||||
|
else
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,204 +0,0 @@
|
|||||||
package gitbucket.core.controller
|
|
||||||
|
|
||||||
import gitbucket.core.service.{RepositoryService, AccountService}
|
|
||||||
import gitbucket.core.admin.users.html
|
|
||||||
import gitbucket.core.util._
|
|
||||||
import gitbucket.core.util.ControlUtil._
|
|
||||||
import gitbucket.core.util.StringUtil._
|
|
||||||
import gitbucket.core.util.Implicits._
|
|
||||||
import gitbucket.core.util.Directory._
|
|
||||||
import io.github.gitbucket.scalatra.forms._
|
|
||||||
import org.scalatra.i18n.Messages
|
|
||||||
import org.apache.commons.io.FileUtils
|
|
||||||
|
|
||||||
class UserManagementController extends UserManagementControllerBase
|
|
||||||
with AccountService with RepositoryService with AdminAuthenticator
|
|
||||||
|
|
||||||
trait UserManagementControllerBase extends AccountManagementControllerBase {
|
|
||||||
self: AccountService with RepositoryService with AdminAuthenticator =>
|
|
||||||
|
|
||||||
case class NewUserForm(userName: String, password: String, fullName: String,
|
|
||||||
mailAddress: String, isAdmin: Boolean,
|
|
||||||
url: Option[String], fileId: Option[String])
|
|
||||||
|
|
||||||
case class EditUserForm(userName: String, password: Option[String], fullName: String,
|
|
||||||
mailAddress: String, isAdmin: Boolean, url: Option[String],
|
|
||||||
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
|
|
||||||
|
|
||||||
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
|
||||||
members: String)
|
|
||||||
|
|
||||||
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
|
||||||
members: String, clearImage: Boolean, isRemoved: Boolean)
|
|
||||||
|
|
||||||
val newUserForm = mapping(
|
|
||||||
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
|
||||||
"password" -> trim(label("Password" ,text(required, maxlength(20)))),
|
|
||||||
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
|
||||||
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
|
|
||||||
"isAdmin" -> trim(label("User Type" ,boolean())),
|
|
||||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
|
||||||
"fileId" -> trim(label("File ID" ,optional(text())))
|
|
||||||
)(NewUserForm.apply)
|
|
||||||
|
|
||||||
val editUserForm = mapping(
|
|
||||||
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))),
|
|
||||||
"password" -> trim(label("Password" ,optional(text(maxlength(20))))),
|
|
||||||
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
|
||||||
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))),
|
|
||||||
"isAdmin" -> trim(label("User Type" ,boolean())),
|
|
||||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
|
||||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
|
||||||
"clearImage" -> trim(label("Clear image" ,boolean())),
|
|
||||||
"removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
|
|
||||||
)(EditUserForm.apply)
|
|
||||||
|
|
||||||
val newGroupForm = mapping(
|
|
||||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
|
||||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
|
||||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
|
||||||
"members" -> trim(label("Members" ,text(required, members)))
|
|
||||||
)(NewGroupForm.apply)
|
|
||||||
|
|
||||||
val editGroupForm = mapping(
|
|
||||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
|
|
||||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
|
||||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
|
||||||
"members" -> trim(label("Members" ,text(required, members))),
|
|
||||||
"clearImage" -> trim(label("Clear image" ,boolean())),
|
|
||||||
"removed" -> trim(label("Disable" ,boolean()))
|
|
||||||
)(EditGroupForm.apply)
|
|
||||||
|
|
||||||
get("/admin/users")(adminOnly {
|
|
||||||
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
|
|
||||||
val users = getAllUsers(includeRemoved)
|
|
||||||
val members = users.collect { case account if(account.isGroupAccount) =>
|
|
||||||
account.userName -> getGroupMembers(account.userName).map(_.userName)
|
|
||||||
}.toMap
|
|
||||||
|
|
||||||
html.list(users, members, includeRemoved)
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/admin/users/_newuser")(adminOnly {
|
|
||||||
html.user(None)
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
|
|
||||||
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url)
|
|
||||||
updateImage(form.userName, form.fileId, false)
|
|
||||||
redirect("/admin/users")
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/admin/users/:userName/_edituser")(adminOnly {
|
|
||||||
val userName = params("userName")
|
|
||||||
html.user(getAccountByUserName(userName, true))
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
|
|
||||||
val userName = params("userName")
|
|
||||||
getAccountByUserName(userName, true).map { account =>
|
|
||||||
|
|
||||||
if(form.isRemoved){
|
|
||||||
// Remove repositories
|
|
||||||
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
|
||||||
// deleteRepository(userName, repositoryName)
|
|
||||||
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
|
||||||
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
|
||||||
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
|
||||||
// }
|
|
||||||
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
|
||||||
removeUserRelatedData(userName)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAccount(account.copy(
|
|
||||||
password = form.password.map(sha1).getOrElse(account.password),
|
|
||||||
fullName = form.fullName,
|
|
||||||
mailAddress = form.mailAddress,
|
|
||||||
isAdmin = form.isAdmin,
|
|
||||||
url = form.url,
|
|
||||||
isRemoved = form.isRemoved))
|
|
||||||
|
|
||||||
updateImage(userName, form.fileId, form.clearImage)
|
|
||||||
redirect("/admin/users")
|
|
||||||
|
|
||||||
} getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/admin/users/_newgroup")(adminOnly {
|
|
||||||
html.group(None, Nil)
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
|
|
||||||
createGroup(form.groupName, form.url)
|
|
||||||
updateGroupMembers(form.groupName, form.members.split(",").map {
|
|
||||||
_.split(":") match {
|
|
||||||
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
|
||||||
}
|
|
||||||
}.toList)
|
|
||||||
updateImage(form.groupName, form.fileId, false)
|
|
||||||
redirect("/admin/users")
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/admin/users/:groupName/_editgroup")(adminOnly {
|
|
||||||
defining(params("groupName")){ groupName =>
|
|
||||||
html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
|
|
||||||
defining(params("groupName"), form.members.split(",").map {
|
|
||||||
_.split(":") match {
|
|
||||||
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
|
||||||
}
|
|
||||||
}.toList){ case (groupName, members) =>
|
|
||||||
getAccountByUserName(groupName, true).map { account =>
|
|
||||||
updateGroup(groupName, form.url, form.isRemoved)
|
|
||||||
|
|
||||||
if(form.isRemoved){
|
|
||||||
// Remove from GROUP_MEMBER
|
|
||||||
updateGroupMembers(form.groupName, Nil)
|
|
||||||
// Remove repositories
|
|
||||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
|
||||||
deleteRepository(groupName, repositoryName)
|
|
||||||
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
|
|
||||||
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
|
|
||||||
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Update GROUP_MEMBER
|
|
||||||
updateGroupMembers(form.groupName, members)
|
|
||||||
// Update COLLABORATOR for group repositories
|
|
||||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
|
||||||
removeCollaborators(form.groupName, repositoryName)
|
|
||||||
members.foreach { case (userName, isManager) =>
|
|
||||||
addCollaborator(form.groupName, repositoryName, userName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateImage(form.groupName, form.fileId, form.clearImage)
|
|
||||||
redirect("/admin/users")
|
|
||||||
|
|
||||||
} getOrElse NotFound
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
private def members: Constraint = new Constraint(){
|
|
||||||
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
|
||||||
if(value.split(",").exists {
|
|
||||||
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
|
|
||||||
}) None else Some("Must select one manager at least.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
|
|
||||||
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
|
||||||
params.get(paramName).flatMap { userName =>
|
|
||||||
if(userName == context.loginAccount.get.userName && params.get("removed") == Some("true"))
|
|
||||||
Some("You can't disable your account yourself")
|
|
||||||
else
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,8 @@ import org.eclipse.jgit.api.Git
|
|||||||
import org.scalatra.i18n.Messages
|
import org.scalatra.i18n.Messages
|
||||||
|
|
||||||
class WikiController extends WikiControllerBase
|
class WikiController extends WikiControllerBase
|
||||||
with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator
|
with WikiService with RepositoryService with AccountService with ActivityService
|
||||||
|
with CollaboratorsAuthenticator with ReferrerAuthenticator
|
||||||
|
|
||||||
trait WikiControllerBase extends ControllerBase {
|
trait WikiControllerBase extends ControllerBase {
|
||||||
self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
|
self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ protected[model] trait TemplateComponent { self: Profile =>
|
|||||||
byRepository(userName, repositoryName) && (this.labelId === labelId)
|
byRepository(userName, repositoryName) && (this.labelId === labelId)
|
||||||
|
|
||||||
def byLabel(owner: String, repository: String, labelName: String) =
|
def byLabel(owner: String, repository: String, labelName: String) =
|
||||||
byRepository(userName, repositoryName) && (this.labelName === labelName.bind)
|
byRepository(owner, repository) && (this.labelName === labelName.bind)
|
||||||
}
|
}
|
||||||
|
|
||||||
trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>
|
trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>
|
||||||
|
|||||||
@@ -3,20 +3,43 @@ package gitbucket.core.model
|
|||||||
trait WebHookComponent extends TemplateComponent { self: Profile =>
|
trait WebHookComponent extends TemplateComponent { self: Profile =>
|
||||||
import profile.simple._
|
import profile.simple._
|
||||||
|
|
||||||
|
implicit val whContentTypeColumnType = MappedColumnType.base[WebHookContentType, String](whct => whct.code , code => WebHookContentType.valueOf(code))
|
||||||
|
|
||||||
lazy val WebHooks = TableQuery[WebHooks]
|
lazy val WebHooks = TableQuery[WebHooks]
|
||||||
|
|
||||||
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
|
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
|
||||||
val url = column[String]("URL")
|
val url = column[String]("URL")
|
||||||
def * = (userName, repositoryName, url) <> ((WebHook.apply _).tupled, WebHook.unapply)
|
val token = column[Option[String]]("TOKEN", O.Nullable)
|
||||||
|
val ctype = column[WebHookContentType]("CTYPE", O.NotNull)
|
||||||
|
def * = (userName, repositoryName, url, ctype, token) <> ((WebHook.apply _).tupled, WebHook.unapply)
|
||||||
|
|
||||||
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
|
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case class WebHookContentType(val code: String, val ctype: String)
|
||||||
|
|
||||||
|
object WebHookContentType {
|
||||||
|
object JSON extends WebHookContentType("json", "application/json")
|
||||||
|
|
||||||
|
object FORM extends WebHookContentType("form", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
val values: Vector[WebHookContentType] = Vector(JSON, FORM)
|
||||||
|
|
||||||
|
private val map: Map[String, WebHookContentType] = values.map(enum => enum.code -> enum).toMap
|
||||||
|
|
||||||
|
def apply(code: String): WebHookContentType = map(code)
|
||||||
|
|
||||||
|
def valueOf(code: String): WebHookContentType = map(code)
|
||||||
|
def valueOpt(code: String): Option[WebHookContentType] = map.get(code)
|
||||||
|
}
|
||||||
|
|
||||||
case class WebHook(
|
case class WebHook(
|
||||||
userName: String,
|
userName: String,
|
||||||
repositoryName: String,
|
repositoryName: String,
|
||||||
url: String
|
url: String,
|
||||||
|
ctype: WebHookContentType,
|
||||||
|
token: Option[String]
|
||||||
)
|
)
|
||||||
|
|
||||||
object WebHook {
|
object WebHook {
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
package gitbucket.core.plugin
|
package gitbucket.core.plugin
|
||||||
|
|
||||||
import javax.servlet.ServletContext
|
import javax.servlet.ServletContext
|
||||||
import gitbucket.core.controller.ControllerBase
|
import gitbucket.core.controller.{Context, ControllerBase}
|
||||||
|
import gitbucket.core.model.Account
|
||||||
|
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||||
import gitbucket.core.util.ControlUtil._
|
import gitbucket.core.util.ControlUtil._
|
||||||
import gitbucket.core.util.Version
|
import gitbucket.core.util.Version
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trait for define plugin interface.
|
* Trait for define plugin interface.
|
||||||
* To provide plugin, put Plugin class which mixed in this trait into the package root.
|
* To provide a plugin, put a Plugin class which extends this class into the package root.
|
||||||
*/
|
*/
|
||||||
trait Plugin {
|
abstract class Plugin {
|
||||||
|
|
||||||
val pluginId: String
|
val pluginId: String
|
||||||
val pluginName: String
|
val pluginName: String
|
||||||
@@ -77,6 +79,76 @@ trait Plugin {
|
|||||||
*/
|
*/
|
||||||
def receiveHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[ReceiveHook] = Nil
|
def receiveHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[ReceiveHook] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add global menus.
|
||||||
|
*/
|
||||||
|
val globalMenus: Seq[(Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add global menus.
|
||||||
|
*/
|
||||||
|
def globalMenus(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add repository menus.
|
||||||
|
*/
|
||||||
|
val repositoryMenus: Seq[(RepositoryInfo, Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add repository menus.
|
||||||
|
*/
|
||||||
|
def repositoryMenus(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(RepositoryInfo, Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add repository setting tabs.
|
||||||
|
*/
|
||||||
|
val repositorySettingTabs: Seq[(RepositoryInfo, Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add repository setting tabs.
|
||||||
|
*/
|
||||||
|
def repositorySettingTabs(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(RepositoryInfo, Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add profile tabs.
|
||||||
|
*/
|
||||||
|
val profileTabs: Seq[(Account, Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add profile tabs.
|
||||||
|
*/
|
||||||
|
def profileTabs(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Account, Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add system setting menus.
|
||||||
|
*/
|
||||||
|
val systemSettingMenus: Seq[(Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add system setting menus.
|
||||||
|
*/
|
||||||
|
def systemSettingMenus(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add account setting menus.
|
||||||
|
*/
|
||||||
|
val accountSettingMenus: Seq[(Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add account setting menus.
|
||||||
|
*/
|
||||||
|
def accountSettingMenus(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add dashboard tabs.
|
||||||
|
*/
|
||||||
|
val dashboardTabs: Seq[(Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add dashboard tabs.
|
||||||
|
*/
|
||||||
|
def dashboardTabs(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is invoked in initialization of plugin system.
|
* This method is invoked in initialization of plugin system.
|
||||||
* Register plugin functionality to PluginRegistry.
|
* Register plugin functionality to PluginRegistry.
|
||||||
@@ -100,6 +172,27 @@ trait Plugin {
|
|||||||
(receiveHooks ++ receiveHooks(registry, context, settings)).foreach { receiveHook =>
|
(receiveHooks ++ receiveHooks(registry, context, settings)).foreach { receiveHook =>
|
||||||
registry.addReceiveHook(receiveHook)
|
registry.addReceiveHook(receiveHook)
|
||||||
}
|
}
|
||||||
|
(globalMenus ++ globalMenus(registry, context, settings)).foreach { globalMenu =>
|
||||||
|
registry.addGlobalMenu(globalMenu)
|
||||||
|
}
|
||||||
|
(repositoryMenus ++ repositoryMenus(registry, context, settings)).foreach { repositoryMenu =>
|
||||||
|
registry.addRepositoryMenu(repositoryMenu)
|
||||||
|
}
|
||||||
|
(repositorySettingTabs ++ repositorySettingTabs(registry, context, settings)).foreach { repositorySettingTab =>
|
||||||
|
registry.addRepositorySettingTab(repositorySettingTab)
|
||||||
|
}
|
||||||
|
(profileTabs ++ profileTabs(registry, context, settings)).foreach { profileTab =>
|
||||||
|
registry.addProfileTab(profileTab)
|
||||||
|
}
|
||||||
|
(systemSettingMenus ++ systemSettingMenus(registry, context, settings)).foreach { systemSettingMenu =>
|
||||||
|
registry.addSystemSettingMenu(systemSettingMenu)
|
||||||
|
}
|
||||||
|
(accountSettingMenus ++ accountSettingMenus(registry, context, settings)).foreach { accountSettingMenu =>
|
||||||
|
registry.addAccountSettingMenu(accountSettingMenu)
|
||||||
|
}
|
||||||
|
(dashboardTabs ++ dashboardTabs(registry, context, settings)).foreach { dashboardTab =>
|
||||||
|
registry.addDashboardTab(dashboardTab)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package gitbucket.core.plugin
|
|||||||
import java.io.{File, FilenameFilter, InputStream}
|
import java.io.{File, FilenameFilter, InputStream}
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
import javax.servlet.ServletContext
|
import javax.servlet.ServletContext
|
||||||
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
|
|
||||||
|
|
||||||
import gitbucket.core.controller.{Context, ControllerBase}
|
import gitbucket.core.controller.{Context, ControllerBase}
|
||||||
|
import gitbucket.core.model.Account
|
||||||
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
|
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
|
||||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||||
@@ -33,6 +33,14 @@ class PluginRegistry {
|
|||||||
private val receiveHooks = new ListBuffer[ReceiveHook]
|
private val receiveHooks = new ListBuffer[ReceiveHook]
|
||||||
receiveHooks += new ProtectedBranchReceiveHook()
|
receiveHooks += new ProtectedBranchReceiveHook()
|
||||||
|
|
||||||
|
private val globalMenus = new ListBuffer[(Context) => Option[Link]]
|
||||||
|
private val repositoryMenus = new ListBuffer[(RepositoryInfo, Context) => Option[Link]]
|
||||||
|
private val repositorySettingTabs = new ListBuffer[(RepositoryInfo, Context) => Option[Link]]
|
||||||
|
private val profileTabs = new ListBuffer[(Account, Context) => Option[Link]]
|
||||||
|
private val systemSettingMenus = new ListBuffer[(Context) => Option[Link]]
|
||||||
|
private val accountSettingMenus = new ListBuffer[(Context) => Option[Link]]
|
||||||
|
private val dashboardTabs = new ListBuffer[(Context) => Option[Link]]
|
||||||
|
|
||||||
def addPlugin(pluginInfo: PluginInfo): Unit = {
|
def addPlugin(pluginInfo: PluginInfo): Unit = {
|
||||||
plugins += pluginInfo
|
plugins += pluginInfo
|
||||||
}
|
}
|
||||||
@@ -107,17 +115,47 @@ class PluginRegistry {
|
|||||||
|
|
||||||
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.toSeq
|
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.toSeq
|
||||||
|
|
||||||
private case class GlobalAction(
|
def addGlobalMenu(globalMenu: (Context) => Option[Link]): Unit = {
|
||||||
method: String,
|
globalMenus += globalMenu
|
||||||
path: String,
|
}
|
||||||
function: (HttpServletRequest, HttpServletResponse, Context) => Any
|
|
||||||
)
|
|
||||||
|
|
||||||
private case class RepositoryAction(
|
def getGlobalMenus: Seq[(Context) => Option[Link]] = globalMenus.toSeq
|
||||||
method: String,
|
|
||||||
path: String,
|
def addRepositoryMenu(repositoryMenu: (RepositoryInfo, Context) => Option[Link]): Unit = {
|
||||||
function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any
|
repositoryMenus += repositoryMenu
|
||||||
)
|
}
|
||||||
|
|
||||||
|
def getRepositoryMenus: Seq[(RepositoryInfo, Context) => Option[Link]] = repositoryMenus.toSeq
|
||||||
|
|
||||||
|
def addRepositorySettingTab(repositorySettingTab: (RepositoryInfo, Context) => Option[Link]): Unit = {
|
||||||
|
repositorySettingTabs += repositorySettingTab
|
||||||
|
}
|
||||||
|
|
||||||
|
def getRepositorySettingTabs: Seq[(RepositoryInfo, Context) => Option[Link]] = repositorySettingTabs.toSeq
|
||||||
|
|
||||||
|
def addProfileTab(profileTab: (Account, Context) => Option[Link]): Unit = {
|
||||||
|
profileTabs += profileTab
|
||||||
|
}
|
||||||
|
|
||||||
|
def getProfileTabs: Seq[(Account, Context) => Option[Link]] = profileTabs.toSeq
|
||||||
|
|
||||||
|
def addSystemSettingMenu(systemSettingMenu: (Context) => Option[Link]): Unit = {
|
||||||
|
systemSettingMenus += systemSettingMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
def getSystemSettingMenus: Seq[(Context) => Option[Link]] = systemSettingMenus.toSeq
|
||||||
|
|
||||||
|
def addAccountSettingMenu(accountSettingMenu: (Context) => Option[Link]): Unit = {
|
||||||
|
accountSettingMenus += accountSettingMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
def getAccountSettingMenus: Seq[(Context) => Option[Link]] = accountSettingMenus.toSeq
|
||||||
|
|
||||||
|
def addDashboardTab(dashboardTab: (Context) => Option[Link]): Unit = {
|
||||||
|
dashboardTabs += dashboardTab
|
||||||
|
}
|
||||||
|
|
||||||
|
def getDashboardTabs: Seq[(Context) => Option[Link]] = dashboardTabs.toSeq
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +239,8 @@ object PluginRegistry {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case class Link(id: String, label: String, path: String, icon: Option[String] = None)
|
||||||
|
|
||||||
case class PluginInfo(
|
case class PluginInfo(
|
||||||
pluginId: String,
|
pluginId: String,
|
||||||
pluginName: String,
|
pluginName: String,
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package gitbucket.core.service
|
||||||
|
|
||||||
|
import gitbucket.core.controller.Context
|
||||||
|
import gitbucket.core.model.Issue
|
||||||
|
import gitbucket.core.model.Profile._
|
||||||
|
import gitbucket.core.util.ControlUtil._
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
|
import gitbucket.core.util.Notifier
|
||||||
|
import profile.simple._
|
||||||
|
|
||||||
|
trait HandleCommentService {
|
||||||
|
self: RepositoryService with IssuesService with ActivityService
|
||||||
|
with WebHookService with WebHookIssueCommentService with WebHookPullRequestService =>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
|
||||||
|
*/
|
||||||
|
def handleComment(issue: Issue, content: Option[String], repository: RepositoryService.RepositoryInfo, actionOpt: Option[String])
|
||||||
|
(implicit context: Context, s: Session) = {
|
||||||
|
|
||||||
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
|
val userName = context.loginAccount.get.userName
|
||||||
|
|
||||||
|
val (action, recordActivity) = actionOpt
|
||||||
|
.collect {
|
||||||
|
case "close" if(!issue.closed) => true ->
|
||||||
|
(Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
|
||||||
|
case "reopen" if(issue.closed) => false ->
|
||||||
|
(Some("reopen") -> Some(recordReopenIssueActivity _))
|
||||||
|
}
|
||||||
|
.map { case (closed, t) =>
|
||||||
|
updateClosed(owner, name, issue.issueId, closed)
|
||||||
|
t
|
||||||
|
}
|
||||||
|
.getOrElse(None -> None)
|
||||||
|
|
||||||
|
val commentId = (content, action) match {
|
||||||
|
case (None, None) => None
|
||||||
|
case (None, Some(action)) => Some(createComment(owner, name, userName, issue.issueId, action.capitalize, action))
|
||||||
|
case (Some(content), _) => Some(createComment(owner, name, userName, issue.issueId, content, action.map(_+ "_comment").getOrElse("comment")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// record comment activity if comment is entered
|
||||||
|
content foreach {
|
||||||
|
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
|
||||||
|
(owner, name, userName, issue.issueId, _)
|
||||||
|
}
|
||||||
|
recordActivity foreach ( _ (owner, name, userName, issue.issueId, issue.title) )
|
||||||
|
|
||||||
|
// extract references and create refer comment
|
||||||
|
content.map { content =>
|
||||||
|
createReferComment(owner, name, issue, content, context.loginAccount.get)
|
||||||
|
}
|
||||||
|
|
||||||
|
// call web hooks
|
||||||
|
action match {
|
||||||
|
case None => commentId.map{ commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) }
|
||||||
|
case Some(act) => val webHookAction = act match {
|
||||||
|
case "open" => "opened"
|
||||||
|
case "reopen" => "reopened"
|
||||||
|
case "close" => "closed"
|
||||||
|
case _ => act
|
||||||
|
}
|
||||||
|
if(issue.isPullRequest){
|
||||||
|
callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get)
|
||||||
|
} else {
|
||||||
|
callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// notifications
|
||||||
|
Notifier() match {
|
||||||
|
case f =>
|
||||||
|
content foreach {
|
||||||
|
f.toNotify(repository, issue, _){
|
||||||
|
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
|
||||||
|
if(issue.isPullRequest) "pull" else "issues"}/${issue.issueId}#comment-${commentId.get}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action foreach {
|
||||||
|
f.toNotify(repository, issue, _){
|
||||||
|
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issue.issueId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commentId.map( issue -> _ )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package gitbucket.core.service
|
package gitbucket.core.service
|
||||||
|
|
||||||
import gitbucket.core.model.Profile._
|
import gitbucket.core.model.Profile._
|
||||||
|
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||||
|
import gitbucket.core.util.StringUtil
|
||||||
import profile.simple._
|
import profile.simple._
|
||||||
|
|
||||||
import gitbucket.core.util.StringUtil._
|
import gitbucket.core.util.StringUtil._
|
||||||
@@ -12,6 +14,7 @@ import Q.interpolation
|
|||||||
|
|
||||||
|
|
||||||
trait IssuesService {
|
trait IssuesService {
|
||||||
|
self: AccountService =>
|
||||||
import IssuesService._
|
import IssuesService._
|
||||||
|
|
||||||
def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) =
|
def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) =
|
||||||
@@ -394,12 +397,35 @@ trait IssuesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String, loginAccount: Account)(implicit s: Session) = {
|
||||||
|
StringUtil.extractIssueId(message).foreach { issueId =>
|
||||||
|
val content = fromIssue.issueId + ":" + fromIssue.title
|
||||||
|
if(getIssue(owner, repository, issueId).isDefined){
|
||||||
|
// Not add if refer comment already exist.
|
||||||
|
if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
|
||||||
|
createComment(owner, repository, loginAccount.userName, issueId.toInt, content, "refer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def createIssueComment(owner: String, repository: String, commit: CommitInfo)(implicit s: Session) = {
|
||||||
|
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
|
||||||
|
if(getIssue(owner, repository, issueId).isDefined){
|
||||||
|
getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
|
||||||
|
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object IssuesService {
|
object IssuesService {
|
||||||
import javax.servlet.http.HttpServletRequest
|
import javax.servlet.http.HttpServletRequest
|
||||||
|
|
||||||
val IssueLimit = 30
|
val IssueLimit = 25
|
||||||
|
|
||||||
case class IssueSearchCondition(
|
case class IssueSearchCondition(
|
||||||
labels: Set[String] = Set.empty,
|
labels: Set[String] = Set.empty,
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package gitbucket.core.service
|
||||||
|
|
||||||
|
import gitbucket.core.model.Profile._
|
||||||
|
import gitbucket.core.util.ControlUtil._
|
||||||
|
import gitbucket.core.util.Directory._
|
||||||
|
import gitbucket.core.util.JGitUtil
|
||||||
|
import gitbucket.core.model.Account
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.eclipse.jgit.dircache.DirCache
|
||||||
|
import org.eclipse.jgit.lib.{FileMode, Constants}
|
||||||
|
import profile.simple._
|
||||||
|
|
||||||
|
trait RepositoryCreationService {
|
||||||
|
self: AccountService with RepositoryService with LabelsService with WikiService with ActivityService =>
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// Insert to the database at first
|
||||||
|
insertRepository(name, owner, description, isPrivate)
|
||||||
|
|
||||||
|
// Add collaborators for group repository
|
||||||
|
if(ownerAccount.isGroupAccount){
|
||||||
|
getGroupMembers(owner).foreach { member =>
|
||||||
|
addCollaborator(owner, name, member.userName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert default labels
|
||||||
|
insertDefaultLabels(owner, name)
|
||||||
|
|
||||||
|
// Create the actual repository
|
||||||
|
val gitdir = getRepositoryDir(owner, name)
|
||||||
|
JGitUtil.initRepository(gitdir)
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Wiki repository
|
||||||
|
createWikiRepository(loginAccount, owner, name)
|
||||||
|
|
||||||
|
// Record activity
|
||||||
|
recordCreateRepositoryActivity(owner, name, loginUserName)
|
||||||
|
}
|
||||||
|
|
||||||
|
def insertDefaultLabels(userName: String, repositoryName: String)(implicit s: Session): Unit = {
|
||||||
|
createLabel(userName, repositoryName, "bug", "fc2929")
|
||||||
|
createLabel(userName, repositoryName, "duplicate", "cccccc")
|
||||||
|
createLabel(userName, repositoryName, "enhancement", "84b6eb")
|
||||||
|
createLabel(userName, repositoryName, "invalid", "e6e6e6")
|
||||||
|
createLabel(userName, repositoryName, "question", "cc317c")
|
||||||
|
createLabel(userName, repositoryName, "wontfix", "ffffff")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -53,7 +53,30 @@ trait RepositorySearchService { self: IssuesService =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = {
|
def countWikiPages(owner: String, repository: String, query: String): Int =
|
||||||
|
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||||
|
if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length
|
||||||
|
}
|
||||||
|
|
||||||
|
def searchWikiPages(owner: String, repository: String, query: String): List[FileSearchResult] =
|
||||||
|
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||||
|
if(JGitUtil.isEmpty(git)){
|
||||||
|
Nil
|
||||||
|
} else {
|
||||||
|
val files = searchRepositoryFiles(git, query)
|
||||||
|
val commits = JGitUtil.getLatestCommitFromPaths(git, files.map(_._1), "HEAD")
|
||||||
|
files.map { case (path, text) =>
|
||||||
|
val (highlightText, lineNumber) = getHighlightText(text, query)
|
||||||
|
FileSearchResult(
|
||||||
|
path.replaceFirst("\\.md$", ""),
|
||||||
|
commits(path).getCommitterIdent.getWhen,
|
||||||
|
highlightText,
|
||||||
|
lineNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = {
|
||||||
val revWalk = new RevWalk(git.getRepository)
|
val revWalk = new RevWalk(git.getRepository)
|
||||||
val objectId = git.getRepository.resolve("HEAD")
|
val objectId = git.getRepository.resolve("HEAD")
|
||||||
val revCommit = revWalk.parseCommit(objectId)
|
val revCommit = revWalk.parseCommit(objectId)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package gitbucket.core.service
|
package gitbucket.core.service
|
||||||
|
|
||||||
|
import gitbucket.core.controller.Context
|
||||||
import gitbucket.core.model.{Collaborator, Repository, Account}
|
import gitbucket.core.model.{Collaborator, Repository, Account}
|
||||||
import gitbucket.core.model.Profile._
|
import gitbucket.core.model.Profile._
|
||||||
import gitbucket.core.util.JGitUtil
|
import gitbucket.core.util.JGitUtil
|
||||||
@@ -18,7 +19,7 @@ trait RepositoryService { self: AccountService =>
|
|||||||
* @param originRepositoryName specify for the forked repository. (default is None)
|
* @param originRepositoryName specify for the forked repository. (default is None)
|
||||||
* @param originUserName specify for the forked repository. (default is None)
|
* @param originUserName specify for the forked repository. (default is None)
|
||||||
*/
|
*/
|
||||||
def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
|
def insertRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
|
||||||
originRepositoryName: Option[String] = None, originUserName: Option[String] = None,
|
originRepositoryName: Option[String] = None, originUserName: Option[String] = None,
|
||||||
parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None)
|
parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None)
|
||||||
(implicit s: Session): Unit = {
|
(implicit s: Session): Unit = {
|
||||||
@@ -194,10 +195,9 @@ trait RepositoryService { self: AccountService =>
|
|||||||
*
|
*
|
||||||
* @param userName the user name of the repository owner
|
* @param userName the user name of the repository owner
|
||||||
* @param repositoryName the repository name
|
* @param repositoryName the repository name
|
||||||
* @param baseUrl the base url of this application
|
|
||||||
* @return the repository information
|
* @return the repository information
|
||||||
*/
|
*/
|
||||||
def getRepository(userName: String, repositoryName: String, baseUrl: String)(implicit s: Session): Option[RepositoryInfo] = {
|
def getRepository(userName: String, repositoryName: String)(implicit s: Session): Option[RepositoryInfo] = {
|
||||||
(Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
|
(Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
|
||||||
// for getting issue count and pull request count
|
// for getting issue count and pull request count
|
||||||
val issues = Issues.filter { t =>
|
val issues = Issues.filter { t =>
|
||||||
@@ -205,7 +205,7 @@ trait RepositoryService { self: AccountService =>
|
|||||||
}.map(_.pullRequest).list
|
}.map(_.pullRequest).list
|
||||||
|
|
||||||
new RepositoryInfo(
|
new RepositoryInfo(
|
||||||
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
|
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName),
|
||||||
repository,
|
repository,
|
||||||
issues.count(_ == false),
|
issues.count(_ == false),
|
||||||
issues.count(_ == true),
|
issues.count(_ == true),
|
||||||
@@ -234,7 +234,7 @@ trait RepositoryService { self: AccountService =>
|
|||||||
}.list
|
}.list
|
||||||
}
|
}
|
||||||
|
|
||||||
def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false)
|
def getUserRepositories(userName: String, withoutPhysicalInfo: Boolean = false)
|
||||||
(implicit s: Session): List[RepositoryInfo] = {
|
(implicit s: Session): List[RepositoryInfo] = {
|
||||||
Repositories.filter { t1 =>
|
Repositories.filter { t1 =>
|
||||||
(t1.userName === userName.bind) ||
|
(t1.userName === userName.bind) ||
|
||||||
@@ -242,9 +242,9 @@ trait RepositoryService { self: AccountService =>
|
|||||||
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
|
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
|
||||||
new RepositoryInfo(
|
new RepositoryInfo(
|
||||||
if(withoutPhysicalInfo){
|
if(withoutPhysicalInfo){
|
||||||
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName)
|
||||||
} else {
|
} else {
|
||||||
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName)
|
||||||
},
|
},
|
||||||
repository,
|
repository,
|
||||||
getForkedCount(
|
getForkedCount(
|
||||||
@@ -260,13 +260,12 @@ trait RepositoryService { self: AccountService =>
|
|||||||
* If repositoryUserName is given then filters results by repository owner.
|
* If repositoryUserName is given then filters results by repository owner.
|
||||||
*
|
*
|
||||||
* @param loginAccount the logged in account
|
* @param loginAccount the logged in account
|
||||||
* @param baseUrl the base url of this application
|
|
||||||
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
|
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
|
||||||
* @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count,
|
* @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count,
|
||||||
* branches and tags
|
* branches and tags
|
||||||
* @return the repository information which is sorted in descending order of lastActivityDate.
|
* @return the repository information which is sorted in descending order of lastActivityDate.
|
||||||
*/
|
*/
|
||||||
def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None,
|
def getVisibleRepositories(loginAccount: Option[Account], repositoryUserName: Option[String] = None,
|
||||||
withoutPhysicalInfo: Boolean = false)
|
withoutPhysicalInfo: Boolean = false)
|
||||||
(implicit s: Session): List[RepositoryInfo] = {
|
(implicit s: Session): List[RepositoryInfo] = {
|
||||||
(loginAccount match {
|
(loginAccount match {
|
||||||
@@ -284,9 +283,9 @@ trait RepositoryService { self: AccountService =>
|
|||||||
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
|
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
|
||||||
new RepositoryInfo(
|
new RepositoryInfo(
|
||||||
if(withoutPhysicalInfo){
|
if(withoutPhysicalInfo){
|
||||||
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName)
|
||||||
} else {
|
} else {
|
||||||
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName)
|
||||||
},
|
},
|
||||||
repository,
|
repository,
|
||||||
getForkedCount(
|
getForkedCount(
|
||||||
@@ -389,32 +388,39 @@ trait RepositoryService { self: AccountService =>
|
|||||||
|
|
||||||
object RepositoryService {
|
object RepositoryService {
|
||||||
|
|
||||||
case class RepositoryInfo(owner: String, name: String, httpUrl: String, repository: Repository,
|
case class RepositoryInfo(owner: String, name: String, repository: Repository,
|
||||||
issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
|
issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
|
||||||
branchList: Seq[String], tags: Seq[JGitUtil.TagInfo], managers: Seq[String]){
|
branchList: Seq[String], tags: Seq[JGitUtil.TagInfo], managers: Seq[String]) {
|
||||||
|
|
||||||
lazy val host = """^https?://(.+?)(:\d+)?/""".r.findFirstMatchIn(httpUrl).get.group(1)
|
|
||||||
|
|
||||||
def sshUrl(port: Int, userName: String) = s"ssh://${userName}@${host}:${port}/${owner}/${name}.git"
|
|
||||||
|
|
||||||
def sshOpenRepoUrl(platform: String, port: Int, userName: String) = openRepoUrl(platform, sshUrl(port, userName))
|
|
||||||
|
|
||||||
def httpOpenRepoUrl(platform: String) = openRepoUrl(platform, httpUrl)
|
|
||||||
|
|
||||||
def openRepoUrl(platform: String, openUrl: String) = s"github-${platform}://openRepo/${openUrl}"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates instance with issue count and pull request count.
|
* Creates instance with issue count and pull request count.
|
||||||
*/
|
*/
|
||||||
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) =
|
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) =
|
||||||
this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers)
|
this(
|
||||||
|
repo.owner, repo.name, model,
|
||||||
|
issueCount, pullCount, repo.commitCount, forkedCount,
|
||||||
|
repo.branchList, repo.tags, managers)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates instance without issue count and pull request count.
|
* Creates instance without issue count and pull request count.
|
||||||
*/
|
*/
|
||||||
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) =
|
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) =
|
||||||
this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers)
|
this(
|
||||||
|
repo.owner, repo.name, model,
|
||||||
|
0, 0, repo.commitCount, forkedCount,
|
||||||
|
repo.branchList, repo.tags, managers)
|
||||||
|
|
||||||
|
def httpUrl(implicit context: Context): String = RepositoryService.httpUrl(owner, name)
|
||||||
|
def sshUrl(implicit context: Context): Option[String] = RepositoryService.sshUrl(owner, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])
|
def httpUrl(owner: String, name: String)(implicit context: Context): String = s"${context.baseUrl}/git/${owner}/${name}.git"
|
||||||
|
def sshUrl(owner: String, name: String)(implicit context: Context): Option[String] =
|
||||||
|
if(context.settings.ssh){
|
||||||
|
context.loginAccount.flatMap { loginAccount =>
|
||||||
|
context.settings.sshAddress.map { x => s"ssh://${loginAccount.userName}@${x.host}:${x.port}/${owner}/${name}.git" }
|
||||||
|
}
|
||||||
|
} else None
|
||||||
|
def openRepoUrl(openUrl: String)(implicit context: Context): String = s"github-${context.platform}://openRepo/${openUrl}"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package gitbucket.core.service
|
package gitbucket.core.service
|
||||||
|
|
||||||
import gitbucket.core.util.{Directory, ControlUtil}
|
import gitbucket.core.util.{Directory, ControlUtil}
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
import Directory._
|
import Directory._
|
||||||
import ControlUtil._
|
import ControlUtil._
|
||||||
import SystemSettingsService._
|
import SystemSettingsService._
|
||||||
@@ -21,6 +22,7 @@ trait SystemSettingsService {
|
|||||||
props.setProperty(Notification, settings.notification.toString)
|
props.setProperty(Notification, settings.notification.toString)
|
||||||
settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString))
|
settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString))
|
||||||
props.setProperty(Ssh, settings.ssh.toString)
|
props.setProperty(Ssh, settings.ssh.toString)
|
||||||
|
settings.sshHost.foreach(x => props.setProperty(SshHost, x.trim))
|
||||||
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
|
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
|
||||||
props.setProperty(UseSMTP, settings.useSMTP.toString)
|
props.setProperty(UseSMTP, settings.useSMTP.toString)
|
||||||
if(settings.useSMTP) {
|
if(settings.useSMTP) {
|
||||||
@@ -75,6 +77,7 @@ trait SystemSettingsService {
|
|||||||
getValue(props, Notification, false),
|
getValue(props, Notification, false),
|
||||||
getOptionValue[Int](props, ActivityLogLimit, None),
|
getOptionValue[Int](props, ActivityLogLimit, None),
|
||||||
getValue(props, Ssh, false),
|
getValue(props, Ssh, false),
|
||||||
|
getOptionValue[String](props, SshHost, None).map(_.trim),
|
||||||
getOptionValue(props, SshPort, Some(DefaultSshPort)),
|
getOptionValue(props, SshPort, Some(DefaultSshPort)),
|
||||||
getValue(props, UseSMTP, getValue(props, Notification, false)), // handle migration scenario from only notification to useSMTP
|
getValue(props, UseSMTP, getValue(props, Notification, false)), // handle migration scenario from only notification to useSMTP
|
||||||
if(getValue(props, UseSMTP, getValue(props, Notification, false))){
|
if(getValue(props, UseSMTP, getValue(props, Notification, false))){
|
||||||
@@ -126,16 +129,19 @@ object SystemSettingsService {
|
|||||||
notification: Boolean,
|
notification: Boolean,
|
||||||
activityLogLimit: Option[Int],
|
activityLogLimit: Option[Int],
|
||||||
ssh: Boolean,
|
ssh: Boolean,
|
||||||
|
sshHost: Option[String],
|
||||||
sshPort: Option[Int],
|
sshPort: Option[Int],
|
||||||
useSMTP: Boolean,
|
useSMTP: Boolean,
|
||||||
smtp: Option[Smtp],
|
smtp: Option[Smtp],
|
||||||
ldapAuthentication: Boolean,
|
ldapAuthentication: Boolean,
|
||||||
ldap: Option[Ldap]){
|
ldap: Option[Ldap]){
|
||||||
def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse {
|
def baseUrl(request: HttpServletRequest): String = baseUrl.fold(request.baseUrl)(_.stripSuffix("/"))
|
||||||
defining(request.getRequestURL.toString){ url =>
|
|
||||||
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
|
def sshAddress:Option[SshAddress] =
|
||||||
|
for {
|
||||||
|
host <- sshHost if ssh
|
||||||
}
|
}
|
||||||
}.stripSuffix("/")
|
yield SshAddress(host, sshPort.getOrElse(DefaultSshPort))
|
||||||
}
|
}
|
||||||
|
|
||||||
case class Ldap(
|
case class Ldap(
|
||||||
@@ -161,6 +167,10 @@ object SystemSettingsService {
|
|||||||
fromAddress: Option[String],
|
fromAddress: Option[String],
|
||||||
fromName: Option[String])
|
fromName: Option[String])
|
||||||
|
|
||||||
|
case class SshAddress(
|
||||||
|
host:String,
|
||||||
|
port:Int)
|
||||||
|
|
||||||
val DefaultSshPort = 29418
|
val DefaultSshPort = 29418
|
||||||
val DefaultSmtpPort = 25
|
val DefaultSmtpPort = 25
|
||||||
val DefaultLdapPort = 389
|
val DefaultLdapPort = 389
|
||||||
@@ -174,6 +184,7 @@ object SystemSettingsService {
|
|||||||
private val Notification = "notification"
|
private val Notification = "notification"
|
||||||
private val ActivityLogLimit = "activity_log_limit"
|
private val ActivityLogLimit = "activity_log_limit"
|
||||||
private val Ssh = "ssh"
|
private val Ssh = "ssh"
|
||||||
|
private val SshHost = "ssh.host"
|
||||||
private val SshPort = "ssh.port"
|
private val SshPort = "ssh.port"
|
||||||
private val UseSMTP = "useSMTP"
|
private val UseSMTP = "useSMTP"
|
||||||
private val SmtpHost = "smtp.host"
|
private val SmtpHost = "smtp.host"
|
||||||
@@ -216,7 +227,4 @@ object SystemSettingsService {
|
|||||||
else value
|
else value
|
||||||
}
|
}
|
||||||
|
|
||||||
// // TODO temporary flag
|
|
||||||
// val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package gitbucket.core.service
|
package gitbucket.core.service
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import fr.brouillard.oss.security.xhub.XHub
|
||||||
|
import fr.brouillard.oss.security.xhub.XHub.{XHubDigest, XHubConverter}
|
||||||
import gitbucket.core.api._
|
import gitbucket.core.api._
|
||||||
import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment}
|
import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment}
|
||||||
import gitbucket.core.model.Profile._
|
import gitbucket.core.model.Profile._
|
||||||
|
import org.apache.http.client.utils.URLEncodedUtils
|
||||||
import profile.simple._
|
import profile.simple._
|
||||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||||
import gitbucket.core.util.RepositoryName
|
import gitbucket.core.util.RepositoryName
|
||||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||||
|
|
||||||
import org.apache.http.NameValuePair
|
import org.apache.http.NameValuePair
|
||||||
import org.apache.http.client.entity.UrlEncodedFormEntity
|
import org.apache.http.client.entity.UrlEncodedFormEntity
|
||||||
import org.apache.http.message.BasicNameValuePair
|
import org.apache.http.message.BasicNameValuePair
|
||||||
@@ -17,6 +20,8 @@ import org.slf4j.LoggerFactory
|
|||||||
import scala.concurrent._
|
import scala.concurrent._
|
||||||
import org.apache.http.HttpRequest
|
import org.apache.http.HttpRequest
|
||||||
import org.apache.http.HttpResponse
|
import org.apache.http.HttpResponse
|
||||||
|
import gitbucket.core.model.WebHookContentType
|
||||||
|
import org.apache.http.client.entity.EntityBuilder
|
||||||
|
|
||||||
|
|
||||||
trait WebHookService {
|
trait WebHookService {
|
||||||
@@ -33,8 +38,11 @@ trait WebHookService {
|
|||||||
|
|
||||||
/** get All WebHook informations of repository event */
|
/** get All WebHook informations of repository event */
|
||||||
def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] =
|
def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] =
|
||||||
WebHookEvents.filter(t => t.byRepository(owner, repository) && t.event === event.bind)
|
WebHooks.filter(_.byRepository(owner, repository))
|
||||||
.list.map(t => WebHook(t.userName, t.repositoryName, t.url))
|
.innerJoin(WebHookEvents).on { (wh, whe) => whe.byWebHook(wh) }
|
||||||
|
.filter{ case (wh, whe) => whe.event === event.bind}
|
||||||
|
.map{ case (wh, whe) => wh }
|
||||||
|
.list.distinct
|
||||||
|
|
||||||
/** get All WebHook information from repository to url */
|
/** get All WebHook information from repository to url */
|
||||||
def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] =
|
def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] =
|
||||||
@@ -44,14 +52,15 @@ trait WebHookService {
|
|||||||
.map{ case (w,t) => w -> t.event }
|
.map{ case (w,t) => w -> t.event }
|
||||||
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
|
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
|
||||||
|
|
||||||
def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = {
|
def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = {
|
||||||
WebHooks insert WebHook(owner, repository, url)
|
WebHooks insert WebHook(owner, repository, url, ctype, token)
|
||||||
events.toSet.map{ event: WebHook.Event =>
|
events.toSet.map{ event: WebHook.Event =>
|
||||||
WebHookEvents insert WebHookEvent(owner, repository, url, event)
|
WebHookEvents insert WebHookEvent(owner, repository, url, event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = {
|
def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = {
|
||||||
|
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).map(w => (w.ctype, w.token)).update((ctype, token))
|
||||||
WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete
|
WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete
|
||||||
events.toSet.map{ event: WebHook.Event =>
|
events.toSet.map{ event: WebHook.Event =>
|
||||||
WebHookEvents insert WebHookEvent(owner, repository, url, event)
|
WebHookEvents insert WebHookEvent(owner, repository, url, event)
|
||||||
@@ -69,17 +78,17 @@ trait WebHookService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def callWebHook(event: WebHook.Event, webHookURLs: List[WebHook], payload: WebHookPayload)
|
def callWebHook(event: WebHook.Event, webHooks: List[WebHook], payload: WebHookPayload)
|
||||||
(implicit c: JsonFormat.Context): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = {
|
(implicit c: JsonFormat.Context): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = {
|
||||||
import org.apache.http.impl.client.HttpClientBuilder
|
import org.apache.http.impl.client.HttpClientBuilder
|
||||||
import ExecutionContext.Implicits.global
|
import ExecutionContext.Implicits.global
|
||||||
import org.apache.http.protocol.HttpContext
|
import org.apache.http.protocol.HttpContext
|
||||||
import org.apache.http.client.methods.HttpPost
|
import org.apache.http.client.methods.HttpPost
|
||||||
|
|
||||||
if(webHookURLs.nonEmpty){
|
if(webHooks.nonEmpty){
|
||||||
val json = JsonFormat(payload)
|
val json = JsonFormat(payload)
|
||||||
|
|
||||||
webHookURLs.map { webHookUrl =>
|
webHooks.map { webHook =>
|
||||||
val reqPromise = Promise[HttpRequest]
|
val reqPromise = Promise[HttpRequest]
|
||||||
val f = Future {
|
val f = Future {
|
||||||
val itcp = new org.apache.http.HttpRequestInterceptor{
|
val itcp = new org.apache.http.HttpRequestInterceptor{
|
||||||
@@ -89,19 +98,36 @@ trait WebHookService {
|
|||||||
}
|
}
|
||||||
try{
|
try{
|
||||||
val httpClient = HttpClientBuilder.create.addInterceptorLast(itcp).build
|
val httpClient = HttpClientBuilder.create.addInterceptorLast(itcp).build
|
||||||
logger.debug(s"start web hook invocation for ${webHookUrl.url}")
|
logger.debug(s"start web hook invocation for ${webHook.url}")
|
||||||
val httpPost = new HttpPost(webHookUrl.url)
|
val httpPost = new HttpPost(webHook.url)
|
||||||
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded")
|
logger.info(s"Content-Type: ${webHook.ctype.ctype}")
|
||||||
|
httpPost.addHeader("Content-Type", webHook.ctype.ctype)
|
||||||
httpPost.addHeader("X-Github-Event", event.name)
|
httpPost.addHeader("X-Github-Event", event.name)
|
||||||
httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString)
|
httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString)
|
||||||
|
|
||||||
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
|
webHook.ctype match {
|
||||||
params.add(new BasicNameValuePair("payload", json))
|
case WebHookContentType.FORM => {
|
||||||
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"))
|
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
|
||||||
|
params.add(new BasicNameValuePair("payload", json))
|
||||||
|
def postContent = new UrlEncodedFormEntity(params, "UTF-8")
|
||||||
|
httpPost.setEntity(postContent)
|
||||||
|
if (webHook.token.exists(_.trim.nonEmpty)) {
|
||||||
|
// TODO find a better way and see how to extract content from postContent
|
||||||
|
val contentAsBytes = URLEncodedUtils.format(params, "UTF-8").getBytes("UTF-8")
|
||||||
|
httpPost.addHeader("X-Hub-Signature", XHub.generateHeaderXHubToken(XHubConverter.HEXA_LOWERCASE, XHubDigest.SHA1, webHook.token.get, contentAsBytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case WebHookContentType.JSON => {
|
||||||
|
httpPost.setEntity(EntityBuilder.create().setText(json).build())
|
||||||
|
if (!webHook.token.isEmpty) {
|
||||||
|
httpPost.addHeader("X-Hub-Signature", XHub.generateHeaderXHubToken(XHubConverter.HEXA_LOWERCASE, XHubDigest.SHA1, webHook.token.orNull, json.getBytes("UTF-8")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val res = httpClient.execute(httpPost)
|
val res = httpClient.execute(httpPost)
|
||||||
httpPost.releaseConnection()
|
httpPost.releaseConnection()
|
||||||
logger.debug(s"end web hook invocation for ${webHookUrl}")
|
logger.debug(s"end web hook invocation for ${webHook}")
|
||||||
res
|
res
|
||||||
}catch{
|
}catch{
|
||||||
case e:Throwable => {
|
case e:Throwable => {
|
||||||
@@ -113,12 +139,12 @@ trait WebHookService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
f.onSuccess {
|
f.onSuccess {
|
||||||
case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}")
|
case s => logger.debug(s"Success: web hook request to ${webHook.url}")
|
||||||
}
|
}
|
||||||
f.onFailure {
|
f.onFailure {
|
||||||
case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t)
|
case t => logger.error(s"Failed: web hook request to ${webHook.url}", t)
|
||||||
}
|
}
|
||||||
(webHookUrl, json, reqPromise.future, f)
|
(webHook, json, reqPromise.future, f)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Nil
|
Nil
|
||||||
@@ -161,7 +187,7 @@ trait WebHookPullRequestService extends WebHookService {
|
|||||||
baseOwner <- users.get(repository.owner)
|
baseOwner <- users.get(repository.owner)
|
||||||
headOwner <- users.get(pullRequest.requestUserName)
|
headOwner <- users.get(pullRequest.requestUserName)
|
||||||
issueUser <- users.get(issue.openedUserName)
|
issueUser <- users.get(issue.openedUserName)
|
||||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
|
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
|
||||||
} yield {
|
} yield {
|
||||||
WebHookPullRequestPayload(
|
WebHookPullRequestPayload(
|
||||||
action = action,
|
action = action,
|
||||||
@@ -200,7 +226,7 @@ trait WebHookPullRequestService extends WebHookService {
|
|||||||
import WebHookService._
|
import WebHookService._
|
||||||
for{
|
for{
|
||||||
((issue, issueUser, pullRequest, baseOwner, headOwner), webHooks) <- getPullRequestsByRequestForWebhook(requestRepository.owner, requestRepository.name, requestBranch)
|
((issue, issueUser, pullRequest, baseOwner, headOwner), webHooks) <- getPullRequestsByRequestForWebhook(requestRepository.owner, requestRepository.name, requestBranch)
|
||||||
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName, baseUrl)
|
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName)
|
||||||
} yield {
|
} yield {
|
||||||
val payload = WebHookPullRequestPayload(
|
val payload = WebHookPullRequestPayload(
|
||||||
action = action,
|
action = action,
|
||||||
@@ -229,7 +255,7 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
|
|||||||
baseOwner <- users.get(repository.owner)
|
baseOwner <- users.get(repository.owner)
|
||||||
headOwner <- users.get(pullRequest.requestUserName)
|
headOwner <- users.get(pullRequest.requestUserName)
|
||||||
issueUser <- users.get(issue.openedUserName)
|
issueUser <- users.get(issue.openedUserName)
|
||||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
|
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
|
||||||
} yield {
|
} yield {
|
||||||
WebHookPullRequestReviewCommentPayload(
|
WebHookPullRequestReviewCommentPayload(
|
||||||
action = action,
|
action = action,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package gitbucket.core.service
|
package gitbucket.core.service
|
||||||
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import gitbucket.core.controller.Context
|
||||||
import gitbucket.core.model.Account
|
import gitbucket.core.model.Account
|
||||||
|
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||||
import gitbucket.core.util._
|
import gitbucket.core.util._
|
||||||
import gitbucket.core.util.ControlUtil._
|
import gitbucket.core.util.ControlUtil._
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
@@ -13,7 +15,6 @@ import java.io.ByteArrayInputStream
|
|||||||
import org.eclipse.jgit.patch._
|
import org.eclipse.jgit.patch._
|
||||||
import org.eclipse.jgit.api.errors.PatchFormatException
|
import org.eclipse.jgit.api.errors.PatchFormatException
|
||||||
import scala.collection.JavaConverters._
|
import scala.collection.JavaConverters._
|
||||||
import RepositoryService.RepositoryInfo
|
|
||||||
|
|
||||||
object WikiService {
|
object WikiService {
|
||||||
|
|
||||||
@@ -38,10 +39,13 @@ object WikiService {
|
|||||||
*/
|
*/
|
||||||
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
|
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
|
||||||
|
|
||||||
def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git")
|
|
||||||
|
|
||||||
def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) =
|
def wikiHttpUrl(repositoryInfo: RepositoryInfo)(implicit context: Context): String
|
||||||
repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git")
|
= RepositoryService.httpUrl(repositoryInfo.owner, repositoryInfo.name + ".wiki")
|
||||||
|
|
||||||
|
def wikiSshUrl(repositoryInfo: RepositoryInfo)(implicit context: Context): Option[String]
|
||||||
|
= RepositoryService.sshUrl(repositoryInfo.owner, repositoryInfo.name + ".wiki")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trait WikiService {
|
trait WikiService {
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ object AutoUpdate {
|
|||||||
* The history of versions. A head of this sequence is the current GitBucket version.
|
* The history of versions. A head of this sequence is the current GitBucket version.
|
||||||
*/
|
*/
|
||||||
val versions = Seq(
|
val versions = Seq(
|
||||||
|
new Version(3, 14),
|
||||||
|
new Version(3, 13),
|
||||||
|
new Version(3, 12),
|
||||||
new Version(3, 11),
|
new Version(3, 11),
|
||||||
new Version(3, 10),
|
new Version(3, 10),
|
||||||
new Version(3, 9),
|
new Version(3, 9),
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
|||||||
|
|
||||||
request.paths match {
|
request.paths match {
|
||||||
case Array(_, repositoryOwner, repositoryName, _*) =>
|
case Array(_, repositoryOwner, repositoryName, _*) =>
|
||||||
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
|
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", "")) match {
|
||||||
case Some(repository) => {
|
case Some(repository) => {
|
||||||
if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){
|
if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){
|
||||||
chain.doFilter(request, response)
|
chain.doFilter(request, response)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import gitbucket.core.service.WebHookService._
|
|||||||
import gitbucket.core.service._
|
import gitbucket.core.service._
|
||||||
import gitbucket.core.util.ControlUtil._
|
import gitbucket.core.util.ControlUtil._
|
||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
|
||||||
import gitbucket.core.util._
|
import gitbucket.core.util._
|
||||||
|
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
@@ -160,7 +159,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
|||||||
countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
|
countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
|
||||||
countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
|
countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
|
||||||
|
|
||||||
val repositoryInfo = getRepository(owner, repository, baseUrl).get
|
val repositoryInfo = getRepository(owner, repository).get
|
||||||
|
|
||||||
// Extract new commit and apply issue comment
|
// Extract new commit and apply issue comment
|
||||||
val defaultBranch = repositoryInfo.repository.defaultBranch
|
val defaultBranch = repositoryInfo.repository.defaultBranch
|
||||||
@@ -168,7 +167,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
|||||||
if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) {
|
if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) {
|
||||||
if (issueCount > 0) {
|
if (issueCount > 0) {
|
||||||
pushedIds.add(commit.id)
|
pushedIds.add(commit.id)
|
||||||
createIssueComment(commit)
|
createIssueComment(owner, repository, commit)
|
||||||
// close issues
|
// close issues
|
||||||
if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
|
if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
|
||||||
closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository)
|
closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository)
|
||||||
@@ -230,13 +229,4 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def createIssueComment(commit: CommitInfo) = {
|
|
||||||
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
|
|
||||||
if(getIssue(owner, repository, issueId).isDefined){
|
|
||||||
getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
|
|
||||||
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,11 +87,11 @@ abstract class DefaultGitCommand(val owner: String, val repoName: String) extend
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DefaultGitUploadPack(owner: String, repoName: String, baseUrl: String) extends DefaultGitCommand(owner, repoName)
|
class DefaultGitUploadPack(owner: String, repoName: String) extends DefaultGitCommand(owner, repoName)
|
||||||
with RepositoryService with AccountService {
|
with RepositoryService with AccountService {
|
||||||
|
|
||||||
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
||||||
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo =>
|
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).foreach { repositoryInfo =>
|
||||||
if(!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)){
|
if(!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)){
|
||||||
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
|
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
|
||||||
val repository = git.getRepository
|
val repository = git.getRepository
|
||||||
@@ -107,7 +107,7 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) ex
|
|||||||
with RepositoryService with AccountService {
|
with RepositoryService with AccountService {
|
||||||
|
|
||||||
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
||||||
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo =>
|
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).foreach { repositoryInfo =>
|
||||||
if(isWritableUser(user, repositoryInfo)){
|
if(isWritableUser(user, repositoryInfo)){
|
||||||
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
|
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
|
||||||
val repository = git.getRepository
|
val repository = git.getRepository
|
||||||
@@ -124,7 +124,7 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) ex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PluginGitUploadPack(repoName: String, baseUrl: String, routing: GitRepositoryRouting) extends GitCommand
|
class PluginGitUploadPack(repoName: String, routing: GitRepositoryRouting) extends GitCommand
|
||||||
with SystemSettingsService {
|
with SystemSettingsService {
|
||||||
|
|
||||||
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
||||||
@@ -139,7 +139,7 @@ class PluginGitUploadPack(repoName: String, baseUrl: String, routing: GitReposit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PluginGitReceivePack(repoName: String, baseUrl: String, routing: GitRepositoryRouting) extends GitCommand
|
class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting) extends GitCommand
|
||||||
with SystemSettingsService {
|
with SystemSettingsService {
|
||||||
|
|
||||||
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
||||||
@@ -163,9 +163,9 @@ class GitCommandFactory(baseUrl: String) extends CommandFactory {
|
|||||||
logger.debug(s"command: $command")
|
logger.debug(s"command: $command")
|
||||||
|
|
||||||
command match {
|
command match {
|
||||||
case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, baseUrl, routing(repoName))
|
case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, routing(repoName))
|
||||||
case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, baseUrl, routing(repoName))
|
case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, routing(repoName))
|
||||||
case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName, baseUrl)
|
case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName)
|
||||||
case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl)
|
case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl)
|
||||||
case _ => new UnknownCommand(command)
|
case _ => new UnknownCommand(command)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package gitbucket.core.ssh
|
package gitbucket.core.ssh
|
||||||
|
|
||||||
import gitbucket.core.service.SystemSettingsService
|
import gitbucket.core.service.SystemSettingsService
|
||||||
|
import gitbucket.core.service.SystemSettingsService.SshAddress
|
||||||
import org.apache.sshd.common.Factory
|
import org.apache.sshd.common.Factory
|
||||||
import org.apache.sshd.server.{Environment, ExitCallback, Command}
|
import org.apache.sshd.server.{Environment, ExitCallback, Command}
|
||||||
import java.io.{OutputStream, InputStream}
|
import java.io.{OutputStream, InputStream}
|
||||||
import org.eclipse.jgit.lib.Constants
|
import org.eclipse.jgit.lib.Constants
|
||||||
|
|
||||||
class NoShell extends Factory[Command] with SystemSettingsService {
|
class NoShell(sshAddress:SshAddress) extends Factory[Command] {
|
||||||
override def create(): Command = new Command() {
|
override def create(): Command = new Command() {
|
||||||
private var in: InputStream = null
|
private var in: InputStream = null
|
||||||
private var out: OutputStream = null
|
private var out: OutputStream = null
|
||||||
@@ -15,7 +16,6 @@ class NoShell extends Factory[Command] with SystemSettingsService {
|
|||||||
|
|
||||||
override def start(env: Environment): Unit = {
|
override def start(env: Environment): Unit = {
|
||||||
val user = env.getEnv.get("USER")
|
val user = env.getEnv.get("USER")
|
||||||
val port = loadSystemSettings().sshPort.getOrElse(SystemSettingsService.DefaultSshPort)
|
|
||||||
val message =
|
val message =
|
||||||
"""
|
"""
|
||||||
| Welcome to
|
| Welcome to
|
||||||
@@ -31,8 +31,8 @@ class NoShell extends Factory[Command] with SystemSettingsService {
|
|||||||
|
|
|
|
||||||
| Please use:
|
| Please use:
|
||||||
|
|
|
|
||||||
| git clone ssh://%s@GITBUCKET_HOST:%d/OWNER/REPOSITORY_NAME.git
|
| git clone ssh://%s@%s:%d/OWNER/REPOSITORY_NAME.git
|
||||||
""".stripMargin.format(user, port).replace("\n", "\r\n") + "\r\n"
|
""".stripMargin.format(user, sshAddress.host, sshAddress.port).replace("\n", "\r\n") + "\r\n"
|
||||||
err.write(Constants.encode(message))
|
err.write(Constants.encode(message))
|
||||||
err.flush()
|
err.flush()
|
||||||
in.close()
|
in.close()
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|||||||
import javax.servlet.{ServletContextEvent, ServletContextListener}
|
import javax.servlet.{ServletContextEvent, ServletContextListener}
|
||||||
|
|
||||||
import gitbucket.core.service.SystemSettingsService
|
import gitbucket.core.service.SystemSettingsService
|
||||||
import gitbucket.core.util.Directory
|
import gitbucket.core.service.SystemSettingsService.SshAddress
|
||||||
|
import gitbucket.core.util.{Directory}
|
||||||
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
|
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
@@ -14,20 +15,20 @@ object SshServer {
|
|||||||
private val server = org.apache.sshd.server.SshServer.setUpDefaultServer()
|
private val server = org.apache.sshd.server.SshServer.setUpDefaultServer()
|
||||||
private val active = new AtomicBoolean(false)
|
private val active = new AtomicBoolean(false)
|
||||||
|
|
||||||
private def configure(port: Int, baseUrl: String) = {
|
private def configure(sshAddress: SshAddress, baseUrl: String) = {
|
||||||
server.setPort(port)
|
server.setPort(sshAddress.port)
|
||||||
val provider = new SimpleGeneratorHostKeyProvider(new File(s"${Directory.GitBucketHome}/gitbucket.ser"))
|
val provider = new SimpleGeneratorHostKeyProvider(new File(s"${Directory.GitBucketHome}/gitbucket.ser"))
|
||||||
provider.setAlgorithm("RSA")
|
provider.setAlgorithm("RSA")
|
||||||
provider.setOverwriteAllowed(false)
|
provider.setOverwriteAllowed(false)
|
||||||
server.setKeyPairProvider(provider)
|
server.setKeyPairProvider(provider)
|
||||||
server.setPublickeyAuthenticator(new PublicKeyAuthenticator)
|
server.setPublickeyAuthenticator(new PublicKeyAuthenticator)
|
||||||
server.setCommandFactory(new GitCommandFactory(baseUrl))
|
server.setCommandFactory(new GitCommandFactory(baseUrl))
|
||||||
server.setShellFactory(new NoShell)
|
server.setShellFactory(new NoShell(sshAddress))
|
||||||
}
|
}
|
||||||
|
|
||||||
def start(port: Int, baseUrl: String) = {
|
def start(sshAddress: SshAddress, baseUrl: String) = {
|
||||||
if(active.compareAndSet(false, true)){
|
if(active.compareAndSet(false, true)){
|
||||||
configure(port, baseUrl)
|
configure(sshAddress, baseUrl)
|
||||||
server.start()
|
server.start()
|
||||||
logger.info(s"Start SSH Server Listen on ${server.getPort}")
|
logger.info(s"Start SSH Server Listen on ${server.getPort}")
|
||||||
}
|
}
|
||||||
@@ -55,20 +56,18 @@ class SshServerListener extends ServletContextListener with SystemSettingsServic
|
|||||||
|
|
||||||
override def contextInitialized(sce: ServletContextEvent): Unit = {
|
override def contextInitialized(sce: ServletContextEvent): Unit = {
|
||||||
val settings = loadSystemSettings()
|
val settings = loadSystemSettings()
|
||||||
if(settings.ssh){
|
if (settings.sshAddress.isDefined && settings.baseUrl.isEmpty) {
|
||||||
settings.baseUrl match {
|
logger.error("Could not start SshServer because the baseUrl is not configured.")
|
||||||
case None =>
|
|
||||||
logger.error("Could not start SshServer because the baseUrl is not configured.")
|
|
||||||
case Some(baseUrl) =>
|
|
||||||
SshServer.start(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), baseUrl)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
for {
|
||||||
|
sshAddress <- settings.sshAddress
|
||||||
|
baseUrl <- settings.baseUrl
|
||||||
|
}
|
||||||
|
SshServer.start(sshAddress, baseUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
override def contextDestroyed(sce: ServletContextEvent): Unit = {
|
override def contextDestroyed(sce: ServletContextEvent): Unit = {
|
||||||
if(loadSystemSettings().ssh){
|
SshServer.stop()
|
||||||
SshServer.stop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService with Acco
|
|||||||
private def authenticate(action: (RepositoryInfo) => Any) = {
|
private def authenticate(action: (RepositoryInfo) => Any) = {
|
||||||
{
|
{
|
||||||
defining(request.paths){ paths =>
|
defining(request.paths){ paths =>
|
||||||
getRepository(paths(0), paths(1), baseUrl).map { repository =>
|
getRepository(paths(0), paths(1)).map { repository =>
|
||||||
context.loginAccount match {
|
context.loginAccount match {
|
||||||
case Some(x) if(x.isAdmin) => action(repository)
|
case Some(x) if(x.isAdmin) => action(repository)
|
||||||
case Some(x) if(repository.owner == x.userName) => action(repository)
|
case Some(x) if(repository.owner == x.userName) => action(repository)
|
||||||
@@ -95,7 +95,7 @@ trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService =
|
|||||||
private def authenticate(action: (RepositoryInfo) => Any) = {
|
private def authenticate(action: (RepositoryInfo) => Any) = {
|
||||||
{
|
{
|
||||||
defining(request.paths){ paths =>
|
defining(request.paths){ paths =>
|
||||||
getRepository(paths(0), paths(1), baseUrl).map { repository =>
|
getRepository(paths(0), paths(1)).map { repository =>
|
||||||
context.loginAccount match {
|
context.loginAccount match {
|
||||||
case Some(x) if(x.isAdmin) => action(repository)
|
case Some(x) if(x.isAdmin) => action(repository)
|
||||||
case Some(x) if(paths(0) == x.userName) => action(repository)
|
case Some(x) if(paths(0) == x.userName) => action(repository)
|
||||||
@@ -118,7 +118,7 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
|
|||||||
private def authenticate(action: (RepositoryInfo) => Any) = {
|
private def authenticate(action: (RepositoryInfo) => Any) = {
|
||||||
{
|
{
|
||||||
defining(request.paths){ paths =>
|
defining(request.paths){ paths =>
|
||||||
getRepository(paths(0), paths(1), baseUrl).map { repository =>
|
getRepository(paths(0), paths(1)).map { repository =>
|
||||||
if(!repository.repository.isPrivate){
|
if(!repository.repository.isPrivate){
|
||||||
action(repository)
|
action(repository)
|
||||||
} else {
|
} else {
|
||||||
@@ -145,7 +145,7 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
|
|||||||
private def authenticate(action: (RepositoryInfo) => Any) = {
|
private def authenticate(action: (RepositoryInfo) => Any) = {
|
||||||
{
|
{
|
||||||
defining(request.paths){ paths =>
|
defining(request.paths){ paths =>
|
||||||
getRepository(paths(0), paths(1), baseUrl).map { repository =>
|
getRepository(paths(0), paths(1)).map { repository =>
|
||||||
context.loginAccount match {
|
context.loginAccount match {
|
||||||
case Some(x) if(x.isAdmin) => action(repository)
|
case Some(x) if(x.isAdmin) => action(repository)
|
||||||
case Some(x) if(!repository.repository.isPrivate) => action(repository)
|
case Some(x) if(!repository.repository.isPrivate) => action(repository)
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ object Implicits {
|
|||||||
|
|
||||||
def gitRepositoryPath: String = request.getRequestURI.replaceFirst("^/git/", "/")
|
def gitRepositoryPath: String = request.getRequestURI.replaceFirst("^/git/", "/")
|
||||||
|
|
||||||
|
def baseUrl:String = {
|
||||||
|
val url = request.getRequestURL.toString
|
||||||
|
val len = url.length - (request.getRequestURI.length - request.getContextPath.length)
|
||||||
|
url.substring(0, len).stripSuffix("/")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
implicit class RichSession(session: HttpSession){
|
implicit class RichSession(session: HttpSession){
|
||||||
|
|||||||
@@ -32,14 +32,13 @@ object JGitUtil {
|
|||||||
*
|
*
|
||||||
* @param owner the user name of the repository owner
|
* @param owner the user name of the repository owner
|
||||||
* @param name the repository name
|
* @param name the repository name
|
||||||
* @param url the repository URL
|
|
||||||
* @param commitCount the commit count. If the repository has over 1000 commits then this property is 1001.
|
* @param commitCount the commit count. If the repository has over 1000 commits then this property is 1001.
|
||||||
* @param branchList the list of branch names
|
* @param branchList the list of branch names
|
||||||
* @param tags the list of tags
|
* @param tags the list of tags
|
||||||
*/
|
*/
|
||||||
case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){
|
case class RepositoryInfo(owner: String, name: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){
|
||||||
def this(owner: String, name: String, baseUrl: String) = {
|
def this(owner: String, name: String) = {
|
||||||
this(owner, name, s"${baseUrl}/git/${owner}/${name}.git", 0, Nil, Nil)
|
this(owner, name, 0, Nil, Nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,14 +173,14 @@ object JGitUtil {
|
|||||||
/**
|
/**
|
||||||
* Returns the repository information. It contains branch names and tag names.
|
* Returns the repository information. It contains branch names and tag names.
|
||||||
*/
|
*/
|
||||||
def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = {
|
def getRepositoryInfo(owner: String, repository: String): RepositoryInfo = {
|
||||||
using(Git.open(getRepositoryDir(owner, repository))){ git =>
|
using(Git.open(getRepositoryDir(owner, repository))){ git =>
|
||||||
try {
|
try {
|
||||||
// get commit count
|
// get commit count
|
||||||
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10001).sum
|
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10001).sum
|
||||||
|
|
||||||
RepositoryInfo(
|
RepositoryInfo(
|
||||||
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
|
owner, repository,
|
||||||
// commit count
|
// commit count
|
||||||
commitCount,
|
commitCount,
|
||||||
// branches
|
// branches
|
||||||
@@ -197,7 +196,7 @@ object JGitUtil {
|
|||||||
} catch {
|
} catch {
|
||||||
// not initialized
|
// not initialized
|
||||||
case e: NoHeadException => RepositoryInfo(
|
case e: NoHeadException => RepositoryInfo(
|
||||||
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", 0, Nil, Nil)
|
owner, repository, 0, Nil, Nil)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import gitbucket.core.plugin.{RenderRequest, PluginRegistry}
|
|||||||
import gitbucket.core.service.{RepositoryService, RequestCache}
|
import gitbucket.core.service.{RepositoryService, RequestCache}
|
||||||
import gitbucket.core.util.{FileUtil, JGitUtil, StringUtil}
|
import gitbucket.core.util.{FileUtil, JGitUtil, StringUtil}
|
||||||
|
|
||||||
import play.twirl.api.Html
|
import play.twirl.api.{Html, HtmlFormat}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides helper methods for Twirl templates.
|
* Provides helper methods for Twirl templates.
|
||||||
@@ -225,6 +225,13 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(implicit context: Context): Html =
|
def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(implicit context: Context): Html =
|
||||||
userWithContent(userName, mailAddress)(avatar(userName, size, tooltip, mailAddress))
|
userWithContent(userName, mailAddress)(avatar(userName, size, tooltip, mailAddress))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the avatar link to the account page.
|
||||||
|
* If user does not exist or disabled, this method returns avatar image without link.
|
||||||
|
*/
|
||||||
|
def avatarLink(commit: JGitUtil.CommitInfo, size: Int)(implicit context: Context): Html =
|
||||||
|
userWithContent(commit.authorName, commit.authorEmailAddress)(avatar(commit, size))
|
||||||
|
|
||||||
private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)(implicit context: Context): Html =
|
private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)(implicit context: Context): Html =
|
||||||
(if(mailAddress.isEmpty){
|
(if(mailAddress.isEmpty){
|
||||||
getAccountByUserName(userName)
|
getAccountByUserName(userName)
|
||||||
@@ -306,6 +313,19 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
private[this] val detectAndRenderLinksRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r
|
private[this] val detectAndRenderLinksRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r
|
||||||
|
|
||||||
def detectAndRenderLinks(text: String): Html = {
|
def detectAndRenderLinks(text: String): Html = {
|
||||||
Html(detectAndRenderLinksRegex.replaceAllIn(text, m => s"""<a href="${m.group(0)}">${m.group(0)}</a>"""))
|
val matches = detectAndRenderLinksRegex.findAllMatchIn(text).toSeq
|
||||||
|
|
||||||
|
val (x, pos) = matches.foldLeft((collection.immutable.Seq.empty[Html], 0)){ case ((x, pos), m) =>
|
||||||
|
val url = m.group(0)
|
||||||
|
val href = url.replace("\"", """)
|
||||||
|
(x ++ (Seq(
|
||||||
|
if(pos < m.start) Some(HtmlFormat.escape(text.substring(pos, m.start))) else None,
|
||||||
|
Some(Html(s"""<a href="${href}">${url}</a>"""))
|
||||||
|
).flatten), m.end)
|
||||||
|
}
|
||||||
|
// append rest fragment
|
||||||
|
val out = if (pos < text.length) x :+ HtmlFormat.escape(text.substring(pos)) else x
|
||||||
|
|
||||||
|
HtmlFormat.fill(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,54 +4,52 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main("Applications"){
|
@html.main("Applications"){
|
||||||
<div class="container">
|
<div class="container body">
|
||||||
<div class="row">
|
@menu("application", settings.ssh){
|
||||||
<div class="col-md-3">
|
<div class="panel panel-default">
|
||||||
@menu("application", settings.ssh)
|
<div class="panel-heading strong">Personal access tokens</div>
|
||||||
</div>
|
<div class="panel-body">
|
||||||
<div class="col-md-9">
|
@if(personalTokens.isEmpty && gneratedToken.isEmpty){
|
||||||
<div class="panel panel-default">
|
No tokens.
|
||||||
<div class="panel-heading strong">Personal access tokens</div>
|
} else {
|
||||||
<div class="panel-body">
|
Tokens you have generated that can be used to access the GitBucket API.
|
||||||
@if(personalTokens.isEmpty && gneratedToken.isEmpty){
|
<hr style="margin-top: 10px;">
|
||||||
No tokens.
|
}
|
||||||
} else {
|
@gneratedToken.map{ case (token, tokenString) =>
|
||||||
Tokens you have generated that can be used to access the GitBucket API.<hr>
|
<div class="alert alert-info">
|
||||||
}
|
Make sure to copy your new personal access token now. You won't be able to see it again!
|
||||||
@gneratedToken.map{ case (token, tokenString) =>
|
</div>
|
||||||
<div class="alert alert-info">
|
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-sm btn-danger pull-right">Delete</a>
|
||||||
Make sure to copy your new personal access token now. You won't be able to see it again!
|
<div style="width: 50%;">
|
||||||
</div>
|
|
||||||
@helper.html.copy("generated-token-copy", tokenString){
|
@helper.html.copy("generated-token-copy", tokenString){
|
||||||
<input type="text" value="@tokenString" style="width:21em" readonly>
|
<input type="text" value="@tokenString" class="form-control input-sm" readonly>
|
||||||
}
|
}
|
||||||
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-mini btn-danger pull-right">Delete</a>
|
</div>
|
||||||
|
<hr style="margin-top: 10px;">
|
||||||
|
}
|
||||||
|
@personalTokens.zipWithIndex.map { case (token, i) =>
|
||||||
|
@if(i != 0){
|
||||||
<hr>
|
<hr>
|
||||||
}
|
}
|
||||||
@personalTokens.zipWithIndex.map { case (token, i) =>
|
<strong style="line-height: 30px;">@token.note</strong>
|
||||||
@if(i != 0){
|
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-sm btn-danger pull-right">Delete</a>
|
||||||
<hr>
|
}
|
||||||
}
|
</div>
|
||||||
<strong>@token.note</strong>
|
</div>
|
||||||
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-mini btn-danger pull-right">Delete</a>
|
<form method="POST" action="@path/@account.userName/_personalToken" validate="true">
|
||||||
}
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading strong">Generate new token</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<fieldset>
|
||||||
|
<label for="note" class="strong">Token description</label>
|
||||||
|
<div><span id="error-note" class="error"></span></div>
|
||||||
|
<input type="text" name="note" id="note" class="form-control"/>
|
||||||
|
<p class="muted">What's this token for?</p>
|
||||||
|
</fieldset>
|
||||||
|
<input type="submit" class="btn btn-success" value="Generate token"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" action="@path/@account.userName/_personalToken" validate="true">
|
</form>
|
||||||
<div class="panel panel-default">
|
}
|
||||||
<div class="panel-heading strong">Generate new token</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<fieldset>
|
|
||||||
<label for="note" class="strong">Token description</label>
|
|
||||||
<div><span id="error-note" class="error"></span></div>
|
|
||||||
<input type="text" name="note" id="note" class="form-control" style="width: 400px;"/>
|
|
||||||
<p class="muted">What's this token for?</p>
|
|
||||||
</fieldset>
|
|
||||||
<input type="submit" class="btn btn-success" value="Generate token"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,11 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main("Edit your profile"){
|
@html.main("Edit your profile"){
|
||||||
<div class="container">
|
<div class="container body">
|
||||||
<div class="row">
|
@menu("profile", settings.ssh){
|
||||||
<div class="col-md-3">
|
@helper.html.information(info)
|
||||||
@menu("profile", settings.ssh)
|
@if(LDAPUtil.isDummyMailAddress(account)){<div class="alert alert-danger">Please register your mail address.</div>}
|
||||||
</div>
|
<form action="@url(account.userName)/_edit" method="POST" validate="true">
|
||||||
<div class="col-md-9">
|
|
||||||
@helper.html.information(info)
|
|
||||||
@if(LDAPUtil.isDummyMailAddress(account)){<div class="alert alert-danger">Please register your mail address.</div>}
|
|
||||||
<form action="@url(account.userName)/_edit" method="POST" validate="true">
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading strong">Profile</div>
|
<div class="panel-heading strong">Profile</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@@ -49,17 +45,17 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 20px;">
|
|
||||||
<div class="pull-right">
|
|
||||||
<a href="@path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
|
|
||||||
</div>
|
|
||||||
<input type="submit" class="btn btn-success" value="Save"/>
|
|
||||||
@if(!LDAPUtil.isDummyMailAddress(account)){<a href="@url(account.userName)" class="btn btn-default">Cancel</a>}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="pull-right">
|
||||||
|
<a href="@path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
|
||||||
|
</div>
|
||||||
|
<input type="submit" class="btn btn-success" value="Save"/>
|
||||||
|
@if(!LDAPUtil.isDummyMailAddress(account)){<a href="@url(account.userName)" class="btn btn-default">Cancel</a>}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main(if(account.isEmpty) "Create group" else "Edit group"){
|
@html.main(if(account.isEmpty) "Create group" else "Edit group"){
|
||||||
<div class="container">
|
<div class="body main-center">
|
||||||
<form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true">
|
<form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
@@ -42,12 +42,12 @@
|
|||||||
<fieldset class="margin">
|
<fieldset class="margin">
|
||||||
@if(account.isDefined){
|
@if(account.isDefined){
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="@url(account.get.userName)/_deletegroup" id="delete" class="btn btn-danger btn-lg">Delete Group</a>
|
<a href="@url(account.get.userName)/_deletegroup" id="delete" class="btn btn-danger">Delete Group</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<input type="submit" class="btn btn-success btn-lg" value="@if(account.isEmpty){Create Group} else {Update Group}"/>
|
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create Group} else {Update Group}"/>
|
||||||
@if(account.isDefined){
|
@if(account.isDefined){
|
||||||
<a href="@url(account.get.userName)" class="btn btn-default btn-lg">Cancel</a>
|
<a href="@url(account.get.userName)" class="btn btn-default">Cancel</a>
|
||||||
}
|
}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
@@ -107,18 +107,18 @@ $(function(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addMemberHTML(userName, isManager){
|
function addMemberHTML(userName, isManager){
|
||||||
var memberButton = $('<button type="button" class="btn btn-default btn-mini" value="false">Member</button>').data('name', userName);
|
var memberButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="false" name="' + userName + '">Member</label>');
|
||||||
if(!isManager){
|
if(!isManager){
|
||||||
memberButton.addClass('active');
|
memberButton.addClass('active');
|
||||||
}
|
}
|
||||||
var managerButton = $('<button type="button" class="btn btn-default btn-mini" value="true">Manager</button>').data('name', userName);
|
var managerButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="true" name="' + userName + '">Manager</label>');
|
||||||
if(isManager){
|
if(isManager){
|
||||||
managerButton.addClass('active');
|
managerButton.addClass('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#member-list').append($('<li>')
|
$('#member-list').append($('<li>')
|
||||||
.data('name', userName)
|
.data('name', userName)
|
||||||
.append($('<div class="btn-group is_manager" data-toggle="buttons-radio">')
|
.append($('<div class="btn-group is_manager" data-toggle="buttons">')
|
||||||
.append(memberButton)
|
.append(memberButton)
|
||||||
.append(managerButton))
|
.append(managerButton))
|
||||||
.append(' ')
|
.append(' ')
|
||||||
@@ -130,9 +130,7 @@ $(function(){
|
|||||||
function updateMembers(){
|
function updateMembers(){
|
||||||
var members = $('#member-list li').map(function(i, e){
|
var members = $('#member-list li').map(function(i, e){
|
||||||
var userName = $(e).data('name');
|
var userName = $(e).data('name');
|
||||||
return userName + ':' + $('button.active').filter(function(i, e){
|
return userName + ':' + $(e).find('label.active input[type=radio]').attr('value');
|
||||||
return $(e).data('name') == userName;
|
|
||||||
}).attr('value');
|
|
||||||
}).get().join(',');
|
}).get().join(',');
|
||||||
$('#members').val(members);
|
$('#members').val(members);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,57 +3,57 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main(account.userName){
|
@html.main(account.userName){
|
||||||
<div class="container">
|
<div class="container body">
|
||||||
<div class="container-fluid">
|
<div class="main-sidebar">
|
||||||
<div class="row">
|
<div class="block">
|
||||||
<div class="col-md-4">
|
<div class="account-image">@avatar(account.userName, 240)</div>
|
||||||
<div class="block">
|
<div class="account-fullname">@account.fullName</div>
|
||||||
<div class="account-image">@avatar(account.userName, 270)</div>
|
<div class="account-username">@account.userName</div>
|
||||||
<div class="account-fullname">@account.fullName</div>
|
|
||||||
<div class="account-username">@account.userName</div>
|
|
||||||
</div>
|
|
||||||
<div class="block">
|
|
||||||
@if(account.url.isDefined){
|
|
||||||
<div><i class="octicon octicon-home"></i> <a href="@account.url">@account.url</a></div>
|
|
||||||
}
|
|
||||||
<div><i class="octicon octicon-clock"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
|
|
||||||
</div>
|
|
||||||
@if(groupNames.nonEmpty){
|
|
||||||
<div>
|
|
||||||
<div>Groups</div>
|
|
||||||
@groupNames.map { groupName =>
|
|
||||||
<a href="@url(groupName)">@avatar(groupName, 36, tooltip = true)</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="col-md-8">
|
|
||||||
<ul class="nav nav-tabs" style="margin-bottom: 5px;">
|
|
||||||
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
|
|
||||||
@if(account.isGroupAccount){
|
|
||||||
<li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li>
|
|
||||||
} else {
|
|
||||||
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
|
|
||||||
}
|
|
||||||
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
|
|
||||||
<li class="pull-right">
|
|
||||||
<div class="button-group">
|
|
||||||
<a href="@url(account.userName)/_edit" class="btn btn-default">Edit Your Profile</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
@if(loginAccount.isDefined && account.isGroupAccount && isGroupManager){
|
|
||||||
<li class="pull-right">
|
|
||||||
<div class="button-group">
|
|
||||||
<a href="@url(account.userName)/_editgroup" class="btn btn-default">Edit Group</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
@body
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="block">
|
||||||
|
@if(account.url.isDefined){
|
||||||
|
<div><i class="octicon octicon-home"></i> <a href="@account.url">@account.url</a></div>
|
||||||
|
}
|
||||||
|
<div><i class="octicon octicon-clock"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
|
||||||
|
</div>
|
||||||
|
@if(groupNames.nonEmpty){
|
||||||
|
<div>
|
||||||
|
<div>Groups</div>
|
||||||
|
@groupNames.map { groupName =>
|
||||||
|
@avatarLink(groupName, 36, tooltip = true)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="main-content">
|
||||||
|
<ul class="nav nav-tabs" style="margin-bottom: 5px;">
|
||||||
|
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
|
||||||
|
@if(account.isGroupAccount){
|
||||||
|
<li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li>
|
||||||
|
} else {
|
||||||
|
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
|
||||||
|
}
|
||||||
|
@gitbucket.core.plugin.PluginRegistry().getProfileTabs.map { tab =>
|
||||||
|
@tab(account, context).map { link =>
|
||||||
|
<li@if(active == link.id){ class="active"}><a href="@path/@link.path">@link.label</a></li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
|
||||||
|
<li class="pull-right">
|
||||||
|
<div class="button-group">
|
||||||
|
<a href="@url(account.userName)/_edit" class="btn btn-default">Edit Your Profile</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
@if(loginAccount.isDefined && account.isGroupAccount && isGroupManager){
|
||||||
|
<li class="pull-right">
|
||||||
|
<div class="button-group">
|
||||||
|
<a href="@url(account.userName)/_editgroup" class="btn btn-default">Edit Group</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
@body
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@(active: String, ssh: Boolean)(implicit context: gitbucket.core.controller.Context)
|
@(active: String, ssh: Boolean)(body: Html)(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
<div class="box">
|
<div class="main-sidebar">
|
||||||
<ul class="nav nav-tabs nav-stacked side-menu">
|
<ul class="nav nav-pills nav-stacked">
|
||||||
<li@if(active=="profile"){ class="active"}>
|
<li@if(active=="profile"){ class="active"}>
|
||||||
<a href="@path/@loginAccount.get.userName/_edit">Profile</a>
|
<a href="@path/@loginAccount.get.userName/_edit">Profile</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -13,5 +13,15 @@
|
|||||||
<li@if(active=="application"){ class="active"}>
|
<li@if(active=="application"){ class="active"}>
|
||||||
<a href="@path/@loginAccount.get.userName/_application">Applications</a>
|
<a href="@path/@loginAccount.get.userName/_application">Applications</a>
|
||||||
</li>
|
</li>
|
||||||
|
@gitbucket.core.plugin.PluginRegistry().getAccountSettingMenus.map { menu =>
|
||||||
|
@menu(context).map { link =>
|
||||||
|
<li@if(active==link.id){ class="active"}>
|
||||||
|
<a href="@path/@link.path">@link.label</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="main-content">
|
||||||
|
@body
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
|||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main("Create a New Repository"){
|
@html.main("Create a New Repository"){
|
||||||
<div style="width: 600px; margin: 10px auto;">
|
<div class="body main-center">
|
||||||
<h2>Create a new repository</h2>
|
<h2>Create a new repository</h2>
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
A repository contains all the files for your project, including the revision history.
|
A repository contains all the files for your project, including the revision history.
|
||||||
@@ -67,7 +67,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
|||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="margin form-actions">
|
<fieldset class="margin form-actions">
|
||||||
<input type="submit" class="btn btn-success btn-lg" value="Create repository"/>
|
<input type="submit" class="btn btn-success" value="Create repository"/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main("Create your account"){
|
@html.main("Create your account"){
|
||||||
<div class="container">
|
<div class="container body">
|
||||||
<h3>Create your account</h3>
|
<h3>Create your account</h3>
|
||||||
<form action="@path/register" method="POST" validate="true">
|
<form action="@path/register" method="POST" validate="true">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@@ -3,46 +3,41 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main("SSH Keys"){
|
@html.main("SSH Keys"){
|
||||||
<div class="container">
|
<div class="container body">
|
||||||
<div class="row">
|
@menu("ssh", settings.ssh){
|
||||||
<div class="col-md-3">
|
<div class="panel panel-default">
|
||||||
@menu("ssh", settings.ssh)
|
<div class="panel-heading strong">SSH Keys</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
@if(sshKeys.isEmpty){
|
||||||
|
No keys
|
||||||
|
}
|
||||||
|
@sshKeys.zipWithIndex.map { case (key, i) =>
|
||||||
|
@if(i != 0){
|
||||||
|
<hr>
|
||||||
|
}
|
||||||
|
<strong style="line-height: 30px;">@key.title</strong> (@SshUtil.fingerPrint(key.publicKey).getOrElse("Key is invalid."))
|
||||||
|
<a href="@path/@account.userName/_ssh/delete/@key.sshKeyId" class="btn btn-sm btn-danger pull-right">Delete</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9">
|
<form method="POST" action="@path/@account.userName/_ssh" validate="true">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading strong">SSH Keys</div>
|
<div class="panel-heading strong">Add an SSH Key</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@if(sshKeys.isEmpty){
|
<fieldset class="form-group">
|
||||||
No keys
|
<label for="title" class="strong">Title</label>
|
||||||
}
|
<div><span id="error-title" class="error"></span></div>
|
||||||
@sshKeys.zipWithIndex.map { case (key, i) =>
|
<input type="text" name="title" id="title" class="form-control"/>
|
||||||
@if(i != 0){
|
</fieldset>
|
||||||
<hr>
|
<fieldset class="form-group">
|
||||||
}
|
<label for="publicKey" class="strong">Key</label>
|
||||||
<strong>@key.title</strong> (@SshUtil.fingerPrint(key.publicKey).getOrElse("Key is invalid."))
|
<div><span id="error-publicKey" class="error"></span></div>
|
||||||
<a href="@path/@account.userName/_ssh/delete/@key.sshKeyId" class="btn btn-mini btn-danger pull-right">Delete</a>
|
<textarea name="publicKey" id="publicKey" class="form-control" style="height: 250px;"></textarea>
|
||||||
}
|
</fieldset>
|
||||||
|
<input type="submit" class="btn btn-success" value="Add"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" action="@path/@account.userName/_ssh" validate="true">
|
</form>
|
||||||
<div class="panel panel-default">
|
}
|
||||||
<div class="panel-heading strong">Add an SSH Key</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<fieldset class="form-group">
|
|
||||||
<label for="title" class="strong">Title</label>
|
|
||||||
<div><span id="error-title" class="error"></span></div>
|
|
||||||
<input type="text" name="title" id="title" class="form-control" style="width: 400px;"/>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="form-group">
|
|
||||||
<label for="publicKey" class="strong">Key</label>
|
|
||||||
<div><span id="error-publicKey" class="error"></span></div>
|
|
||||||
<textarea name="publicKey" id="publicKey" class="form-control" style="width: 600px; height: 250px;"></textarea>
|
|
||||||
</fieldset>
|
|
||||||
<input type="submit" class="btn btn-success" value="Add"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
@(active: String)(body: Html)(implicit context: gitbucket.core.controller.Context)
|
@(active: String)(body: Html)(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
<div class="container body">
|
<div class="container body">
|
||||||
<div class="row">
|
<div class="main-sidebar">
|
||||||
<div class="col-md-3">
|
<ul class="nav nav-pills nav-stacked" id="system-admin-menu-container">
|
||||||
<ul class="nav nav-tabs nav-stacked side-menu" id="system-admin-menu-container">
|
<li@if(active=="users"){ class="active"}>
|
||||||
<li@if(active=="users"){ class="active"}>
|
<a href="@path/admin/users">User Management</a>
|
||||||
<a href="@path/admin/users">User Management</a>
|
</li>
|
||||||
</li>
|
<li@if(active=="system"){ class="active"}>
|
||||||
<li@if(active=="system"){ class="active"}>
|
<a href="@path/admin/system">System Settings</a>
|
||||||
<a href="@path/admin/system">System Settings</a>
|
</li>
|
||||||
</li>
|
<li@if(active=="plugins"){ class="active"}>
|
||||||
<li@if(active=="plugins"){ class="active"}>
|
|
||||||
<a href="@path/admin/plugins">Plugins</a>
|
<a href="@path/admin/plugins">Plugins</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="@path/console/login.jsp">H2 Console</a>
|
<a href="@path/console/login.jsp">H2 Console</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
@gitbucket.core.plugin.PluginRegistry().getSystemSettingMenus.map { menu =>
|
||||||
</div>
|
@menu(context).map { link =>
|
||||||
<div class="col-md-9">
|
<li@if(active==link.id){ class="active"}>
|
||||||
@body
|
<a href="@path/@link.path">@link.label</a>
|
||||||
</div>
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="main-content">
|
||||||
|
@body
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,15 +111,24 @@
|
|||||||
Enable SSH access to git repository
|
Enable SSH access to git repository
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="form-group ssh">
|
<div class="ssh">
|
||||||
<label class="control-label col-md-3" for="sshPort">SSH Port</label>
|
<div class="form-group">
|
||||||
<div class="col-md-9">
|
<label class="control-label col-md-3" for="sshHost">SSH Host</label>
|
||||||
<input type="text" id="sshPort" name="sshPort" class="form-control" value="@settings.sshPort"/>
|
<div class="col-md-9">
|
||||||
<span id="error-sshPort" class="error"></span>
|
<input type="text" id="sshHost" name="sshHost" class="form-control" value="@settings.sshHost"/>
|
||||||
|
<span id="error-sshHost" class="error"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-md-3" for="sshPort">SSH Port</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<input type="text" id="sshPort" name="sshPort" class="form-control" value="@settings.sshPort"/>
|
||||||
|
<span id="error-sshPort" class="error"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
Base URL is required if SSH access is enabled.
|
Both of SSH host and Base URL are required if SSH access is enabled.
|
||||||
</p>
|
</p>
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
<!-- Authentication -->
|
<!-- Authentication -->
|
||||||
|
|||||||
@@ -4,18 +4,8 @@
|
|||||||
groups: List[String])(implicit context: gitbucket.core.controller.Context)
|
groups: List[String])(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
<span class="small">
|
<div id="table-issues-control">
|
||||||
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
|
@helper.html.dropdown("Visibility"){
|
||||||
<i class="octicon octicon-issue-opened @(if(condition.state == "open"){"active"})"></i>
|
|
||||||
@openCount Open
|
|
||||||
</a>
|
|
||||||
<a class="button-link@if(condition.state == "closed"){ selected}" href="@condition.copy(state = "closed").toURL">
|
|
||||||
<i class="octicon octicon-check @(if(condition.state == "closed"){"active"})"></i>
|
|
||||||
@closedCount Closed
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<div class="pull-right" id="table-issues-control">
|
|
||||||
@helper.html.dropdown("Visibility", flat = true){
|
|
||||||
<li>
|
<li>
|
||||||
<a href="@(condition.copy(visibility = (if(condition.visibility == Some("private")) None else Some("private"))).toURL)">
|
<a href="@(condition.copy(visibility = (if(condition.visibility == Some("private")) None else Some("private"))).toURL)">
|
||||||
@helper.html.checkicon(condition.visibility == Some("private"))
|
@helper.html.checkicon(condition.visibility == Some("private"))
|
||||||
@@ -29,7 +19,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
@helper.html.dropdown("Organization", flat = true){
|
@helper.html.dropdown("Organization"){
|
||||||
@groups.map { group =>
|
@groups.map { group =>
|
||||||
<li>
|
<li>
|
||||||
<a href="@((if(condition.groups.contains(group)) condition.copy(groups = condition.groups - group) else condition.copy(groups = condition.groups + group)).toURL)">
|
<a href="@((if(condition.groups.contains(group)) condition.copy(groups = condition.groups - group) else condition.copy(groups = condition.groups + group)).toURL)">
|
||||||
@@ -39,7 +29,7 @@
|
|||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@helper.html.dropdown("Sort", flat = true){
|
@helper.html.dropdown("Sort"){
|
||||||
<li>
|
<li>
|
||||||
<a href="@condition.copy(sort="created", direction="desc").toURL">
|
<a href="@condition.copy(sort="created", direction="desc").toURL">
|
||||||
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
|
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
|
||||||
|
|||||||
@@ -4,13 +4,17 @@
|
|||||||
closedCount: Int,
|
closedCount: Int,
|
||||||
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
|
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
|
||||||
filter: String,
|
filter: String,
|
||||||
groups: List[String])(implicit context: gitbucket.core.controller.Context)
|
groups: List[String],
|
||||||
|
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
|
||||||
|
userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main("Issues"){
|
@html.main("Issues"){
|
||||||
@dashboard.html.tab("issues")
|
@sidebar(recentRepositories, userRepositories){
|
||||||
<div class="container">
|
@dashboard.html.tab("issues")
|
||||||
@issuesnavi(filter, "issues", condition)
|
<div class="container">
|
||||||
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
@issuesnavi(filter, openCount, closedCount, condition)
|
||||||
</div>
|
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,6 @@
|
|||||||
@issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) =>
|
@issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) =>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding-top: 12px; padding-bottom: 12px;">
|
<td style="padding-top: 12px; padding-bottom: 12px;">
|
||||||
@if(issue.isPullRequest){
|
|
||||||
<i class="octicon octicon-git-pull-request @(if(issue.closed) "closed" else "open")"></i>
|
|
||||||
} else {
|
|
||||||
<i class="octicon octicon-issue-@(if(issue.closed) "closed" else "opened")"></i>
|
|
||||||
}
|
|
||||||
<a href="@path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a> ・
|
<a href="@path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a> ・
|
||||||
@if(issue.isPullRequest){
|
@if(issue.isPullRequest){
|
||||||
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
|
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
|
||||||
@@ -50,7 +45,7 @@
|
|||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
<div class="small muted" style="margin-left: 20px; margin-top: 2px;">
|
<div class="small muted" style="margin-top: 2px;">
|
||||||
#@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
|
#@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
|
||||||
@milestone.map { milestone =>
|
@milestone.map { milestone =>
|
||||||
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><i class="octicon octicon-milestone"></i> @milestone</a></span>
|
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><i class="octicon octicon-milestone"></i> @milestone</a></span>
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
@(filter: String,
|
@(filter: String,
|
||||||
active: String,
|
openCount: Int,
|
||||||
|
closedCount: Int,
|
||||||
condition: gitbucket.core.service.IssuesService.IssueSearchCondition)(implicit context: gitbucket.core.controller.Context)
|
condition: gitbucket.core.service.IssuesService.IssueSearchCondition)(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
<ul class="nav nav-pills pull-left" style="line-height: 14px;">
|
<ul class="nav nav-pills pull-left" style="line-height: 14px; margin-bottom: 10px;">
|
||||||
|
<li class="@(if(condition.state == "open"){"active"})">
|
||||||
|
<a href="@condition.copy(state = "open").toURL">Open <span class="badge">@openCount</span></a>
|
||||||
|
</li>
|
||||||
|
<li class="@(if(condition.state == "closed"){"active"})">
|
||||||
|
<a href="@condition.copy(state = "closed").toURL">Closed <span class="badge">@closedCount</span></a>
|
||||||
|
</li>
|
||||||
|
@*
|
||||||
<li class="@if(filter == "created_by"){active}">
|
<li class="@if(filter == "created_by"){active}">
|
||||||
<a href="@path/dashboard/@active/created_by@condition.copy(author = None, assigned = None).toURL">Created</a>
|
<a href="@path/dashboard/@active/created_by@condition.copy(author = None, assigned = None).toURL">Created</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -13,8 +21,5 @@
|
|||||||
<li class="@if(filter == "mentioned"){active}">
|
<li class="@if(filter == "mentioned"){active}">
|
||||||
<a href="@path/dashboard/@active/mentioned@condition.copy(author = None, assigned = None).toURL">Mentioned</a>
|
<a href="@path/dashboard/@active/mentioned@condition.copy(author = None, assigned = None).toURL">Mentioned</a>
|
||||||
</li>
|
</li>
|
||||||
|
*@
|
||||||
</ul>
|
</ul>
|
||||||
<form method="GET" id="search-filter-form" action="@path/dashboard/@active" class="pull-right">
|
|
||||||
<input type="text" id="search-filter-box" class="form-control input-lg" name="q" style="width: 400px;"
|
|
||||||
value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
|
|
||||||
</form>
|
|
||||||
|
|||||||
@@ -4,13 +4,17 @@
|
|||||||
closedCount: Int,
|
closedCount: Int,
|
||||||
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
|
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
|
||||||
filter: String,
|
filter: String,
|
||||||
groups: List[String])(implicit context: gitbucket.core.controller.Context)
|
groups: List[String],
|
||||||
|
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
|
||||||
|
userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main("Pull Requests"){
|
@html.main("Pull Requests"){
|
||||||
@dashboard.html.tab("pulls")
|
@sidebar(recentRepositories, userRepositories){
|
||||||
<div class="container">
|
@dashboard.html.tab("pulls")
|
||||||
@issuesnavi(filter, "pulls", condition)
|
<div class="container">
|
||||||
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
@issuesnavi(filter, openCount, closedCount, condition)
|
||||||
</div>
|
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
73
src/main/twirl/gitbucket/core/dashboard/sidebar.scala.html
Normal file
73
src/main/twirl/gitbucket/core/dashboard/sidebar.scala.html
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
@(recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
|
||||||
|
userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(body: Html)(implicit context: gitbucket.core.controller.Context)
|
||||||
|
@import context._
|
||||||
|
@import gitbucket.core.view.helpers._
|
||||||
|
<div class="container body">
|
||||||
|
<div class="dashboard-sidebar">
|
||||||
|
@if(loginAccount.isEmpty){
|
||||||
|
<div id="dashboard-signin-form">@html.signinform(settings)</div>
|
||||||
|
} else {
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading strong">
|
||||||
|
Your repositories <span class="badge">@userRepositories.size</span>
|
||||||
|
</div>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
@if(userRepositories.isEmpty){
|
||||||
|
<li class="list-group-item">No repositories</li>
|
||||||
|
} else {
|
||||||
|
@defining(20){ max =>
|
||||||
|
@userRepositories.zipWithIndex.map { case (repository, i) =>
|
||||||
|
<li class="list-group-item repo-link" style="@if(i > max - 1){display:none;}">
|
||||||
|
@helper.html.repositoryicon(repository, false)
|
||||||
|
@if(repository.owner == loginAccount.get.userName){
|
||||||
|
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
|
||||||
|
} else {
|
||||||
|
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
@if(userRepositories.size > max){
|
||||||
|
<li class="list-group-item show-more">
|
||||||
|
<a href="javascript:void(0);" id="show-more-repos">Show @{userRepositories.size - max} more repositories...</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading strong">Recent updated repositories</div>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
@if(recentRepositories.isEmpty){
|
||||||
|
<li class="list-group-item">No repositories</li>
|
||||||
|
} else {
|
||||||
|
@defining(20){ max =>
|
||||||
|
@recentRepositories.zipWithIndex.map { case (repository, i) =>
|
||||||
|
<li class="list-group-item repo-link" style="@if(i > max - 1){display:none;}">
|
||||||
|
@helper.html.repositoryicon(repository, false)
|
||||||
|
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
@if(recentRepositories.size > max){
|
||||||
|
<li class="list-group-item show-more">
|
||||||
|
<a href="javascript:void(0);" id="show-more-recent-repos">Show @{recentRepositories.size - max} more repositories...</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-content">
|
||||||
|
@body
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
$(function(){
|
||||||
|
$('#show-more-repos, #show-more-recent-repos').click(function(e){
|
||||||
|
$(e.target).parents('ul.list-group').find('li.repo-link').show();
|
||||||
|
$(e.target).parents('li.show-more').remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -1,52 +1,15 @@
|
|||||||
@(active: String = "")(implicit context: gitbucket.core.controller.Context)
|
@(active: String = "")(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
<div class="dashboard-nav">
|
<ul class="nav nav-tabs" style="margin-bottom: 20px;">
|
||||||
<div class="container">
|
<li @if(active == ""){ class="active"}><a href="@path/">News Feed</a></li>
|
||||||
<a href="@path/" @if(active == ""){ class="active"}>
|
@if(loginAccount.isDefined){
|
||||||
<i class="octicon octicon-rss"></i> News Feed
|
<li @if(active == "pulls" ){ class="active"}><a href="@path/dashboard/pulls">Pull Requests</a></li>
|
||||||
</a>
|
<li @if(active == "issues"){ class="active"}><a href="@path/dashboard/issues">Issues</a></li>
|
||||||
@if(loginAccount.isDefined){
|
@gitbucket.core.plugin.PluginRegistry().getDashboardTabs.map { tab =>
|
||||||
<a href="@path/dashboard/pulls" @if(active == "pulls" ){ class="active"}>
|
@tab(context).map { link =>
|
||||||
<i class="octicon octicon-git-pull-request"></i> Pull Requests
|
<li @if(active == link.id){ class="active"}><a href="@path/@link.path">@link.label</a></li>
|
||||||
</a>
|
}
|
||||||
<a href="@path/dashboard/issues" @if(active == "issues"){ class="active"}>
|
|
||||||
<i class="octicon octicon-issue-opened"></i> Issues
|
|
||||||
</a>
|
|
||||||
}
|
}
|
||||||
</div>
|
}
|
||||||
</div>
|
</ul>
|
||||||
<style type="text/css">
|
|
||||||
div.dashboard-nav {
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
text-align: right;
|
|
||||||
height: 32px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.dashboard-nav a {
|
|
||||||
line-height: 10px;
|
|
||||||
margin-left: 20px;
|
|
||||||
padding-bottom: 13px;
|
|
||||||
padding-left: 4px;
|
|
||||||
padding-right: 4px;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.dashboard-nav .octicon{
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
font-size: 16px;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.dashboard-nav a:hover,div.dashboard-nav a:hover .octicon {
|
|
||||||
text-decoration: none;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.dashboard-nav a.active {
|
|
||||||
border-bottom: 2px solid #bb4444;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -61,8 +61,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@detailActivity(activity: gitbucket.core.model.Activity, image: String) = {
|
@detailActivity(activity: gitbucket.core.model.Activity, image: String) = {
|
||||||
|
@*
|
||||||
<div class="activity-icon-large"><i class="mega-octicon octicon-@image"></i></div>
|
<div class="activity-icon-large"><i class="mega-octicon octicon-@image"></i></div>
|
||||||
<div class="activity-content">
|
*@
|
||||||
|
<div>
|
||||||
<div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
|
<div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
|
||||||
<div class="strong">
|
<div class="strong">
|
||||||
@avatar(activity.activityUserName, 16)
|
@avatar(activity.activityUserName, 16)
|
||||||
@@ -75,8 +77,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@customActivity(activity: gitbucket.core.model.Activity, image: String)(additionalInfo: Any) = {
|
@customActivity(activity: gitbucket.core.model.Activity, image: String)(additionalInfo: Any) = {
|
||||||
|
@*
|
||||||
<div class="activity-icon-large"><i class="mega-octicon octicon-@image"></i></div>
|
<div class="activity-icon-large"><i class="mega-octicon octicon-@image"></i></div>
|
||||||
<div class="activity-content">
|
*@
|
||||||
|
<div>
|
||||||
<div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
|
<div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
|
||||||
<div class="strong">
|
<div class="strong">
|
||||||
@avatar(activity.activityUserName, 16)
|
@avatar(activity.activityUserName, 16)
|
||||||
@@ -87,12 +91,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@simpleActivity(activity: gitbucket.core.model.Activity, image: String) = {
|
@simpleActivity(activity: gitbucket.core.model.Activity, image: String) = {
|
||||||
|
@*
|
||||||
<div class="activity-icon-small"><i class="octicon octicon-@image"></i></div>
|
<div class="activity-icon-small"><i class="octicon octicon-@image"></i></div>
|
||||||
<div class="activity-content">
|
*@
|
||||||
|
<div>
|
||||||
|
<span class="muted small">@helper.html.datetimeago(activity.activityDate)</span>
|
||||||
<div>
|
<div>
|
||||||
@avatar(activity.activityUserName, 16)
|
@avatar(activity.activityUserName, 16)
|
||||||
@activityMessage(activity.message)
|
@activityMessage(activity.message)
|
||||||
<span class="muted small">@helper.html.datetimeago(activity.activityDate)</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,12 +27,6 @@ $(function(){
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust clickable area width
|
|
||||||
$('#@textareaId').next('div.clickable').css({
|
|
||||||
'width': ($('#@textareaId').width() + 18) + 'px',
|
|
||||||
'font-size': '13px'
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
@if(comment.fileName.isDefined){filename="@comment.fileName.get"}
|
@if(comment.fileName.isDefined){filename="@comment.fileName.get"}
|
||||||
@if(comment.newLine.isDefined){newline="@comment.newLine.get"}
|
@if(comment.newLine.isDefined){newline="@comment.newLine.get"}
|
||||||
@if(comment.oldLine.isDefined){oldline="@comment.oldLine.get"}>
|
@if(comment.oldLine.isDefined){oldline="@comment.oldLine.get"}>
|
||||||
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
|
<div class="issue-avatar-image">@avatarLink(comment.commentedUserName, 48)</div>
|
||||||
<div class="panel- panel-default commit-comment-box commit-comment-@comment.commentId">
|
<div class="panel panel-default commit-comment-box commit-comment-@comment.commentId">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
@user(comment.commentedUserName, styleClass="username strong")
|
@user(comment.commentedUserName, styleClass="username strong")
|
||||||
<span class="muted">
|
<span class="muted">
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
@(id: String, value: String, prepend: Boolean = false)(html: Html)
|
@(id: String, value: String, style: String = "")(html: Html = Html(""))
|
||||||
<div class="input-group @if(prepend){input-prepend}" style="margin-bottom: 0px;">
|
@if(html.body.nonEmpty){
|
||||||
@html
|
<div class="input-group" style="margin-bottom: 0px;">
|
||||||
<span class="input-group-btn"><span id="@id" class="btn btn-sm btn-default" data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span></span>
|
@html
|
||||||
</div>
|
<span class="input-group-btn">
|
||||||
|
<span id="@id" class="btn btn-default" @if(style.nonEmpty){style="@style"}
|
||||||
|
data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<span id="@id" class="btn btn-default" @if(style.nonEmpty){style="@style"}
|
||||||
|
data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span>
|
||||||
|
}
|
||||||
<script>
|
<script>
|
||||||
// copy to clipboard
|
// copy to clipboard
|
||||||
(function() {
|
(function() {
|
||||||
|
|||||||
@@ -49,8 +49,8 @@
|
|||||||
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
|
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
|
||||||
@if(newCommitId.isDefined){
|
@if(newCommitId.isDefined){
|
||||||
<div class="pull-right align-right">
|
<div class="pull-right align-right">
|
||||||
<label class="checkbox no-margin" style="display: inline-block;"><input type="checkbox" class="ignore-whitespace" value="1"/>Ignore Space</label>
|
<label class="no-margin"><input type="checkbox" class="ignore-whitespace" value="1"/> Ignore Space</label>
|
||||||
<label class="checkbox no-margin" style="display: inline-block;"><input type="checkbox" class="toggle-notes" checked><span>Show notes</span></label>
|
<label class="no-margin"><input type="checkbox" class="toggle-notes" checked> Show notes</label>
|
||||||
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-default btn-sm" title="View the whole file at version @newCommitId.get.substring(0, 10)" data-toggle="tooltip">View</a>
|
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-default btn-sm" title="View the whole file at version @newCommitId.get.substring(0, 10)" data-toggle="tooltip">View</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -60,8 +60,8 @@
|
|||||||
@if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){
|
@if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){
|
||||||
@if(newCommitId.isDefined){
|
@if(newCommitId.isDefined){
|
||||||
<div class="pull-right align-right">
|
<div class="pull-right align-right">
|
||||||
<label class="checkbox no-margin" style="display: inline-block;"><input type="checkbox" class="ignore-whitespace" value="1"/>Ignore Space</label>
|
<label class="no-margin"><input type="checkbox" class="ignore-whitespace" value="1"/> Ignore Space</label>
|
||||||
<label class="checkbox no-margin" style="display: inline-block;"><input type="checkbox" class="toggle-notes" checked><span>Show notes</span></label>
|
<label class="no-margin"><input type="checkbox" class="toggle-notes" checked> Show notes</label>
|
||||||
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-default btn-sm" title="View the whole file at version @newCommitId.get.substring(0, 10)" data-toggle="tooltip">View</a>
|
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-default btn-sm" title="View the whole file at version @newCommitId.get.substring(0, 10)" data-toggle="tooltip">View</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
@if(diff.changeType == ChangeType.DELETE){
|
@if(diff.changeType == ChangeType.DELETE){
|
||||||
@if(oldCommitId.isDefined){
|
@if(oldCommitId.isDefined){
|
||||||
<div class="pull-right align-right">
|
<div class="pull-right align-right">
|
||||||
<label class="checkbox no-margin" style="display: inline-block;"><input type="checkbox" class="toggle-notes" checked><span>Show notes</span></label>
|
<label class="no-margin"><input type="checkbox" class="toggle-notes" checked> Show notes</label>
|
||||||
<a href="@url(repository)/blob/@oldCommitId.get/@diff.oldPath" class="btn btn-default btn-sm" title="View the whole file at version @oldCommitId.get.substring(0, 10)" data-toggle="tooltip">View</a>
|
<a href="@url(repository)/blob/@oldCommitId.get/@diff.oldPath" class="btn btn-default btn-sm" title="View the whole file at version @oldCommitId.get.substring(0, 10)" data-toggle="tooltip">View</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
@(value : String = "",
|
@(value : String = "",
|
||||||
prefix: String = "",
|
prefix: String = "",
|
||||||
style : String = "",
|
style : String = "",
|
||||||
right : Boolean = false,
|
right : Boolean = false)(body: Html)
|
||||||
flat : Boolean = false)(body: Html)
|
|
||||||
<div class="btn-group" @if(style.nonEmpty){style="@style"}>
|
<div class="btn-group" @if(style.nonEmpty){style="@style"}>
|
||||||
<button
|
<button
|
||||||
@if(flat){style="border: none; background-color: #eee;"}
|
class="dropdown-toggle btn btn-default btn-sm" data-toggle="dropdown">
|
||||||
class="dropdown-toggle @if(!flat){btn btn-default} else {flat} btn-sm" data-toggle="dropdown">
|
|
||||||
@if(value.isEmpty){
|
@if(value.isEmpty){
|
||||||
<i class="octicon octicon-gear"></i>
|
<i class="octicon octicon-gear"></i>
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
@import gitbucket.core._
|
@import gitbucket.core._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
<div class="tabbable">
|
<div class="tabbable">
|
||||||
<ul class="nav nav-tabs fill-width" style="margin-top: 12px; margin-bottom: 10px;">
|
<ul class="nav nav-tabs fill-width" style="margin-bottom: 10px;">
|
||||||
<li class="active"><a href="#tab@uid" data-toggle="tab">Write</a></li>
|
<li class="active"><a href="#tab@uid" data-toggle="tab">Write</a></li>
|
||||||
<li><a href="#tab@(uid+1)" data-toggle="tab" id="preview@uid">Preview</a></li>
|
<li><a href="#tab@(uid+1)" data-toggle="tab" id="preview@uid">Preview</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -4,89 +4,19 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@main("GitBucket"){
|
@main("GitBucket"){
|
||||||
@dashboard.html.tab()
|
@dashboard.html.sidebar(recentRepositories, userRepositories){
|
||||||
<div class="container">
|
@settings.information.map { information =>
|
||||||
<div class="row">
|
<div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
|
||||||
<div class="col-md-8">
|
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||||
<div class="pull-right">
|
@Html(information)
|
||||||
<a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a>
|
|
||||||
</div>
|
|
||||||
@helper.html.activities(activities)
|
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div class="col-md-4">
|
@dashboard.html.tab()
|
||||||
@settings.information.map { information =>
|
<div class="container">
|
||||||
<div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
|
<div class="pull-right">
|
||||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
<a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a>
|
||||||
@Html(information)
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if(loginAccount.isEmpty){
|
|
||||||
@signinform(settings)
|
|
||||||
} else {
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading strong">
|
|
||||||
<div class="pull-right">
|
|
||||||
<a href="@path/new" class="btn btn-success btn-sm">New repository</a>
|
|
||||||
</div>
|
|
||||||
Your repositories <span class="badge">@userRepositories.size</span>
|
|
||||||
</div>
|
|
||||||
<ul class="list-group list-group-flush">
|
|
||||||
@if(userRepositories.isEmpty){
|
|
||||||
<li class="list-group-item">No repositories</li>
|
|
||||||
} else {
|
|
||||||
@defining(20){ max =>
|
|
||||||
@userRepositories.zipWithIndex.map { case (repository, i) =>
|
|
||||||
<li class="list-group-item repo-link" style="@if(i > max - 1){display:none;}">
|
|
||||||
@helper.html.repositoryicon(repository, false)
|
|
||||||
@if(repository.owner == loginAccount.get.userName){
|
|
||||||
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
|
|
||||||
} else {
|
|
||||||
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
|
||||||
}
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
@if(userRepositories.size > max){
|
|
||||||
<li class="list-group-item show-more">
|
|
||||||
<a href="javascript:void(0);" id="show-more-repos">Show @{userRepositories.size - max} more repositories...</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading strong">Recent updated repositories</div>
|
|
||||||
<ul class="list-group list-group-flush">
|
|
||||||
@if(recentRepositories.isEmpty){
|
|
||||||
<li class="list-group-item">No repositories</li>
|
|
||||||
} else {
|
|
||||||
@defining(20){ max =>
|
|
||||||
@recentRepositories.zipWithIndex.map { case (repository, i) =>
|
|
||||||
<li class="list-group-item repo-link" style="@if(i > max - 1){display:none;}">
|
|
||||||
@helper.html.repositoryicon(repository, false)
|
|
||||||
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
@if(recentRepositories.size > max){
|
|
||||||
<li class="list-group-item show-more">
|
|
||||||
<a href="javascript:void(0);" id="show-more-recent-repos">Show @{recentRepositories.size - max} more repositories...</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
@helper.html.activities(activities)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
}
|
||||||
<script>
|
|
||||||
$(function(){
|
|
||||||
$('#show-more-repos, #show-more-recent-repos').click(function(e){
|
|
||||||
$(e.target).parents('ul.list-group').find('li.repo-link').show();
|
|
||||||
$(e.target).parents('li.show-more').remove();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
@if(loginAccount.isDefined){
|
@if(loginAccount.isDefined){
|
||||||
<hr/><br/>
|
<hr/><br/>
|
||||||
<form method="POST" validate="true">
|
<form method="POST" validate="true">
|
||||||
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
|
<div class="issue-avatar-image">@avatarLink(loginAccount.get.userName, 48)</div>
|
||||||
<div class="panel panel-default issue-comment-box">
|
<div class="panel panel-default issue-comment-box">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@helper.html.preview(
|
@helper.html.preview(
|
||||||
@@ -25,9 +25,9 @@
|
|||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<input type="hidden" name="issueId" value="@issue.issueId"/>
|
<input type="hidden" name="issueId" value="@issue.issueId"/>
|
||||||
@if((reopenable || !issue.closed) && (hasWritePermission || issue.openedUserName == loginAccount.get.userName)){
|
@if((reopenable || !issue.closed) && (hasWritePermission || issue.openedUserName == loginAccount.get.userName)){
|
||||||
<input type="submit" class="btn btn-lg btn-default" tabindex="3" formaction="@url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
|
<input type="submit" class="btn btn-default" tabindex="3" formaction="@url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
|
||||||
}
|
}
|
||||||
<input type="submit" class="btn btn-lg btn-success" tabindex="2" formaction="@url(repository)/issue_comments/new" value="Comment"/>
|
<input type="submit" class="btn btn-success" tabindex="2" formaction="@url(repository)/issue_comments/new" value="Comment"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@import gitbucket.core.model.CommitComment
|
@import gitbucket.core.model.CommitComment
|
||||||
@if(issue.isDefined){
|
@if(issue.isDefined){
|
||||||
<div class="issue-avatar-image">@avatar(issue.get.openedUserName, 48)</div>
|
<div class="issue-avatar-image">@avatarLink(issue.get.openedUserName, 48)</div>
|
||||||
<div class="panel panel-default issue-comment-box">
|
<div class="panel panel-default issue-comment-box">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
@user(issue.get.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.get.registeredDate)</span>
|
@user(issue.get.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.get.registeredDate)</span>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
case comment: gitbucket.core.model.IssueComment => {
|
case comment: gitbucket.core.model.IssueComment => {
|
||||||
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"
|
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"
|
||||||
&& comment.action != "commit" && comment.action != "refer"){
|
&& comment.action != "commit" && comment.action != "refer"){
|
||||||
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
|
<div class="issue-avatar-image">@avatarLink(comment.commentedUserName, 48)</div>
|
||||||
<div class="panel panel-default issue-comment-box" id="comment-@comment.commentId">
|
<div class="panel panel-default issue-comment-box" id="comment-@comment.commentId">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
@user(comment.commentedUserName, styleClass="username strong")
|
@user(comment.commentedUserName, styleClass="username strong")
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
<span class="discussion-item-icon"><i class="octicon octicon-circle-slash"></i></span>
|
<span class="discussion-item-icon"><i class="octicon octicon-circle-slash"></i></span>
|
||||||
@avatar(comment.commentedUserName, 16)
|
@avatar(comment.commentedUserName, 16)
|
||||||
@user(comment.commentedUserName, styleClass="username strong")
|
@user(comment.commentedUserName, styleClass="username strong")
|
||||||
close @issueOrPullRequest()
|
closed this @issueOrPullRequest()
|
||||||
@helper.html.datetimeago(comment.registeredDate)
|
@helper.html.datetimeago(comment.registeredDate)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,7 +210,7 @@ $(function(){
|
|||||||
$(document).on('click', '.commit-comment-box i.octicon-pencil', function(){
|
$(document).on('click', '.commit-comment-box i.octicon-pencil', function(){
|
||||||
var id = $(this).closest('a').data('comment-id');
|
var id = $(this).closest('a').data('comment-id');
|
||||||
var url = '@url(repository)/commit_comments/_data/' + id;
|
var url = '@url(repository)/commit_comments/_data/' + id;
|
||||||
var $content = $('.commit-commentContent-' + id, $(this).closest('.box'));
|
var $content = $('.commit-commentContent-' + id, $(this).closest('.commit-comment-box'));
|
||||||
|
|
||||||
$.get(url,
|
$.get(url,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,31 +8,26 @@
|
|||||||
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
|
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
|
||||||
@html.menu("issues", repository){
|
@html.menu("issues", repository){
|
||||||
<form action="@url(repository)/issues/new" method="POST" validate="true" class="form-group">
|
<form action="@url(repository)/issues/new" method="POST" validate="true" class="form-group">
|
||||||
<div class="row">
|
<div class="row-fluid">
|
||||||
<div class="col-md-10">
|
<div class="col-md-9">
|
||||||
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
|
<span id="error-title" class="error"></span>
|
||||||
<div class="panel panel-default issue-box">
|
<input type="text" id="issue-title" name="title" class="form-control" value="" placeholder="Title" style="margin-bottom: 6px;" autofocus/>
|
||||||
<div class="panel-body">
|
@helper.html.preview(
|
||||||
<span id="error-title" class="error"></span>
|
repository = repository,
|
||||||
<input type="text" name="title" class="form-control input-lg" value="" placeholder="Title" style="width: 680px;" autofocus/>
|
content = "",
|
||||||
@helper.html.preview(
|
enableWikiLink = false,
|
||||||
repository = repository,
|
enableRefsLink = true,
|
||||||
content = "",
|
enableLineBreaks = true,
|
||||||
enableWikiLink = false,
|
enableTaskList = true,
|
||||||
enableRefsLink = true,
|
hasWritePermission = hasWritePermission,
|
||||||
enableLineBreaks = true,
|
style = "height: 200px; max-height: 250px;",
|
||||||
enableTaskList = true,
|
elastic = true
|
||||||
hasWritePermission = hasWritePermission,
|
)
|
||||||
style = "width: 680px; height: 200px; max-height: 250px;",
|
<div class="align-right">
|
||||||
elastic = true
|
<input type="submit" class="btn btn-success" value="Submit new issue"/>
|
||||||
)
|
|
||||||
<div class="align-right">
|
|
||||||
<input type="submit" class="btn btn-lg btn-success" value="Submit new issue"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-3">
|
||||||
@issueinfo(None, Nil, Nil, collaborators, milestones.map(x => (x, 0, 0)), labels, hasWritePermission, repository)
|
@issueinfo(None, Nil, Nil, collaborators, milestones.map(x => (x, 0, 0)), labels, hasWritePermission, repository)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<textarea id="edit-content-@commentId" class="form-control">@content</textarea>
|
<textarea id="edit-content-@commentId" class="form-control">@content</textarea>
|
||||||
}
|
}
|
||||||
<div>
|
<div>
|
||||||
<input type="button" id="cancel-comment-@commentId" class="btn btn-lg btn-danger" value="Cancel"/>
|
<input type="button" id="cancel-comment-@commentId" class="btn btn-danger" value="Cancel"/>
|
||||||
<input type="button" id="update-comment-@commentId" class="btn btn-lg btn-default pull-right" value="Update comment"/>
|
<input type="button" id="update-comment-@commentId" class="btn btn-default pull-right" value="Update comment"/>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
<textarea id="edit-content" class="form-control">@content.getOrElse("")</textarea>
|
<textarea id="edit-content" class="form-control">@content.getOrElse("")</textarea>
|
||||||
}
|
}
|
||||||
<div>
|
<div>
|
||||||
<input type="button" id="cancel-issue" class="btn btn-lg btn-danger" value="Cancel"/>
|
<input type="button" id="cancel-issue" class="btn btn-danger" value="Cancel"/>
|
||||||
<input type="button" id="update-issue" class="btn btn-lg btn-default pull-right" value="Update comment"/>
|
<input type="button" id="update-issue" class="btn btn-default pull-right" value="Update comment"/>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<a class="btn btn-success" href="@url(repository)/issues/new">New issue</a>
|
<a class="btn btn-success" href="@url(repository)/issues/new">New issue</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-title pull-right" style="display: none;">
|
<div class="edit-title pull-right" style="display: none;">
|
||||||
<a class="btn btn-default" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
|
<a class="btn btn-success" href="#" id="update">Save</a> <a class="btn btn-default" href="#" id="cancel">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
<h1>
|
<h1>
|
||||||
<span class="show-title">
|
<span class="show-title">
|
||||||
@@ -46,12 +46,12 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="row" style="margin-top: 15px;">
|
<div class="row-fluid" style="margin-top: 15px;">
|
||||||
<div class="col-md-10">
|
<div class="col-md-9">
|
||||||
@commentlist(Some(issue), comments, hasWritePermission, repository)
|
@commentlist(Some(issue), comments, hasWritePermission, repository)
|
||||||
@commentform(issue, true, hasWritePermission, repository)
|
@commentform(issue, true, hasWritePermission, repository)
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-3">
|
||||||
@issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
|
@issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
<div style="margin-bottom: 8px;">
|
<div style="margin-bottom: 14px;">
|
||||||
<span class="muted small strong">Labels</span>
|
<span class="muted small strong">Labels</span>
|
||||||
@if(hasWritePermission){
|
@if(hasWritePermission){
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
@helper.html.dropdown(right = true) {
|
@helper.html.dropdown("Edit", right = true) {
|
||||||
@labels.map { label =>
|
@labels.map { label =>
|
||||||
<li>
|
<li>
|
||||||
<a href="#" class="toggle-label" data-label-id="@label.labelId">
|
<a href="#" class="toggle-label" data-label-id="@label.labelId">
|
||||||
@@ -33,11 +33,11 @@
|
|||||||
@labellist(issueLabels)
|
@labellist(issueLabels)
|
||||||
</ul>
|
</ul>
|
||||||
<hr/>
|
<hr/>
|
||||||
<div style="margin-bottom: 8px;">
|
<div style="margin-bottom: 14px;">
|
||||||
<span class="muted small strong">Milestone</span>
|
<span class="muted small strong">Milestone</span>
|
||||||
@if(hasWritePermission){
|
@if(hasWritePermission){
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
@helper.html.dropdown(right = true) {
|
@helper.html.dropdown("Edit", right = true) {
|
||||||
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="octicon octicon-x"></i> Clear this milestone</a></li>
|
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="octicon octicon-x"></i> Clear this milestone</a></li>
|
||||||
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
|
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
|
||||||
<li>
|
<li>
|
||||||
@@ -49,7 +49,8 @@
|
|||||||
<div class="small" style="padding-left: 20px;">
|
<div class="small" style="padding-left: 20px;">
|
||||||
@milestone.dueDate.map { dueDate =>
|
@milestone.dueDate.map { dueDate =>
|
||||||
@if(isPast(dueDate)){
|
@if(isPast(dueDate)){
|
||||||
<i class="octicon octicon-alert" style="color:#BD2C00;"></i><span class="milestone-alert">Due by @date(dueDate)</span>
|
<i class="octicon octicon-alert" style="color:#BD2C00;"></i>
|
||||||
|
<span class="milestone-alert">Due by @date(dueDate)</span>
|
||||||
} else {
|
} else {
|
||||||
<span class="muted">Due by @date(dueDate)</span>
|
<span class="muted">Due by @date(dueDate)</span>
|
||||||
}
|
}
|
||||||
@@ -84,11 +85,11 @@
|
|||||||
<input type="hidden" name="milestoneId" value=""/>
|
<input type="hidden" name="milestoneId" value=""/>
|
||||||
}
|
}
|
||||||
<hr/>
|
<hr/>
|
||||||
<div style="margin-bottom: 8px;">
|
<div style="margin-bottom: 14px;">
|
||||||
<span class="muted small strong">Assignee</span>
|
<span class="muted small strong">Assignee</span>
|
||||||
@if(hasWritePermission){
|
@if(hasWritePermission){
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
@helper.html.dropdown(right = true) {
|
@helper.html.dropdown("Edit", right = true) {
|
||||||
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="octicon octicon-x"></i> Clear assignee</a></li>
|
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="octicon octicon-x"></i> Clear assignee</a></li>
|
||||||
@collaborators.map { collaborator =>
|
@collaborators.map { collaborator =>
|
||||||
<li>
|
<li>
|
||||||
@@ -113,7 +114,7 @@
|
|||||||
}
|
}
|
||||||
@issue.map { issue =>
|
@issue.map { issue =>
|
||||||
<hr/>
|
<hr/>
|
||||||
<div style="margin-bottom: 8px;">
|
<div style="margin-bottom: 14px;">
|
||||||
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
|
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
|
||||||
<div class="muted small strong">@participants.size @plural(participants.size, "participant")</div>
|
<div class="muted small strong">@participants.size @plural(participants.size, "participant")</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@(issueLabels: List[gitbucket.core.model.Label])
|
@(issueLabels: List[gitbucket.core.model.Label])
|
||||||
@if(issueLabels.isEmpty){
|
@if(issueLabels.isEmpty){
|
||||||
<li><span class="muted small">None yet</span></li>
|
<li><span class="muted">None yet</span></li>
|
||||||
}
|
}
|
||||||
@issueLabels.map { label =>
|
@issueLabels.map { label =>
|
||||||
<li><span class="issue-label" style="background-color: #@label.color; color: #@label.fontColor;">@label.labelName</span></li>
|
<li><span class="issue-label" style="background-color: #@label.color; color: #@label.fontColor;">@label.labelName</span></li>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<span class="input-group-addon"><i style="background-color: #@label.map(_.color).getOrElse("888888");"></i></span>
|
<span class="input-group-addon"><i style="background-color: #@label.map(_.color).getOrElse("888888");"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
$('div#label-color-@labelId').colorpicker();
|
$('div#label-color-@labelId').colorpicker({format: "hex"});
|
||||||
</script>
|
</script>
|
||||||
<span class="pull-right">
|
<span class="pull-right">
|
||||||
<span id="label-error-@labelId" class="error"></span>
|
<span id="label-error-@labelId" class="error"></span>
|
||||||
|
|||||||
@@ -5,9 +5,12 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main(s"Labels - ${repository.owner}/${repository.name}"){
|
@html.main(s"Labels - ${repository.owner}/${repository.name}"){
|
||||||
@html.menu("issues", repository){
|
@html.menu("labels", repository){
|
||||||
@issues.html.navigation("labels", hasWritePermission, repository)
|
@if(loginAccount.isDefined){
|
||||||
<br>
|
<div class="pull-right" style="margin-bottom: 10px;">
|
||||||
|
<a class="btn btn-success" href="javascript:void(0);" id="new-label-button">New label</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<table class="table table-bordered table-hover table-issues" id="new-label-table" style="display: none;">
|
<table class="table table-bordered table-hover table-issues" id="new-label-table" style="display: none;">
|
||||||
<tr><td></td></tr>
|
<tr><td></td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -13,7 +13,24 @@
|
|||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){
|
@html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){
|
||||||
@html.menu(target, repository){
|
@html.menu(target, repository){
|
||||||
@navigation(target, true, repository, Some(condition))
|
<ul class="nav nav-pills pull-left" style="line-height: 14px; margin-bottom: 10px;">
|
||||||
|
<li class="@if(condition.state == "open"){active}">
|
||||||
|
<a href="@condition.copy(state = "open").toURL">Open <span class="badge">@openCount</span></a>
|
||||||
|
</li>
|
||||||
|
<li class="@if(condition.state == "closed"){active}">
|
||||||
|
<a href="@condition.copy(state = "closed").toURL">Closed <span class="badge">@closedCount</span></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<form method="GET" id="search-filter-form" class="form-inline pull-right">
|
||||||
|
@if(loginAccount.isDefined){
|
||||||
|
@if(target == "issues"){
|
||||||
|
<a class="btn btn-success" href="@url(repository)/issues/new">New issue</a>
|
||||||
|
}
|
||||||
|
@if(target == "pulls"){
|
||||||
|
<a class="btn btn-success" href="@url(repository)/compare">New pull request</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</form>
|
||||||
@listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
|
@listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
|
||||||
@if(hasWritePermission){
|
@if(hasWritePermission){
|
||||||
<form id="batcheditForm" method="POST">
|
<form id="batcheditForm" method="POST">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@import gitbucket.core.service.IssuesService.IssueInfo
|
@import gitbucket.core.service.IssuesService.IssueInfo
|
||||||
|
@*
|
||||||
@if(condition.nonEmpty){
|
@if(condition.nonEmpty){
|
||||||
<div style="width: 100%; display: inline-block;">
|
<div style="width: 100%; display: inline-block;">
|
||||||
<a href="@gitbucket.core.service.IssuesService.IssueSearchCondition().toURL" class="header-link">
|
<a href="@gitbucket.core.service.IssuesService.IssueSearchCondition().toURL" class="header-link">
|
||||||
@@ -20,23 +21,14 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
*@
|
||||||
<table class="table table-bordered table-hover table-issues">
|
<table class="table table-bordered table-hover table-issues">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="background-color: #eee;">
|
<th style="background-color: #eee;">
|
||||||
<input type="checkbox"/>
|
<input type="checkbox"/>
|
||||||
<span class="small">
|
<span id="table-issues-control">
|
||||||
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
|
@helper.html.dropdown("Author") {
|
||||||
<i class="octicon octicon-issue-opened @(if(condition.state == "open"){"active"})"></i>
|
|
||||||
@openCount Open
|
|
||||||
</a>
|
|
||||||
<a class="button-link@if(condition.state == "closed"){ selected}" href="@condition.copy(state = "closed").toURL">
|
|
||||||
<i class="octicon octicon-check @(if(condition.state == "closed"){"active"})"></i>
|
|
||||||
@closedCount Closed
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<div class="pull-right" id="table-issues-control">
|
|
||||||
@helper.html.dropdown("Author", flat = true) {
|
|
||||||
@collaborators.map { collaborator =>
|
@collaborators.map { collaborator =>
|
||||||
<li>
|
<li>
|
||||||
<a href="@condition.copy(author = (if(condition.author == Some(collaborator)) None else Some(collaborator))).toURL">
|
<a href="@condition.copy(author = (if(condition.author == Some(collaborator)) None else Some(collaborator))).toURL">
|
||||||
@@ -46,7 +38,7 @@
|
|||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@helper.html.dropdown("Label", flat = true) {
|
@helper.html.dropdown("Label") {
|
||||||
@labels.map { label =>
|
@labels.map { label =>
|
||||||
<li>
|
<li>
|
||||||
<a href="@condition.copy(labels = (if(condition.labels.contains(label.labelName)) condition.labels - label.labelName else condition.labels + label.labelName)).toURL">
|
<a href="@condition.copy(labels = (if(condition.labels.contains(label.labelName)) condition.labels - label.labelName else condition.labels + label.labelName)).toURL">
|
||||||
@@ -57,7 +49,7 @@
|
|||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@helper.html.dropdown("Milestone", flat = true) {
|
@helper.html.dropdown("Milestone") {
|
||||||
<li>
|
<li>
|
||||||
<a href="@condition.copy(milestone = Some(None)).toURL">
|
<a href="@condition.copy(milestone = Some(None)).toURL">
|
||||||
@helper.html.checkicon(condition.milestone == Some(None)) Issues with no milestone
|
@helper.html.checkicon(condition.milestone == Some(None)) Issues with no milestone
|
||||||
@@ -71,7 +63,7 @@
|
|||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@helper.html.dropdown("Assignee", flat = true) {
|
@helper.html.dropdown("Assignee") {
|
||||||
@collaborators.map { collaborator =>
|
@collaborators.map { collaborator =>
|
||||||
<li>
|
<li>
|
||||||
<a href="@condition.copy(assigned = Some(collaborator)).toURL">
|
<a href="@condition.copy(assigned = Some(collaborator)).toURL">
|
||||||
@@ -81,7 +73,7 @@
|
|||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@helper.html.dropdown("Sort", flat = true){
|
@helper.html.dropdown("Sort"){
|
||||||
<li>
|
<li>
|
||||||
<a href="@condition.copy(sort="created", direction="desc").toURL">
|
<a href="@condition.copy(sort="created", direction="desc").toURL">
|
||||||
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
|
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
|
||||||
@@ -113,14 +105,14 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</div>
|
</span>
|
||||||
@if(hasWritePermission){
|
@if(hasWritePermission){
|
||||||
<div class="pull-right" id="table-issues-batchedit">
|
<span id="table-issues-batchedit">
|
||||||
@helper.html.dropdown("Mark as", flat = true) {
|
@helper.html.dropdown("Mark as") {
|
||||||
<li><a href="javascript:void(0);" class="toggle-state" data-id="open">Open</a></li>
|
<li><a href="javascript:void(0);" class="toggle-state" data-id="open">Open</a></li>
|
||||||
<li><a href="javascript:void(0);" class="toggle-state" data-id="close">Close</a></li>
|
<li><a href="javascript:void(0);" class="toggle-state" data-id="close">Close</a></li>
|
||||||
}
|
}
|
||||||
@helper.html.dropdown("Label", flat = true) {
|
@helper.html.dropdown("Label") {
|
||||||
@labels.map { label =>
|
@labels.map { label =>
|
||||||
<li>
|
<li>
|
||||||
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
|
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
|
||||||
@@ -131,19 +123,19 @@
|
|||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@helper.html.dropdown("Milestone", flat = true) {
|
@helper.html.dropdown("Milestone") {
|
||||||
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="">No milestone</a></li>
|
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="">No milestone</a></li>
|
||||||
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
|
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
|
||||||
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">@milestone.title</a></li>
|
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">@milestone.title</a></li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@helper.html.dropdown("Assignee", flat = true) {
|
@helper.html.dropdown("Assignee") {
|
||||||
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="octicon octicon-x"></i> Clear assignee</a></li>
|
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="octicon octicon-x"></i> Clear assignee</a></li>
|
||||||
@collaborators.map { collaborator =>
|
@collaborators.map { collaborator =>
|
||||||
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="octicon"></i>@avatar(collaborator, 20) @collaborator</a></li>
|
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="octicon"></i>@avatar(collaborator, 20) @collaborator</a></li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</span>
|
||||||
}
|
}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -157,9 +149,11 @@
|
|||||||
} else {
|
} else {
|
||||||
No pull requests to show.
|
No pull requests to show.
|
||||||
}
|
}
|
||||||
|
@*
|
||||||
@if(condition.labels.nonEmpty || condition.milestone.isDefined){
|
@if(condition.labels.nonEmpty || condition.milestone.isDefined){
|
||||||
<a href="@condition.copy(labels = Set.empty, milestone = None).toURL">Clear active filters.</a>
|
<a href="@condition.copy(labels = Set.empty, milestone = None).toURL">Clear active filters.</a>
|
||||||
} else {
|
} else {
|
||||||
|
*@
|
||||||
@if(repository.isDefined){
|
@if(repository.isDefined){
|
||||||
@if(target == "issues"){
|
@if(target == "issues"){
|
||||||
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
|
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
|
||||||
@@ -167,7 +161,9 @@
|
|||||||
<a href="@url(repository.get)/compare">Create a new pull request.</a>
|
<a href="@url(repository.get)/compare">Create a new pull request.</a>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@*
|
||||||
}
|
}
|
||||||
|
*@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
@@ -177,7 +173,9 @@
|
|||||||
@if(hasWritePermission){
|
@if(hasWritePermission){
|
||||||
<input type="checkbox" value="@issue.issueId"/>
|
<input type="checkbox" value="@issue.issueId"/>
|
||||||
}
|
}
|
||||||
|
@*
|
||||||
<i class="octicon octicon-issue-@(if(issue.closed) "closed" else "opened")" style="margin-right: 3px;"></i>
|
<i class="octicon octicon-issue-@(if(issue.closed) "closed" else "opened")" style="margin-right: 3px;"></i>
|
||||||
|
*@
|
||||||
@if(repository.isEmpty){
|
@if(repository.isEmpty){
|
||||||
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a> ・
|
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a> ・
|
||||||
}
|
}
|
||||||
@@ -204,7 +202,7 @@
|
|||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
<div class="small muted" style="margin-left: 40px; margin-top: 2px;">
|
<div class="small muted" style="margin-left: 12px; margin-top: 2px;">
|
||||||
#@issue.issueId opened @helper.html.datetimeago(issue.registeredDate) by @user(issue.openedUserName, styleClass="username")
|
#@issue.issueId opened @helper.html.datetimeago(issue.registeredDate) by @user(issue.openedUserName, styleClass="username")
|
||||||
@milestone.map { milestone =>
|
@milestone.map { milestone =>
|
||||||
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><i class="octicon octicon-milestone"></i> @milestone</a></span>
|
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><i class="octicon octicon-milestone"></i> @milestone</a></span>
|
||||||
|
|||||||
@@ -3,13 +3,10 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
|
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
|
||||||
@html.menu("issues", repository){
|
@html.menu("milestones", repository){
|
||||||
@if(milestone.isEmpty){
|
@if(milestone.isEmpty){
|
||||||
<h4>New milestone</h4>
|
<h4>New milestone</h4>
|
||||||
<div class="muted">Create a new milestone to help organize your issues and pull requests.</div>
|
<div class="muted">Create a new milestone to help organize your issues and pull requests.</div>
|
||||||
} else {
|
|
||||||
@issues.html.navigation("milestones", false, repository)
|
|
||||||
<br><br>
|
|
||||||
}
|
}
|
||||||
<hr style="margin-top: 12px; margin-bottom: 18px;" class="fill-width"/>
|
<hr style="margin-top: 12px; margin-bottom: 18px;" class="fill-width"/>
|
||||||
<form method="POST" action="@url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true">
|
<form method="POST" action="@url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true">
|
||||||
@@ -30,7 +27,7 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
@if(milestone.isEmpty){
|
@if(milestone.isEmpty){
|
||||||
<input type="submit" class="btn btn-default" value="Create milestone"/>
|
<input type="submit" class="btn btn-success" value="Create milestone"/>
|
||||||
} else {
|
} else {
|
||||||
@if(milestone.get.closedDate.isDefined){
|
@if(milestone.get.closedDate.isDefined){
|
||||||
<input type="button" class="btn btn-default" value="Open" id="open"
|
<input type="button" class="btn btn-default" value="Open" id="open"
|
||||||
@@ -39,7 +36,7 @@
|
|||||||
<input type="button" class="btn btn-default" value="Close" id="close"
|
<input type="button" class="btn btn-default" value="Close" id="close"
|
||||||
onclick="location.href='@url(repository)/issues/milestones/@milestone.get.milestoneId/open';"/>
|
onclick="location.href='@url(repository)/issues/milestones/@milestone.get.milestoneId/open';"/>
|
||||||
}
|
}
|
||||||
<input type="submit" class="btn btn-default" value="Update milestone"/>
|
<input type="submit" class="btn btn-success" value="Update milestone"/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user