mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-07 21:06:36 +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 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
|
||||
--------
|
||||
@@ -56,10 +59,35 @@ Support
|
||||
- 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.
|
||||
- First priority of GitBucket is easy installation and API compatibility with GitHub, so we might reject if your request is against it.
|
||||
|
||||
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
|
||||
- Upgrade Scalatra to 2.4
|
||||
- Sidebar and Footer for Wiki
|
||||
@@ -67,6 +95,7 @@ Release Notes
|
||||
- Limit recent updated repositories list
|
||||
- Issue actions look-alike GitHub
|
||||
- Web API for labels
|
||||
- Requires Java 8
|
||||
|
||||
### 3.10 - 30 Dec 2015
|
||||
- Move to Bootstrap3
|
||||
|
||||
84
build.sbt
84
build.sbt
@@ -1,6 +1,6 @@
|
||||
val Organization = "gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val GitBucketVersion = "3.11.0"
|
||||
val GitBucketVersion = "3.14.0"
|
||||
val ScalatraVersion = "2.4.0"
|
||||
val JettyVersion = "9.3.6.v20151106"
|
||||
|
||||
@@ -10,15 +10,15 @@ sourcesInBase := false
|
||||
organization := Organization
|
||||
name := Name
|
||||
version := GitBucketVersion
|
||||
scalaVersion := "2.11.6"
|
||||
scalaVersion := "2.11.8"
|
||||
|
||||
// dependency settings
|
||||
resolvers ++= Seq(
|
||||
Classpaths.typesafeReleases,
|
||||
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/",
|
||||
"amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/"
|
||||
"sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/"
|
||||
)
|
||||
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.archive" % "4.1.1.201511131810-r",
|
||||
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
||||
@@ -26,7 +26,7 @@ libraryDependencies ++= Seq(
|
||||
"org.json4s" %% "json4s-jackson" % "3.3.0",
|
||||
"io.github.gitbucket" %% "scalatra-forms" % "1.0.0",
|
||||
"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-email" % "1.4",
|
||||
"org.apache.httpcomponents" % "httpclient" % "4.5.1",
|
||||
@@ -39,20 +39,21 @@ libraryDependencies ++= Seq(
|
||||
"com.mchange" % "c3p0" % "0.9.5.2",
|
||||
"com.typesafe" % "config" % "1.3.0",
|
||||
"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"),
|
||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
|
||||
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
|
||||
"junit" % "junit" % "4.12" % "test",
|
||||
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
|
||||
"org.specs2" %% "specs2-junit" % "3.6.6" % "test"
|
||||
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
|
||||
"org.scalaz" %% "scalaz-core" % "7.2.0" % "test"
|
||||
)
|
||||
|
||||
// Twirl settings
|
||||
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._"
|
||||
|
||||
// Compiler settings
|
||||
scalacOptions := Seq("-deprecation", "-language:postfixOps")
|
||||
javacOptions in compile ++= Seq("-target", "7", "-source", "7")
|
||||
scalacOptions := Seq("-deprecation", "-language:postfixOps", "-Ybackend:GenBCode", "-Ydelambdafy:method", "-target:jvm-1.8")
|
||||
javacOptions in compile ++= Seq("-target", "8", "-source", "8")
|
||||
javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml"
|
||||
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console")
|
||||
javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test"
|
||||
@@ -82,36 +83,36 @@ jrebelSettings
|
||||
// Create executable war file
|
||||
val executableConfig = config("executable").hide
|
||||
Keys.ivyConfigurations += executableConfig
|
||||
libraryDependencies ++= Seq(
|
||||
"org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable"
|
||||
libraryDependencies ++= Seq(
|
||||
"org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable"
|
||||
)
|
||||
|
||||
val executableKey = TaskKey[File]("executable")
|
||||
executableKey := {
|
||||
val executableKey = TaskKey[File]("executable")
|
||||
executableKey := {
|
||||
import org.apache.ivy.util.ChecksumHelper
|
||||
import java.util.jar.{ Manifest => JarManifest }
|
||||
import java.util.jar.Attributes.{ Name => AttrName }
|
||||
|
||||
val workDir = Keys.target.value / "executable"
|
||||
val warName = Keys.name.value + ".war"
|
||||
val workDir = Keys.target.value / "executable"
|
||||
val warName = Keys.name.value + ".war"
|
||||
|
||||
val log = streams.value.log
|
||||
val log = streams.value.log
|
||||
log info s"building executable webapp in ${workDir}"
|
||||
|
||||
// initialize temp directory
|
||||
val temp = workDir / "webapp"
|
||||
val temp = workDir / "webapp"
|
||||
IO delete temp
|
||||
|
||||
// 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 =>
|
||||
IO unzip (jar, temp, (name:String) =>
|
||||
(name startsWith "javax/") ||
|
||||
@@ -120,31 +121,34 @@ executableKey := {
|
||||
}
|
||||
|
||||
// include original war file
|
||||
val warFile = (Keys.`package`).value
|
||||
val warFile = (Keys.`package`).value
|
||||
IO unzip (warFile, temp)
|
||||
|
||||
// include launcher classes
|
||||
val classDir = (Keys.classDirectory in Compile).value
|
||||
val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */)
|
||||
val classDir = (Keys.classDirectory in Compile).value
|
||||
val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */)
|
||||
launchClasses foreach { name =>
|
||||
IO copyFile (classDir / name, temp / name)
|
||||
}
|
||||
|
||||
// zip it up
|
||||
IO delete (temp / "META-INF" / "MANIFEST.MF")
|
||||
val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp)
|
||||
val manifest = new JarManifest
|
||||
manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
|
||||
manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
|
||||
val outputFile = workDir / warName
|
||||
val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp)
|
||||
val manifest = new JarManifest
|
||||
manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
|
||||
manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
|
||||
val outputFile = workDir / warName
|
||||
IO jar (contentMappings, outputFile, manifest)
|
||||
|
||||
// generate checksums
|
||||
Seq("md5", "sha1") foreach { algorithm =>
|
||||
IO.write(
|
||||
workDir / (warName + "." + algorithm),
|
||||
ChecksumHelper computeAsString (outputFile, algorithm)
|
||||
)
|
||||
Seq(
|
||||
"md5" -> "MD5",
|
||||
"sha1" -> "SHA-1",
|
||||
"sha256" -> "SHA-256"
|
||||
)
|
||||
.foreach { case (extension, algorithm) =>
|
||||
val checksumFile = workDir / (warName + "." + extension)
|
||||
Checksums generate (outputFile, checksumFile, algorithm)
|
||||
}
|
||||
|
||||
// done
|
||||
@@ -153,7 +157,7 @@ executableKey := {
|
||||
}
|
||||
/*
|
||||
Keys.artifact in (Compile, executableKey) ~= {
|
||||
_ copy (`type` = "war", extension = "war"))
|
||||
_ copy (`type` = "war", extension = "war"))
|
||||
}
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
### project/build.scala
|
||||
### build.sbt
|
||||
|
||||
```scala
|
||||
object MyBuild extends Build {
|
||||
val Organization = "gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val Version = "3.3.0" // <---- update version!!
|
||||
val ScalaVersion = "2.11.6"
|
||||
val ScalatraVersion = "2.3.1"
|
||||
val Organization = "gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val GitBucketVersion = "3.12.0" // <---- update version!!
|
||||
val ScalatraVersion = "2.4.0"
|
||||
val JettyVersion = "9.3.6.v20151106"
|
||||
```
|
||||
|
||||
### 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.
|
||||
*/
|
||||
val versions = Seq(
|
||||
new Version(3, 3), // <---- add this line!!
|
||||
new Version(3, 2),
|
||||
new Version(3, 12), // <---- add this line!!
|
||||
new Version(3, 11),
|
||||
```
|
||||
|
||||
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
|
||||
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"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<extension>
|
||||
<groupId>org.apache.maven.wagon</groupId>
|
||||
<artifactId>wagon-ssh</artifactId>
|
||||
<version>1.0-beta-6</version>
|
||||
<version>2.10</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
</build>
|
||||
|
||||
Binary file not shown.
2
sbt.bat
2
sbt.bat
@@ -1,2 +1,2 @@
|
||||
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
|
||||
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 SearchController, "/")
|
||||
context.mount(new ApiController, "/api/v3")
|
||||
context.mount(new FileUploadController, "/upload")
|
||||
context.mount(new SystemSettingsController, "/admin")
|
||||
context.mount(new DashboardController, "/*")
|
||||
context.mount(new UserManagementController, "/*")
|
||||
context.mount(new SystemSettingsController, "/*")
|
||||
context.mount(new PluginsController, "/*")
|
||||
context.mount(new AccountController, "/*")
|
||||
context.mount(new RepositoryViewerController, "/*")
|
||||
context.mount(new WikiController, "/*")
|
||||
|
||||
@@ -20,6 +20,16 @@ case class ApiIssue(
|
||||
body: String)(repositoryName: RepositoryName, isPullRequest: Boolean){
|
||||
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 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{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.account.html
|
||||
import gitbucket.core.api._
|
||||
import gitbucket.core.helper
|
||||
import gitbucket.core.model.GroupMember
|
||||
import gitbucket.core.service._
|
||||
@@ -14,22 +13,19 @@ import gitbucket.core.util._
|
||||
|
||||
import io.github.gitbucket.scalatra.forms._
|
||||
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
|
||||
|
||||
|
||||
class AccountController extends AccountControllerBase
|
||||
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
||||
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
|
||||
with AccessTokenService with WebHookService
|
||||
with AccessTokenService with WebHookService with RepositoryCreationService
|
||||
|
||||
|
||||
trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
||||
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,
|
||||
url: Option[String], fileId: Option[String])
|
||||
@@ -133,7 +129,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
val members = getGroupMembers(account.userName)
|
||||
gitbucket.core.account.html.repositories(account,
|
||||
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 }))
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName).map { x =>
|
||||
@@ -366,8 +343,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
*/
|
||||
post("/new", newRepositoryForm)(usersOnly { form =>
|
||||
LockUtil.lock(s"${form.owner}/${form.name}"){
|
||||
if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){
|
||||
createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme)
|
||||
if(getRepository(form.owner, form.name).isEmpty){
|
||||
createRepository(context.loginAccount.get, form.owner, form.name, form.description, form.isPrivate, form.createReadme)
|
||||
}
|
||||
|
||||
// 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 =>
|
||||
val loginAccount = context.loginAccount.get
|
||||
val loginUserName = loginAccount.userName
|
||||
@@ -447,7 +376,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
val accountName = form.accountName
|
||||
|
||||
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))){
|
||||
// redirect to the repository if repository already exists
|
||||
redirect(s"/${accountName}/${repository.name}")
|
||||
@@ -456,7 +385,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
|
||||
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
|
||||
|
||||
createRepository(
|
||||
insertRepository(
|
||||
repositoryName = repository.name,
|
||||
userName = accountName,
|
||||
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(){
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
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.
|
||||
*/
|
||||
case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){
|
||||
|
||||
val path = settings.baseUrl.getOrElse(request.getContextPath)
|
||||
val currentPath = request.getRequestURI.substring(request.getContextPath.length)
|
||||
val baseUrl = settings.baseUrl(request)
|
||||
|
||||
@@ -94,7 +94,7 @@ trait DashboardControllerBase extends ControllerBase {
|
||||
|
||||
val userName = context.loginAccount.get.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)
|
||||
|
||||
html.issues(
|
||||
@@ -108,7 +108,9 @@ trait DashboardControllerBase extends ControllerBase {
|
||||
case _ => condition.copy(author = Some(userName))
|
||||
},
|
||||
filter,
|
||||
getGroupNames(userName))
|
||||
getGroupNames(userName),
|
||||
getVisibleRepositories(context.loginAccount, withoutPhysicalInfo = true),
|
||||
getUserRepositories(userName, withoutPhysicalInfo = true))
|
||||
}
|
||||
|
||||
private def searchPullRequests(filter: String) = {
|
||||
@@ -131,7 +133,9 @@ trait DashboardControllerBase extends ControllerBase {
|
||||
case _ => condition.copy(author = Some(userName))
|
||||
},
|
||||
filter,
|
||||
getGroupNames(userName))
|
||||
getGroupNames(userName),
|
||||
getVisibleRepositories(context.loginAccount, withoutPhysicalInfo = true),
|
||||
getUserRepositories(userName, withoutPhysicalInfo = true))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
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.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.servlet.{MultipartConfig, FileUploadSupport, FileItem}
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.{IOUtils, FileUtils}
|
||||
|
||||
/**
|
||||
* Provides Ajax based file upload functionality.
|
||||
*
|
||||
* 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)))
|
||||
|
||||
@@ -31,6 +37,54 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport {
|
||||
}, 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 {
|
||||
case Some(file) if(mimeTypeChcker(file.name)) =>
|
||||
defining(FileUtil.generateFileId){ fileId =>
|
||||
|
||||
@@ -1,36 +1,46 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.api._
|
||||
import gitbucket.core.helper.xml
|
||||
import gitbucket.core.html
|
||||
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.{LDAPUtil, Keys, UsersAuthenticator}
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator, ReferrerAuthenticator, StringUtil}
|
||||
|
||||
import io.github.gitbucket.scalatra.forms._
|
||||
|
||||
|
||||
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 {
|
||||
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)
|
||||
|
||||
val form = mapping(
|
||||
val signinForm = mapping(
|
||||
"userName" -> trim(label("Username", text(required))),
|
||||
"password" -> trim(label("Password", text(required)))
|
||||
)(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("/"){
|
||||
val loginAccount = context.loginAccount
|
||||
if(loginAccount.isEmpty) {
|
||||
html.index(getRecentActivities(),
|
||||
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
|
||||
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
|
||||
gitbucket.core.html.index(getRecentActivities(),
|
||||
getVisibleRepositories(loginAccount, withoutPhysicalInfo = true),
|
||||
loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil)
|
||||
)
|
||||
} else {
|
||||
val loginUserName = loginAccount.get.userName
|
||||
@@ -39,9 +49,9 @@ trait IndexControllerBase extends ControllerBase {
|
||||
|
||||
visibleOwnerSet ++= loginUserGroups
|
||||
|
||||
html.index(getRecentActivitiesByOwners(visibleOwnerSet),
|
||||
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
|
||||
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
|
||||
gitbucket.core.html.index(getRecentActivitiesByOwners(visibleOwnerSet),
|
||||
getVisibleRepositories(loginAccount, withoutPhysicalInfo = true),
|
||||
loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -51,10 +61,10 @@ trait IndexControllerBase extends ControllerBase {
|
||||
if(redirect.isDefined && redirect.get.startsWith("/")){
|
||||
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 {
|
||||
case Some(account) => signin(account)
|
||||
case None => redirect("/signin")
|
||||
@@ -110,13 +120,40 @@ trait IndexControllerBase extends ControllerBase {
|
||||
getAccountByUserName(params("userName")).isDefined
|
||||
})
|
||||
|
||||
/**
|
||||
* @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."))
|
||||
// TODO Move to RepositoryViwerController?
|
||||
post("/search", searchForm){ form =>
|
||||
redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
import gitbucket.core.api._
|
||||
import gitbucket.core.issues.html
|
||||
import gitbucket.core.model.Issue
|
||||
import gitbucket.core.service.IssuesService._
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
@@ -16,11 +14,11 @@ import org.scalatra.Ok
|
||||
|
||||
|
||||
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
|
||||
|
||||
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 =>
|
||||
|
||||
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 =>
|
||||
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||
html.create(
|
||||
@@ -128,7 +114,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
|
||||
getIssue(owner, name, issueId.toString).foreach { issue =>
|
||||
// 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
|
||||
callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)
|
||||
@@ -150,7 +136,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
// update issue
|
||||
updateIssue(owner, name, issue.issueId, title, issue.content)
|
||||
// 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}")
|
||||
} else Unauthorized
|
||||
@@ -165,7 +151,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
// update issue
|
||||
updateIssue(owner, name, issue.issueId, issue.title, content)
|
||||
// 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}")
|
||||
} else Unauthorized
|
||||
@@ -174,30 +160,22 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||
handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
|
||||
redirect(s"/${repository.owner}/${repository.name}/${
|
||||
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
||||
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
|
||||
val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
|
||||
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
|
||||
})
|
||||
|
||||
/**
|
||||
* 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) =>
|
||||
handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
|
||||
redirect(s"/${repository.owner}/${repository.name}/${
|
||||
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
||||
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
|
||||
val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
|
||||
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
|
||||
})
|
||||
|
||||
@@ -315,8 +293,16 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
|
||||
defining(params.get("value")){ action =>
|
||||
action match {
|
||||
case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) }
|
||||
case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) }
|
||||
case Some("open") => executeBatch(repository) { issueId =>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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) = {
|
||||
defining(repository.owner, repository.name){ case (owner, repoName) =>
|
||||
val page = IssueSearchCondition.page(request)
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.api.{ApiError, CreateALabel, ApiLabel, JsonFormat}
|
||||
import gitbucket.core.issues.labels.html
|
||||
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 io.github.gitbucket.scalatra.forms._
|
||||
import org.scalatra.i18n.Messages
|
||||
import org.scalatra.{NoContent, UnprocessableEntity, Created, Ok}
|
||||
import org.scalatra.Ok
|
||||
|
||||
class LabelsController extends LabelsControllerBase
|
||||
with LabelsService with IssuesService with RepositoryService with AccountService
|
||||
@@ -24,6 +23,7 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
"labelColor" -> trim(label("Color", text(required, color)))
|
||||
)(LabelForm.apply)
|
||||
|
||||
|
||||
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
|
||||
html.list(
|
||||
getLabels(repository.owner, repository.name),
|
||||
@@ -32,26 +32,6 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
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 =>
|
||||
html.edit(None, repository)
|
||||
})
|
||||
@@ -66,31 +46,6 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
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 =>
|
||||
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
|
||||
html.edit(Some(label), repository)
|
||||
@@ -107,50 +62,11 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
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 =>
|
||||
deleteLabel(repository.owner, repository.name, params("labelId").toInt)
|
||||
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.
|
||||
*/
|
||||
@@ -169,7 +85,11 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = {
|
||||
val owner = params("owner")
|
||||
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
|
||||
|
||||
import gitbucket.core.api._
|
||||
import gitbucket.core.model.{Account, CommitStatus, CommitState, Repository, PullRequest, Issue, WebHook}
|
||||
import gitbucket.core.model.WebHook
|
||||
import gitbucket.core.pulls.html
|
||||
import gitbucket.core.service.CommitStatusService
|
||||
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 =>
|
||||
params("id").toIntOpt.flatMap{ issueId =>
|
||||
val owner = repository.owner
|
||||
@@ -126,47 +107,6 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
} 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 =>
|
||||
params("id").toIntOpt.flatMap{ issueId =>
|
||||
val owner = repository.owner
|
||||
@@ -196,7 +136,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
issue,
|
||||
pullreq,
|
||||
repository,
|
||||
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get)
|
||||
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName).get)
|
||||
}
|
||||
} getOrElse NotFound
|
||||
})
|
||||
@@ -229,7 +169,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
if(branchProtection.needStatusCheck(loginAccount.userName)){
|
||||
flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check."
|
||||
} else {
|
||||
val repository = getRepository(owner, name, context.baseUrl).get
|
||||
val repository = getRepository(owner, name).get
|
||||
LockUtil.lock(s"${owner}/${name}"){
|
||||
val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){
|
||||
pullreq.branch
|
||||
@@ -238,7 +178,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
}
|
||||
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,
|
||||
"Merge branch '${alias}' into ${pullreq.requestBranch}") match {
|
||||
s"Merge branch '${alias}' into ${pullreq.requestBranch}") match {
|
||||
case None => // conflict
|
||||
flash += "error" -> s"Can't automatic merging branch '${alias}' into ${pullreq.requestBranch}."
|
||||
case Some(oldId) =>
|
||||
@@ -310,7 +250,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
|
||||
|
||||
// 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){
|
||||
commits.flatten.foreach { commit =>
|
||||
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
|
||||
@@ -343,7 +283,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
val headBranch:Option[String] = params.get("head")
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(originUserName), Some(originRepositoryName)) => {
|
||||
getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository =>
|
||||
getRepository(originUserName, originRepositoryName).map { originRepository =>
|
||||
using(
|
||||
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
|
||||
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||
@@ -384,12 +324,12 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
forkedRepository.repository.originRepositoryName
|
||||
} else {
|
||||
// Sibling repository
|
||||
getUserRepositories(originOwner, context.baseUrl).find { x =>
|
||||
getUserRepositories(originOwner).find { x =>
|
||||
x.repository.originUserName == forkedRepository.repository.originUserName &&
|
||||
x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName
|
||||
}.map(_.repository.repositoryName)
|
||||
};
|
||||
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
|
||||
originRepository <- getRepository(originOwner, originRepositoryName)
|
||||
) yield {
|
||||
using(
|
||||
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)
|
||||
}
|
||||
};
|
||||
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
|
||||
originRepository <- getRepository(originOwner, originRepositoryName)
|
||||
) yield {
|
||||
using(
|
||||
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
|
||||
@@ -523,7 +463,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
|
||||
getIssue(owner, name, issueId.toString) foreach { issue =>
|
||||
// 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
|
||||
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.
|
||||
*
|
||||
@@ -611,14 +538,4 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
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.lib.Constants
|
||||
import org.eclipse.jgit.lib.ObjectId
|
||||
import gitbucket.core.model.WebHookContentType
|
||||
|
||||
|
||||
class RepositorySettingsController extends RepositorySettingsControllerBase
|
||||
@@ -49,12 +50,16 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
)(CollaboratorForm.apply)
|
||||
|
||||
// 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(
|
||||
"url" -> trim(label("url", text(required, webHook(update)))),
|
||||
"events" -> webhookEvents
|
||||
)(WebHookForm.apply)
|
||||
"events" -> webhookEvents,
|
||||
"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
|
||||
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.
|
||||
*/
|
||||
@@ -198,7 +187,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Display the web hook edit page.
|
||||
*/
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -206,7 +195,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Add the web hook URL.
|
||||
*/
|
||||
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"
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||
})
|
||||
@@ -235,7 +224,9 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
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 ownerAccount = getAccountByUserName(repository.owner).get
|
||||
val commits = if(repository.commitCount == 0) List.empty else git.log
|
||||
@@ -294,7 +285,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Update web hook settings.
|
||||
*/
|
||||
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"
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@ package gitbucket.core.controller
|
||||
|
||||
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||
|
||||
import gitbucket.core.api._
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.repo.html
|
||||
import gitbucket.core.helper
|
||||
@@ -13,7 +12,7 @@ import gitbucket.core.util.StringUtil._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.Implicits._
|
||||
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.view
|
||||
import gitbucket.core.view.helpers
|
||||
@@ -122,13 +121,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
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.
|
||||
*/
|
||||
@@ -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 =>
|
||||
val (branch, path) = splitPath(repository, multiParams("splat").head)
|
||||
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
|
||||
@@ -560,11 +493,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
html.forked(
|
||||
getRepository(
|
||||
repository.repository.originUserName.getOrElse(repository.owner),
|
||||
repository.repository.originRepositoryName.getOrElse(repository.name),
|
||||
context.baseUrl),
|
||||
repository.repository.originRepositoryName.getOrElse(repository.name)),
|
||||
getForkedRepositories(
|
||||
repository.repository.originUserName.getOrElse(repository.owner),
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -575,13 +511,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
val ref = multiParams("splat").head
|
||||
JGitUtil.getTreeId(git, ref).map{ treeId =>
|
||||
html.find(ref,
|
||||
treeId,
|
||||
repository,
|
||||
context.loginAccount match {
|
||||
case None => List()
|
||||
case account: Option[Account] => getGroupsByUserName(account.get.userName)
|
||||
})
|
||||
html.find(ref, treeId, repository)
|
||||
} getOrElse NotFound
|
||||
}
|
||||
})
|
||||
@@ -643,10 +573,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
|
||||
html.files(revision, repository,
|
||||
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
|
||||
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
||||
getPullRequestFromBranch(repository.owner, repository.name, revstr, repository.repository.defaultBranch),
|
||||
@@ -759,8 +685,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
.setTree(revCommit.getTree)
|
||||
.setOutputStream(response.getOutputStream)
|
||||
.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
|
||||
|
||||
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.ssh.SshServer
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
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 org.apache.commons.io.FileUtils
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
class SystemSettingsController extends SystemSettingsControllerBase
|
||||
with AccountService with AdminAuthenticator
|
||||
with AccountService with RepositoryService with AdminAuthenticator
|
||||
|
||||
trait SystemSettingsControllerBase extends ControllerBase {
|
||||
self: AccountService with AdminAuthenticator =>
|
||||
trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
self: AccountService with RepositoryService with AdminAuthenticator =>
|
||||
|
||||
private val form = mapping(
|
||||
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
||||
@@ -23,6 +30,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
||||
"notification" -> trim(label("Notification", boolean())),
|
||||
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
|
||||
"ssh" -> trim(label("SSH access", boolean())),
|
||||
"sshHost" -> trim(label("SSH host", optional(text()))),
|
||||
"sshPort" -> trim(label("SSH port", optional(number()))),
|
||||
"useSMTP" -> trim(label("SMTP", boolean())),
|
||||
"smtp" -> optionalIfNotChecked("useSMTP", mapping(
|
||||
@@ -50,9 +58,14 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
||||
"keystore" -> trim(label("Keystore", optional(text())))
|
||||
)(Ldap.apply))
|
||||
)(SystemSettings.apply).verifying { settings =>
|
||||
if(settings.ssh && settings.baseUrl.isEmpty){
|
||||
Seq("baseUrl" -> "Base URL is required if SSH access is enabled.")
|
||||
} else Nil
|
||||
Vector(
|
||||
if(settings.ssh && settings.baseUrl.isEmpty){
|
||||
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(
|
||||
@@ -61,6 +74,61 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
||||
|
||||
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 {
|
||||
html.system(flash.get("info"))
|
||||
})
|
||||
@@ -68,20 +136,155 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
||||
post("/admin/system", form)(adminOnly { form =>
|
||||
saveSystemSettings(form)
|
||||
|
||||
if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){
|
||||
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){
|
||||
if (form.sshAddress != context.settings.sshAddress) {
|
||||
SshServer.stop()
|
||||
for {
|
||||
sshAddress <- form.sshAddress
|
||||
baseUrl <- form.baseUrl
|
||||
}
|
||||
SshServer.start(sshAddress, baseUrl)
|
||||
}
|
||||
|
||||
flash += "info" -> "System settings has been updated."
|
||||
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
|
||||
|
||||
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 {
|
||||
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)
|
||||
|
||||
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[_] =>
|
||||
|
||||
@@ -3,20 +3,43 @@ package gitbucket.core.model
|
||||
trait WebHookComponent extends TemplateComponent { self: Profile =>
|
||||
import profile.simple._
|
||||
|
||||
implicit val whContentTypeColumnType = MappedColumnType.base[WebHookContentType, String](whct => whct.code , code => WebHookContentType.valueOf(code))
|
||||
|
||||
lazy val WebHooks = TableQuery[WebHooks]
|
||||
|
||||
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
url: String
|
||||
url: String,
|
||||
ctype: WebHookContentType,
|
||||
token: Option[String]
|
||||
)
|
||||
|
||||
object WebHook {
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
package gitbucket.core.plugin
|
||||
|
||||
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.util.ControlUtil._
|
||||
import gitbucket.core.util.Version
|
||||
|
||||
/**
|
||||
* 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 pluginName: String
|
||||
@@ -77,6 +79,76 @@ trait Plugin {
|
||||
*/
|
||||
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.
|
||||
* Register plugin functionality to PluginRegistry.
|
||||
@@ -100,6 +172,27 @@ trait Plugin {
|
||||
(receiveHooks ++ receiveHooks(registry, context, settings)).foreach { 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.net.URLClassLoader
|
||||
import javax.servlet.ServletContext
|
||||
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
|
||||
|
||||
import gitbucket.core.controller.{Context, ControllerBase}
|
||||
import gitbucket.core.model.Account
|
||||
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||
@@ -33,6 +33,14 @@ class PluginRegistry {
|
||||
private val receiveHooks = new ListBuffer[ReceiveHook]
|
||||
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 = {
|
||||
plugins += pluginInfo
|
||||
}
|
||||
@@ -107,17 +115,47 @@ class PluginRegistry {
|
||||
|
||||
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.toSeq
|
||||
|
||||
private case class GlobalAction(
|
||||
method: String,
|
||||
path: String,
|
||||
function: (HttpServletRequest, HttpServletResponse, Context) => Any
|
||||
)
|
||||
def addGlobalMenu(globalMenu: (Context) => Option[Link]): Unit = {
|
||||
globalMenus += globalMenu
|
||||
}
|
||||
|
||||
private case class RepositoryAction(
|
||||
method: String,
|
||||
path: String,
|
||||
function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any
|
||||
)
|
||||
def getGlobalMenus: Seq[(Context) => Option[Link]] = globalMenus.toSeq
|
||||
|
||||
def addRepositoryMenu(repositoryMenu: (RepositoryInfo, Context) => Option[Link]): Unit = {
|
||||
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(
|
||||
pluginId: 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
|
||||
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||
import gitbucket.core.util.StringUtil
|
||||
import profile.simple._
|
||||
|
||||
import gitbucket.core.util.StringUtil._
|
||||
@@ -12,6 +14,7 @@ import Q.interpolation
|
||||
|
||||
|
||||
trait IssuesService {
|
||||
self: AccountService =>
|
||||
import IssuesService._
|
||||
|
||||
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 {
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
val IssueLimit = 30
|
||||
val IssueLimit = 25
|
||||
|
||||
case class IssueSearchCondition(
|
||||
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 objectId = git.getRepository.resolve("HEAD")
|
||||
val revCommit = revWalk.parseCommit(objectId)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.model.{Collaborator, Repository, Account}
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.util.JGitUtil
|
||||
@@ -18,7 +19,7 @@ trait RepositoryService { self: AccountService =>
|
||||
* @param originRepositoryName 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,
|
||||
parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None)
|
||||
(implicit s: Session): Unit = {
|
||||
@@ -194,10 +195,9 @@ trait RepositoryService { self: AccountService =>
|
||||
*
|
||||
* @param userName the user name of the repository owner
|
||||
* @param repositoryName the repository name
|
||||
* @param baseUrl the base url of this application
|
||||
* @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 =>
|
||||
// for getting issue count and pull request count
|
||||
val issues = Issues.filter { t =>
|
||||
@@ -205,7 +205,7 @@ trait RepositoryService { self: AccountService =>
|
||||
}.map(_.pullRequest).list
|
||||
|
||||
new RepositoryInfo(
|
||||
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
|
||||
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName),
|
||||
repository,
|
||||
issues.count(_ == false),
|
||||
issues.count(_ == true),
|
||||
@@ -234,7 +234,7 @@ trait RepositoryService { self: AccountService =>
|
||||
}.list
|
||||
}
|
||||
|
||||
def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false)
|
||||
def getUserRepositories(userName: String, withoutPhysicalInfo: Boolean = false)
|
||||
(implicit s: Session): List[RepositoryInfo] = {
|
||||
Repositories.filter { t1 =>
|
||||
(t1.userName === userName.bind) ||
|
||||
@@ -242,9 +242,9 @@ trait RepositoryService { self: AccountService =>
|
||||
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
|
||||
new RepositoryInfo(
|
||||
if(withoutPhysicalInfo){
|
||||
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
||||
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName)
|
||||
} else {
|
||||
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
||||
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName)
|
||||
},
|
||||
repository,
|
||||
getForkedCount(
|
||||
@@ -260,13 +260,12 @@ trait RepositoryService { self: AccountService =>
|
||||
* If repositoryUserName is given then filters results by repository owner.
|
||||
*
|
||||
* @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 withoutPhysicalInfo if true then the result does not include physical repository information such as commit count,
|
||||
* branches and tags
|
||||
* @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)
|
||||
(implicit s: Session): List[RepositoryInfo] = {
|
||||
(loginAccount match {
|
||||
@@ -284,9 +283,9 @@ trait RepositoryService { self: AccountService =>
|
||||
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
|
||||
new RepositoryInfo(
|
||||
if(withoutPhysicalInfo){
|
||||
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
||||
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName)
|
||||
} else {
|
||||
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
|
||||
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName)
|
||||
},
|
||||
repository,
|
||||
getForkedCount(
|
||||
@@ -389,32 +388,39 @@ trait RepositoryService { self: AccountService =>
|
||||
|
||||
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,
|
||||
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}"
|
||||
branchList: Seq[String], tags: Seq[JGitUtil.TagInfo], managers: Seq[String]) {
|
||||
|
||||
/**
|
||||
* 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]) =
|
||||
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.
|
||||
*/
|
||||
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)
|
||||
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) =
|
||||
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
|
||||
|
||||
import gitbucket.core.util.{Directory, ControlUtil}
|
||||
import gitbucket.core.util.Implicits._
|
||||
import Directory._
|
||||
import ControlUtil._
|
||||
import SystemSettingsService._
|
||||
@@ -21,6 +22,7 @@ trait SystemSettingsService {
|
||||
props.setProperty(Notification, settings.notification.toString)
|
||||
settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.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))
|
||||
props.setProperty(UseSMTP, settings.useSMTP.toString)
|
||||
if(settings.useSMTP) {
|
||||
@@ -75,6 +77,7 @@ trait SystemSettingsService {
|
||||
getValue(props, Notification, false),
|
||||
getOptionValue[Int](props, ActivityLogLimit, None),
|
||||
getValue(props, Ssh, false),
|
||||
getOptionValue[String](props, SshHost, None).map(_.trim),
|
||||
getOptionValue(props, SshPort, Some(DefaultSshPort)),
|
||||
getValue(props, UseSMTP, getValue(props, Notification, false)), // handle migration scenario from only notification to useSMTP
|
||||
if(getValue(props, UseSMTP, getValue(props, Notification, false))){
|
||||
@@ -126,16 +129,19 @@ object SystemSettingsService {
|
||||
notification: Boolean,
|
||||
activityLogLimit: Option[Int],
|
||||
ssh: Boolean,
|
||||
sshHost: Option[String],
|
||||
sshPort: Option[Int],
|
||||
useSMTP: Boolean,
|
||||
smtp: Option[Smtp],
|
||||
ldapAuthentication: Boolean,
|
||||
ldap: Option[Ldap]){
|
||||
def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse {
|
||||
defining(request.getRequestURL.toString){ url =>
|
||||
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
|
||||
def baseUrl(request: HttpServletRequest): String = baseUrl.fold(request.baseUrl)(_.stripSuffix("/"))
|
||||
|
||||
def sshAddress:Option[SshAddress] =
|
||||
for {
|
||||
host <- sshHost if ssh
|
||||
}
|
||||
}.stripSuffix("/")
|
||||
yield SshAddress(host, sshPort.getOrElse(DefaultSshPort))
|
||||
}
|
||||
|
||||
case class Ldap(
|
||||
@@ -161,6 +167,10 @@ object SystemSettingsService {
|
||||
fromAddress: Option[String],
|
||||
fromName: Option[String])
|
||||
|
||||
case class SshAddress(
|
||||
host:String,
|
||||
port:Int)
|
||||
|
||||
val DefaultSshPort = 29418
|
||||
val DefaultSmtpPort = 25
|
||||
val DefaultLdapPort = 389
|
||||
@@ -174,6 +184,7 @@ object SystemSettingsService {
|
||||
private val Notification = "notification"
|
||||
private val ActivityLogLimit = "activity_log_limit"
|
||||
private val Ssh = "ssh"
|
||||
private val SshHost = "ssh.host"
|
||||
private val SshPort = "ssh.port"
|
||||
private val UseSMTP = "useSMTP"
|
||||
private val SmtpHost = "smtp.host"
|
||||
@@ -216,7 +227,4 @@ object SystemSettingsService {
|
||||
else value
|
||||
}
|
||||
|
||||
// // TODO temporary flag
|
||||
// val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
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.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment}
|
||||
import gitbucket.core.model.Profile._
|
||||
import org.apache.http.client.utils.URLEncodedUtils
|
||||
import profile.simple._
|
||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||
import gitbucket.core.util.RepositoryName
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
|
||||
import org.apache.http.NameValuePair
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity
|
||||
import org.apache.http.message.BasicNameValuePair
|
||||
@@ -17,6 +20,8 @@ import org.slf4j.LoggerFactory
|
||||
import scala.concurrent._
|
||||
import org.apache.http.HttpRequest
|
||||
import org.apache.http.HttpResponse
|
||||
import gitbucket.core.model.WebHookContentType
|
||||
import org.apache.http.client.entity.EntityBuilder
|
||||
|
||||
|
||||
trait WebHookService {
|
||||
@@ -33,8 +38,11 @@ trait WebHookService {
|
||||
|
||||
/** get All WebHook informations of repository event */
|
||||
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)
|
||||
.list.map(t => WebHook(t.userName, t.repositoryName, t.url))
|
||||
WebHooks.filter(_.byRepository(owner, repository))
|
||||
.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 */
|
||||
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 }
|
||||
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
|
||||
|
||||
def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = {
|
||||
WebHooks insert WebHook(owner, repository, url)
|
||||
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, ctype, token)
|
||||
events.toSet.map{ event: WebHook.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
|
||||
events.toSet.map{ event: WebHook.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])] = {
|
||||
import org.apache.http.impl.client.HttpClientBuilder
|
||||
import ExecutionContext.Implicits.global
|
||||
import org.apache.http.protocol.HttpContext
|
||||
import org.apache.http.client.methods.HttpPost
|
||||
|
||||
if(webHookURLs.nonEmpty){
|
||||
if(webHooks.nonEmpty){
|
||||
val json = JsonFormat(payload)
|
||||
|
||||
webHookURLs.map { webHookUrl =>
|
||||
webHooks.map { webHook =>
|
||||
val reqPromise = Promise[HttpRequest]
|
||||
val f = Future {
|
||||
val itcp = new org.apache.http.HttpRequestInterceptor{
|
||||
@@ -89,19 +98,36 @@ trait WebHookService {
|
||||
}
|
||||
try{
|
||||
val httpClient = HttpClientBuilder.create.addInterceptorLast(itcp).build
|
||||
logger.debug(s"start web hook invocation for ${webHookUrl.url}")
|
||||
val httpPost = new HttpPost(webHookUrl.url)
|
||||
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
logger.debug(s"start web hook invocation for ${webHook.url}")
|
||||
val httpPost = new HttpPost(webHook.url)
|
||||
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-Delivery", java.util.UUID.randomUUID().toString)
|
||||
|
||||
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
|
||||
params.add(new BasicNameValuePair("payload", json))
|
||||
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"))
|
||||
webHook.ctype match {
|
||||
case WebHookContentType.FORM => {
|
||||
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)
|
||||
httpPost.releaseConnection()
|
||||
logger.debug(s"end web hook invocation for ${webHookUrl}")
|
||||
logger.debug(s"end web hook invocation for ${webHook}")
|
||||
res
|
||||
}catch{
|
||||
case e:Throwable => {
|
||||
@@ -113,12 +139,12 @@ trait WebHookService {
|
||||
}
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
Nil
|
||||
@@ -161,7 +187,7 @@ trait WebHookPullRequestService extends WebHookService {
|
||||
baseOwner <- users.get(repository.owner)
|
||||
headOwner <- users.get(pullRequest.requestUserName)
|
||||
issueUser <- users.get(issue.openedUserName)
|
||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
|
||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
|
||||
} yield {
|
||||
WebHookPullRequestPayload(
|
||||
action = action,
|
||||
@@ -200,7 +226,7 @@ trait WebHookPullRequestService extends WebHookService {
|
||||
import WebHookService._
|
||||
for{
|
||||
((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 {
|
||||
val payload = WebHookPullRequestPayload(
|
||||
action = action,
|
||||
@@ -229,7 +255,7 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
|
||||
baseOwner <- users.get(repository.owner)
|
||||
headOwner <- users.get(pullRequest.requestUserName)
|
||||
issueUser <- users.get(issue.openedUserName)
|
||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
|
||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
|
||||
} yield {
|
||||
WebHookPullRequestReviewCommentPayload(
|
||||
action = action,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import java.util.Date
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.model.Account
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import org.eclipse.jgit.api.Git
|
||||
@@ -13,7 +15,6 @@ import java.io.ByteArrayInputStream
|
||||
import org.eclipse.jgit.patch._
|
||||
import org.eclipse.jgit.api.errors.PatchFormatException
|
||||
import scala.collection.JavaConverters._
|
||||
import RepositoryService.RepositoryInfo
|
||||
|
||||
object WikiService {
|
||||
|
||||
@@ -38,10 +39,13 @@ object WikiService {
|
||||
*/
|
||||
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) =
|
||||
repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git")
|
||||
def wikiHttpUrl(repositoryInfo: RepositoryInfo)(implicit context: Context): String
|
||||
= 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 {
|
||||
|
||||
@@ -21,6 +21,9 @@ object AutoUpdate {
|
||||
* The history of versions. A head of this sequence is the current GitBucket version.
|
||||
*/
|
||||
val versions = Seq(
|
||||
new Version(3, 14),
|
||||
new Version(3, 13),
|
||||
new Version(3, 12),
|
||||
new Version(3, 11),
|
||||
new Version(3, 10),
|
||||
new Version(3, 9),
|
||||
|
||||
@@ -74,7 +74,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
||||
|
||||
request.paths match {
|
||||
case Array(_, repositoryOwner, repositoryName, _*) =>
|
||||
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
|
||||
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", "")) match {
|
||||
case Some(repository) => {
|
||||
if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){
|
||||
chain.doFilter(request, response)
|
||||
|
||||
@@ -10,7 +10,6 @@ import gitbucket.core.service.WebHookService._
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||
import gitbucket.core.util._
|
||||
|
||||
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 = "closed"), false, owner -> repository)
|
||||
|
||||
val repositoryInfo = getRepository(owner, repository, baseUrl).get
|
||||
val repositoryInfo = getRepository(owner, repository).get
|
||||
|
||||
// Extract new commit and apply issue comment
|
||||
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 (issueCount > 0) {
|
||||
pushedIds.add(commit.id)
|
||||
createIssueComment(commit)
|
||||
createIssueComment(owner, repository, commit)
|
||||
// close issues
|
||||
if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
|
||||
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 {
|
||||
|
||||
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)){
|
||||
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
|
||||
val repository = git.getRepository
|
||||
@@ -107,7 +107,7 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) ex
|
||||
with RepositoryService with AccountService {
|
||||
|
||||
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)){
|
||||
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
|
||||
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 {
|
||||
|
||||
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 {
|
||||
|
||||
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")
|
||||
|
||||
command match {
|
||||
case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, baseUrl, routing(repoName))
|
||||
case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, baseUrl, routing(repoName))
|
||||
case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName, baseUrl)
|
||||
case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, routing(repoName))
|
||||
case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, routing(repoName))
|
||||
case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName)
|
||||
case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl)
|
||||
case _ => new UnknownCommand(command)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package gitbucket.core.ssh
|
||||
|
||||
import gitbucket.core.service.SystemSettingsService
|
||||
import gitbucket.core.service.SystemSettingsService.SshAddress
|
||||
import org.apache.sshd.common.Factory
|
||||
import org.apache.sshd.server.{Environment, ExitCallback, Command}
|
||||
import java.io.{OutputStream, InputStream}
|
||||
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() {
|
||||
private var in: InputStream = null
|
||||
private var out: OutputStream = null
|
||||
@@ -15,7 +16,6 @@ class NoShell extends Factory[Command] with SystemSettingsService {
|
||||
|
||||
override def start(env: Environment): Unit = {
|
||||
val user = env.getEnv.get("USER")
|
||||
val port = loadSystemSettings().sshPort.getOrElse(SystemSettingsService.DefaultSshPort)
|
||||
val message =
|
||||
"""
|
||||
| Welcome to
|
||||
@@ -31,8 +31,8 @@ class NoShell extends Factory[Command] with SystemSettingsService {
|
||||
|
|
||||
| Please use:
|
||||
|
|
||||
| git clone ssh://%s@GITBUCKET_HOST:%d/OWNER/REPOSITORY_NAME.git
|
||||
""".stripMargin.format(user, port).replace("\n", "\r\n") + "\r\n"
|
||||
| git clone ssh://%s@%s:%d/OWNER/REPOSITORY_NAME.git
|
||||
""".stripMargin.format(user, sshAddress.host, sshAddress.port).replace("\n", "\r\n") + "\r\n"
|
||||
err.write(Constants.encode(message))
|
||||
err.flush()
|
||||
in.close()
|
||||
|
||||
@@ -5,7 +5,8 @@ import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.servlet.{ServletContextEvent, ServletContextListener}
|
||||
|
||||
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.slf4j.LoggerFactory
|
||||
|
||||
@@ -14,20 +15,20 @@ object SshServer {
|
||||
private val server = org.apache.sshd.server.SshServer.setUpDefaultServer()
|
||||
private val active = new AtomicBoolean(false)
|
||||
|
||||
private def configure(port: Int, baseUrl: String) = {
|
||||
server.setPort(port)
|
||||
private def configure(sshAddress: SshAddress, baseUrl: String) = {
|
||||
server.setPort(sshAddress.port)
|
||||
val provider = new SimpleGeneratorHostKeyProvider(new File(s"${Directory.GitBucketHome}/gitbucket.ser"))
|
||||
provider.setAlgorithm("RSA")
|
||||
provider.setOverwriteAllowed(false)
|
||||
server.setKeyPairProvider(provider)
|
||||
server.setPublickeyAuthenticator(new PublicKeyAuthenticator)
|
||||
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)){
|
||||
configure(port, baseUrl)
|
||||
configure(sshAddress, baseUrl)
|
||||
server.start()
|
||||
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 = {
|
||||
val settings = loadSystemSettings()
|
||||
if(settings.ssh){
|
||||
settings.baseUrl match {
|
||||
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)
|
||||
}
|
||||
if (settings.sshAddress.isDefined && settings.baseUrl.isEmpty) {
|
||||
logger.error("Could not start SshServer because the baseUrl is not configured.")
|
||||
}
|
||||
for {
|
||||
sshAddress <- settings.sshAddress
|
||||
baseUrl <- settings.baseUrl
|
||||
}
|
||||
SshServer.start(sshAddress, baseUrl)
|
||||
}
|
||||
|
||||
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) = {
|
||||
{
|
||||
defining(request.paths){ paths =>
|
||||
getRepository(paths(0), paths(1), baseUrl).map { repository =>
|
||||
getRepository(paths(0), paths(1)).map { repository =>
|
||||
context.loginAccount match {
|
||||
case Some(x) if(x.isAdmin) => 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) = {
|
||||
{
|
||||
defining(request.paths){ paths =>
|
||||
getRepository(paths(0), paths(1), baseUrl).map { repository =>
|
||||
getRepository(paths(0), paths(1)).map { repository =>
|
||||
context.loginAccount match {
|
||||
case Some(x) if(x.isAdmin) => 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) = {
|
||||
{
|
||||
defining(request.paths){ paths =>
|
||||
getRepository(paths(0), paths(1), baseUrl).map { repository =>
|
||||
getRepository(paths(0), paths(1)).map { repository =>
|
||||
if(!repository.repository.isPrivate){
|
||||
action(repository)
|
||||
} else {
|
||||
@@ -145,7 +145,7 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
|
||||
private def authenticate(action: (RepositoryInfo) => Any) = {
|
||||
{
|
||||
defining(request.paths){ paths =>
|
||||
getRepository(paths(0), paths(1), baseUrl).map { repository =>
|
||||
getRepository(paths(0), paths(1)).map { repository =>
|
||||
context.loginAccount match {
|
||||
case Some(x) if(x.isAdmin) => 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 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){
|
||||
|
||||
@@ -32,14 +32,13 @@ object JGitUtil {
|
||||
*
|
||||
* @param owner the user name of the repository owner
|
||||
* @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 branchList the list of branch names
|
||||
* @param tags the list of tags
|
||||
*/
|
||||
case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){
|
||||
def this(owner: String, name: String, baseUrl: String) = {
|
||||
this(owner, name, s"${baseUrl}/git/${owner}/${name}.git", 0, Nil, Nil)
|
||||
case class RepositoryInfo(owner: String, name: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){
|
||||
def this(owner: String, name: String) = {
|
||||
this(owner, name, 0, Nil, Nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,14 +173,14 @@ object JGitUtil {
|
||||
/**
|
||||
* 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 =>
|
||||
try {
|
||||
// get commit count
|
||||
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10001).sum
|
||||
|
||||
RepositoryInfo(
|
||||
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
|
||||
owner, repository,
|
||||
// commit count
|
||||
commitCount,
|
||||
// branches
|
||||
@@ -197,7 +196,7 @@ object JGitUtil {
|
||||
} catch {
|
||||
// not initialized
|
||||
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.util.{FileUtil, JGitUtil, StringUtil}
|
||||
|
||||
import play.twirl.api.Html
|
||||
import play.twirl.api.{Html, HtmlFormat}
|
||||
|
||||
/**
|
||||
* 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 =
|
||||
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 =
|
||||
(if(mailAddress.isEmpty){
|
||||
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
|
||||
|
||||
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 gitbucket.core.view.helpers._
|
||||
@html.main("Applications"){
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
@menu("application", settings.ssh)
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading strong">Personal access tokens</div>
|
||||
<div class="panel-body">
|
||||
@if(personalTokens.isEmpty && gneratedToken.isEmpty){
|
||||
No tokens.
|
||||
} else {
|
||||
Tokens you have generated that can be used to access the GitBucket API.<hr>
|
||||
}
|
||||
@gneratedToken.map{ case (token, tokenString) =>
|
||||
<div class="alert alert-info">
|
||||
Make sure to copy your new personal access token now. You won't be able to see it again!
|
||||
</div>
|
||||
<div class="container body">
|
||||
@menu("application", settings.ssh){
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading strong">Personal access tokens</div>
|
||||
<div class="panel-body">
|
||||
@if(personalTokens.isEmpty && gneratedToken.isEmpty){
|
||||
No tokens.
|
||||
} else {
|
||||
Tokens you have generated that can be used to access the GitBucket API.
|
||||
<hr style="margin-top: 10px;">
|
||||
}
|
||||
@gneratedToken.map{ case (token, tokenString) =>
|
||||
<div class="alert alert-info">
|
||||
Make sure to copy your new personal access token now. You won't be able to see it again!
|
||||
</div>
|
||||
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-sm btn-danger pull-right">Delete</a>
|
||||
<div style="width: 50%;">
|
||||
@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>
|
||||
}
|
||||
@personalTokens.zipWithIndex.map { case (token, i) =>
|
||||
@if(i != 0){
|
||||
<hr>
|
||||
}
|
||||
<strong>@token.note</strong>
|
||||
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-mini btn-danger pull-right">Delete</a>
|
||||
}
|
||||
<strong style="line-height: 30px;">@token.note</strong>
|
||||
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-sm btn-danger pull-right">Delete</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<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" 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>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -3,15 +3,11 @@
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
@html.main("Edit your profile"){
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
@menu("profile", settings.ssh)
|
||||
</div>
|
||||
<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="container body">
|
||||
@menu("profile", settings.ssh){
|
||||
@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-heading strong">Profile</div>
|
||||
<div class="panel-body">
|
||||
@@ -49,17 +45,17 @@
|
||||
</fieldset>
|
||||
</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 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>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
@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">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
@@ -42,12 +42,12 @@
|
||||
<fieldset class="margin">
|
||||
@if(account.isDefined){
|
||||
<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>
|
||||
}
|
||||
<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){
|
||||
<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>
|
||||
</form>
|
||||
@@ -107,18 +107,18 @@ $(function(){
|
||||
}
|
||||
|
||||
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){
|
||||
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){
|
||||
managerButton.addClass('active');
|
||||
}
|
||||
|
||||
$('#member-list').append($('<li>')
|
||||
.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(managerButton))
|
||||
.append(' ')
|
||||
@@ -130,9 +130,7 @@ $(function(){
|
||||
function updateMembers(){
|
||||
var members = $('#member-list li').map(function(i, e){
|
||||
var userName = $(e).data('name');
|
||||
return userName + ':' + $('button.active').filter(function(i, e){
|
||||
return $(e).data('name') == userName;
|
||||
}).attr('value');
|
||||
return userName + ':' + $(e).find('label.active input[type=radio]').attr('value');
|
||||
}).get().join(',');
|
||||
$('#members').val(members);
|
||||
}
|
||||
|
||||
@@ -3,57 +3,57 @@
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
@html.main(account.userName){
|
||||
<div class="container">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="block">
|
||||
<div class="account-image">@avatar(account.userName, 270)</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 class="container body">
|
||||
<div class="main-sidebar">
|
||||
<div class="block">
|
||||
<div class="account-image">@avatar(account.userName, 240)</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 =>
|
||||
@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>
|
||||
}
|
||||
|
||||
@@ -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._
|
||||
<div class="box">
|
||||
<ul class="nav nav-tabs nav-stacked side-menu">
|
||||
<div class="main-sidebar">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li@if(active=="profile"){ class="active"}>
|
||||
<a href="@path/@loginAccount.get.userName/_edit">Profile</a>
|
||||
</li>
|
||||
@@ -13,5 +13,15 @@
|
||||
<li@if(active=="application"){ class="active"}>
|
||||
<a href="@path/@loginAccount.get.userName/_application">Applications</a>
|
||||
</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>
|
||||
</div>
|
||||
<div class="main-content">
|
||||
@body
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
@html.main("Create a New Repository"){
|
||||
<div style="width: 600px; margin: 10px auto;">
|
||||
<div class="body main-center">
|
||||
<h2>Create a new repository</h2>
|
||||
<p class="muted">
|
||||
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>
|
||||
</fieldset>
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
@html.main("Create your account"){
|
||||
<div class="container">
|
||||
<div class="container body">
|
||||
<h3>Create your account</h3>
|
||||
<form action="@path/register" method="POST" validate="true">
|
||||
<div class="row">
|
||||
|
||||
@@ -3,46 +3,41 @@
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
@html.main("SSH Keys"){
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
@menu("ssh", settings.ssh)
|
||||
<div class="container body">
|
||||
@menu("ssh", settings.ssh){
|
||||
<div class="panel panel-default">
|
||||
<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 class="col-md-9">
|
||||
<form method="POST" action="@path/@account.userName/_ssh" validate="true">
|
||||
<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">
|
||||
@if(sshKeys.isEmpty){
|
||||
No keys
|
||||
}
|
||||
@sshKeys.zipWithIndex.map { case (key, i) =>
|
||||
@if(i != 0){
|
||||
<hr>
|
||||
}
|
||||
<strong>@key.title</strong> (@SshUtil.fingerPrint(key.publicKey).getOrElse("Key is invalid."))
|
||||
<a href="@path/@account.userName/_ssh/delete/@key.sshKeyId" class="btn btn-mini btn-danger pull-right">Delete</a>
|
||||
}
|
||||
<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"/>
|
||||
</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="height: 250px;"></textarea>
|
||||
</fieldset>
|
||||
<input type="submit" class="btn btn-success" value="Add"/>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="@path/@account.userName/_ssh" validate="true">
|
||||
<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>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
@(active: String)(body: Html)(implicit context: gitbucket.core.controller.Context)
|
||||
@import context._
|
||||
<div class="container body">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<ul class="nav nav-tabs nav-stacked side-menu" id="system-admin-menu-container">
|
||||
<li@if(active=="users"){ class="active"}>
|
||||
<a href="@path/admin/users">User Management</a>
|
||||
</li>
|
||||
<li@if(active=="system"){ class="active"}>
|
||||
<a href="@path/admin/system">System Settings</a>
|
||||
</li>
|
||||
<li@if(active=="plugins"){ class="active"}>
|
||||
<div class="main-sidebar">
|
||||
<ul class="nav nav-pills nav-stacked" id="system-admin-menu-container">
|
||||
<li@if(active=="users"){ class="active"}>
|
||||
<a href="@path/admin/users">User Management</a>
|
||||
</li>
|
||||
<li@if(active=="system"){ class="active"}>
|
||||
<a href="@path/admin/system">System Settings</a>
|
||||
</li>
|
||||
<li@if(active=="plugins"){ class="active"}>
|
||||
<a href="@path/admin/plugins">Plugins</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@path/console/login.jsp">H2 Console</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
@body
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@path/console/login.jsp">H2 Console</a>
|
||||
</li>
|
||||
@gitbucket.core.plugin.PluginRegistry().getSystemSettingMenus.map { menu =>
|
||||
@menu(context).map { link =>
|
||||
<li@if(active==link.id){ class="active"}>
|
||||
<a href="@path/@link.path">@link.label</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="main-content">
|
||||
@body
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,15 +111,24 @@
|
||||
Enable SSH access to git repository
|
||||
</label>
|
||||
</fieldset>
|
||||
<div class="form-group ssh">
|
||||
<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 class="ssh">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="sshHost">SSH Host</label>
|
||||
<div class="col-md-9">
|
||||
<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>
|
||||
<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>
|
||||
<!--====================================================================-->
|
||||
<!-- Authentication -->
|
||||
|
||||
@@ -4,18 +4,8 @@
|
||||
groups: List[String])(implicit context: gitbucket.core.controller.Context)
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
<span class="small">
|
||||
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
|
||||
<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){
|
||||
<div id="table-issues-control">
|
||||
@helper.html.dropdown("Visibility"){
|
||||
<li>
|
||||
<a href="@(condition.copy(visibility = (if(condition.visibility == Some("private")) None else Some("private"))).toURL)">
|
||||
@helper.html.checkicon(condition.visibility == Some("private"))
|
||||
@@ -29,7 +19,7 @@
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
@helper.html.dropdown("Organization", flat = true){
|
||||
@helper.html.dropdown("Organization"){
|
||||
@groups.map { group =>
|
||||
<li>
|
||||
<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>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown("Sort", flat = true){
|
||||
@helper.html.dropdown("Sort"){
|
||||
<li>
|
||||
<a href="@condition.copy(sort="created", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
|
||||
|
||||
@@ -4,13 +4,17 @@
|
||||
closedCount: Int,
|
||||
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
|
||||
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 gitbucket.core.view.helpers._
|
||||
@html.main("Issues"){
|
||||
@dashboard.html.tab("issues")
|
||||
<div class="container">
|
||||
@issuesnavi(filter, "issues", condition)
|
||||
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
||||
</div>
|
||||
@sidebar(recentRepositories, userRepositories){
|
||||
@dashboard.html.tab("issues")
|
||||
<div class="container">
|
||||
@issuesnavi(filter, openCount, closedCount, condition)
|
||||
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,6 @@
|
||||
@issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) =>
|
||||
<tr>
|
||||
<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> ・
|
||||
@if(issue.isPullRequest){
|
||||
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
|
||||
@@ -50,7 +45,7 @@
|
||||
</a>
|
||||
}
|
||||
</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)
|
||||
@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>
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
@(filter: String,
|
||||
active: String,
|
||||
openCount: Int,
|
||||
closedCount: Int,
|
||||
condition: gitbucket.core.service.IssuesService.IssueSearchCondition)(implicit context: gitbucket.core.controller.Context)
|
||||
@import context._
|
||||
@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}">
|
||||
<a href="@path/dashboard/@active/created_by@condition.copy(author = None, assigned = None).toURL">Created</a>
|
||||
</li>
|
||||
@@ -13,8 +21,5 @@
|
||||
<li class="@if(filter == "mentioned"){active}">
|
||||
<a href="@path/dashboard/@active/mentioned@condition.copy(author = None, assigned = None).toURL">Mentioned</a>
|
||||
</li>
|
||||
*@
|
||||
</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,
|
||||
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
|
||||
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 gitbucket.core.view.helpers._
|
||||
@html.main("Pull Requests"){
|
||||
@dashboard.html.tab("pulls")
|
||||
<div class="container">
|
||||
@issuesnavi(filter, "pulls", condition)
|
||||
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
||||
</div>
|
||||
@sidebar(recentRepositories, userRepositories){
|
||||
@dashboard.html.tab("pulls")
|
||||
<div class="container">
|
||||
@issuesnavi(filter, openCount, closedCount, condition)
|
||||
@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)
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
<div class="dashboard-nav">
|
||||
<div class="container">
|
||||
<a href="@path/" @if(active == ""){ class="active"}>
|
||||
<i class="octicon octicon-rss"></i> News Feed
|
||||
</a>
|
||||
@if(loginAccount.isDefined){
|
||||
<a href="@path/dashboard/pulls" @if(active == "pulls" ){ class="active"}>
|
||||
<i class="octicon octicon-git-pull-request"></i> Pull Requests
|
||||
</a>
|
||||
<a href="@path/dashboard/issues" @if(active == "issues"){ class="active"}>
|
||||
<i class="octicon octicon-issue-opened"></i> Issues
|
||||
</a>
|
||||
<ul class="nav nav-tabs" style="margin-bottom: 20px;">
|
||||
<li @if(active == ""){ class="active"}><a href="@path/">News Feed</a></li>
|
||||
@if(loginAccount.isDefined){
|
||||
<li @if(active == "pulls" ){ class="active"}><a href="@path/dashboard/pulls">Pull Requests</a></li>
|
||||
<li @if(active == "issues"){ class="active"}><a href="@path/dashboard/issues">Issues</a></li>
|
||||
@gitbucket.core.plugin.PluginRegistry().getDashboardTabs.map { tab =>
|
||||
@tab(context).map { link =>
|
||||
<li @if(active == link.id){ class="active"}><a href="@path/@link.path">@link.label</a></li>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@@ -61,8 +61,10 @@
|
||||
}
|
||||
|
||||
@detailActivity(activity: gitbucket.core.model.Activity, image: String) = {
|
||||
@*
|
||||
<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="strong">
|
||||
@avatar(activity.activityUserName, 16)
|
||||
@@ -75,8 +77,10 @@
|
||||
}
|
||||
|
||||
@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-content">
|
||||
*@
|
||||
<div>
|
||||
<div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
|
||||
<div class="strong">
|
||||
@avatar(activity.activityUserName, 16)
|
||||
@@ -87,12 +91,14 @@
|
||||
}
|
||||
|
||||
@simpleActivity(activity: gitbucket.core.model.Activity, image: String) = {
|
||||
@*
|
||||
<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>
|
||||
@avatar(activity.activityUserName, 16)
|
||||
@activityMessage(activity.message)
|
||||
<span class="muted small">@helper.html.datetimeago(activity.activityDate)</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -27,12 +27,6 @@ $(function(){
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust clickable area width
|
||||
$('#@textareaId').next('div.clickable').css({
|
||||
'width': ($('#@textareaId').width() + 18) + 'px',
|
||||
'font-size': '13px'
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
@if(comment.fileName.isDefined){filename="@comment.fileName.get"}
|
||||
@if(comment.newLine.isDefined){newline="@comment.newLine.get"}
|
||||
@if(comment.oldLine.isDefined){oldline="@comment.oldLine.get"}>
|
||||
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
|
||||
<div class="panel- panel-default commit-comment-box commit-comment-@comment.commentId">
|
||||
<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-heading">
|
||||
@user(comment.commentedUserName, styleClass="username strong")
|
||||
<span class="muted">
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
@(id: String, value: String, prepend: Boolean = false)(html: Html)
|
||||
<div class="input-group @if(prepend){input-prepend}" style="margin-bottom: 0px;">
|
||||
@html
|
||||
<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>
|
||||
</div>
|
||||
@(id: String, value: String, style: String = "")(html: Html = Html(""))
|
||||
@if(html.body.nonEmpty){
|
||||
<div class="input-group" style="margin-bottom: 0px;">
|
||||
@html
|
||||
<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>
|
||||
// copy to clipboard
|
||||
(function() {
|
||||
|
||||
@@ -49,8 +49,8 @@
|
||||
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
|
||||
@if(newCommitId.isDefined){
|
||||
<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="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="ignore-whitespace" value="1"/> Ignore Space</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>
|
||||
</div>
|
||||
}
|
||||
@@ -60,8 +60,8 @@
|
||||
@if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){
|
||||
@if(newCommitId.isDefined){
|
||||
<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="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="ignore-whitespace" value="1"/> Ignore Space</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>
|
||||
</div>
|
||||
}
|
||||
@@ -77,7 +77,7 @@
|
||||
@if(diff.changeType == ChangeType.DELETE){
|
||||
@if(oldCommitId.isDefined){
|
||||
<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>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
@(value : String = "",
|
||||
prefix: String = "",
|
||||
style : String = "",
|
||||
right : Boolean = false,
|
||||
flat : Boolean = false)(body: Html)
|
||||
right : Boolean = false)(body: Html)
|
||||
<div class="btn-group" @if(style.nonEmpty){style="@style"}>
|
||||
<button
|
||||
@if(flat){style="border: none; background-color: #eee;"}
|
||||
class="dropdown-toggle @if(!flat){btn btn-default} else {flat} btn-sm" data-toggle="dropdown">
|
||||
class="dropdown-toggle btn btn-default btn-sm" data-toggle="dropdown">
|
||||
@if(value.isEmpty){
|
||||
<i class="octicon octicon-gear"></i>
|
||||
} else {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
@import gitbucket.core._
|
||||
@import gitbucket.core.view.helpers._
|
||||
<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><a href="#tab@(uid+1)" data-toggle="tab" id="preview@uid">Preview</a></li>
|
||||
</ul>
|
||||
|
||||
@@ -4,89 +4,19 @@
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
@main("GitBucket"){
|
||||
@dashboard.html.tab()
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="pull-right">
|
||||
<a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a>
|
||||
</div>
|
||||
@helper.html.activities(activities)
|
||||
@dashboard.html.sidebar(recentRepositories, userRepositories){
|
||||
@settings.information.map { information =>
|
||||
<div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
@Html(information)
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
@settings.information.map { information =>
|
||||
<div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
@Html(information)
|
||||
</div>
|
||||
}
|
||||
@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>
|
||||
}
|
||||
@dashboard.html.tab()
|
||||
<div class="container">
|
||||
<div class="pull-right">
|
||||
<a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a>
|
||||
</div>
|
||||
@helper.html.activities(activities)
|
||||
</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){
|
||||
<hr/><br/>
|
||||
<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-body">
|
||||
@helper.html.preview(
|
||||
@@ -25,9 +25,9 @@
|
||||
<div class="text-right">
|
||||
<input type="hidden" name="issueId" value="@issue.issueId"/>
|
||||
@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>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
@import gitbucket.core.view.helpers._
|
||||
@import gitbucket.core.model.CommitComment
|
||||
@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-heading">
|
||||
@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 => {
|
||||
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"
|
||||
&& 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-heading">
|
||||
@user(comment.commentedUserName, styleClass="username strong")
|
||||
@@ -138,7 +138,7 @@
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-circle-slash"></i></span>
|
||||
@avatar(comment.commentedUserName, 16)
|
||||
@user(comment.commentedUserName, styleClass="username strong")
|
||||
close @issueOrPullRequest()
|
||||
closed this @issueOrPullRequest()
|
||||
@helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,7 +210,7 @@ $(function(){
|
||||
$(document).on('click', '.commit-comment-box i.octicon-pencil', function(){
|
||||
var id = $(this).closest('a').data('comment-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,
|
||||
{
|
||||
|
||||
@@ -8,31 +8,26 @@
|
||||
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.menu("issues", repository){
|
||||
<form action="@url(repository)/issues/new" method="POST" validate="true" class="form-group">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
|
||||
<div class="panel panel-default issue-box">
|
||||
<div class="panel-body">
|
||||
<span id="error-title" class="error"></span>
|
||||
<input type="text" name="title" class="form-control input-lg" value="" placeholder="Title" style="width: 680px;" autofocus/>
|
||||
@helper.html.preview(
|
||||
repository = repository,
|
||||
content = "",
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = hasWritePermission,
|
||||
style = "width: 680px; height: 200px; max-height: 250px;",
|
||||
elastic = true
|
||||
)
|
||||
<div class="align-right">
|
||||
<input type="submit" class="btn btn-lg btn-success" value="Submit new issue"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="col-md-9">
|
||||
<span id="error-title" class="error"></span>
|
||||
<input type="text" id="issue-title" name="title" class="form-control" value="" placeholder="Title" style="margin-bottom: 6px;" autofocus/>
|
||||
@helper.html.preview(
|
||||
repository = repository,
|
||||
content = "",
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = hasWritePermission,
|
||||
style = "height: 200px; max-height: 250px;",
|
||||
elastic = true
|
||||
)
|
||||
<div class="align-right">
|
||||
<input type="submit" class="btn btn-success" value="Submit new issue"/>
|
||||
</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)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<textarea id="edit-content-@commentId" class="form-control">@content</textarea>
|
||||
}
|
||||
<div>
|
||||
<input type="button" id="cancel-comment-@commentId" class="btn btn-lg 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="cancel-comment-@commentId" class="btn btn-danger" value="Cancel"/>
|
||||
<input type="button" id="update-comment-@commentId" class="btn btn-default pull-right" value="Update comment"/>
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<textarea id="edit-content" class="form-control">@content.getOrElse("")</textarea>
|
||||
}
|
||||
<div>
|
||||
<input type="button" id="cancel-issue" class="btn btn-lg 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="cancel-issue" class="btn btn-danger" value="Cancel"/>
|
||||
<input type="button" id="update-issue" class="btn btn-default pull-right" value="Update comment"/>
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<a class="btn btn-success" href="@url(repository)/issues/new">New issue</a>
|
||||
</div>
|
||||
<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>
|
||||
<h1>
|
||||
<span class="show-title">
|
||||
@@ -46,12 +46,12 @@
|
||||
</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row" style="margin-top: 15px;">
|
||||
<div class="col-md-10">
|
||||
<div class="row-fluid" style="margin-top: 15px;">
|
||||
<div class="col-md-9">
|
||||
@commentlist(Some(issue), comments, hasWritePermission, repository)
|
||||
@commentform(issue, true, hasWritePermission, repository)
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-3">
|
||||
@issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="margin-bottom: 14px;">
|
||||
<span class="muted small strong">Labels</span>
|
||||
@if(hasWritePermission){
|
||||
<div class="pull-right">
|
||||
@helper.html.dropdown(right = true) {
|
||||
@helper.html.dropdown("Edit", right = true) {
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="#" class="toggle-label" data-label-id="@label.labelId">
|
||||
@@ -33,11 +33,11 @@
|
||||
@labellist(issueLabels)
|
||||
</ul>
|
||||
<hr/>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="margin-bottom: 14px;">
|
||||
<span class="muted small strong">Milestone</span>
|
||||
@if(hasWritePermission){
|
||||
<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>
|
||||
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
|
||||
<li>
|
||||
@@ -49,7 +49,8 @@
|
||||
<div class="small" style="padding-left: 20px;">
|
||||
@milestone.dueDate.map { 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 {
|
||||
<span class="muted">Due by @date(dueDate)</span>
|
||||
}
|
||||
@@ -84,11 +85,11 @@
|
||||
<input type="hidden" name="milestoneId" value=""/>
|
||||
}
|
||||
<hr/>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="margin-bottom: 14px;">
|
||||
<span class="muted small strong">Assignee</span>
|
||||
@if(hasWritePermission){
|
||||
<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>
|
||||
@collaborators.map { collaborator =>
|
||||
<li>
|
||||
@@ -113,7 +114,7 @@
|
||||
}
|
||||
@issue.map { issue =>
|
||||
<hr/>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="margin-bottom: 14px;">
|
||||
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
|
||||
<div class="muted small strong">@participants.size @plural(participants.size, "participant")</div>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@(issueLabels: List[gitbucket.core.model.Label])
|
||||
@if(issueLabels.isEmpty){
|
||||
<li><span class="muted small">None yet</span></li>
|
||||
<li><span class="muted">None yet</span></li>
|
||||
}
|
||||
@issueLabels.map { label =>
|
||||
<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>
|
||||
</div>
|
||||
<script>
|
||||
$('div#label-color-@labelId').colorpicker();
|
||||
$('div#label-color-@labelId').colorpicker({format: "hex"});
|
||||
</script>
|
||||
<span class="pull-right">
|
||||
<span id="label-error-@labelId" class="error"></span>
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
@html.main(s"Labels - ${repository.owner}/${repository.name}"){
|
||||
@html.menu("issues", repository){
|
||||
@issues.html.navigation("labels", hasWritePermission, repository)
|
||||
<br>
|
||||
@html.menu("labels", repository){
|
||||
@if(loginAccount.isDefined){
|
||||
<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;">
|
||||
<tr><td></td></tr>
|
||||
</table>
|
||||
|
||||
@@ -13,7 +13,24 @@
|
||||
@import gitbucket.core.view.helpers._
|
||||
@html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(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)
|
||||
@if(hasWritePermission){
|
||||
<form id="batcheditForm" method="POST">
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
@import gitbucket.core.service.IssuesService.IssueInfo
|
||||
@*
|
||||
@if(condition.nonEmpty){
|
||||
<div style="width: 100%; display: inline-block;">
|
||||
<a href="@gitbucket.core.service.IssuesService.IssueSearchCondition().toURL" class="header-link">
|
||||
@@ -20,23 +21,14 @@
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
*@
|
||||
<table class="table table-bordered table-hover table-issues">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="background-color: #eee;">
|
||||
<input type="checkbox"/>
|
||||
<span class="small">
|
||||
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
|
||||
<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) {
|
||||
<span id="table-issues-control">
|
||||
@helper.html.dropdown("Author") {
|
||||
@collaborators.map { collaborator =>
|
||||
<li>
|
||||
<a href="@condition.copy(author = (if(condition.author == Some(collaborator)) None else Some(collaborator))).toURL">
|
||||
@@ -46,7 +38,7 @@
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown("Label", flat = true) {
|
||||
@helper.html.dropdown("Label") {
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<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>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown("Milestone", flat = true) {
|
||||
@helper.html.dropdown("Milestone") {
|
||||
<li>
|
||||
<a href="@condition.copy(milestone = Some(None)).toURL">
|
||||
@helper.html.checkicon(condition.milestone == Some(None)) Issues with no milestone
|
||||
@@ -71,7 +63,7 @@
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown("Assignee", flat = true) {
|
||||
@helper.html.dropdown("Assignee") {
|
||||
@collaborators.map { collaborator =>
|
||||
<li>
|
||||
<a href="@condition.copy(assigned = Some(collaborator)).toURL">
|
||||
@@ -81,7 +73,7 @@
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown("Sort", flat = true){
|
||||
@helper.html.dropdown("Sort"){
|
||||
<li>
|
||||
<a href="@condition.copy(sort="created", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
|
||||
@@ -113,14 +105,14 @@
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</div>
|
||||
</span>
|
||||
@if(hasWritePermission){
|
||||
<div class="pull-right" id="table-issues-batchedit">
|
||||
@helper.html.dropdown("Mark as", flat = true) {
|
||||
<span id="table-issues-batchedit">
|
||||
@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="close">Close</a></li>
|
||||
}
|
||||
@helper.html.dropdown("Label", flat = true) {
|
||||
@helper.html.dropdown("Label") {
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
|
||||
@@ -131,19 +123,19 @@
|
||||
</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>
|
||||
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
|
||||
<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>
|
||||
@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>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</span>
|
||||
}
|
||||
</th>
|
||||
</tr>
|
||||
@@ -157,9 +149,11 @@
|
||||
} else {
|
||||
No pull requests to show.
|
||||
}
|
||||
@*
|
||||
@if(condition.labels.nonEmpty || condition.milestone.isDefined){
|
||||
<a href="@condition.copy(labels = Set.empty, milestone = None).toURL">Clear active filters.</a>
|
||||
} else {
|
||||
*@
|
||||
@if(repository.isDefined){
|
||||
@if(target == "issues"){
|
||||
<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>
|
||||
}
|
||||
}
|
||||
@*
|
||||
}
|
||||
*@
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -177,7 +173,9 @@
|
||||
@if(hasWritePermission){
|
||||
<input type="checkbox" value="@issue.issueId"/>
|
||||
}
|
||||
@*
|
||||
<i class="octicon octicon-issue-@(if(issue.closed) "closed" else "opened")" style="margin-right: 3px;"></i>
|
||||
*@
|
||||
@if(repository.isEmpty){
|
||||
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a> ・
|
||||
}
|
||||
@@ -204,7 +202,7 @@
|
||||
</a>
|
||||
}
|
||||
</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")
|
||||
@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>
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
|
||||
@html.menu("issues", repository){
|
||||
@html.menu("milestones", repository){
|
||||
@if(milestone.isEmpty){
|
||||
<h4>New milestone</h4>
|
||||
<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"/>
|
||||
<form method="POST" action="@url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true">
|
||||
@@ -30,7 +27,7 @@
|
||||
<hr>
|
||||
<div class="pull-right">
|
||||
@if(milestone.isEmpty){
|
||||
<input type="submit" class="btn btn-default" value="Create milestone"/>
|
||||
<input type="submit" class="btn btn-success" value="Create milestone"/>
|
||||
} else {
|
||||
@if(milestone.get.closedDate.isDefined){
|
||||
<input type="button" class="btn btn-default" value="Open" id="open"
|
||||
@@ -39,7 +36,7 @@
|
||||
<input type="button" class="btn btn-default" value="Close" id="close"
|
||||
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>
|
||||
</form>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user