Merge branch 'master' into solidbase-integration

# Conflicts:
#	build.sbt
#	src/main/scala/gitbucket/core/controller/SystemSettingsController.scala
#	src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
This commit is contained in:
Naoki Takezoe
2016-03-06 13:07:03 +09:00
110 changed files with 1872 additions and 1989 deletions

7
.github/CONTRIBUTING.md vendored Normal file
View 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
View 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
View 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

View File

@@ -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.

View File

@@ -60,6 +60,22 @@ Support
Release Notes Release Notes
-------- --------
### 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
- Branch protection and receive hook extension point for plug-in
- Limit recent updated repositories list
- Issue actions look-alike GitHub
- Web API for labels
- Requires Java 8
### 3.10 - 30 Dec 2015 ### 3.10 - 30 Dec 2015
- Move to Bootstrap3 - Move to Bootstrap3
- New URL for raw contents (`raw/master/doc/activity.md` instead of `blob/master/doc/activity.md?raw=true`) - New URL for raw contents (`raw/master/doc/activity.md` instead of `blob/master/doc/activity.md?raw=true`)

View File

@@ -1,6 +1,6 @@
val Organization = "gitbucket" val Organization = "gitbucket"
val Name = "gitbucket" val Name = "gitbucket"
val GitBucketVersion = "3.11.0-SNAPSHOT" val GitBucketVersion = "3.12.0"
val ScalatraVersion = "2.4.0" val ScalatraVersion = "2.4.0"
val JettyVersion = "9.3.6.v20151106" val JettyVersion = "9.3.6.v20151106"
@@ -10,13 +10,12 @@ sourcesInBase := false
organization := Organization organization := Organization
name := Name name := Name
version := GitBucketVersion version := GitBucketVersion
scalaVersion := "2.11.6" scalaVersion := "2.11.7"
// dependency settings // dependency settings
resolvers ++= Seq( resolvers ++= Seq(
Classpaths.typesafeReleases, Classpaths.typesafeReleases,
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/", "sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/"
"amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/"
) )
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.1.201511131810-r", "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.1.201511131810-r",
@@ -26,8 +25,8 @@ libraryDependencies ++= Seq(
"org.json4s" %% "json4s-jackson" % "3.3.0", "org.json4s" %% "json4s-jackson" % "3.3.0",
"io.github.gitbucket" %% "scalatra-forms" % "1.0.0", "io.github.gitbucket" %% "scalatra-forms" % "1.0.0",
"commons-io" % "commons-io" % "2.4", "commons-io" % "commons-io" % "2.4",
"io.github.gitbucket" % "markedj" % "1.0.6",
"io.github.gitbucket" % "solidbase" % "1.0.0-SNAPSHOT", "io.github.gitbucket" % "solidbase" % "1.0.0-SNAPSHOT",
"io.github.gitbucket" % "markedj" % "1.0.7-SNAPSHOT",
"org.apache.commons" % "commons-compress" % "1.10", "org.apache.commons" % "commons-compress" % "1.10",
"org.apache.commons" % "commons-email" % "1.4", "org.apache.commons" % "commons-email" % "1.4",
"org.apache.httpcomponents" % "httpclient" % "4.5.1", "org.apache.httpcomponents" % "httpclient" % "4.5.1",
@@ -40,12 +39,13 @@ libraryDependencies ++= Seq(
"com.mchange" % "c3p0" % "0.9.5.2", "com.mchange" % "c3p0" % "0.9.5.2",
"com.typesafe" % "config" % "1.3.0", "com.typesafe" % "config" % "1.3.0",
"com.typesafe.akka" %% "akka-actor" % "2.3.14", "com.typesafe.akka" %% "akka-actor" % "2.3.14",
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.0.0",
"com.enragedginger" %% "akka-quartz-scheduler" % "1.4.0-akka-2.3.x" exclude("c3p0","c3p0"), "com.enragedginger" %% "akka-quartz-scheduler" % "1.4.0-akka-2.3.x" exclude("c3p0","c3p0"),
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided", "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"junit" % "junit" % "4.12" % "test", "junit" % "junit" % "4.12" % "test",
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test", "org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
"org.specs2" %% "specs2-junit" % "3.6.6" % "test" "org.scalaz" %% "scalaz-core" % "7.2.0" % "test"
) )
// Twirl settings // Twirl settings
@@ -53,7 +53,7 @@ play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._"
// Compiler settings // Compiler settings
scalacOptions := Seq("-deprecation", "-language:postfixOps") scalacOptions := Seq("-deprecation", "-language:postfixOps")
javacOptions in compile ++= Seq("-target", "7", "-source", "7") javacOptions in compile ++= Seq("-target", "8", "-source", "8")
javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml" javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml"
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console") testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console")
javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test" javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test"
@@ -83,36 +83,36 @@ jrebelSettings
// Create executable war file // Create executable war file
val executableConfig = config("executable").hide val executableConfig = config("executable").hide
Keys.ivyConfigurations += executableConfig Keys.ivyConfigurations += executableConfig
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable" "org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable"
) )
val executableKey = TaskKey[File]("executable") val executableKey = TaskKey[File]("executable")
executableKey := { executableKey := {
import org.apache.ivy.util.ChecksumHelper import org.apache.ivy.util.ChecksumHelper
import java.util.jar.{ Manifest => JarManifest } import java.util.jar.{ Manifest => JarManifest }
import java.util.jar.Attributes.{ Name => AttrName } import java.util.jar.Attributes.{ Name => AttrName }
val workDir = Keys.target.value / "executable" val workDir = Keys.target.value / "executable"
val warName = Keys.name.value + ".war" val warName = Keys.name.value + ".war"
val log = streams.value.log val log = streams.value.log
log info s"building executable webapp in ${workDir}" log info s"building executable webapp in ${workDir}"
// initialize temp directory // initialize temp directory
val temp = workDir / "webapp" val temp = workDir / "webapp"
IO delete temp IO delete temp
// include jetty classes // include jetty classes
val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name) val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name)
jettyJars foreach { jar => jettyJars foreach { jar =>
IO unzip (jar, temp, (name:String) => IO unzip (jar, temp, (name:String) =>
(name startsWith "javax/") || (name startsWith "javax/") ||
@@ -121,31 +121,34 @@ executableKey := {
} }
// include original war file // include original war file
val warFile = (Keys.`package`).value val warFile = (Keys.`package`).value
IO unzip (warFile, temp) IO unzip (warFile, temp)
// include launcher classes // include launcher classes
val classDir = (Keys.classDirectory in Compile).value val classDir = (Keys.classDirectory in Compile).value
val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */) val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */)
launchClasses foreach { name => launchClasses foreach { name =>
IO copyFile (classDir / name, temp / name) IO copyFile (classDir / name, temp / name)
} }
// zip it up // zip it up
IO delete (temp / "META-INF" / "MANIFEST.MF") IO delete (temp / "META-INF" / "MANIFEST.MF")
val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp) val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp)
val manifest = new JarManifest val manifest = new JarManifest
manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0") manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher") manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
val outputFile = workDir / warName val outputFile = workDir / warName
IO jar (contentMappings, outputFile, manifest) IO jar (contentMappings, outputFile, manifest)
// generate checksums // generate checksums
Seq("md5", "sha1") foreach { algorithm => Seq(
IO.write( "md5" -> "MD5",
workDir / (warName + "." + algorithm), "sha1" -> "SHA-1",
ChecksumHelper computeAsString (outputFile, algorithm) "sha256" -> "SHA-256"
) )
.foreach { case (extension, algorithm) =>
val checksumFile = workDir / (warName + "." + extension)
Checksums generate (outputFile, checksumFile, algorithm)
} }
// done // done
@@ -154,7 +157,7 @@ executableKey := {
} }
/* /*
Keys.artifact in (Compile, executableKey) ~= { Keys.artifact in (Compile, executableKey) ~= {
_ copy (`type` = "war", extension = "war")) _ copy (`type` = "war", extension = "war"))
} }
addArtifact(Keys.artifact in (Compile, executableKey), executableKey) addArtifact(Keys.artifact in (Compile, executableKey), executableKey)
*/ */

View File

@@ -27,7 +27,16 @@ $ sbt package
To build executable war file, run To build executable war file, run
* Windows: Not available ```
* Linux: `./release/make-release-war.sh` $ sbt executable
```
at the top of the source tree. It generates executable `gitbucket.war` into `target/scala-2.11`. We release this war file as release artifact. at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. We release this war file as release artifact.
Run tests spec
---------
To run the full serie of tests, run the following command:
```
sbt test
```

View File

@@ -6,15 +6,14 @@ Update version number
Note to update version number in files below: Note to update version number in files below:
### project/build.scala ### build.sbt
```scala ```scala
object MyBuild extends Build { val Organization = "gitbucket"
val Organization = "gitbucket" val Name = "gitbucket"
val Name = "gitbucket" val GitBucketVersion = "3.12.0" // <---- update version!!
val Version = "3.3.0" // <---- update version!! val ScalatraVersion = "2.4.0"
val ScalaVersion = "2.11.6" val JettyVersion = "9.3.6.v20151106"
val ScalatraVersion = "2.3.1"
``` ```
### src/main/scala/gitbucket/core/servlet/AutoUpdate.scala ### src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
@@ -26,8 +25,8 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current GitBucket version. * The history of versions. A head of this sequence is the current GitBucket version.
*/ */
val versions = Seq( val versions = Seq(
new Version(3, 3), // <---- add this line!! new Version(3, 12), // <---- add this line!!
new Version(3, 2), new Version(3, 11),
``` ```
Generate release files Generate release files

34
project/Checksums.scala Normal file
View 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 ""
}

View File

@@ -1 +1 @@
sbt.version=0.13.8 sbt.version=0.13.9

View File

@@ -1,3 +1,3 @@
#!/bin/sh #!/bin/sh
export GITBUCKET_VERSION=`cat ../project/build.scala | grep 'val Version' | cut -d \" -f 2` export GITBUCKET_VERSION=`cat ../build.sbt | grep 'val GitBucketVersion' | cut -d \" -f 2`
echo "GITBUCKET_VERSION: $GITBUCKET_VERSION" echo "GITBUCKET_VERSION: $GITBUCKET_VERSION"

View File

@@ -1,2 +1,2 @@
set SCRIPT_DIR=%~dp0 set SCRIPT_DIR=%~dp0
java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.8.jar" %* java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.9.jar" %*

2
sbt.sh
View File

@@ -1,2 +1,2 @@
#!/bin/sh #!/bin/sh
java $JAVA_OPTS -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.8.jar "$@" java $JAVA_OPTS -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.9.jar "$@"

View File

@@ -0,0 +1 @@
ALTER TABLE WEB_HOOK ADD COLUMN TOKEN VARCHAR(100);

View File

@@ -27,12 +27,9 @@ class ScalatraBootstrap extends LifeCycle {
} }
context.mount(new IndexController, "/") context.mount(new IndexController, "/")
context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload") context.mount(new FileUploadController, "/upload")
context.mount(new DashboardController, "/*") context.mount(new DashboardController, "/*")
context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*") context.mount(new SystemSettingsController, "/*")
context.mount(new PluginsController, "/*")
context.mount(new AccountController, "/*") context.mount(new AccountController, "/*")
context.mount(new RepositoryViewerController, "/*") context.mount(new RepositoryViewerController, "/*")
context.mount(new WikiController, "/*") context.mount(new WikiController, "/*")

View File

@@ -133,7 +133,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val members = getGroupMembers(account.userName) val members = getGroupMembers(account.userName)
gitbucket.core.account.html.repositories(account, gitbucket.core.account.html.repositories(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName), if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getVisibleRepositories(context.loginAccount, context.baseUrl, Some(userName)), getVisibleRepositories(context.loginAccount, Some(userName)),
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })) context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
} }
} }
@@ -366,7 +366,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
*/ */
post("/new", newRepositoryForm)(usersOnly { form => post("/new", newRepositoryForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}"){ LockUtil.lock(s"${form.owner}/${form.name}"){
if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){ if(getRepository(form.owner, form.name).isEmpty){
createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme) createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme)
} }
@@ -385,9 +385,9 @@ trait AccountControllerBase extends AccountManagementControllerBase {
data <- extractFromJsonBody[CreateARepository] if data.isValid data <- extractFromJsonBody[CreateARepository] if data.isValid
} yield { } yield {
LockUtil.lock(s"${owner}/${data.name}") { LockUtil.lock(s"${owner}/${data.name}") {
if(getRepository(owner, data.name, context.baseUrl).isEmpty){ if(getRepository(owner, data.name).isEmpty){
createRepository(owner, data.name, data.description, data.`private`, data.auto_init) createRepository(owner, data.name, data.description, data.`private`, data.auto_init)
val repository = getRepository(owner, data.name, context.baseUrl).get val repository = getRepository(owner, data.name).get
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get))) JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
} else { } else {
ApiError( ApiError(
@@ -409,9 +409,9 @@ trait AccountControllerBase extends AccountManagementControllerBase {
data <- extractFromJsonBody[CreateARepository] if data.isValid data <- extractFromJsonBody[CreateARepository] if data.isValid
} yield { } yield {
LockUtil.lock(s"${groupName}/${data.name}") { LockUtil.lock(s"${groupName}/${data.name}") {
if(getRepository(groupName, data.name, context.baseUrl).isEmpty){ if(getRepository(groupName, data.name).isEmpty){
createRepository(groupName, data.name, data.description, data.`private`, data.auto_init) createRepository(groupName, data.name, data.description, data.`private`, data.auto_init)
val repository = getRepository(groupName, data.name, context.baseUrl).get val repository = getRepository(groupName, data.name).get
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get))) JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get)))
} else { } else {
ApiError( ApiError(
@@ -447,7 +447,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val accountName = form.accountName val accountName = form.accountName
LockUtil.lock(s"${accountName}/${repository.name}"){ LockUtil.lock(s"${accountName}/${repository.name}"){
if(getRepository(accountName, repository.name, baseUrl).isDefined || if(getRepository(accountName, repository.name).isDefined ||
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){ (accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){
// redirect to the repository if repository already exists // redirect to the repository if repository already exists
redirect(s"/${accountName}/${repository.name}") redirect(s"/${accountName}/${repository.name}")

View File

@@ -180,7 +180,6 @@ abstract class ControllerBase extends ScalatraFilter
* Context object for the current request. * Context object for the current request.
*/ */
case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){ case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){
val path = settings.baseUrl.getOrElse(request.getContextPath) val path = settings.baseUrl.getOrElse(request.getContextPath)
val currentPath = request.getRequestURI.substring(request.getContextPath.length) val currentPath = request.getRequestURI.substring(request.getContextPath.length)
val baseUrl = settings.baseUrl(request) val baseUrl = settings.baseUrl(request)

View File

@@ -94,7 +94,7 @@ trait DashboardControllerBase extends ControllerBase {
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName) val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName)
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name) val userRepos = getUserRepositories(userName, true).map(repo => repo.owner -> repo.name)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
html.issues( html.issues(

View File

@@ -2,35 +2,46 @@ package gitbucket.core.controller
import gitbucket.core.api._ import gitbucket.core.api._
import gitbucket.core.helper.xml import gitbucket.core.helper.xml
import gitbucket.core.html
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.service.{RepositoryService, ActivityService, AccountService} import gitbucket.core.service.{RepositoryService, ActivityService, AccountService, RepositorySearchService, IssuesService}
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator} import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator, ReferrerAuthenticator, StringUtil}
import io.github.gitbucket.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
class IndexController extends IndexControllerBase class IndexController extends IndexControllerBase
with RepositoryService with ActivityService with AccountService with UsersAuthenticator with RepositoryService with ActivityService with AccountService with RepositorySearchService with IssuesService
with UsersAuthenticator with ReferrerAuthenticator
trait IndexControllerBase extends ControllerBase { trait IndexControllerBase extends ControllerBase {
self: RepositoryService with ActivityService with AccountService with UsersAuthenticator => self: RepositoryService with ActivityService with AccountService with RepositorySearchService
with UsersAuthenticator with ReferrerAuthenticator =>
case class SignInForm(userName: String, password: String) case class SignInForm(userName: String, password: String)
val form = mapping( val signinForm = mapping(
"userName" -> trim(label("Username", text(required))), "userName" -> trim(label("Username", text(required))),
"password" -> trim(label("Password", text(required))) "password" -> trim(label("Password", text(required)))
)(SignInForm.apply) )(SignInForm.apply)
val searchForm = mapping(
"query" -> trim(text(required)),
"owner" -> trim(text(required)),
"repository" -> trim(text(required))
)(SearchForm.apply)
case class SearchForm(query: String, owner: String, repository: String)
get("/"){ get("/"){
val loginAccount = context.loginAccount val loginAccount = context.loginAccount
if(loginAccount.isEmpty) { if(loginAccount.isEmpty) {
html.index(getRecentActivities(), gitbucket.core.html.index(getRecentActivities(),
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true), getVisibleRepositories(loginAccount, withoutPhysicalInfo = true),
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil) loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil)
) )
} else { } else {
val loginUserName = loginAccount.get.userName val loginUserName = loginAccount.get.userName
@@ -39,9 +50,9 @@ trait IndexControllerBase extends ControllerBase {
visibleOwnerSet ++= loginUserGroups visibleOwnerSet ++= loginUserGroups
html.index(getRecentActivitiesByOwners(visibleOwnerSet), gitbucket.core.html.index(getRecentActivitiesByOwners(visibleOwnerSet),
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true), getVisibleRepositories(loginAccount, withoutPhysicalInfo = true),
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil) loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil)
) )
} }
} }
@@ -51,10 +62,10 @@ trait IndexControllerBase extends ControllerBase {
if(redirect.isDefined && redirect.get.startsWith("/")){ if(redirect.isDefined && redirect.get.startsWith("/")){
flash += Keys.Flash.Redirect -> redirect.get flash += Keys.Flash.Redirect -> redirect.get
} }
html.signin() gitbucket.core.html.signin()
} }
post("/signin", form){ form => post("/signin", signinForm){ form =>
authenticate(context.settings, form.userName, form.password) match { authenticate(context.settings, form.userName, form.password) match {
case Some(account) => signin(account) case Some(account) => signin(account)
case None => redirect("/signin") case None => redirect("/signin")
@@ -119,4 +130,33 @@ trait IndexControllerBase extends ControllerBase {
// this message is same as github enterprise... // this message is same as github enterprise...
org.scalatra.NotFound(ApiError("Rate limiting is not enabled.")) 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(
searchIssues(repository.owner, repository.name, query),
countFiles(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),
query, page, repository)
}
}
})
} }

View File

@@ -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())
})
}

View File

@@ -137,7 +137,7 @@ trait PullRequestsControllerBase extends ControllerBase {
baseOwner <- users.get(repository.owner) baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName) headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName) issueUser <- users.get(issue.openedUserName)
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl) headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield { } yield {
JsonFormat(ApiPullRequest( JsonFormat(ApiPullRequest(
issue, issue,
@@ -196,7 +196,7 @@ trait PullRequestsControllerBase extends ControllerBase {
issue, issue,
pullreq, pullreq,
repository, repository,
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get) getRepository(pullreq.requestUserName, pullreq.requestRepositoryName).get)
} }
} getOrElse NotFound } getOrElse NotFound
}) })
@@ -229,7 +229,7 @@ trait PullRequestsControllerBase extends ControllerBase {
if(branchProtection.needStatusCheck(loginAccount.userName)){ if(branchProtection.needStatusCheck(loginAccount.userName)){
flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check." flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check."
} else { } else {
val repository = getRepository(owner, name, context.baseUrl).get val repository = getRepository(owner, name).get
LockUtil.lock(s"${owner}/${name}"){ LockUtil.lock(s"${owner}/${name}"){
val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){ val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){
pullreq.branch pullreq.branch
@@ -310,7 +310,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo) pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
// close issue by content of pull request // close issue by content of pull request
val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch val defaultBranch = getRepository(owner, name).get.repository.defaultBranch
if(pullreq.branch == defaultBranch){ if(pullreq.branch == defaultBranch){
commits.flatten.foreach { commit => commits.flatten.foreach { commit =>
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name) closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
@@ -343,7 +343,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val headBranch:Option[String] = params.get("head") val headBranch:Option[String] = params.get("head")
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => { case (Some(originUserName), Some(originRepositoryName)) => {
getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository => getRepository(originUserName, originRepositoryName).map { originRepository =>
using( using(
Git.open(getRepositoryDir(originUserName, originRepositoryName)), Git.open(getRepositoryDir(originUserName, originRepositoryName)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
@@ -384,12 +384,12 @@ trait PullRequestsControllerBase extends ControllerBase {
forkedRepository.repository.originRepositoryName forkedRepository.repository.originRepositoryName
} else { } else {
// Sibling repository // Sibling repository
getUserRepositories(originOwner, context.baseUrl).find { x => getUserRepositories(originOwner).find { x =>
x.repository.originUserName == forkedRepository.repository.originUserName && x.repository.originUserName == forkedRepository.repository.originUserName &&
x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName
}.map(_.repository.repositoryName) }.map(_.repository.repositoryName)
}; };
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl) originRepository <- getRepository(originOwner, originRepositoryName)
) yield { ) yield {
using( using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
@@ -457,7 +457,7 @@ trait PullRequestsControllerBase extends ControllerBase {
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2) getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
} }
}; };
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl) originRepository <- getRepository(originOwner, originRepositoryName)
) yield { ) yield {
using( using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),

View File

@@ -49,11 +49,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
)(CollaboratorForm.apply) )(CollaboratorForm.apply)
// for web hook url addition // for web hook url addition
case class WebHookForm(url: String, events: Set[WebHook.Event]) case class WebHookForm(url: String, events: Set[WebHook.Event], token: Option[String])
def webHookForm(update:Boolean) = mapping( def webHookForm(update:Boolean) = mapping(
"url" -> trim(label("url", text(required, webHook(update)))), "url" -> trim(label("url", text(required, webHook(update)))),
"events" -> webhookEvents "events" -> webhookEvents,
"token" -> optional(trim(label("token", text(maxlength(100)))))
)(WebHookForm.apply) )(WebHookForm.apply)
// for transfer ownership // for transfer ownership
@@ -198,7 +199,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the web hook edit page. * Display the web hook edit page.
*/ */
get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository => get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository =>
val webhook = WebHook(repository.owner, repository.name, "") val webhook = WebHook(repository.owner, repository.name, "", None)
html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true) html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true)
}) })
@@ -206,7 +207,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Add the web hook URL. * Add the web hook URL.
*/ */
post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) => post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) =>
addWebHook(repository.owner, repository.name, form.url, form.events) addWebHook(repository.owner, repository.name, form.url, form.events, form.token)
flash += "info" -> s"Webhook ${form.url} created" flash += "info" -> s"Webhook ${form.url} created"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
}) })
@@ -235,7 +236,8 @@ trait RepositorySettingsControllerBase extends ControllerBase {
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
val url = params("url") val url = params("url")
val dummyWebHookInfo = WebHook(repository.owner, repository.name, url) val token = Some(params("token"))
val dummyWebHookInfo = WebHook(repository.owner, repository.name, url, token)
val dummyPayload = { val dummyPayload = {
val ownerAccount = getAccountByUserName(repository.owner).get val ownerAccount = getAccountByUserName(repository.owner).get
val commits = if(repository.commitCount == 0) List.empty else git.log val commits = if(repository.commitCount == 0) List.empty else git.log
@@ -294,7 +296,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Update web hook settings. * Update web hook settings.
*/ */
post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) => post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) =>
updateWebHook(repository.owner, repository.name, form.url, form.events) updateWebHook(repository.owner, repository.name, form.url, form.events, form.token)
flash += "info" -> s"webhook ${form.url} updated" flash += "info" -> s"webhook ${form.url} updated"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
}) })

View File

@@ -560,8 +560,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
html.forked( html.forked(
getRepository( getRepository(
repository.repository.originUserName.getOrElse(repository.owner), repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name), repository.repository.originRepositoryName.getOrElse(repository.name)),
context.baseUrl),
getForkedRepositories( getForkedRepositories(
repository.repository.originUserName.getOrElse(repository.owner), repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name)), repository.repository.originRepositoryName.getOrElse(repository.name)),
@@ -759,8 +758,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
.setTree(revCommit.getTree) .setTree(revCommit.getTree)
.setOutputStream(response.getOutputStream) .setOutputStream(response.getOutputStream)
.call() .call()
Unit
} }
} }

View File

@@ -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)
}
}
})
}

View File

@@ -1,17 +1,24 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.admin.html import gitbucket.core.admin.html
import gitbucket.core.service.{AccountService, SystemSettingsService} import gitbucket.core.service.{AccountService, SystemSettingsService, RepositoryService}
import gitbucket.core.util.AdminAuthenticator import gitbucket.core.util.AdminAuthenticator
import gitbucket.core.ssh.SshServer import gitbucket.core.ssh.SshServer
import gitbucket.core.plugin.PluginRegistry
import SystemSettingsService._ import SystemSettingsService._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
import gitbucket.core.util.StringUtil._
import io.github.gitbucket.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
class SystemSettingsController extends SystemSettingsControllerBase class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with AdminAuthenticator with AccountService with RepositoryService with AdminAuthenticator
trait SystemSettingsControllerBase extends ControllerBase { trait SystemSettingsControllerBase extends AccountManagementControllerBase {
self: AccountService with AdminAuthenticator => self: AccountService with RepositoryService with AdminAuthenticator =>
private val form = mapping( private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))), "baseUrl" -> trim(label("Base URL", optional(text()))),
@@ -23,6 +30,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
"notification" -> trim(label("Notification", boolean())), "notification" -> trim(label("Notification", boolean())),
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))), "activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
"ssh" -> trim(label("SSH access", boolean())), "ssh" -> trim(label("SSH access", boolean())),
"sshHost" -> trim(label("SSH host", optional(text()))),
"sshPort" -> trim(label("SSH port", optional(number()))), "sshPort" -> trim(label("SSH port", optional(number()))),
"useSMTP" -> trim(label("SMTP", boolean())), "useSMTP" -> trim(label("SMTP", boolean())),
"smtp" -> optionalIfNotChecked("useSMTP", mapping( "smtp" -> optionalIfNotChecked("useSMTP", mapping(
@@ -50,11 +58,77 @@ trait SystemSettingsControllerBase extends ControllerBase {
"keystore" -> trim(label("Keystore", optional(text()))) "keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply)) )(Ldap.apply))
)(SystemSettings.apply).verifying { settings => )(SystemSettings.apply).verifying { settings =>
if(settings.ssh && settings.baseUrl.isEmpty){ Vector(
Seq("baseUrl" -> "Base URL is required if SSH access is enabled.") if(settings.ssh && settings.baseUrl.isEmpty){
} else Nil Some("baseUrl" -> "Base URL is required if SSH access is enabled.")
} else None,
if(settings.ssh && settings.sshHost.isEmpty){
Some("sshHost" -> "SSH host is required if SSH access is enabled.")
} else None
).flatten
} }
private val pluginForm = mapping(
"pluginId" -> list(trim(label("", text())))
)(PluginForm.apply)
case class PluginForm(pluginIds: List[String])
case class NewUserForm(userName: String, password: String, fullName: String,
mailAddress: String, isAdmin: Boolean,
url: Option[String], fileId: Option[String])
case class EditUserForm(userName: String, password: Option[String], fullName: String,
mailAddress: String, isAdmin: Boolean, url: Option[String],
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
members: String)
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
members: String, clearImage: Boolean, isRemoved: Boolean)
val newUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" ,text(required, maxlength(20)))),
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
"isAdmin" -> trim(label("User Type" ,boolean())),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text())))
)(NewUserForm.apply)
val editUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))),
"password" -> trim(label("Password" ,optional(text(maxlength(20))))),
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))),
"isAdmin" -> trim(label("User Type" ,boolean())),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
)(EditUserForm.apply)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members)))
)(NewGroupForm.apply)
val editGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members))),
"clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean()))
)(EditGroupForm.apply)
get("/admin/system")(adminOnly { get("/admin/system")(adminOnly {
html.system(flash.get("info")) html.system(flash.get("info"))
}) })
@@ -62,20 +136,155 @@ trait SystemSettingsControllerBase extends ControllerBase {
post("/admin/system", form)(adminOnly { form => post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(form) saveSystemSettings(form)
if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){ if (form.sshAddress != context.settings.sshAddress) {
SshServer.stop()
}
if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){
SshServer.start(
form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort),
form.baseUrl.get)
} else if(!form.ssh && SshServer.isActive){
SshServer.stop() SshServer.stop()
for {
sshAddress <- form.sshAddress
baseUrl <- form.baseUrl
}
SshServer.start(sshAddress, baseUrl)
} }
flash += "info" -> "System settings has been updated." flash += "info" -> "System settings has been updated."
redirect("/admin/system") redirect("/admin/system")
}) })
get("/admin/plugins")(adminOnly {
html.plugins(PluginRegistry().getPlugins())
})
get("/admin/users")(adminOnly {
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
val users = getAllUsers(includeRemoved)
val members = users.collect { case account if(account.isGroupAccount) =>
account.userName -> getGroupMembers(account.userName).map(_.userName)
}.toMap
html.userlist(users, members, includeRemoved)
})
get("/admin/users/_newuser")(adminOnly {
html.user(None)
})
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url)
updateImage(form.userName, form.fileId, false)
redirect("/admin/users")
})
get("/admin/users/:userName/_edituser")(adminOnly {
val userName = params("userName")
html.user(getAccountByUserName(userName, true))
})
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
val userName = params("userName")
getAccountByUserName(userName, true).map { account =>
if(form.isRemoved){
// Remove repositories
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
// deleteRepository(userName, repositoryName)
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
// }
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
removeUserRelatedData(userName)
}
updateAccount(account.copy(
password = form.password.map(sha1).getOrElse(account.password),
fullName = form.fullName,
mailAddress = form.mailAddress,
isAdmin = form.isAdmin,
url = form.url,
isRemoved = form.isRemoved))
updateImage(userName, form.fileId, form.clearImage)
redirect("/admin/users")
} getOrElse NotFound
})
get("/admin/users/_newgroup")(adminOnly {
html.usergroup(None, Nil)
})
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
createGroup(form.groupName, form.url)
updateGroupMembers(form.groupName, form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList)
updateImage(form.groupName, form.fileId, false)
redirect("/admin/users")
})
get("/admin/users/:groupName/_editgroup")(adminOnly {
defining(params("groupName")){ groupName =>
html.usergroup(getAccountByUserName(groupName, true), getGroupMembers(groupName))
}
})
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
defining(params("groupName"), form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList){ case (groupName, members) =>
getAccountByUserName(groupName, true).map { account =>
updateGroup(groupName, form.url, form.isRemoved)
if(form.isRemoved){
// Remove from GROUP_MEMBER
updateGroupMembers(form.groupName, Nil)
// Remove repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
deleteRepository(groupName, repositoryName)
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
}
} else {
// Update GROUP_MEMBER
updateGroupMembers(form.groupName, members)
// Update COLLABORATOR for group repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName)
members.foreach { case (userName, isManager) =>
addCollaborator(form.groupName, repositoryName, userName)
}
}
}
updateImage(form.groupName, form.fileId, form.clearImage)
redirect("/admin/users")
} getOrElse NotFound
}
})
private def members: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
if(value.split(",").exists {
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
}) None else Some("Must select one manager at least.")
}
}
protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
params.get(paramName).flatMap { userName =>
if(userName == context.loginAccount.get.userName && params.get("removed") == Some("true"))
Some("You can't disable your account yourself")
else
None
}
}
}
} }

View File

@@ -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
}
}
}
}

View File

@@ -35,7 +35,7 @@ protected[model] trait TemplateComponent { self: Profile =>
byRepository(userName, repositoryName) && (this.labelId === labelId) byRepository(userName, repositoryName) && (this.labelId === labelId)
def byLabel(owner: String, repository: String, labelName: String) = def byLabel(owner: String, repository: String, labelName: String) =
byRepository(userName, repositoryName) && (this.labelName === labelName.bind) byRepository(owner, repository) && (this.labelName === labelName.bind)
} }
trait MilestoneTemplate extends BasicTemplate { self: Table[_] => trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>

View File

@@ -7,7 +7,8 @@ trait WebHookComponent extends TemplateComponent { self: Profile =>
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate { class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
val url = column[String]("URL") val url = column[String]("URL")
def * = (userName, repositoryName, url) <> ((WebHook.apply _).tupled, WebHook.unapply) val token = column[Option[String]]("TOKEN", O.Nullable)
def * = (userName, repositoryName, url, token) <> ((WebHook.apply _).tupled, WebHook.unapply)
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind) def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
} }
@@ -16,7 +17,8 @@ trait WebHookComponent extends TemplateComponent { self: Profile =>
case class WebHook( case class WebHook(
userName: String, userName: String,
repositoryName: String, repositoryName: String,
url: String url: String,
token: Option[String]
) )
object WebHook { object WebHook {

View File

@@ -399,7 +399,7 @@ trait IssuesService {
object IssuesService { object IssuesService {
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
val IssueLimit = 30 val IssueLimit = 25
case class IssueSearchCondition( case class IssueSearchCondition(
labels: Set[String] = Set.empty, labels: Set[String] = Set.empty,

View File

@@ -1,5 +1,6 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.controller.Context
import gitbucket.core.model.{Collaborator, Repository, Account} import gitbucket.core.model.{Collaborator, Repository, Account}
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import gitbucket.core.util.JGitUtil import gitbucket.core.util.JGitUtil
@@ -194,10 +195,9 @@ trait RepositoryService { self: AccountService =>
* *
* @param userName the user name of the repository owner * @param userName the user name of the repository owner
* @param repositoryName the repository name * @param repositoryName the repository name
* @param baseUrl the base url of this application
* @return the repository information * @return the repository information
*/ */
def getRepository(userName: String, repositoryName: String, baseUrl: String)(implicit s: Session): Option[RepositoryInfo] = { def getRepository(userName: String, repositoryName: String)(implicit s: Session): Option[RepositoryInfo] = {
(Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository => (Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
// for getting issue count and pull request count // for getting issue count and pull request count
val issues = Issues.filter { t => val issues = Issues.filter { t =>
@@ -205,7 +205,7 @@ trait RepositoryService { self: AccountService =>
}.map(_.pullRequest).list }.map(_.pullRequest).list
new RepositoryInfo( new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName),
repository, repository,
issues.count(_ == false), issues.count(_ == false),
issues.count(_ == true), issues.count(_ == true),
@@ -234,7 +234,7 @@ trait RepositoryService { self: AccountService =>
}.list }.list
} }
def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false) def getUserRepositories(userName: String, withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = { (implicit s: Session): List[RepositoryInfo] = {
Repositories.filter { t1 => Repositories.filter { t1 =>
(t1.userName === userName.bind) || (t1.userName === userName.bind) ||
@@ -242,9 +242,9 @@ trait RepositoryService { self: AccountService =>
}.sortBy(_.lastActivityDate desc).list.map{ repository => }.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo( new RepositoryInfo(
if(withoutPhysicalInfo){ if(withoutPhysicalInfo){
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl) new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName)
} else { } else {
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl) JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName)
}, },
repository, repository,
getForkedCount( getForkedCount(
@@ -260,13 +260,12 @@ trait RepositoryService { self: AccountService =>
* If repositoryUserName is given then filters results by repository owner. * If repositoryUserName is given then filters results by repository owner.
* *
* @param loginAccount the logged in account * @param loginAccount the logged in account
* @param baseUrl the base url of this application
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user) * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
* @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count, * @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count,
* branches and tags * branches and tags
* @return the repository information which is sorted in descending order of lastActivityDate. * @return the repository information which is sorted in descending order of lastActivityDate.
*/ */
def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None, def getVisibleRepositories(loginAccount: Option[Account], repositoryUserName: Option[String] = None,
withoutPhysicalInfo: Boolean = false) withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = { (implicit s: Session): List[RepositoryInfo] = {
(loginAccount match { (loginAccount match {
@@ -284,9 +283,9 @@ trait RepositoryService { self: AccountService =>
}.sortBy(_.lastActivityDate desc).list.map{ repository => }.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo( new RepositoryInfo(
if(withoutPhysicalInfo){ if(withoutPhysicalInfo){
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl) new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName)
} else { } else {
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl) JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName)
}, },
repository, repository,
getForkedCount( getForkedCount(
@@ -389,32 +388,39 @@ trait RepositoryService { self: AccountService =>
object RepositoryService { object RepositoryService {
case class RepositoryInfo(owner: String, name: String, httpUrl: String, repository: Repository, case class RepositoryInfo(owner: String, name: String, repository: Repository,
issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int, issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
branchList: Seq[String], tags: Seq[JGitUtil.TagInfo], managers: Seq[String]){ branchList: Seq[String], tags: Seq[JGitUtil.TagInfo], managers: Seq[String]) {
lazy val host = """^https?://(.+?)(:\d+)?/""".r.findFirstMatchIn(httpUrl).get.group(1)
def sshUrl(port: Int, userName: String) = s"ssh://${userName}@${host}:${port}/${owner}/${name}.git"
def sshOpenRepoUrl(platform: String, port: Int, userName: String) = openRepoUrl(platform, sshUrl(port, userName))
def httpOpenRepoUrl(platform: String) = openRepoUrl(platform, httpUrl)
def openRepoUrl(platform: String, openUrl: String) = s"github-${platform}://openRepo/${openUrl}"
/** /**
* Creates instance with issue count and pull request count. * Creates instance with issue count and pull request count.
*/ */
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) = def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) =
this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) this(
repo.owner, repo.name, model,
issueCount, pullCount, repo.commitCount, forkedCount,
repo.branchList, repo.tags, managers)
/** /**
* Creates instance without issue count and pull request count. * Creates instance without issue count and pull request count.
*/ */
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) = def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) =
this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) this(
repo.owner, repo.name, model,
0, 0, repo.commitCount, forkedCount,
repo.branchList, repo.tags, managers)
def httpUrl(implicit context: Context): String = RepositoryService.httpUrl(owner, name)
def sshUrl(implicit context: Context): Option[String] = RepositoryService.sshUrl(owner, name)
} }
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode]) def httpUrl(owner: String, name: String)(implicit context: Context): String = s"${context.baseUrl}/git/${owner}/${name}.git"
def sshUrl(owner: String, name: String)(implicit context: Context): Option[String] =
if(context.settings.ssh){
context.loginAccount.flatMap { loginAccount =>
context.settings.sshAddress.map { x => s"ssh://${loginAccount.userName}@${x.host}:${x.port}/${owner}/${name}.git" }
}
} else None
def openRepoUrl(openUrl: String)(implicit context: Context): String = s"github-${context.platform}://openRepo/${openUrl}"
} }

View File

@@ -1,6 +1,7 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.util.{Directory, ControlUtil} import gitbucket.core.util.{Directory, ControlUtil}
import gitbucket.core.util.Implicits._
import Directory._ import Directory._
import ControlUtil._ import ControlUtil._
import SystemSettingsService._ import SystemSettingsService._
@@ -21,6 +22,7 @@ trait SystemSettingsService {
props.setProperty(Notification, settings.notification.toString) props.setProperty(Notification, settings.notification.toString)
settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString)) settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString))
props.setProperty(Ssh, settings.ssh.toString) props.setProperty(Ssh, settings.ssh.toString)
settings.sshHost.foreach(x => props.setProperty(SshHost, x.trim))
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString)) settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
props.setProperty(UseSMTP, settings.useSMTP.toString) props.setProperty(UseSMTP, settings.useSMTP.toString)
if(settings.useSMTP) { if(settings.useSMTP) {
@@ -75,6 +77,7 @@ trait SystemSettingsService {
getValue(props, Notification, false), getValue(props, Notification, false),
getOptionValue[Int](props, ActivityLogLimit, None), getOptionValue[Int](props, ActivityLogLimit, None),
getValue(props, Ssh, false), getValue(props, Ssh, false),
getOptionValue[String](props, SshHost, None).map(_.trim),
getOptionValue(props, SshPort, Some(DefaultSshPort)), getOptionValue(props, SshPort, Some(DefaultSshPort)),
getValue(props, UseSMTP, getValue(props, Notification, false)), // handle migration scenario from only notification to useSMTP getValue(props, UseSMTP, getValue(props, Notification, false)), // handle migration scenario from only notification to useSMTP
if(getValue(props, UseSMTP, getValue(props, Notification, false))){ if(getValue(props, UseSMTP, getValue(props, Notification, false))){
@@ -126,16 +129,19 @@ object SystemSettingsService {
notification: Boolean, notification: Boolean,
activityLogLimit: Option[Int], activityLogLimit: Option[Int],
ssh: Boolean, ssh: Boolean,
sshHost: Option[String],
sshPort: Option[Int], sshPort: Option[Int],
useSMTP: Boolean, useSMTP: Boolean,
smtp: Option[Smtp], smtp: Option[Smtp],
ldapAuthentication: Boolean, ldapAuthentication: Boolean,
ldap: Option[Ldap]){ ldap: Option[Ldap]){
def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse { def baseUrl(request: HttpServletRequest): String = baseUrl.fold(request.baseUrl)(_.stripSuffix("/"))
defining(request.getRequestURL.toString){ url =>
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) def sshAddress:Option[SshAddress] =
for {
host <- sshHost if ssh
} }
}.stripSuffix("/") yield SshAddress(host, sshPort.getOrElse(DefaultSshPort))
} }
case class Ldap( case class Ldap(
@@ -161,6 +167,10 @@ object SystemSettingsService {
fromAddress: Option[String], fromAddress: Option[String],
fromName: Option[String]) fromName: Option[String])
case class SshAddress(
host:String,
port:Int)
val DefaultSshPort = 29418 val DefaultSshPort = 29418
val DefaultSmtpPort = 25 val DefaultSmtpPort = 25
val DefaultLdapPort = 389 val DefaultLdapPort = 389
@@ -174,6 +184,7 @@ object SystemSettingsService {
private val Notification = "notification" private val Notification = "notification"
private val ActivityLogLimit = "activity_log_limit" private val ActivityLogLimit = "activity_log_limit"
private val Ssh = "ssh" private val Ssh = "ssh"
private val SshHost = "ssh.host"
private val SshPort = "ssh.port" private val SshPort = "ssh.port"
private val UseSMTP = "useSMTP" private val UseSMTP = "useSMTP"
private val SmtpHost = "smtp.host" private val SmtpHost = "smtp.host"
@@ -216,7 +227,4 @@ object SystemSettingsService {
else value else value
} }
// // TODO temporary flag
// val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
} }

View File

@@ -1,8 +1,13 @@
package gitbucket.core.service package gitbucket.core.service
import java.io.ByteArrayInputStream
import fr.brouillard.oss.security.xhub.XHub
import fr.brouillard.oss.security.xhub.XHub.{XHubDigest, XHubConverter}
import gitbucket.core.api._ import gitbucket.core.api._
import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment} import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment}
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import org.apache.http.client.utils.URLEncodedUtils
import profile.simple._ import profile.simple._
import gitbucket.core.util.JGitUtil.CommitInfo import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.RepositoryName import gitbucket.core.util.RepositoryName
@@ -33,8 +38,11 @@ trait WebHookService {
/** get All WebHook informations of repository event */ /** get All WebHook informations of repository event */
def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] = def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] =
WebHookEvents.filter(t => t.byRepository(owner, repository) && t.event === event.bind) WebHooks.filter(_.byRepository(owner, repository))
.list.map(t => WebHook(t.userName, t.repositoryName, t.url)) .innerJoin(WebHookEvents).on { (wh, whe) => whe.byWebHook(wh) }
.filter{ case (wh, whe) => whe.event === event.bind}
.map{ case (wh, whe) => wh }
.list.distinct
/** get All WebHook information from repository to url */ /** get All WebHook information from repository to url */
def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] = def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] =
@@ -44,14 +52,15 @@ trait WebHookService {
.map{ case (w,t) => w -> t.event } .map{ case (w,t) => w -> t.event }
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption .list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = { def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], token: Option[String])(implicit s: Session): Unit = {
WebHooks insert WebHook(owner, repository, url) WebHooks insert WebHook(owner, repository, url, token)
events.toSet.map{ event: WebHook.Event => events.toSet.map{ event: WebHook.Event =>
WebHookEvents insert WebHookEvent(owner, repository, url, event) WebHookEvents insert WebHookEvent(owner, repository, url, event)
} }
} }
def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = { def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], token: Option[String])(implicit s: Session): Unit = {
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).map(w => w.token).update(token)
WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete
events.toSet.map{ event: WebHook.Event => events.toSet.map{ event: WebHook.Event =>
WebHookEvents insert WebHookEvent(owner, repository, url, event) WebHookEvents insert WebHookEvent(owner, repository, url, event)
@@ -69,17 +78,17 @@ trait WebHookService {
} }
} }
def callWebHook(event: WebHook.Event, webHookURLs: List[WebHook], payload: WebHookPayload) def callWebHook(event: WebHook.Event, webHooks: List[WebHook], payload: WebHookPayload)
(implicit c: JsonFormat.Context): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = { (implicit c: JsonFormat.Context): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = {
import org.apache.http.impl.client.HttpClientBuilder import org.apache.http.impl.client.HttpClientBuilder
import ExecutionContext.Implicits.global import ExecutionContext.Implicits.global
import org.apache.http.protocol.HttpContext import org.apache.http.protocol.HttpContext
import org.apache.http.client.methods.HttpPost import org.apache.http.client.methods.HttpPost
if(webHookURLs.nonEmpty){ if(webHooks.nonEmpty){
val json = JsonFormat(payload) val json = JsonFormat(payload)
webHookURLs.map { webHookUrl => webHooks.map { webHook =>
val reqPromise = Promise[HttpRequest] val reqPromise = Promise[HttpRequest]
val f = Future { val f = Future {
val itcp = new org.apache.http.HttpRequestInterceptor{ val itcp = new org.apache.http.HttpRequestInterceptor{
@@ -89,19 +98,26 @@ trait WebHookService {
} }
try{ try{
val httpClient = HttpClientBuilder.create.addInterceptorLast(itcp).build val httpClient = HttpClientBuilder.create.addInterceptorLast(itcp).build
logger.debug(s"start web hook invocation for ${webHookUrl.url}") logger.debug(s"start web hook invocation for ${webHook.url}")
val httpPost = new HttpPost(webHookUrl.url) val httpPost = new HttpPost(webHook.url)
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded") httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded")
httpPost.addHeader("X-Github-Event", event.name) httpPost.addHeader("X-Github-Event", event.name)
httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString) httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString)
val params: java.util.List[NameValuePair] = new java.util.ArrayList() val params: java.util.List[NameValuePair] = new java.util.ArrayList()
params.add(new BasicNameValuePair("payload", json)) params.add(new BasicNameValuePair("payload", json))
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8")) def postContent = new UrlEncodedFormEntity(params, "UTF-8")
httpPost.setEntity(postContent)
if (!webHook.token.isEmpty) {
// 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.orNull, contentAsBytes))
}
val res = httpClient.execute(httpPost) val res = httpClient.execute(httpPost)
httpPost.releaseConnection() httpPost.releaseConnection()
logger.debug(s"end web hook invocation for ${webHookUrl}") logger.debug(s"end web hook invocation for ${webHook}")
res res
}catch{ }catch{
case e:Throwable => { case e:Throwable => {
@@ -113,12 +129,12 @@ trait WebHookService {
} }
} }
f.onSuccess { f.onSuccess {
case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}") case s => logger.debug(s"Success: web hook request to ${webHook.url}")
} }
f.onFailure { f.onFailure {
case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t) case t => logger.error(s"Failed: web hook request to ${webHook.url}", t)
} }
(webHookUrl, json, reqPromise.future, f) (webHook, json, reqPromise.future, f)
} }
} else { } else {
Nil Nil
@@ -161,7 +177,7 @@ trait WebHookPullRequestService extends WebHookService {
baseOwner <- users.get(repository.owner) baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName) headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName) issueUser <- users.get(issue.openedUserName)
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl) headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield { } yield {
WebHookPullRequestPayload( WebHookPullRequestPayload(
action = action, action = action,
@@ -200,7 +216,7 @@ trait WebHookPullRequestService extends WebHookService {
import WebHookService._ import WebHookService._
for{ for{
((issue, issueUser, pullRequest, baseOwner, headOwner), webHooks) <- getPullRequestsByRequestForWebhook(requestRepository.owner, requestRepository.name, requestBranch) ((issue, issueUser, pullRequest, baseOwner, headOwner), webHooks) <- getPullRequestsByRequestForWebhook(requestRepository.owner, requestRepository.name, requestBranch)
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName, baseUrl) baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName)
} yield { } yield {
val payload = WebHookPullRequestPayload( val payload = WebHookPullRequestPayload(
action = action, action = action,
@@ -229,7 +245,7 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
baseOwner <- users.get(repository.owner) baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName) headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName) issueUser <- users.get(issue.openedUserName)
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl) headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield { } yield {
WebHookPullRequestReviewCommentPayload( WebHookPullRequestReviewCommentPayload(
action = action, action = action,

View File

@@ -1,7 +1,9 @@
package gitbucket.core.service package gitbucket.core.service
import java.util.Date import java.util.Date
import gitbucket.core.controller.Context
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util._ import gitbucket.core.util._
import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.ControlUtil._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
@@ -13,7 +15,6 @@ import java.io.ByteArrayInputStream
import org.eclipse.jgit.patch._ import org.eclipse.jgit.patch._
import org.eclipse.jgit.api.errors.PatchFormatException import org.eclipse.jgit.api.errors.PatchFormatException
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import RepositoryService.RepositoryInfo
object WikiService { object WikiService {
@@ -38,10 +39,13 @@ object WikiService {
*/ */
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date) case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git")
def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) = def wikiHttpUrl(repositoryInfo: RepositoryInfo)(implicit context: Context): String
repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git") = RepositoryService.httpUrl(repositoryInfo.owner, repositoryInfo.name + ".wiki")
def wikiSshUrl(repositoryInfo: RepositoryInfo)(implicit context: Context): Option[String]
= RepositoryService.sshUrl(repositoryInfo.owner, repositoryInfo.name + ".wiki")
} }
trait WikiService { trait WikiService {

View File

@@ -74,7 +74,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
request.paths match { request.paths match {
case Array(_, repositoryOwner, repositoryName, _*) => case Array(_, repositoryOwner, repositoryName, _*) =>
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match { getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", "")) match {
case Some(repository) => { case Some(repository) => {
if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){ if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){
chain.doFilter(request, response) chain.doFilter(request, response)

View File

@@ -160,7 +160,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) + countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository) countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
val repositoryInfo = getRepository(owner, repository, baseUrl).get val repositoryInfo = getRepository(owner, repository).get
// Extract new commit and apply issue comment // Extract new commit and apply issue comment
val defaultBranch = repositoryInfo.repository.defaultBranch val defaultBranch = repositoryInfo.repository.defaultBranch

View File

@@ -87,11 +87,11 @@ abstract class DefaultGitCommand(val owner: String, val repoName: String) extend
} }
class DefaultGitUploadPack(owner: String, repoName: String, baseUrl: String) extends DefaultGitCommand(owner, repoName) class DefaultGitUploadPack(owner: String, repoName: String) extends DefaultGitCommand(owner, repoName)
with RepositoryService with AccountService { with RepositoryService with AccountService {
override protected def runTask(user: String)(implicit session: Session): Unit = { override protected def runTask(user: String)(implicit session: Session): Unit = {
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo => getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).foreach { repositoryInfo =>
if(!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)){ if(!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)){
using(Git.open(getRepositoryDir(owner, repoName))) { git => using(Git.open(getRepositoryDir(owner, repoName))) { git =>
val repository = git.getRepository val repository = git.getRepository
@@ -107,7 +107,7 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) ex
with RepositoryService with AccountService { with RepositoryService with AccountService {
override protected def runTask(user: String)(implicit session: Session): Unit = { override protected def runTask(user: String)(implicit session: Session): Unit = {
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo => getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).foreach { repositoryInfo =>
if(isWritableUser(user, repositoryInfo)){ if(isWritableUser(user, repositoryInfo)){
using(Git.open(getRepositoryDir(owner, repoName))) { git => using(Git.open(getRepositoryDir(owner, repoName))) { git =>
val repository = git.getRepository val repository = git.getRepository
@@ -124,7 +124,7 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) ex
} }
} }
class PluginGitUploadPack(repoName: String, baseUrl: String, routing: GitRepositoryRouting) extends GitCommand class PluginGitUploadPack(repoName: String, routing: GitRepositoryRouting) extends GitCommand
with SystemSettingsService { with SystemSettingsService {
override protected def runTask(user: String)(implicit session: Session): Unit = { override protected def runTask(user: String)(implicit session: Session): Unit = {
@@ -139,7 +139,7 @@ class PluginGitUploadPack(repoName: String, baseUrl: String, routing: GitReposit
} }
} }
class PluginGitReceivePack(repoName: String, baseUrl: String, routing: GitRepositoryRouting) extends GitCommand class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting) extends GitCommand
with SystemSettingsService { with SystemSettingsService {
override protected def runTask(user: String)(implicit session: Session): Unit = { override protected def runTask(user: String)(implicit session: Session): Unit = {
@@ -163,9 +163,9 @@ class GitCommandFactory(baseUrl: String) extends CommandFactory {
logger.debug(s"command: $command") logger.debug(s"command: $command")
command match { command match {
case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, baseUrl, routing(repoName)) case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, routing(repoName))
case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, baseUrl, routing(repoName)) case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, routing(repoName))
case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName, baseUrl) case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName)
case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl) case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl)
case _ => new UnknownCommand(command) case _ => new UnknownCommand(command)
} }

View File

@@ -1,12 +1,13 @@
package gitbucket.core.ssh package gitbucket.core.ssh
import gitbucket.core.service.SystemSettingsService import gitbucket.core.service.SystemSettingsService
import gitbucket.core.service.SystemSettingsService.SshAddress
import org.apache.sshd.common.Factory import org.apache.sshd.common.Factory
import org.apache.sshd.server.{Environment, ExitCallback, Command} import org.apache.sshd.server.{Environment, ExitCallback, Command}
import java.io.{OutputStream, InputStream} import java.io.{OutputStream, InputStream}
import org.eclipse.jgit.lib.Constants import org.eclipse.jgit.lib.Constants
class NoShell extends Factory[Command] with SystemSettingsService { class NoShell(sshAddress:SshAddress) extends Factory[Command] {
override def create(): Command = new Command() { override def create(): Command = new Command() {
private var in: InputStream = null private var in: InputStream = null
private var out: OutputStream = null private var out: OutputStream = null
@@ -15,7 +16,6 @@ class NoShell extends Factory[Command] with SystemSettingsService {
override def start(env: Environment): Unit = { override def start(env: Environment): Unit = {
val user = env.getEnv.get("USER") val user = env.getEnv.get("USER")
val port = loadSystemSettings().sshPort.getOrElse(SystemSettingsService.DefaultSshPort)
val message = val message =
""" """
| Welcome to | Welcome to
@@ -31,8 +31,8 @@ class NoShell extends Factory[Command] with SystemSettingsService {
| |
| Please use: | Please use:
| |
| git clone ssh://%s@GITBUCKET_HOST:%d/OWNER/REPOSITORY_NAME.git | git clone ssh://%s@%s:%d/OWNER/REPOSITORY_NAME.git
""".stripMargin.format(user, port).replace("\n", "\r\n") + "\r\n" """.stripMargin.format(user, sshAddress.host, sshAddress.port).replace("\n", "\r\n") + "\r\n"
err.write(Constants.encode(message)) err.write(Constants.encode(message))
err.flush() err.flush()
in.close() in.close()

View File

@@ -5,7 +5,8 @@ import java.util.concurrent.atomic.AtomicBoolean
import javax.servlet.{ServletContextEvent, ServletContextListener} import javax.servlet.{ServletContextEvent, ServletContextListener}
import gitbucket.core.service.SystemSettingsService import gitbucket.core.service.SystemSettingsService
import gitbucket.core.util.Directory import gitbucket.core.service.SystemSettingsService.SshAddress
import gitbucket.core.util.{Directory}
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -14,20 +15,20 @@ object SshServer {
private val server = org.apache.sshd.server.SshServer.setUpDefaultServer() private val server = org.apache.sshd.server.SshServer.setUpDefaultServer()
private val active = new AtomicBoolean(false) private val active = new AtomicBoolean(false)
private def configure(port: Int, baseUrl: String) = { private def configure(sshAddress: SshAddress, baseUrl: String) = {
server.setPort(port) server.setPort(sshAddress.port)
val provider = new SimpleGeneratorHostKeyProvider(new File(s"${Directory.GitBucketHome}/gitbucket.ser")) val provider = new SimpleGeneratorHostKeyProvider(new File(s"${Directory.GitBucketHome}/gitbucket.ser"))
provider.setAlgorithm("RSA") provider.setAlgorithm("RSA")
provider.setOverwriteAllowed(false) provider.setOverwriteAllowed(false)
server.setKeyPairProvider(provider) server.setKeyPairProvider(provider)
server.setPublickeyAuthenticator(new PublicKeyAuthenticator) server.setPublickeyAuthenticator(new PublicKeyAuthenticator)
server.setCommandFactory(new GitCommandFactory(baseUrl)) server.setCommandFactory(new GitCommandFactory(baseUrl))
server.setShellFactory(new NoShell) server.setShellFactory(new NoShell(sshAddress))
} }
def start(port: Int, baseUrl: String) = { def start(sshAddress: SshAddress, baseUrl: String) = {
if(active.compareAndSet(false, true)){ if(active.compareAndSet(false, true)){
configure(port, baseUrl) configure(sshAddress, baseUrl)
server.start() server.start()
logger.info(s"Start SSH Server Listen on ${server.getPort}") logger.info(s"Start SSH Server Listen on ${server.getPort}")
} }
@@ -55,20 +56,18 @@ class SshServerListener extends ServletContextListener with SystemSettingsServic
override def contextInitialized(sce: ServletContextEvent): Unit = { override def contextInitialized(sce: ServletContextEvent): Unit = {
val settings = loadSystemSettings() val settings = loadSystemSettings()
if(settings.ssh){ if (settings.sshAddress.isDefined && settings.baseUrl.isEmpty) {
settings.baseUrl match { logger.error("Could not start SshServer because the baseUrl is not configured.")
case None =>
logger.error("Could not start SshServer because the baseUrl is not configured.")
case Some(baseUrl) =>
SshServer.start(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), baseUrl)
}
} }
for {
sshAddress <- settings.sshAddress
baseUrl <- settings.baseUrl
}
SshServer.start(sshAddress, baseUrl)
} }
override def contextDestroyed(sce: ServletContextEvent): Unit = { override def contextDestroyed(sce: ServletContextEvent): Unit = {
if(loadSystemSettings().ssh){ SshServer.stop()
SshServer.stop()
}
} }
} }

View File

@@ -36,7 +36,7 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService with Acco
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: (RepositoryInfo) => Any) = {
{ {
defining(request.paths){ paths => defining(request.paths){ paths =>
getRepository(paths(0), paths(1), baseUrl).map { repository => getRepository(paths(0), paths(1)).map { repository =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(repository.owner == x.userName) => action(repository) case Some(x) if(repository.owner == x.userName) => action(repository)
@@ -95,7 +95,7 @@ trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService =
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: (RepositoryInfo) => Any) = {
{ {
defining(request.paths){ paths => defining(request.paths){ paths =>
getRepository(paths(0), paths(1), baseUrl).map { repository => getRepository(paths(0), paths(1)).map { repository =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(paths(0) == x.userName) => action(repository) case Some(x) if(paths(0) == x.userName) => action(repository)
@@ -118,7 +118,7 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: (RepositoryInfo) => Any) = {
{ {
defining(request.paths){ paths => defining(request.paths){ paths =>
getRepository(paths(0), paths(1), baseUrl).map { repository => getRepository(paths(0), paths(1)).map { repository =>
if(!repository.repository.isPrivate){ if(!repository.repository.isPrivate){
action(repository) action(repository)
} else { } else {
@@ -145,7 +145,7 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: (RepositoryInfo) => Any) = {
{ {
defining(request.paths){ paths => defining(request.paths){ paths =>
getRepository(paths(0), paths(1), baseUrl).map { repository => getRepository(paths(0), paths(1)).map { repository =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(!repository.repository.isPrivate) => action(repository) case Some(x) if(!repository.repository.isPrivate) => action(repository)

View File

@@ -75,6 +75,11 @@ object Implicits {
def gitRepositoryPath: String = request.getRequestURI.replaceFirst("^/git/", "/") def gitRepositoryPath: String = request.getRequestURI.replaceFirst("^/git/", "/")
def baseUrl:String = {
val url = request.getRequestURL.toString
val len = url.length - (request.getRequestURI.length - request.getContextPath.length)
url.substring(0, len).stripSuffix("/")
}
} }
implicit class RichSession(session: HttpSession){ implicit class RichSession(session: HttpSession){

View File

@@ -32,14 +32,13 @@ object JGitUtil {
* *
* @param owner the user name of the repository owner * @param owner the user name of the repository owner
* @param name the repository name * @param name the repository name
* @param url the repository URL
* @param commitCount the commit count. If the repository has over 1000 commits then this property is 1001. * @param commitCount the commit count. If the repository has over 1000 commits then this property is 1001.
* @param branchList the list of branch names * @param branchList the list of branch names
* @param tags the list of tags * @param tags the list of tags
*/ */
case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){ case class RepositoryInfo(owner: String, name: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){
def this(owner: String, name: String, baseUrl: String) = { def this(owner: String, name: String) = {
this(owner, name, s"${baseUrl}/git/${owner}/${name}.git", 0, Nil, Nil) this(owner, name, 0, Nil, Nil)
} }
} }
@@ -174,14 +173,14 @@ object JGitUtil {
/** /**
* Returns the repository information. It contains branch names and tag names. * Returns the repository information. It contains branch names and tag names.
*/ */
def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = { def getRepositoryInfo(owner: String, repository: String): RepositoryInfo = {
using(Git.open(getRepositoryDir(owner, repository))){ git => using(Git.open(getRepositoryDir(owner, repository))){ git =>
try { try {
// get commit count // get commit count
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10001).sum val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10001).sum
RepositoryInfo( RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", owner, repository,
// commit count // commit count
commitCount, commitCount,
// branches // branches
@@ -197,7 +196,7 @@ object JGitUtil {
} catch { } catch {
// not initialized // not initialized
case e: NoHeadException => RepositoryInfo( case e: NoHeadException => RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", 0, Nil, Nil) owner, repository, 0, Nil, Nil)
} }
} }

View File

@@ -9,7 +9,7 @@ import gitbucket.core.plugin.{RenderRequest, PluginRegistry}
import gitbucket.core.service.{RepositoryService, RequestCache} import gitbucket.core.service.{RepositoryService, RequestCache}
import gitbucket.core.util.{FileUtil, JGitUtil, StringUtil} import gitbucket.core.util.{FileUtil, JGitUtil, StringUtil}
import play.twirl.api.Html import play.twirl.api.{Html, HtmlFormat}
/** /**
* Provides helper methods for Twirl templates. * Provides helper methods for Twirl templates.
@@ -225,6 +225,13 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(implicit context: Context): Html = def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(implicit context: Context): Html =
userWithContent(userName, mailAddress)(avatar(userName, size, tooltip, mailAddress)) userWithContent(userName, mailAddress)(avatar(userName, size, tooltip, mailAddress))
/**
* Generates the avatar link to the account page.
* If user does not exist or disabled, this method returns avatar image without link.
*/
def avatarLink(commit: JGitUtil.CommitInfo, size: Int)(implicit context: Context): Html =
userWithContent(commit.authorName, commit.authorEmailAddress)(avatar(commit, size))
private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)(implicit context: Context): Html = private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)(implicit context: Context): Html =
(if(mailAddress.isEmpty){ (if(mailAddress.isEmpty){
getAccountByUserName(userName) getAccountByUserName(userName)
@@ -306,6 +313,19 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
private[this] val detectAndRenderLinksRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r private[this] val detectAndRenderLinksRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r
def detectAndRenderLinks(text: String): Html = { def detectAndRenderLinks(text: String): Html = {
Html(detectAndRenderLinksRegex.replaceAllIn(text, m => s"""<a href="${m.group(0)}">${m.group(0)}</a>""")) val matches = detectAndRenderLinksRegex.findAllMatchIn(text).toSeq
val (x, pos) = matches.foldLeft((collection.immutable.Seq.empty[Html], 0)){ case ((x, pos), m) =>
val url = m.group(0)
val href = url.replace("\"", "&quot;")
(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)
} }
} }

View File

@@ -4,7 +4,7 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main("Applications"){ @html.main("Applications"){
<div class="container"> <div class="container body">
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
@menu("application", settings.ssh) @menu("application", settings.ssh)
@@ -16,24 +16,27 @@
@if(personalTokens.isEmpty && gneratedToken.isEmpty){ @if(personalTokens.isEmpty && gneratedToken.isEmpty){
No tokens. No tokens.
} else { } else {
Tokens you have generated that can be used to access the GitBucket API.<hr> Tokens you have generated that can be used to access the GitBucket API.
<hr style="margin-top: 10px;">
} }
@gneratedToken.map{ case (token, tokenString) => @gneratedToken.map{ case (token, tokenString) =>
<div class="alert alert-info"> <div class="alert alert-info">
Make sure to copy your new personal access token now. You won't be able to see it again! Make sure to copy your new personal access token now. You won't be able to see it again!
</div> </div>
@helper.html.copy("generated-token-copy", tokenString){ <a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-sm btn-danger pull-right">Delete</a>
<input type="text" value="@tokenString" style="width:21em" readonly> <div style="width: 50%;">
} @helper.html.copy("generated-token-copy", tokenString){
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-mini btn-danger pull-right">Delete</a> <input type="text" value="@tokenString" class="form-control input-sm" readonly>
<hr> }
</div>
<hr style="margin-top: 10px;">
} }
@personalTokens.zipWithIndex.map { case (token, i) => @personalTokens.zipWithIndex.map { case (token, i) =>
@if(i != 0){ @if(i != 0){
<hr> <hr style="margin-top: 10px;">
} }
<strong>@token.note</strong> <strong>@token.note</strong>
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-mini btn-danger pull-right">Delete</a> <a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-sm btn-danger pull-right">Delete</a>
} }
</div> </div>
</div> </div>
@@ -44,7 +47,7 @@
<fieldset> <fieldset>
<label for="note" class="strong">Token description</label> <label for="note" class="strong">Token description</label>
<div><span id="error-note" class="error"></span></div> <div><span id="error-note" class="error"></span></div>
<input type="text" name="note" id="note" class="form-control" style="width: 400px;"/> <input type="text" name="note" id="note" class="form-control"/>
<p class="muted">What's this token for?</p> <p class="muted">What's this token for?</p>
</fieldset> </fieldset>
<input type="submit" class="btn btn-success" value="Generate token"/> <input type="submit" class="btn btn-success" value="Generate token"/>

View File

@@ -3,7 +3,7 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main("Edit your profile"){ @html.main("Edit your profile"){
<div class="container"> <div class="container body">
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
@menu("profile", settings.ssh) @menu("profile", settings.ssh)

View File

@@ -2,7 +2,7 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main(if(account.isEmpty) "Create group" else "Edit group"){ @html.main(if(account.isEmpty) "Create group" else "Edit group"){
<div class="container"> <div class="container body">
<form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true"> <form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true">
<div class="row"> <div class="row">
<div class="col-md-5"> <div class="col-md-5">

View File

@@ -3,7 +3,7 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main(account.userName){ @html.main(account.userName){
<div class="container"> <div class="container body">
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
@@ -22,7 +22,7 @@
<div> <div>
<div>Groups</div> <div>Groups</div>
@groupNames.map { groupName => @groupNames.map { groupName =>
<a href="@url(groupName)">@avatar(groupName, 36, tooltip = true)</a> @avatarLink(groupName, 36, tooltip = true)
} }
</div> </div>
} }

View File

@@ -3,7 +3,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main("Create a New Repository"){ @html.main("Create a New Repository"){
<div style="width: 600px; margin: 10px auto;"> <div class="body" style="width: 600px; margin: 10px auto;">
<h2>Create a new repository</h2> <h2>Create a new repository</h2>
<p class="muted"> <p class="muted">
A repository contains all the files for your project, including the revision history. A repository contains all the files for your project, including the revision history.

View File

@@ -2,7 +2,7 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main("Create your account"){ @html.main("Create your account"){
<div class="container"> <div class="container body">
<h3>Create your account</h3> <h3>Create your account</h3>
<form action="@path/register" method="POST" validate="true"> <form action="@path/register" method="POST" validate="true">
<div class="row"> <div class="row">

View File

@@ -3,7 +3,7 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main("SSH Keys"){ @html.main("SSH Keys"){
<div class="container"> <div class="container body">
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
@menu("ssh", settings.ssh) @menu("ssh", settings.ssh)
@@ -17,10 +17,10 @@
} }
@sshKeys.zipWithIndex.map { case (key, i) => @sshKeys.zipWithIndex.map { case (key, i) =>
@if(i != 0){ @if(i != 0){
<hr> <hr style="margin-top: 10px;">
} }
<strong>@key.title</strong> (@SshUtil.fingerPrint(key.publicKey).getOrElse("Key is invalid.")) <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> <a href="@path/@account.userName/_ssh/delete/@key.sshKeyId" class="btn btn-sm btn-danger pull-right">Delete</a>
} }
</div> </div>
</div> </div>
@@ -31,12 +31,12 @@
<fieldset class="form-group"> <fieldset class="form-group">
<label for="title" class="strong">Title</label> <label for="title" class="strong">Title</label>
<div><span id="error-title" class="error"></span></div> <div><span id="error-title" class="error"></span></div>
<input type="text" name="title" id="title" class="form-control" style="width: 400px;"/> <input type="text" name="title" id="title" class="form-control"/>
</fieldset> </fieldset>
<fieldset class="form-group"> <fieldset class="form-group">
<label for="publicKey" class="strong">Key</label> <label for="publicKey" class="strong">Key</label>
<div><span id="error-publicKey" class="error"></span></div> <div><span id="error-publicKey" class="error"></span></div>
<textarea name="publicKey" id="publicKey" class="form-control" style="width: 600px; height: 250px;"></textarea> <textarea name="publicKey" id="publicKey" class="form-control" style="height: 250px;"></textarea>
</fieldset> </fieldset>
<input type="submit" class="btn btn-success" value="Add"/> <input type="submit" class="btn btn-success" value="Add"/>
</div> </div>

View File

@@ -11,7 +11,7 @@
<a href="@path/admin/system">System Settings</a> <a href="@path/admin/system">System Settings</a>
</li> </li>
<li@if(active=="plugins"){ class="active"}> <li@if(active=="plugins"){ class="active"}>
<a href="@path/admin/plugins">Plugins</a> <a href="@path/admin/plugins">Plugins</a>
</li> </li>
<li> <li>
<a href="@path/console/login.jsp">H2 Console</a> <a href="@path/console/login.jsp">H2 Console</a>

View File

@@ -111,15 +111,24 @@
Enable SSH access to git repository Enable SSH access to git repository
</label> </label>
</fieldset> </fieldset>
<div class="form-group ssh"> <div class="ssh">
<label class="control-label col-md-3" for="sshPort">SSH Port</label> <div class="form-group">
<div class="col-md-9"> <label class="control-label col-md-3" for="sshHost">SSH Host</label>
<input type="text" id="sshPort" name="sshPort" class="form-control" value="@settings.sshPort"/> <div class="col-md-9">
<span id="error-sshPort" class="error"></span> <input type="text" id="sshHost" name="sshHost" class="form-control" value="@settings.sshHost"/>
<span id="error-sshHost" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="sshPort">SSH Port</label>
<div class="col-md-9">
<input type="text" id="sshPort" name="sshPort" class="form-control" value="@settings.sshPort"/>
<span id="error-sshPort" class="error"></span>
</div>
</div> </div>
</div> </div>
<p class="muted"> <p class="muted">
Base URL is required if SSH access is enabled. Both of SSH host and Base URL are required if SSH access is enabled.
</p> </p>
<!--====================================================================--> <!--====================================================================-->
<!-- Authentication --> <!-- Authentication -->

View File

@@ -8,9 +8,11 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main("Issues"){ @html.main("Issues"){
@dashboard.html.tab("issues") <div class="body">
<div class="container"> @dashboard.html.tab("issues")
@issuesnavi(filter, "issues", condition) <div class="container">
@issueslist(issues, page, openCount, closedCount, condition, filter, groups) @issuesnavi(filter, "issues", condition)
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
</div>
</div> </div>
} }

View File

@@ -8,9 +8,11 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main("Pull Requests"){ @html.main("Pull Requests"){
@dashboard.html.tab("pulls") <div class="body">
<div class="container"> @dashboard.html.tab("pulls")
@issuesnavi(filter, "pulls", condition) <div class="container">
@issueslist(issues, page, openCount, closedCount, condition, filter, groups) @issuesnavi(filter, "pulls", condition)
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
</div>
</div> </div>
} }

View File

@@ -27,12 +27,6 @@ $(function(){
throw e; throw e;
} }
} }
// Adjust clickable area width
$('#@textareaId').next('div.clickable').css({
'width': ($('#@textareaId').width() + 18) + 'px',
'font-size': '13px'
});
}); });
</script> </script>
} }

View File

@@ -10,8 +10,8 @@
@if(comment.fileName.isDefined){filename="@comment.fileName.get"} @if(comment.fileName.isDefined){filename="@comment.fileName.get"}
@if(comment.newLine.isDefined){newline="@comment.newLine.get"} @if(comment.newLine.isDefined){newline="@comment.newLine.get"}
@if(comment.oldLine.isDefined){oldline="@comment.oldLine.get"}> @if(comment.oldLine.isDefined){oldline="@comment.oldLine.get"}>
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div> <div class="issue-avatar-image">@avatarLink(comment.commentedUserName, 48)</div>
<div class="panel- panel-default commit-comment-box commit-comment-@comment.commentId"> <div class="panel panel-default commit-comment-box commit-comment-@comment.commentId">
<div class="panel-heading"> <div class="panel-heading">
@user(comment.commentedUserName, styleClass="username strong") @user(comment.commentedUserName, styleClass="username strong")
<span class="muted"> <span class="muted">

View File

@@ -1,8 +1,16 @@
@(id: String, value: String, prepend: Boolean = false)(html: Html) @(id: String, value: String, style: String = "")(html: Html = Html(""))
<div class="input-group @if(prepend){input-prepend}" style="margin-bottom: 0px;"> @if(html.body.nonEmpty){
@html <div class="input-group" style="margin-bottom: 0px;">
<span class="input-group-btn"><span id="@id" class="btn btn-sm btn-default" data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span></span> @html
</div> <span class="input-group-btn">
<span id="@id" class="btn btn-sm 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-sm btn-default" @if(style.nonEmpty){style="@style"}
data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span>
}
<script> <script>
// copy to clipboard // copy to clipboard
(function() { (function() {

View File

@@ -4,79 +4,81 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@main("GitBucket"){ @main("GitBucket"){
@dashboard.html.tab() <div class="body">
<div class="container"> @dashboard.html.tab()
<div class="row"> <div class="container">
<div class="col-md-8"> <div class="row">
<div class="pull-right"> <div class="col-md-8">
<a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a> <div class="pull-right">
</div> <a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a>
@helper.html.activities(activities)
</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">&times;</button>
@Html(information)
</div> </div>
} @helper.html.activities(activities)
@if(loginAccount.isEmpty){ </div>
@signinform(settings)
} else { <div class="col-md-4">
<div class="panel panel-default"> @settings.information.map { information =>
<div class="panel-heading strong"> <div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
<div class="pull-right"> <button type="button" class="close" data-dismiss="alert">&times;</button>
<a href="@path/new" class="btn btn-success btn-sm">New repository</a> @Html(information)
</div>
Your repositories <span class="badge">@userRepositories.size</span>
</div> </div>
<ul class="list-group list-group-flush"> }
@if(userRepositories.isEmpty){ @if(loginAccount.isEmpty){
<li class="list-group-item">No repositories</li> <div id="dashboard-signin-form">@signinform(settings)</div>
} else { } else {
@defining(20){ max => <div class="panel panel-default">
@userRepositories.zipWithIndex.map { case (repository, i) => <div class="panel-heading strong">
<li class="list-group-item repo-link" style="@if(i > max - 1){display:none;}"> <div class="pull-right">
@helper.html.repositoryicon(repository, false) <a href="@path/new" class="btn btn-success btn-sm">New repository</a>
@if(repository.owner == loginAccount.get.userName){ </div>
<a href="@url(repository)"><span class="strong">@repository.name</span></a> Your repositories <span class="badge">@userRepositories.size</span>
} else { </div>
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a> <ul class="list-group list-group-flush">
} @if(userRepositories.isEmpty){
</li> <li class="list-group-item">No repositories</li>
} } else {
@if(userRepositories.size > max){ @defining(20){ max =>
<li class="list-group-item show-more"> @userRepositories.zipWithIndex.map { case (repository, i) =>
<a href="javascript:void(0);" id="show-more-repos">Show @{userRepositories.size - max} more repositories...</a> <li class="list-group-item repo-link" style="@if(i > max - 1){display:none;}">
</li> @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>
</ul> </div>
</div> }
} <div class="panel panel-default">
<div class="panel panel-default"> <div class="panel-heading strong">Recent updated repositories</div>
<div class="panel-heading strong">Recent updated repositories</div> <ul class="list-group list-group-flush">
<ul class="list-group list-group-flush"> @if(recentRepositories.isEmpty){
@if(recentRepositories.isEmpty){ <li class="list-group-item">No repositories</li>
<li class="list-group-item">No repositories</li> } else {
} else { @defining(20){ max =>
@defining(20){ max => @recentRepositories.zipWithIndex.map { case (repository, i) =>
@recentRepositories.zipWithIndex.map { case (repository, i) => <li class="list-group-item repo-link" style="@if(i > max - 1){display:none;}">
<li class="list-group-item repo-link" style="@if(i > max - 1){display:none;}"> @helper.html.repositoryicon(repository, false)
@helper.html.repositoryicon(repository, false) <a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a> </li>
</li> }
} @if(recentRepositories.size > max){
@if(recentRepositories.size > max){ <li class="list-group-item show-more">
<li class="list-group-item show-more"> <a href="javascript:void(0);" id="show-more-recent-repos">Show @{recentRepositories.size - max} more repositories...</a>
<a href="javascript:void(0);" id="show-more-recent-repos">Show @{recentRepositories.size - max} more repositories...</a> </li>
</li> }
} }
} }
} </ul>
</ul> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -7,7 +7,7 @@
@if(loginAccount.isDefined){ @if(loginAccount.isDefined){
<hr/><br/> <hr/><br/>
<form method="POST" validate="true"> <form method="POST" validate="true">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div> <div class="issue-avatar-image">@avatarLink(loginAccount.get.userName, 48)</div>
<div class="panel panel-default issue-comment-box"> <div class="panel panel-default issue-comment-box">
<div class="panel-body"> <div class="panel-body">
@helper.html.preview( @helper.html.preview(

View File

@@ -7,7 +7,7 @@
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@import gitbucket.core.model.CommitComment @import gitbucket.core.model.CommitComment
@if(issue.isDefined){ @if(issue.isDefined){
<div class="issue-avatar-image">@avatar(issue.get.openedUserName, 48)</div> <div class="issue-avatar-image">@avatarLink(issue.get.openedUserName, 48)</div>
<div class="panel panel-default issue-comment-box"> <div class="panel panel-default issue-comment-box">
<div class="panel-heading"> <div class="panel-heading">
@user(issue.get.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.get.registeredDate)</span> @user(issue.get.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.get.registeredDate)</span>
@@ -36,7 +36,7 @@
case comment: gitbucket.core.model.IssueComment => { case comment: gitbucket.core.model.IssueComment => {
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch" @if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"
&& comment.action != "commit" && comment.action != "refer"){ && comment.action != "commit" && comment.action != "refer"){
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div> <div class="issue-avatar-image">@avatarLink(comment.commentedUserName, 48)</div>
<div class="panel panel-default issue-comment-box" id="comment-@comment.commentId"> <div class="panel panel-default issue-comment-box" id="comment-@comment.commentId">
<div class="panel-heading"> <div class="panel-heading">
@user(comment.commentedUserName, styleClass="username strong") @user(comment.commentedUserName, styleClass="username strong")
@@ -138,7 +138,7 @@
<span class="discussion-item-icon"><i class="octicon octicon-circle-slash"></i></span> <span class="discussion-item-icon"><i class="octicon octicon-circle-slash"></i></span>
@avatar(comment.commentedUserName, 16) @avatar(comment.commentedUserName, 16)
@user(comment.commentedUserName, styleClass="username strong") @user(comment.commentedUserName, styleClass="username strong")
close @issueOrPullRequest() closed this @issueOrPullRequest()
@helper.html.datetimeago(comment.registeredDate) @helper.html.datetimeago(comment.registeredDate)
</div> </div>
</div> </div>
@@ -210,7 +210,7 @@ $(function(){
$(document).on('click', '.commit-comment-box i.octicon-pencil', function(){ $(document).on('click', '.commit-comment-box i.octicon-pencil', function(){
var id = $(this).closest('a').data('comment-id'); var id = $(this).closest('a').data('comment-id');
var url = '@url(repository)/commit_comments/_data/' + id; var url = '@url(repository)/commit_comments/_data/' + id;
var $content = $('.commit-commentContent-' + id, $(this).closest('.box')); var $content = $('.commit-commentContent-' + id, $(this).closest('.commit-comment-box'));
$.get(url, $.get(url,
{ {

View File

@@ -10,11 +10,11 @@
<form action="@url(repository)/issues/new" method="POST" validate="true" class="form-group"> <form action="@url(repository)/issues/new" method="POST" validate="true" class="form-group">
<div class="row"> <div class="row">
<div class="col-md-10"> <div class="col-md-10">
<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-box"> <div class="panel panel-default issue-box">
<div class="panel-body"> <div class="panel-body">
<span id="error-title" class="error"></span> <span id="error-title" class="error"></span>
<input type="text" name="title" class="form-control input-lg" value="" placeholder="Title" style="width: 680px;" autofocus/> <input type="text" id="issue-title" name="title" class="form-control input-lg" value="" placeholder="Title" autofocus/>
@helper.html.preview( @helper.html.preview(
repository = repository, repository = repository,
content = "", content = "",
@@ -23,7 +23,7 @@
enableLineBreaks = true, enableLineBreaks = true,
enableTaskList = true, enableTaskList = true,
hasWritePermission = hasWritePermission, hasWritePermission = hasWritePermission,
style = "width: 680px; height: 200px; max-height: 250px;", style = "height: 200px; max-height: 250px;",
elastic = true elastic = true
) )
<div class="align-right"> <div class="align-right">

View File

@@ -49,7 +49,8 @@
<div class="small" style="padding-left: 20px;"> <div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate => @milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){ @if(isPast(dueDate)){
<i class="octicon octicon-alert" style="color:#BD2C00;"></i><span class="milestone-alert">Due by @date(dueDate)</span> <i class="octicon octicon-alert" style="color:#BD2C00;"></i>
<span class="milestone-alert">Due by @date(dueDate)</span>
} else { } else {
<span class="muted">Due by @date(dueDate)</span> <span class="muted">Due by @date(dueDate)</span>
} }

View File

@@ -11,7 +11,7 @@
<span class="input-group-addon"><i style="background-color: #@label.map(_.color).getOrElse("888888");"></i></span> <span class="input-group-addon"><i style="background-color: #@label.map(_.color).getOrElse("888888");"></i></span>
</div> </div>
<script> <script>
$('div#label-color-@labelId').colorpicker(); $('div#label-color-@labelId').colorpicker({format: "hex"});
</script> </script>
<span class="pull-right"> <span class="pull-right">
<span id="label-error-@labelId" class="error"></span> <span id="label-error-@labelId" class="error"></span>

View File

@@ -41,7 +41,8 @@
} else { } else {
@milestone.dueDate.map { dueDate => @milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){ @if(isPast(dueDate)){
<i class="octicon octicon-alert" style="color:#BD2C00;"></i><span class="muted milestone-alert">Due by @date(dueDate)</span> <i class="octicon octicon-alert" style="color:#BD2C00;"></i>
<span class="muted milestone-alert">Due by @date(dueDate)</span>
} else { } else {
<span class="muted">Due by @date(dueDate)</span> <span class="muted">Due by @date(dueDate)</span>
} }

View File

@@ -1,30 +1,28 @@
@(active: String, @(active: String,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo, repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
id: Option[String] = None, id: Option[String] = None,
expand: Boolean = false, isRepoTop: Boolean = false,
isNoGroup: Boolean = true, isNoGroup: Boolean = true,
info: Option[Any] = None, info: Option[Any] = None,
error: Option[Any] = None)(body: Html)(implicit context: gitbucket.core.controller.Context) error: Option[Any] = None)(body: Html)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.service.SystemSettingsService
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@sidemenu(path: String, name: String, icon: String, label: String, count: Int = 0) = { @menuitem(path: String, name: String, icon: String, label: String, count: Int = 0) = {
<li @if(active == name){class="active"} @if(!expand){data-toggle="tooltip" data-placement="left" data-original-title="@label"}> <li @if(active == name){class="active"}>
<a href="@url(repository)@path"> <a href="@url(repository)@path">
<i class="menu-icon @if(active == name){menu-icon-active} octicon octicon-@{icon} "></i> <i class="menu-icon @if(active == name){menu-icon-active} octicon octicon-@{icon} "></i> <span class="pc">@label</span>
@if(expand){ @label} @if(count > 0){
@if(expand && count > 0){ <span class="badge">@count</span>
<div class="pull-right"><span class="badge">@count</span></div>
} }
</a> </a>
</li> </li>
} }
<div class="container"> <div class="headbar">
@helper.html.information(info) <div class="container">
@helper.html.error(error) @helper.html.information(info)
<div class="row"> @helper.html.error(error)
<div class="head"> <div class="head">
@if(repository.commitCount > 0){ @if(repository.commitCount > 0){
<div class="input-group pull-right"> <div class="input-group pull-right">
@@ -60,83 +58,52 @@
} }
} }
</div> </div>
<ul class="headmenu">
@menuitem("" , "code" , "code" , "Code")
@menuitem("/issues", "issues" , "issue-opened" , "Issues", repository.issueCount)
@menuitem("/pulls" , "pulls" , "git-pull-request" , "Pull Requests", repository.pullCount)
@menuitem("/wiki" , "wiki" , "book" , "Wiki")
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){
@menuitem("/settings" , "settings" , "tools", "Settings")
}
</ul>
</div> </div>
</div> </div>
<hr style="margin-bottom: 20px;"/>
<div class="container body"> <div class="container body">
<div style="width: @if(expand){170px} else {40px};" class="pull-right"> @if(isRepoTop){
<ul class="sidemenu"> @repository.repository.description.map { description =>
<li style="height: 12px"><div class="gradient pull-left" style="height: 12px"></div></li> <p class="description">@detectAndRenderLinks(description)</p>
@sidemenu("" , "code" , "code" , "Code")
@sidemenu("/issues", "issues" , "issue-opened" , "Issues", repository.issueCount)
@sidemenu("/pulls" , "pulls" , "git-pull-request" , "Pull Requests", repository.pullCount)
@sidemenu("/wiki" , "wiki" , "book" , "Wiki")
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){
@sidemenu("/settings" , "settings" , "tools", "Settings")
}
<li style="height: 12px"><div class="gradient pull-left" style="height: 12px"></div></li>
</ul>
@if(expand){
<div class="small">
<strong id="repository-url-proto">HTTP</strong> <span class="mute">clone URL</span>
</div>
@helper.html.copy("repository-url-copy", repository.httpUrl){
<input type="text" value="@repository.httpUrl" id="repository-url" class="form-control input-sm" readonly>
}
@if(settings.ssh && loginAccount.isDefined){
<div class="small">
<span class="mute">You can clone <a href="javascript:void(0);" id="repository-url-http">HTTP</a> or <a href="javascript:void(0);" id="repository-url-ssh">SSH</a>.</span>
</div>
}
@id.map { id =>
@if(context.platform != "linux" && context.platform != null){
<div style="margin-top: 10px;">
<a href="@repository.httpOpenRepoUrl(context.platform)" id="repository-clone-url" class="btn btn-sm btn-default btn-block"><i class="octicon octicon-desktop-download"></i>&nbsp;&nbsp;Clone in Desktop</a>
</div>
}
<div style="margin-top: 10px;">
<a href="@{url(repository)}/archive/@{encodeRefName(id)}.zip" class="btn btn-sm btn-default btn-block"><i class="octicon octicon-cloud-download"></i>Download ZIP</a>
</div>
@*
<div style="margin-top: 10px;">
<a href="@{url(repository)}/archive/@{encodeRefName(id)}.tar.gz" class="btn btn-sm btn-default btn-block "><i class="octicon octicon-cloud-download"></i>Download TAR.GZ</a>
</div>
*@
}
} }
</div> <div style="margin-bottom: 10px; padding: 4px;" class="panel panel-default">
<div class="pull-left" style="width: @if(expand){770px} else {895px};"> <table class="fill-width">
@if(expand){ <tr>
@repository.repository.description.map { description => <td style="width: 33%; text-align: center;">
<p class="description">@detectAndRenderLinks(description)</p> <a href="@url(repository)/commits/@encodeRefName(id.getOrElse(""))" class="header-link">
} <i class="octicon octicon-history"></i>
<div style="margin-bottom: 10px; padding: 4px;" class="panel panel-default"> @if(repository.commitCount > 10000){
<table class="fill-width"> <strong>10000+</strong> commits
<tr> } else {
<td style="width: 33%; text-align: center;">
<a href="@url(repository)/commits/@encodeRefName(id.getOrElse(""))" class="header-link">
<i class="octicon octicon-history"></i>
<strong>@repository.commitCount</strong> commits <strong>@repository.commitCount</strong> commits
</a> }
</td> </a>
<td style="width: 33%; text-align: center;"> </td>
<a href="@url(repository)/branches" class="header-link" class="header-link"> <td style="width: 33%; text-align: center;">
<i class="octicon octicon-git-branch"></i> <a href="@url(repository)/branches" class="header-link" class="header-link">
<strong>@repository.branchList.length</strong> branches <i class="octicon octicon-git-branch"></i>
</a> <strong>@repository.branchList.length</strong> branches
</td> </a>
<td style="width: 33%; text-align: center;"> </td>
<a href="@url(repository)/tags" class="header-link" class="header-link"> <td style="width: 33%; text-align: center;">
<i class="octicon octicon-tag"></i> <a href="@url(repository)/tags" class="header-link" class="header-link">
<strong>@repository.tags.length</strong> releases <i class="octicon octicon-tag"></i>
</a> <strong>@repository.tags.length</strong> releases
</td> </a>
</tr> </td>
</table> </tr>
</div> </table>
} </div>
@body }
</div> @body
</div> </div>
<script> <script>
$(function(){ $(function(){
@@ -146,7 +113,7 @@ $(function(){
if(e.target.tagName == "I"){ if(e.target.tagName == "I"){
target = e.target.parentElement; target = e.target.parentElement;
} }
$(target).prev ('div.gradient' ).css('border-left', '1px solid silver'); $(target).prev('div.gradient').css('border-left', '1px solid silver');
}); });
$('ul.sidemenu a').mouseout(function(e){ $('ul.sidemenu a').mouseout(function(e){
@@ -154,7 +121,7 @@ $(function(){
if(e.target.tagName == "I"){ if(e.target.tagName == "I"){
target = e.target.parentElement; target = e.target.parentElement;
} }
$(target).prev ('div.gradient' ).css('border-left', '1px solid #eee'); $(target).prev('div.gradient').css('border-left', '1px solid #eee');
}); });
$('a[rel*=facebox]').facebox({ $('a[rel*=facebox]').facebox({
@@ -176,21 +143,5 @@ $(function(){
$('#fork-form').submit(); $('#fork-form').submit();
}); });
} }
@if(settings.ssh && loginAccount.isDefined){
$('#repository-url-http').click(function(){
$('#repository-url-proto').text('HTTP');
$('#repository-url').val('@repository.httpUrl');
$('#repository-clone-url').attr('href', '@repository.httpOpenRepoUrl(context.platform)')
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
});
$('#repository-url-ssh').click(function(){
$('#repository-url-proto').text('SSH');
$('#repository-url').val('@repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)');
$('#repository-clone-url').attr('href', '@repository.sshOpenRepoUrl(context.platform, settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
});
}
}); });
</script> </script>

View File

@@ -17,7 +17,7 @@
<li><a href="@url(repository)/tree/@commit.id" style="line-height: 16px;"><i class="octicon octicon-code link"></i></a></li> <li><a href="@url(repository)/tree/@commit.id" style="line-height: 16px;"><i class="octicon octicon-code link"></i></a></li>
</ul> </ul>
<div> <div>
<div class="commit-avatar-image">@avatar(commit, 40)</div> <div class="commit-avatar-image">@avatarLink(commit, 40)</div>
<div> <div>
<a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a> <a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a>
@if(commit.description.isDefined){ @if(commit.description.isDefined){

View File

@@ -55,11 +55,11 @@
<form method="POST" action="@path/@originRepository.owner/@originRepository.name/pulls/new" validate="true"> <form method="POST" action="@path/@originRepository.owner/@originRepository.name/pulls/new" validate="true">
<div class="row"> <div class="row">
<div class="col-md-10"> <div class="col-md-10">
<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-box"> <div class="panel panel-default issue-box">
<div class="panel-body"> <div class="panel-body">
<span class="error" id="error-title"></span> <span class="error" id="error-title"></span>
<input type="text" name="title" class="form-control input-lg" style="width: 680px" placeholder="Title"/> <input type="text" name="title" class="form-control input-lg" placeholder="Title"/>
@helper.html.preview( @helper.html.preview(
repository = repository, repository = repository,
content = "", content = "",
@@ -68,7 +68,7 @@
enableLineBreaks = true, enableLineBreaks = true,
enableTaskList = true, enableTaskList = true,
hasWritePermission = true, hasWritePermission = true,
style = "width: 680px; height: 200px;" style = "height: 200px;"
) )
<input type="hidden" name="targetUserName" value="@originRepository.owner"/> <input type="hidden" name="targetUserName" value="@originRepository.owner"/>
<input type="hidden" name="targetBranch" value="@originId"/> <input type="hidden" name="targetBranch" value="@originId"/>

View File

@@ -83,8 +83,8 @@
</div> </div>
@if(status.hasMergePermission){ @if(status.hasMergePermission){
<div style="padding:15px;border-top:solid 1px #e5e5e5;background:#fafafa"> <div style="padding:15px;border-top:solid 1px #e5e5e5;background:#fafafa">
<input type="button" class="btn @if(!status.hasProblem){ btn-success }" id="merge-pull-request-button" value="Merge pull request"@if(!status.canMerge){ disabled="true"}/> <input type="button" class="btn btn-lg @if(!status.hasProblem){btn-success} else {btn-default}" id="merge-pull-request-button" value="Merge pull request"@if(!status.canMerge){ disabled="true"}/>
You can also merge branches on the <a href="#" class="show-command-line">command line</a>. &nbsp;&nbsp;You can also merge branches on the <a href="#" class="show-command-line">command line</a>.
<div id="command-line" style="display: none;margin-top: 15px;"> <div id="command-line" style="display: none;margin-top: 15px;">
<hr /> <hr />
@if(status.hasConflict){ @if(status.hasConflict){
@@ -100,24 +100,23 @@
you can perform a manual merge on the command line. you can perform a manual merge on the command line.
</p> </p>
} }
@helper.html.copy("repository-url-copy", forkedRepository.httpUrl, true){ @helper.html.copy("repository-url-copy", forkedRepository.httpUrl){
<div class="btn-group" data-toggle="buttons-radio"> <div class="input-group-btn" data-toggle="buttons">
<button class="btn btn-small active" type="button" id="repository-url-http">HTTP</button> <label class="btn btn-sm btn-default active" id="repository-url-http"><input type="radio" checked>HTTP</label>
@if(settings.ssh && loginAccount.isDefined){ @if(settings.ssh && loginAccount.isDefined){
<button class="btn btn-small" type="button" id="repository-url-ssh" style="border-radius: 0px;">SSH</button> <label class="btn btn-sm btn-default" id="repository-url-ssh"><input type="radio">SSH</label>
} }
</div> </div>
<input type="text" style="width: 500px;" value="@forkedRepository.httpUrl" id="repository-url" readonly /> <input type="text" class="form-control input-sm" value="@forkedRepository.httpUrl" id="repository-url" readonly />
} }
<div> <div style="margin-top: 10px;">
<p> <p>
<span class="strong">Step 1:</span> From your project repository, check out a new branch and test the changes. <span class="strong">Step 1:</span> From your project repository, check out a new branch and test the changes.
</p> </p>
@defining(s"git checkout -b ${pullreq.requestUserName}-${pullreq.requestBranch} ${pullreq.branch}\n" + @defining(s"git checkout -b ${pullreq.requestUserName}-${pullreq.requestBranch} ${pullreq.branch}\n" +
s"git pull ${forkedRepository.httpUrl} ${pullreq.requestBranch}"){ command => s"git pull ${forkedRepository.httpUrl} ${pullreq.requestBranch}"){ command =>
@helper.html.copy("merge-command-copy-1", command){ @helper.html.copy("merge-command-copy-1", command, "position: absolute; right: 31px;")()
<pre style="width: 600px; float: left; font-size: 12px; border-radius: 3px 0px 3px 3px;" id="merge-command">@Html(command)</pre> <pre style="font-size: 12px; border-radius: 3px;" id="merge-command">@Html(command)</pre>
}
} }
</div> </div>
<div> <div>
@@ -126,9 +125,8 @@
</p> </p>
@defining(s"git checkout ${pullreq.branch}\ngit merge --no-ff ${pullreq.requestUserName}-${pullreq.requestBranch}\n" + @defining(s"git checkout ${pullreq.branch}\ngit merge --no-ff ${pullreq.requestUserName}-${pullreq.requestBranch}\n" +
s"git push origin ${pullreq.branch}"){ command => s"git push origin ${pullreq.branch}"){ command =>
@helper.html.copy("merge-command-copy-2", command){ @helper.html.copy("merge-command-copy-2", command, "position: absolute; right: 31px;")()
<pre style="width: 600px; float: left; font-size: 12px; border-radius: 3px 0px 3px 3px;">@command</pre> <pre style="font-size: 12px; border-radius: 3px;">@command</pre>
}
} }
</div> </div>
</div> </div>
@@ -141,7 +139,7 @@
Merge pull request #@issue.issueId from @{pullreq.requestUserName}/@{pullreq.requestBranch} Merge pull request #@issue.issueId from @{pullreq.requestUserName}/@{pullreq.requestBranch}
</div> </div>
<span id="error-message" class="error"></span> <span id="error-message" class="error"></span>
<textarea name="message" style="width: 635px; height: 80px;">@issue.title</textarea> <textarea name="message" style="height: 80px;" class="form-control">@issue.title</textarea>
<div> <div>
<input type="button" class="btn" value="Cancel" id="cancel-merge-pull-request"/> <input type="button" class="btn" value="Cancel" id="cancel-merge-pull-request"/>
<input type="submit" class="btn btn-success" value="Confirm merge"/> <input type="submit" class="btn btn-success" value="Confirm merge"/>
@@ -171,27 +169,25 @@ $(function(){
$('#confirm-merge-form').show(); $('#confirm-merge-form').show();
}); });
@if(settings.ssh && loginAccount.isDefined){ @forkedRepository.sshUrl.map { sshUrl =>
$('#repository-url-http').click(function(){ $('#repository-url-http').click(function(e){
// Update URL box // Update URL box
$('#repository-url').val('@forkedRepository.httpUrl'); $('#repository-url').val('@forkedRepository.httpUrl');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val()); $('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
// Update command guidance // Update command guidance
$('#merge-command').text($('#merge-command').text().replace( $('#merge-command').text($('#merge-command').text().replace(
'@forkedRepository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)', '@sshUrl', '@forkedRepository.httpUrl'
'@forkedRepository.httpUrl'
)); ));
$('#merge-command-copy-1').attr('data-clipboard-text', $('#merge-command').text()); $('#merge-command-copy-1').attr('data-clipboard-text', $('#merge-command').text());
}); });
$('#repository-url-ssh').click(function(){ $('#repository-url-ssh').click(function(e){
// Update URL box // Update URL box
$('#repository-url').val('@forkedRepository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)'); $('#repository-url').val('@sshUrl');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val()); $('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
// Update command guidance // Update command guidance
$('#merge-command').text($('#merge-command').text().replace( $('#merge-command').text($('#merge-command').text().replace(
'@forkedRepository.httpUrl', '@forkedRepository.httpUrl', '@sshUrl'
'@forkedRepository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)'
)); ));
$('#merge-command-copy-1').attr('data-clipboard-text', $('#merge-command').text()); $('#merge-command-copy-1').attr('data-clipboard-text', $('#merge-command').text());
}); });

View File

@@ -21,7 +21,9 @@
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){ @if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn" href="#" id="edit">Edit</a> <a class="btn" href="#" id="edit">Edit</a>
} }
<a class="btn btn-success" href="@url(repository)/issues/new">New issue</a> @if(loginAccount.isDefined){
<a class="btn btn-success" href="@url(repository)/compare">New pull request</a>
}
</div> </div>
<div class="edit-title pull-right" style="display: none;"> <div class="edit-title pull-right" style="display: none;">
<a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a> <a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
@@ -65,7 +67,7 @@
} }
</div> </div>
<ul class="nav nav-tabs fill-width pull-left" id="pullreq-tab"> <ul class="nav nav-tabs fill-width pull-left" id="pullreq-tab">
<li class="active"><a href="#conversation">Conversation <span class="badge">@comments.flatMap @{ <li><a href="#conversation">Conversation <span class="badge">@comments.flatMap @{
case comment: IssueComment => Some(comment) case comment: IssueComment => Some(comment)
case _: CommitComment => None case _: CommitComment => None
}.size</span></a></li> }.size</span></a></li>
@@ -73,7 +75,7 @@
<li><a href="#files">Files Changed <span class="badge">@diffs.size</span></a></li> <li><a href="#files">Files Changed <span class="badge">@diffs.size</span></a></li>
</ul> </ul>
<div class="tab-content fill-width pull-left"> <div class="tab-content fill-width pull-left">
<div class="tab-pane active" id="conversation"> <div class="tab-pane" id="conversation">
@flash.get("error").map{ error => @flash.get("error").map{ error =>
<div class="alert alert-error">@error</div> <div class="alert alert-error">@error</div>
} }
@@ -94,6 +96,22 @@
} }
<script> <script>
$(function(){ $(function(){
// Determine active tab from hash
if(location.hash == '#commits'){
$('li:has(a[href=#commits])').addClass('active');
$('div#commits').addClass('active');
} else if(location.hash == '#files'){
$('li:has(a[href=#files])').addClass('active');
$('div#files').addClass('active');
} else {
$('li:has(a[href=#conversation])').addClass('active');
$('div#conversation').addClass('active');
}
// Set hash when tab is clicked
$('ul.nav-tabs li a').click(function(e){
location.href = $(e.delegateTarget).attr("href");
});
$('#pullreq-tab a').click(function (e) { $('#pullreq-tab a').click(function (e) {
e.preventDefault(); e.preventDefault();
$(this).tab('show'); $(this).tab('show');

View File

@@ -11,7 +11,7 @@
@html.menu("code", repository){ @html.menu("code", repository){
<div class="head"> <div class="head">
<div class="pull-right hide-if-blame"><div class="btn-group"> <div class="pull-right hide-if-blame"><div class="btn-group">
<a href="@url(repository)/find/@encodeRefName(branch)" class="btn btn-sm btn-default" data-toggle="tooltip" data-placement="bottom" data-hotkey="t" title="Quickly jump between files"><i class="octicon octicon-list-unordered"></i></a> <a href="@url(repository)/find/@encodeRefName(branch)" class="btn btn-sm btn-default" data-hotkey="t">Find file</a>
</div></div> </div></div>
<div class="line-age-legend"> <div class="line-age-legend">
<span>Newer</span> <span>Newer</span>

View File

@@ -11,7 +11,7 @@
@if(!fileName.isDefined){<hr/><br/>} @if(!fileName.isDefined){<hr/><br/>}
<form method="POST" validate="true" style="max-width: 874px;"> <form method="POST" validate="true" style="max-width: 874px;">
@if(!fileName.isDefined){ @if(!fileName.isDefined){
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div> <div class="issue-avatar-image">@avatarLink(loginAccount.get.userName, 48)</div>
} }
<div class="panel panel-default issue-comment-box"> <div class="panel panel-default issue-comment-box">
<div class="panel-body"> <div class="panel-body">
@@ -23,7 +23,7 @@
enableLineBreaks = true, enableLineBreaks = true,
enableTaskList = true, enableTaskList = true,
hasWritePermission = hasWritePermission, hasWritePermission = hasWritePermission,
style = "width: 635px; height: 100px; max-height: 150px;", style = "height: 100px; max-height: 150px;",
elastic = true elastic = true
) )
</div> </div>

View File

@@ -46,7 +46,7 @@
<li><a href="@url(repository)/tree/@commit.id" style="line-height: 16px;"><i class="octicon octicon-code link"></i></a></li> <li><a href="@url(repository)/tree/@commit.id" style="line-height: 16px;"><i class="octicon octicon-code link"></i></a></li>
</ul> </ul>
<div> <div>
<div class="commit-avatar-image">@avatar(commit, 40)</div> <div class="commit-avatar-image">@avatarLink(commit, 40)</div>
<div> <div>
<a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a> <a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a>
@if(commit.description.isDefined){ @if(commit.description.isDefined){

View File

@@ -33,7 +33,7 @@
</td> </td>
</tr> </tr>
</table> </table>
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div> <div class="issue-avatar-image">@avatarLink(loginAccount.get.userName, 48)</div>
<div class="box issue-comment-box"> <div class="box issue-comment-box">
<div class="box-content"> <div class="box-content">
<div> <div>

View File

@@ -2,11 +2,11 @@
@import context._ @import context._
<span class="error-edit-content-@commentId error"></span> <span class="error-edit-content-@commentId error"></span>
@helper.html.attached(owner, repository){ @helper.html.attached(owner, repository){
<textarea style="width: 635px; height: 100px;" id="edit-content-@commentId">@content</textarea> <textarea style="height: 100px;" id="edit-content-@commentId" class="form-control">@content</textarea>
} }
<div> <div>
<input type="button" class="cancel-comment-@commentId btn btn-small btn-danger" value="Cancel"/> <input type="button" class="cancel-comment-@commentId btn btn-small btn-danger" value="Cancel"/>
<input type="button" class="update-comment-@commentId btn btn-small pull-right" value="Update comment"/> <input type="button" class="update-comment-@commentId btn btn-small btn-default pull-right" value="Update comment"/>
</div> </div>
<script> <script>
$(function(){ $(function(){
@@ -19,7 +19,7 @@ $(function(){
} }
$(document).on('click', '.update-comment-@commentId', function(){ $(document).on('click', '.update-comment-@commentId', function(){
$box = $(this).closest('.box'); $box = $(this).closest('.commit-comment-box');
$('.update-comment-@commentId, .cancel-comment-@commentId', $box).attr('disabled', 'disabled'); $('.update-comment-@commentId, .cancel-comment-@commentId', $box).attr('disabled', 'disabled');
$.ajax({ $.ajax({
url: '@path/@owner/@repository/commit_comments/edit/@commentId', url: '@path/@owner/@repository/commit_comments/edit/@commentId',

View File

@@ -47,7 +47,7 @@
</td> </td>
</tr> </tr>
</table> </table>
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div> <div class="issue-avatar-image">@avatarLink(loginAccount.get.userName, 48)</div>
<div class="panel panel-default issue-comment-box"> <div class="panel panel-default issue-comment-box">
<div class="panel-body"> <div class="panel-body">
<div> <div>

View File

@@ -11,6 +11,7 @@
error: Option[Any] = None)(implicit context: gitbucket.core.controller.Context) error: Option[Any] = None)(implicit context: gitbucket.core.controller.Context)
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@import gitbucket.core.service.RepositoryService._
@html.main( @html.main(
if(pathList.isEmpty){ if(pathList.isEmpty){
if(branch == repository.repository.defaultBranch){ if(branch == repository.repository.defaultBranch){
@@ -23,32 +24,68 @@
}, Some(repository)) { }, Some(repository)) {
@html.menu("code", repository, Some(branch), pathList.isEmpty, groupNames.isEmpty, info, error){ @html.menu("code", repository, Some(branch), pathList.isEmpty, groupNames.isEmpty, info, error){
<div class="head"> <div class="head">
<div class="pull-right"><div class="btn-group"> @if(pathList.isEmpty){
<a href="@url(repository)/find/@encodeRefName(branch)" class="btn btn-sm btn-default" data-toggle="tooltip" data-placement="bottom" data-hotkey="t" title="Quickly jump between files"><i class="octicon octicon-list-unordered"></i></a> <div class="pull-right pc">
@if(pathList.nonEmpty){ @if(platform != "linux" && platform != null){
<a href="@url(repository)/commits/@encodeRefName(branch)/@pathList.mkString("/")" class="btn btn-sm btn-default" data-toggle="tooltip" data-placement="bottom" title="Browse commits for this branch"><i class="octicon octicon-clock"></i></a> <a href="@openRepoUrl(repository.httpUrl)" id="repository-clone-url" class="btn btn-sm btn-default"><i class="octicon octicon-desktop-download"></i></a>
} }
</div></div> <a href="@{url(repository)}/archive/@{encodeRefName(branch)}.zip" class="btn btn-sm btn-default">Download ZIP</a>
@branchPullRequest.map{ case (pullRequest, issue) => </div>
<a href="@url(repository)/pull/@pullRequest.issueId" class="btn btn-sm btn-pullrequest-branch" title="@issue.title" data-toggle="tooltip">#@pullRequest.issueId</a> <div class="pull-right pc">
}.getOrElse{ <div style="width: 240px; margin-top: 2px; margin-right: 5px; margin-left: 5px;">
<a href="@url(repository)/compare?head=@urlEncode(encodeRefName(branch))" class="btn btn-sm btn-success"><i class="octicon octicon-git-compare" data-toggle="tooltip" title="Compare, review, create a pull request"></i></a> @helper.html.copy("repository-url-copy", repository.httpUrl){
@if(repository.sshUrl.isDefined){
<div class="btn-group input-group-btn">
<button type="button" class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span id="repository-url-proto">HTTP</span> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li>
<a href="javascript:void(0);" id="repository-url-http">
<strong>HTTP (recommended)</strong><br>
Clone with Git using the repository's web address.
</a>
</li>
<li>
<a href="javascript:void(0);" id="repository-url-ssh">
<strong>SSH</strong><br>
Clone with an SSH key and passphrase from your GitBucket settings.
</a>
</li>
</ul>
</div>
}
<input type="text" value="@repository.httpUrl" id="repository-url" class="form-control input-sm" readonly>
}
</div>
</div>
} }
@helper.html.branchcontrol( <div class="pull-right">
branch, <div class="btn-group">
repository, <a href="@url(repository)/new/@encodeRefName(branch)/@pathList.mkString("/")" class="btn btn-sm btn-default pc" title="Create a new file here" @if(!hasWritePermission){disabled}>New file</i></a>
hasWritePermission <a href="@url(repository)/find/@encodeRefName(branch)" class="btn btn-sm btn-default pc" data-hotkey="t">Find file</a>
){ @if(pathList.nonEmpty){
<a href="@url(repository)/commits/@encodeRefName(branch)/@pathList.mkString("/")" class="btn btn-sm btn-default" data-toggle="tooltip" data-placement="bottom" title="Browse commits for this branch">History</a>
}
</div>
</div>
@helper.html.branchcontrol(branch, repository, hasWritePermission){
@repository.branchList.map { x => @repository.branchList.map { x =>
<li><a href="@url(repository)/tree/@encodeRefName(x)">@helper.html.checkicon(x == branch) @x</a></li> <li><a href="@url(repository)/tree/@encodeRefName(x)">@helper.html.checkicon(x == branch) @x</a></li>
} }
} }
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> / @if(pathList.isEmpty){
@pathList.zipWithIndex.map { case (section, i) => @branchPullRequest.map{ case (pullRequest, issue) =>
<a href="@url(repository)/tree/@encodeRefName(branch)/@pathList.take(i + 1).mkString("/")">@section</a> / <a href="@url(repository)/pull/@pullRequest.issueId" class="btn btn-sm btn-pullrequest-branch" title="@issue.title" data-toggle="tooltip">View #@pullRequest.issueId</a>
} }.getOrElse {
@if(hasWritePermission){ <a href="@url(repository)/compare?head=@urlEncode(encodeRefName(branch))" class="btn btn-sm btn-success" @if(loginAccount.isEmpty){disabled}>New pull request</a>
<a href="@url(repository)/new/@encodeRefName(branch)/@pathList.mkString("/")" title="Create a new file here" style="text-decoration: none;">+</i></a> }
} else {
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> /
@pathList.zipWithIndex.map { case (section, i) =>
<a href="@url(repository)/tree/@encodeRefName(branch)/@pathList.take(i + 1).mkString("/")">@section</a> /
}
} }
</div> </div>
<table class="table table-file-list"> <table class="table table-file-list">
@@ -141,3 +178,20 @@
} }
} }
} }
<script>
@repository.sshUrl.map { sshUrl =>
$('#repository-url-http').click(function(){
$('#repository-url-proto').text('HTTP');
$('#repository-url').val('@repository.httpUrl');
$('#repository-clone-url').attr('href', '@openRepoUrl(repository.httpUrl)')
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
});
$('#repository-url-ssh').click(function(){
$('#repository-url-proto').text('SSH');
$('#repository-url').val('@sshUrl');
$('#repository-clone-url').attr('href', '@openRepoUrl(sshUrl)');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
});
}
</script>

View File

@@ -13,7 +13,7 @@
@if(originRepository.isDefined){ @if(originRepository.isDefined){
@avatar(originRepository.get.owner, 20) @avatar(originRepository.get.owner, 20)
<span@if(repository.owner == originRepository.get.owner){ class="highlight"}> <span@if(repository.owner == originRepository.get.owner){ class="highlight"}>
<a href="@url(originRepository.get)">@originRepository.get.owner</a> / <a href="@path/@originRepository.get.owner/@originRepository.get.name">@originRepository.get.name</a> <a href="@url(originRepository.get.owner)">@originRepository.get.owner</a> / <a href="@path/@originRepository.get.owner/@originRepository.get.name">@originRepository.get.name</a>
</span> </span>
} else { } else {
@avatar(repository.repository.originUserName.get, 20) @avatar(repository.repository.originUserName.get, 20)

View File

@@ -11,9 +11,9 @@
<h3><strong>Quick setup</strong> — if you've done this kind of thing before</h3> <h3><strong>Quick setup</strong> — if you've done this kind of thing before</h3>
<div class="empty-repo-options"> <div class="empty-repo-options">
via <a href="@repository.httpUrl" class="git-protocol-selector">HTTP</a> via <a href="@repository.httpUrl" class="git-protocol-selector">HTTP</a>
@if(settings.ssh && loginAccount.isDefined){ @repository.sshUrl.map { sshUrl =>
or or
<a href="@repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)" class="git-protocol-selector">SSH</a> <a href="@sshUrl" class="git-protocol-selector">SSH</a>
} }
</div> </div>
<h3 style="margin-top: 30px;">Create a new repository on the command line</h3> <h3 style="margin-top: 30px;">Create a new repository on the command line</h3>

View File

@@ -24,7 +24,8 @@
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a>
@helper.html.datetimeago(issue.registeredDate) @helper.html.datetimeago(issue.registeredDate)
@if(issue.commentCount > 0){ @if(issue.commentCount > 0){
&nbsp;&nbsp;<i class="octicon octicon-comment"></i><span class="strong">@issue.commentCount</span> @plural(issue.commentCount, "comment") &nbsp;&nbsp;<i class="octicon octicon-comment"></i>
<span class="strong">@issue.commentCount</span> @plural(issue.commentCount, "comment")
} }
</div> </div>
</div> </div>

View File

@@ -30,6 +30,11 @@
} }
<button class="btn btn-default" id="test">Test Hook</button> <button class="btn btn-default" id="test">Test Hook</button>
</fieldset> </fieldset>
<fieldset class="form-group">
<label class="strong">Security Token</label>
<div></div>
<input type="text" name="token" id="token" placeholder="leave blank for no X-Hub-Signature usage" value="@webHook.token" class="form-control" style="display: inline; width: 500px; vertical-align: middle;" />
</fieldset>
<hr /> <hr />
<label class="strong">Which events would you like to trigger this webhook?</label> <label class="strong">Which events would you like to trigger this webhook?</label>
<div> <div>
@@ -123,6 +128,7 @@ $(function(){
e.stopImmediatePropagation(); e.stopImmediatePropagation();
e.preventDefault(); e.preventDefault();
var url = this.form.url.value; var url = this.form.url.value;
var token = this.form.token.value;
if(!/^https?:\/\/.+/.test(url)){ if(!/^https?:\/\/.+/.test(url)){
alert("invalid url"); alert("invalid url");
return; return;
@@ -132,7 +138,7 @@ $(function(){
$("#test-report").hide(); $("#test-report").hide();
$.ajax({ $.ajax({
method:'POST', method:'POST',
url:'@url(repository)/settings/hooks/test?url=' + encodeURIComponent(url), url:'@url(repository)/settings/hooks/test?url=' + encodeURIComponent(url) + '&token=' + encodeURIComponent(token),
success: function(e){ success: function(e){
//console.log(e); //console.log(e);
$('#test-report-tab a:first').tab('show'); $('#test-report-tab a:first').tab('show');

View File

@@ -21,7 +21,7 @@
</ul> </ul>
<form action="@url(repository)/wiki/@if(page.isEmpty){_new} else {_edit}" method="POST" validate="true"> <form action="@url(repository)/wiki/@if(page.isEmpty){_new} else {_edit}" method="POST" validate="true">
<span id="error-pageName" class="error"></span> <span id="error-pageName" class="error"></span>
<input type="text" name="pageName" value="@pageName" class="form-control input-lg" style="width: 900px; font-weight: bold;" placeholder="Input a page name."/> <input type="text" name="pageName" value="@pageName" class="form-control input-lg" style="font-weight: bold;" placeholder="Input a page name."/>
@helper.html.preview( @helper.html.preview(
repository = repository, repository = repository,
content = page.map(_.content).getOrElse(""), content = page.map(_.content).getOrElse(""),
@@ -30,13 +30,13 @@
enableLineBreaks = false, enableLineBreaks = false,
enableTaskList = false, enableTaskList = false,
hasWritePermission = false, hasWritePermission = false,
style = "width: 900px; height: 400px;", style = "height: 400px;",
styleClass = "monospace", styleClass = "monospace",
placeholder = "" placeholder = ""
) )
<div class="form-group"> <div class="form-group">
<label for="message">Edit Message</label> <label for="message">Edit Message</label>
<input type="text" id="message" name="message" value="" class="form-control" style="width: 900px;" placeholder="Write a small message here explaining this change. (Optional)"/> <input type="text" id="message" name="message" value="" class="form-control" placeholder="Write a small message here explaining this change. (Optional)"/>
</div> </div>
<div class="form-group pull-right"> <div class="form-group pull-right">
<input type="hidden" name="currentPageName" value="@pageName"/> <input type="hidden" name="currentPageName" value="@pageName"/>

View File

@@ -7,7 +7,7 @@
footer: Option[gitbucket.core.service.WikiService.WikiPageInfo])(implicit context: gitbucket.core.controller.Context) footer: Option[gitbucket.core.service.WikiService.WikiPageInfo])(implicit context: gitbucket.core.controller.Context)
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@import gitbucket.core.service.WikiService._ @import gitbucket.core.service.WikiService.{wikiHttpUrl, wikiSshUrl}
@html.main(s"${pageName} - ${repository.owner}/${repository.name}", Some(repository)){ @html.main(s"${pageName} - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("wiki", repository){ @html.menu("wiki", repository){
<ul class="nav nav-tabs fill-width"> <ul class="nav nav-tabs fill-width">
@@ -26,7 +26,7 @@
} }
</li> </li>
</ul> </ul>
<div style="width: 200px; margin-top: 20px;" class="pull-right"> <div style="width: 200px; margin-top: 20px;" class="pull-right pc">
@defining(15){ max => @defining(15){ max =>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading strong"> <div class="panel-heading strong">
@@ -67,10 +67,10 @@
<div class="small"> <div class="small">
<strong>Clone this wiki locally</strong> <strong>Clone this wiki locally</strong>
</div> </div>
@helper.html.copy("repository-url-copy", httpUrl(repository)){ @helper.html.copy("repository-url-copy", wikiHttpUrl(repository)){
<input type="text" value="@httpUrl(repository)" id="repository-url" class="form-control input-sm" readonly> <input type="text" value="@wikiHttpUrl(repository)" id="repository-url" class="form-control input-sm" readonly>
} }
@if(settings.ssh && loginAccount.isDefined){ @if(wikiSshUrl(repository).isDefined) {
<div class="small"> <div class="small">
<span class="mute">You can clone <a href="javascript:void(0);" id="repository-url-http">HTTP</a> or <a href="javascript:void(0);" id="repository-url-ssh">SSH</a>.</span> <span class="mute">You can clone <a href="javascript:void(0);" id="repository-url-http">HTTP</a> or <a href="javascript:void(0);" id="repository-url-ssh">SSH</a>.</span>
</div> </div>
@@ -131,13 +131,13 @@ $(function(){
$('#triangle-right').show(); $('#triangle-right').show();
} }
@if(settings.ssh && loginAccount.isDefined){ @wikiSshUrl(repository).map { sshUrl =>
$('#repository-url-http').click(function(){ $('#repository-url-http').click(function(){
$('#repository-url').val('@httpUrl(repository)'); $('#repository-url').val('@wikiHttpUrl(repository)');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val()); $('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
}); });
$('#repository-url-ssh').click(function(){ $('#repository-url-ssh').click(function(){
$('#repository-url').val('@sshUrl(repository, settings, loginAccount.get.userName)'); $('#repository-url').val('@sshUrl');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val()); $('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
}); });
} }

View File

@@ -58,7 +58,7 @@ h6 {
margin-right: 5px; margin-right: 5px;
} }
.head .octicon,.head .mega-octicon{ .head .octicon, .head .mega-octicon{
color : #BBB; color : #BBB;
} }
@@ -70,57 +70,6 @@ blockquote p {
font-size: 15px; font-size: 15px;
} }
/*
.nav {
margin-bottom: 12px;
}
.table-bordered {
border-collapse: inherit;
border: none;
}
.table-bordered > thead > tr > th,
.table-bordered > tbody > tr > th,
.table-bordered > tbody > tr > td {
border-bottom: none;
}
.table-bordered > thead > tr:first-child > th:nth-of-type(1),
.table-bordered > tbody > tr:first-child > th:nth-of-type(1),
.table-bordered > tbody > tr:first-child > td:nth-of-type(1) {
border-top-left-radius: 4px;
}
.table-bordered > thead > tr:first-child > th:nth-last-of-type(1),
.table-bordered > tbody > tr:first-child > th:nth-last-of-type(1),
.table-bordered > tbody > tr:first-child > td:nth-last-of-type(1) {
border-top-right-radius: 4px;
}
.table-bordered > tbody > tr:last-child > td:nth-of-type(1) {
border-bottom-left-radius: 4px;
}
.table-bordered > tbody > tr:last-child > td:nth-last-of-type(1) {
border-bottom-right-radius: 4px;
}
.table-bordered > tbody > tr:last-child > td {
border-bottom: 1px solid #dddddd;
}
*/
/*
.table-bordered > thead > tr > th,
.table-bordered > thead > tr > td {
border-bottom-width: 1px;
}
*/
/*
.tab-content {
margin-top: 20px;
}
*/
.danger { .danger {
color: #900; color: #900;
} }
@@ -167,43 +116,6 @@ pre.reset {
/* ======================================================================== */ /* ======================================================================== */
/* Global Header */ /* Global Header */
/* ======================================================================== */ /* ======================================================================== */
/*
div.navbar-inner {
border-radius: 0px;
-webkit-border-radius: 0px;
-moz-border-radius: 0px;
border-top: none;
border-left: none;
border-right: none;
border-bottom: 1px solid #d4d4d4;
padding-right: 0px;
}
div.header-menu {
line-height: 40px;
}
div.header-menu .octicon{
color: #333;
}
div.header-menu input,
div.header-menu a.btn {
margin-top: 0px;
margin-bottom: 0px;
}
/*
div.nav-collapse a.menu {
margin-right: 12px;
}
div.nav-collapse a.btn-last,
div.nav-collapse a.menu-last {
margin-right: 30px;
}
*/
.navbar-brand { .navbar-brand {
height: unset; height: unset;
padding: 8px; padding: 8px;
@@ -211,6 +123,7 @@ div.nav-collapse a.menu-last {
.navbar { .navbar {
min-height: unset; min-height: unset;
margin-bottom: 0px;
} }
span.header-version { span.header-version {
@@ -277,6 +190,7 @@ div.pagination {
*/ */
div.body { div.body {
margin-top: 20px;
margin-bottom: 40px; margin-bottom: 40px;
} }
@@ -412,27 +326,13 @@ div.box-content {
padding: 4px; padding: 4px;
border-radius: 3px; border-radius: 3px;
} }
/*
div > div.box-content-row:nth-of-type(1) {
border: none;
}
div.box-content-row {
border-top: 1px solid #d8d8d8;
padding: 4px;
}
*/
/*
div.repo-link {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
*/
li.repo-link, li.page-link { li.repo-link, li.page-link {
padding-top: 4px; padding-top: 4px;
padding-bottom: 4px; padding-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
div.box-content-bottom { div.box-content-bottom {
@@ -444,6 +344,10 @@ div.box-content-bottom {
margin-bottom: 20px; margin-bottom: 20px;
} }
div.box-content-bottom img {
max-width: 100%;
}
table.table th.box-header { table.table th.box-header {
background-color: #f5f5f5; background-color: #f5f5f5;
} }
@@ -533,61 +437,56 @@ a.btn-danger:hover .octicon {
} }
/****************************************************************************/ /****************************************************************************/
/* Side Menu */ /* Head Menu */
/****************************************************************************/ /****************************************************************************/
ul.sidemenu { div.headbar {
background-color: #fafafa;
padding-top: 19px;
border-bottom: 1px solid #eee;
margin-bottom: 20px;
}
ul.headmenu {
margin-top: 20px;
margin-left: 0px; margin-left: 0px;
padding-left: 0px; padding-left: 0px;
margin-bottom: -1px;
} }
ul.sidemenu a { ul.headmenu a:hover {
display: block;
padding: 8px 10px;
}
ul.sidemenu a:hover {
text-decoration: none; text-decoration: none;
color: black;
} }
ul.sidemenu li.active { ul.headmenu li {
border-top: 1px solid #eee; display: inline-block;
border-bottom: 1px solid #eee; list-style-type: none;
border-right: 3px solid #bb4444; font-size: 13px;
border-left: 1px solid white;
} }
ul.sidemenu li.active a { ul.headmenu li a {
color: #666;
padding: 8px 10px;
display: block;
}
ul.headmenu li.active a {
color: black;
}
ul.headmenu li.active {
border-top: 3px solid #bb4444;
border-left: 1px solid #eee;
border-right: 1px solid #eee;
border-bottom: none;
background-color: white;
}
ul.headmenu li.active a {
background-color: #fff; background-color: #fff;
} }
ul.sidemenu {
background-image: -webkit-linear-gradient(left, #f6f6f6 0%, #fff 8px);
background-image: linear-gradient(to right, #f6f6f6 0%, #fff 8px);
box-shadow: inset 1px 0 0 #eee;
}
ul.sidemenu div.margin {
width: 5px;
height: 30px;
margin-right: 4px;
border-left: 1px solid white;
}
ul.sidemenu li {
border-left: 1px solid #eee;
margin-left:0px;
border-right: 2px solid white;
list-style-type: none;
font-size: 90%;
}
ul.sidemenu span.badge {
}
ul.sidemenu a:hover i.menu-icon, ul.sidemenu i.menu-icon-active {
color: #333;
}
/****************************************************************************/ /****************************************************************************/
/* Create Repository */ /* Create Repository */
/****************************************************************************/ /****************************************************************************/
@@ -636,18 +535,6 @@ a#show-pages-index {
text-decoration: none; text-decoration: none;
} }
/*
ul.nav-stacked.side-menu li span.header {
border-top-right-radius: 3px;
border-top-left-radius: 3px;
border: 1px solid #d8d8d8;
display: block;
padding: 8px 15px 9px;
margin-right: 2px;
background-color: #f5f5f5;
}
*/
ul.nav-stacked.side-menu li a:hover { ul.nav-stacked.side-menu li a:hover {
background-color: transparent; background-color: transparent;
} }
@@ -750,7 +637,6 @@ div.repository-content {
padding: 0 3px; padding: 0 3px;
} }
/****************************************************************************/ /****************************************************************************/
/* Activity */ /* Activity */
/****************************************************************************/ /****************************************************************************/
@@ -978,73 +864,6 @@ span.simplified-path {
color: #0088cc; color: #0088cc;
} }
/****************************************************************************/
/* nav pulls group */
/****************************************************************************/
/*
.nav-pills-group:after {
display: table;
line-height: 0;
content: "";
}
.nav-pills-group:after {
clear: both;
}
.nav-pills-group > li {
float: left;
}
*/
/*
.nav-pills > li + li {
margin-left: 0px;
}
.nav-pills > li > a {
padding-right: 12px;
padding-left: 12px;
line-height: 14px;
color: #666;
font-weight: bold;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
}
.nav-pills > li > a {
padding-top: 10px;
padding-bottom: 10px;
border-left : 1px solid #e5e5e5;
border-top : 1px solid #e5e5e5;
border-bottom : 1px solid #e5e5e5;
}
.nav-pills > li:nth-of-type(1) > a {
-webkit-border-radius: 4px 0 0 4px;
-moz-border-radius: 4px 0 0 4px;
border-radius: 4px 0 0 4px;
}
.nav-pills > li:nth-last-of-type(1) > a {
-webkit-border-radius: 0 4px 4px 0;
-moz-border-radius: 0 4px 4px 0;
border-radius: 0 4px 4px 0;
border-right : 1px solid #e5e5e5;
}
*/
/*
.nav-pills-group > .active > a,
.nav-pills-group > .active > a:hover,
.nav-pills-group > .active > a:focus {
color: #ffffff;
background-color: #0088cc;
border-color: #0088cc;
}
*/
/****************************************************************************/ /****************************************************************************/
/* Issues */ /* Issues */
/****************************************************************************/ /****************************************************************************/
@@ -1081,11 +900,11 @@ table.table-issues {
margin-top: 12px; margin-top: 12px;
} }
table.table-issues td .octicon-issue-opened,table.table-issues td .octicon-git-pull-request .open { table.table-issues td .octicon-issue-opened, table.table-issues td .octicon-git-pull-request .open {
color: #6CC644; color: #6CC644;
} }
table.table-issues td .octicon-issue-closed,table.table-issues td .octicon-git-pull-request .closed{ table.table-issues td .octicon-issue-closed, table.table-issues td .octicon-git-pull-request .closed{
color : #BD2C00;; color : #BD2C00;;
} }
@@ -1099,6 +918,10 @@ a.issue-title {
background-color: #6cc644; background-color: #6cc644;
} }
.label-important {
background-color: #bd2c00;
}
ul.label-list { ul.label-list {
list-style-type: none; list-style-type: none;
padding-left: 0px; padding-left: 0px;
@@ -1153,47 +976,6 @@ div.milestone-menu a.delete {
color: #b00; color: #b00;
} }
/*
div#milestone-progress-area {
display: inline-block;
}
div#milestone-progress-area div.milestone-progress {
width: 130px;
margin-bottom: -6px;
}
div.milestone-progress {
position: relative;
height: 10px;
color: white;
margin-bottom: 4px;
font-weight: bold;
font-size: 12px;
text-shadow: 0px 0px 5px #444;
background-color: silver;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
}
span.milestone-progress {
position: absolute;
height: 100%;
background-color: green;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
}
div.issue-header {
padding-left: 8px;
padding-right: 8px;
padding-top: 12px;
padding-bottom: 12px;
}
*/
div.issue-info { div.issue-info {
border-top: 1px solid #e5e5e5; border-top: 1px solid #e5e5e5;
border-bottom: 1px solid #e5e5e5; border-bottom: 1px solid #e5e5e5;
@@ -1203,21 +985,6 @@ div.issue-info {
margin-right: 0px; margin-right: 0px;
} }
/*
div.issue-content {
padding: 13px;
background-color: #fff;
}
div.issue-content p:first-child {
margin-top: 0;
}
div.issue-content p:last-child {
margin-bottom: 0;
}
*/
h4#issueTitle { h4#issueTitle {
font-size: large; font-size: large;
font-weight: bold; font-weight: bold;
@@ -1258,7 +1025,6 @@ div.commit-comment-box > div.panel-body {
div.issue-comment-box textarea { div.issue-comment-box textarea {
width: 650px;
height: 100px; height: 100px;
max-height: 300px; max-height: 300px;
} }
@@ -1425,18 +1191,6 @@ a.absent {
color: #c00; color: #c00;
} }
/*
div.wiki-index-header {
background-color: #f5f5f5;
color: #333333;
margin: 0;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
border: 1px solid #d8d8d8;
padding: 8px 8px 8px 8px;
}
*/
div.wiki-sidebar { div.wiki-sidebar {
background-color: white; background-color: white;
border: 1px solid #d8d8d8; border: 1px solid #d8d8d8;
@@ -1466,18 +1220,6 @@ div.wiki-footer {
color: gray; color: gray;
} }
/*
div.wiki-index-content {
background-color: white;
border: 1px solid #d8d8d8;
padding: 0px;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
margin-bottom: 20px;
border-top: none;
}
*/
/****************************************************************************/ /****************************************************************************/
/* Commit */ /* Commit */
/****************************************************************************/ /****************************************************************************/
@@ -2216,94 +1958,96 @@ div.container.blame-container{
/* Mobile */ /* Mobile */
/****************************************************************************/ /****************************************************************************/
@media (max-width: 767px) { @media (max-width: 767px) {
body>form#search { /* Hide header search box */
margin: 0 -20px 20px -20px; input[name=query] {
} display: none;
}
#dashboard-signin-form {
display: none;
}
.container {
width: auto !important;
}
.body>div.pull-left {
width: auto !important;
}
.pc {
display: none;
}
body>div.dashboard-nav { /* Adjust issue / comment form */
margin: 0 -20px 20px -20px; #issue-title {
padding: 0 10px; width: 100% !important;
} }
div.attachable>textarea,
div.attachable>div.clickable {
width: 100% !important;
}
.container { /* Adjust issue search box size and position */
width: auto !important; #search-filter-box {
} width: 100% !important;
}
/* Adjust issue search box size and position */ form#search-filter-form {
#search-filter-box { float: none !important;
width: 90% !important; margin-bottom: 10px;
position: absolute; }
left: 14px; form#search-filter-form>div.form-group {
right: 20px; width: 100% !important;
margin-top: 42px; margin-bottom: 10px;
} }
.table-issues a.button-link {
form#search-filter-form { width: 42px;
float: none !important; height: 16px;
margin-bottom: 80px !important; overflow: hidden;
} display: inline-block;
}
.table-issues a.button-link { /*
width: 42px; .nav-tabs a.btn[href$="/_edit"] {
height: 16px; width: 24px;
overflow: hidden; white-space: nowrap;
display: inline-block; overflow: hidden;
} padding: 4px 6px;
margin: 3px 4px 0 0;
.nav-tabs a.btn[href$="/_edit"] { }
width: 24px; */
white-space: nowrap; body>div.container.body {
overflow: hidden; margin: 0 -12px 40px -12px;
padding: 4px 6px; }
margin: 3px 4px 0 0; /* Adjust sidemenu */
} .container.body>div[style="width: 170px;"]{
width: 32px !important;
body>div.container.body { margin-right: -5px;
margin: 0 -12px 40px -12px; overflow: hidden;
} white-space: nowrap;
}
.container.body>div[style="width: 170px;"]{ /* Hide badge of sidemenu */
width: 32px !important; .container.body>div[style="width: 170px;"] span.badge {
margin-right: -5px; display: none;
overflow: hidden; }
white-space: nowrap; /* Hide download button */
} .container.body>div[style="width: 170px;"] a.btn-sm {
display: none;
.container.body>div[style="margin-right: 180px;"]{ }
margin-right: 32px !important; /* Hide repository url box */
} .container.body>div[style="width: 170px;"] .small,
.container.body>div[style="width: 170px;"] .input-group {
.container.body>div[style="width: 170px;"] .sidemenu i, .container.body>div[style="width: 170px;"] .sidemenu img { display: none;
padding-right: 5px; }
} /* Hide fork button */
div.input-group>span.fork {
/* Hide repository url box */ display: none;
.container.body>div[style="width: 170px;"] .small,.container.body>div[style="width: 170px;"] .input-group { }
display: none;
}
.container.body>div[style="width: 170px;"] div[style="margin-top: 10px;"] a.btn{
width: 26px !important;
padding: 2px;
}
.container.body>div[style="width: 170px;"] div[style="margin-top: 10px;"] a.btn i {
margin: 5px 10px 5px 6px;
}
/* Hide fork button */
div.col-md-1>div.input-group.pull-right {
display: none;
}
body>.container>#fork-form{
display: inline;
}
} }
/****************************************************************************/ /****************************************************************************/
/* Print */ /* Print */
/****************************************************************************/ /****************************************************************************/
a[href]:after { @media print {
display: none; div.headbar {
display: none;
}
a[href]:after {
display: none;
}
} }

View File

@@ -2,16 +2,14 @@ package gitbucket.core.api
import gitbucket.core.util.RepositoryName import gitbucket.core.util.RepositoryName
import org.specs2.mutable.Specification import org.json4s.jackson.JsonMethods.parse
import org.json4s.jackson.JsonMethods.{pretty, parse}
import org.json4s._ import org.json4s._
import org.specs2.matcher._ import org.scalatest.FunSuite
import java.util.{Calendar, TimeZone, Date} import java.util.{Calendar, TimeZone, Date}
class JsonFormatSpec extends FunSuite {
class JsonFormatSpec extends Specification {
val date1 = { val date1 = {
val d = Calendar.getInstance(TimeZone.getTimeZone("UTC")) val d = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
d.set(2011,3,14,16,0,49) d.set(2011,3,14,16,0,49)
@@ -374,67 +372,58 @@ class JsonFormatSpec extends Specification {
} }
}""" }"""
def beFormatted(json2Arg:String) = new Matcher[String] { def assertJson(resultJson: String, expectJson: String) = {
def apply[S <: String](e: Expectable[S]) = { import java.util.regex.Pattern
import java.util.regex.Pattern val json2 = Pattern.compile("""^\s*//.*$""", Pattern.MULTILINE).matcher(expectJson).replaceAll("")
val json2 = Pattern.compile("""^\s*//.*$""", Pattern.MULTILINE).matcher(json2Arg).replaceAll("") val js2 = try {
val js2 = try{ parse(json2)
parse(json2) } catch {
}catch{ case e: com.fasterxml.jackson.core.JsonParseException => {
case e:com.fasterxml.jackson.core.JsonParseException => { val p = java.lang.Math.max(e.getLocation.getCharOffset() - 10, 0).toInt
val p = java.lang.Math.max(e.getLocation.getCharOffset()-10,0).toInt val message = json2.substring(p, java.lang.Math.min(p + 100, json2.length))
val message = json2.substring(p,java.lang.Math.min(p+100,json2.length)) throw new com.fasterxml.jackson.core.JsonParseException(message + e.getMessage, e.getLocation)
throw new com.fasterxml.jackson.core.JsonParseException(message + e.getMessage , e.getLocation)
}
} }
val js1 = parse(e.value)
result(js1 == js2,
"expected",
{
val diff = js2 diff js1
s"${pretty(js1)} is not ${pretty(js2)} \n\n ${pretty(Extraction.decompose(diff)(org.json4s.DefaultFormats))}"
},
e)
} }
val js1 = parse(resultJson)
assert(js1 === js2)
} }
"JsonFormat" should {
"apiUser" in { test("apiUser") {
JsonFormat(apiUser) must beFormatted(apiUserJson) assertJson(JsonFormat(apiUser), apiUserJson)
} }
"repository" in { test("repository") {
JsonFormat(repository) must beFormatted(repositoryJson) assertJson(JsonFormat(repository), repositoryJson)
} }
"apiPushCommit" in { test("apiPushCommit") {
JsonFormat(apiPushCommit) must beFormatted(apiPushCommitJson) assertJson(JsonFormat(apiPushCommit), apiPushCommitJson)
} }
"apiComment" in { test("apiComment") {
JsonFormat(apiComment) must beFormatted(apiCommentJson) assertJson(JsonFormat(apiComment), apiCommentJson)
JsonFormat(apiCommentPR) must beFormatted(apiCommentPRJson) assertJson(JsonFormat(apiCommentPR), apiCommentPRJson)
} }
"apiCommitListItem" in { test("apiCommitListItem") {
JsonFormat(apiCommitListItem) must beFormatted(apiCommitListItemJson) assertJson(JsonFormat(apiCommitListItem), apiCommitListItemJson)
} }
"apiCommitStatus" in { test("apiCommitStatus") {
JsonFormat(apiCommitStatus) must beFormatted(apiCommitStatusJson) assertJson(JsonFormat(apiCommitStatus), apiCommitStatusJson)
} }
"apiCombinedCommitStatus" in { test("apiCombinedCommitStatus") {
JsonFormat(apiCombinedCommitStatus) must beFormatted(apiCombinedCommitStatusJson) assertJson(JsonFormat(apiCombinedCommitStatus), apiCombinedCommitStatusJson)
} }
"apiLabel" in { test("apiLabel") {
JsonFormat(apiLabel) must beFormatted(apiLabelJson) assertJson(JsonFormat(apiLabel), apiLabelJson)
} }
"apiIssue" in { test("apiIssue") {
JsonFormat(apiIssue) must beFormatted(apiIssueJson) assertJson(JsonFormat(apiIssue), apiIssueJson)
JsonFormat(apiIssuePR) must beFormatted(apiIssuePRJson) assertJson(JsonFormat(apiIssuePR), apiIssuePRJson)
} }
"apiPullRequest" in { test("apiPullRequest") {
JsonFormat(apiPullRequest) must beFormatted(apiPullRequestJson) assertJson(JsonFormat(apiPullRequest), apiPullRequestJson)
} }
"apiPullRequestReviewComment" in { test("apiPullRequestReviewComment") {
JsonFormat(apiPullRequestReviewComment) must beFormatted(apiPullRequestReviewCommentJson) assertJson(JsonFormat(apiPullRequestReviewComment), apiPullRequestReviewCommentJson)
} }
"apiBranchProtection" in { test("apiBranchProtection") {
JsonFormat(apiBranchProtection) must beFormatted(apiBranchProtectionJson) assertJson(JsonFormat(apiBranchProtection), apiBranchProtectionJson)
}
} }
} }

View File

@@ -1,26 +1,25 @@
package gitbucket.core.model package gitbucket.core.model
import gitbucket.core.model.CommitState._ import gitbucket.core.model.CommitState._
import org.scalatest.FunSpec
import org.specs2.mutable.Specification
class CommitStateSpec extends Specification { class CommitStateSpec extends FunSpec {
"CommitState" should { describe("CommitState") {
"combine empty must eq PENDING" in { it("should combine empty must eq PENDING") {
combine(Set()) must_== PENDING assert(combine(Set()) == PENDING)
} }
"combine includes ERROR must eq FAILURE" in { it("should combine includes ERROR must eq FAILURE") {
combine(Set(ERROR, SUCCESS, PENDING)) must_== FAILURE assert(combine(Set(ERROR, SUCCESS, PENDING)) == FAILURE)
} }
"combine includes FAILURE must eq peinding" in { it("should combine includes FAILURE must eq peinding") {
combine(Set(FAILURE, SUCCESS, PENDING)) must_== FAILURE assert(combine(Set(FAILURE, SUCCESS, PENDING)) == FAILURE)
} }
"combine includes PENDING must eq peinding" in { it("should combine includes PENDING must eq peinding") {
combine(Set(PENDING, SUCCESS)) must_== PENDING assert(combine(Set(PENDING, SUCCESS)) == PENDING)
} }
"combine only SUCCESS must eq SUCCESS" in { it("should combine only SUCCESS must eq SUCCESS") {
combine(Set(SUCCESS)) must_== SUCCESS assert(combine(Set(SUCCESS)) == SUCCESS)
} }
} }
} }

View File

@@ -1,91 +1,78 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model._ import gitbucket.core.model._
import org.scalatest.FunSuite
import org.specs2.mutable.Specification
import java.util.Date
class AccessTokenServiceSpec extends Specification with ServiceSpecBase { class AccessTokenServiceSpec extends FunSuite with ServiceSpecBase {
"AccessTokenService" should { test("generateAccessToken") { withTestDB { implicit session =>
"generateAccessToken" in { withTestDB { implicit session => assert(AccessTokenService.generateAccessToken("root", "note") match {
AccessTokenService.generateAccessToken("root", "note") must be like{ case (id, token) => id != 0
case (id, token) if id != 0 => ok })
} }}
}}
"getAccessTokens" in { withTestDB { implicit session => test("getAccessTokens") { withTestDB { implicit session =>
val (id, token) = AccessTokenService.generateAccessToken("root", "note") val (id, token) = AccessTokenService.generateAccessToken("root", "note")
val tokenHash = AccessTokenService.tokenToHash(token) val tokenHash = AccessTokenService.tokenToHash(token)
AccessTokenService.getAccessTokens("root") must be like{ assert(AccessTokenService.getAccessTokens("root") == List(AccessToken(`id`, "root", `tokenHash`, "note")))
case List(AccessToken(`id`, "root", `tokenHash`, "note")) => ok }}
}
}}
"getAccessTokens(root) get root's tokens" in { withTestDB { implicit session => test("getAccessTokens(root) get root's tokens") { withTestDB { implicit session =>
val (id, token) = AccessTokenService.generateAccessToken("root", "note") val (id, token) = AccessTokenService.generateAccessToken("root", "note")
val tokenHash = AccessTokenService.tokenToHash(token) val tokenHash = AccessTokenService.tokenToHash(token)
val user2 = generateNewAccount("user2") val user2 = generateNewAccount("user2")
AccessTokenService.generateAccessToken("user2", "note2") AccessTokenService.generateAccessToken("user2", "note2")
AccessTokenService.getAccessTokens("root") must be like{ assert(AccessTokenService.getAccessTokens("root") == List(AccessToken(`id`, "root", `tokenHash`, "note")))
case List(AccessToken(`id`, "root", `tokenHash`, "note")) => ok }}
}
}}
"deleteAccessToken" in { withTestDB { implicit session => test("deleteAccessToken") { withTestDB { implicit session =>
val (id, token) = AccessTokenService.generateAccessToken("root", "note") val (id, token) = AccessTokenService.generateAccessToken("root", "note")
val user2 = generateNewAccount("user2") val user2 = generateNewAccount("user2")
AccessTokenService.generateAccessToken("user2", "note2") AccessTokenService.generateAccessToken("user2", "note2")
AccessTokenService.deleteAccessToken("root", id) AccessTokenService.deleteAccessToken("root", id)
AccessTokenService.getAccessTokens("root") must beEmpty assert(AccessTokenService.getAccessTokens("root").isEmpty)
}} }}
"getAccountByAccessToken" in { withTestDB { implicit session => test("getAccountByAccessToken") { withTestDB { implicit session =>
val (id, token) = AccessTokenService.generateAccessToken("root", "note") val (id, token) = AccessTokenService.generateAccessToken("root", "note")
AccessTokenService.getAccountByAccessToken(token) must beSome.like { assert(AccessTokenService.getAccountByAccessToken(token) match {
case user => user.userName must_== "root" case Some(user) => user.userName == "root"
} })
}} }}
"getAccountByAccessToken don't get removed account" in { withTestDB { implicit session => test("getAccountByAccessToken don't get removed account") { withTestDB { implicit session =>
val user2 = generateNewAccount("user2") val user2 = generateNewAccount("user2")
val (id, token) = AccessTokenService.generateAccessToken("user2", "note") val (id, token) = AccessTokenService.generateAccessToken("user2", "note")
AccountService.updateAccount(user2.copy(isRemoved=true)) AccountService.updateAccount(user2.copy(isRemoved=true))
AccessTokenService.getAccountByAccessToken(token) must beEmpty assert(AccessTokenService.getAccountByAccessToken(token).isEmpty)
}} }}
"generateAccessToken create uniq token" in { withTestDB { implicit session => test("generateAccessToken create uniq token") { withTestDB { implicit session =>
val tokenIt = List("token1","token1","token1","token2").iterator val tokenIt = List("token1","token1","token1","token2").iterator
val service = new AccessTokenService{ val service = new AccessTokenService{
override def makeAccessTokenString:String = tokenIt.next override def makeAccessTokenString:String = tokenIt.next
} }
service.generateAccessToken("root", "note1") must like{ assert(service.generateAccessToken("root", "note1")._2 == "token1")
case (_, "token1") => ok assert(service.generateAccessToken("root", "note2")._2 == "token2")
} }}
service.generateAccessToken("root", "note2") must like{
case (_, "token2") => ok
}
}}
"when update Account.userName then AccessToken.userName changed" in { withTestDB { implicit session => test("when update Account.userName then AccessToken.userName changed") { withTestDB { implicit session =>
val user2 = generateNewAccount("user2") val user2 = generateNewAccount("user2")
val (id, token) = AccessTokenService.generateAccessToken("user2", "note") val (id, token) = AccessTokenService.generateAccessToken("user2", "note")
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import profile.simple._ import profile.simple._
Accounts.filter(_.userName === "user2".bind).map(_.userName).update("user3") Accounts.filter(_.userName === "user2".bind).map(_.userName).update("user3")
AccessTokenService.getAccountByAccessToken(token) must beSome.like { assert(AccessTokenService.getAccountByAccessToken(token) match {
case user => user.userName must_== "user3" case Some(user) => user.userName == "user3"
} })
}} }}
}
} }

View File

@@ -1,79 +1,71 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model.{Account, GroupMember} import gitbucket.core.model.{Account, GroupMember}
import org.specs2.mutable.Specification
import java.util.Date import java.util.Date
import org.scalatest.FunSuite
class AccountServiceSpec extends Specification with ServiceSpecBase { class AccountServiceSpec extends FunSuite with ServiceSpecBase {
"AccountService" should { val RootMailAddress = "root@localhost"
val RootMailAddress = "root@localhost"
"getAllUsers" in { withTestDB { implicit session => test("getAllUsers") { withTestDB { implicit session =>
AccountService.getAllUsers() must be like{ assert(AccountService.getAllUsers() match {
case List(Account("root", "root", RootMailAddress, _, true, _, _, _, None, None, false, false)) => ok case List(Account("root", "root", RootMailAddress, _, true, _, _, _, None, None, false, false)) => true
} case _ => false
}} })
}}
"getAccountByUserName" in { withTestDB { implicit session => test("getAccountByUserName") { withTestDB { implicit session =>
AccountService.getAccountByUserName("root") must beSome.like { assert(AccountService.getAccountByUserName("root").get.userName == "root")
case user => user.userName must_== "root" assert(AccountService.getAccountByUserName("invalid user name").isEmpty)
} }}
AccountService.getAccountByUserName("invalid user name") must beNone test("getAccountByMailAddress") { withTestDB { implicit session =>
}} assert(AccountService.getAccountByMailAddress(RootMailAddress).isDefined)
}}
"getAccountByMailAddress" in { withTestDB { implicit session => test("updateLastLoginDate") { withTestDB { implicit session =>
AccountService.getAccountByMailAddress(RootMailAddress) must beSome val root = "root"
}} def user() = AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists"))
"updateLastLoginDate" in { withTestDB { implicit session => assert(user().lastLoginDate.isEmpty)
val root = "root"
def user() =
AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists"))
user().lastLoginDate must beNone val date1 = new Date
val date1 = new Date AccountService.updateLastLoginDate(root)
AccountService.updateLastLoginDate(root) assert(user().lastLoginDate.get.compareTo(date1) > 0)
user().lastLoginDate must beSome.like{ case date =>
date must be_>(date1)
}
val date2 = new Date
Thread.sleep(1000)
AccountService.updateLastLoginDate(root)
user().lastLoginDate must beSome.like{ case date =>
date must be_>(date2)
}
}}
"updateAccount" in { withTestDB { implicit session => val date2 = new Date
val root = "root" Thread.sleep(1000)
def user() = AccountService.updateLastLoginDate(root)
AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists")) assert(user().lastLoginDate.get.compareTo(date2) > 0)
}}
val newAddress = "new mail address" test("updateAccount") { withTestDB { implicit session =>
AccountService.updateAccount(user().copy(mailAddress = newAddress)) val root = "root"
user().mailAddress must_== newAddress def user() = AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists"))
}}
"group" in { withTestDB { implicit session => val newAddress = "new mail address"
val group1 = "group1" AccountService.updateAccount(user().copy(mailAddress = newAddress))
val user1 = "root" assert(user().mailAddress == newAddress)
AccountService.createGroup(group1, None) }}
AccountService.getGroupMembers(group1) must_== Nil test("group") { withTestDB { implicit session =>
AccountService.getGroupsByUserName(user1) must_== Nil val group1 = "group1"
val user1 = "root"
AccountService.createGroup(group1, None)
AccountService.updateGroupMembers(group1, List((user1, true))) assert(AccountService.getGroupMembers(group1) == Nil)
assert(AccountService.getGroupsByUserName(user1) == Nil)
AccountService.getGroupMembers(group1) must_== List(GroupMember(group1, user1, true)) AccountService.updateGroupMembers(group1, List((user1, true)))
AccountService.getGroupsByUserName(user1) must_== List(group1)
AccountService.updateGroupMembers(group1, Nil) assert(AccountService.getGroupMembers(group1) == List(GroupMember(group1, user1, true)))
assert(AccountService.getGroupsByUserName(user1) == List(group1))
AccountService.getGroupMembers(group1) must_== Nil AccountService.updateGroupMembers(group1, Nil)
AccountService.getGroupsByUserName(user1) must_== Nil
}} assert(AccountService.getGroupMembers(group1) == Nil)
} assert(AccountService.getGroupsByUserName(user1) == Nil)
}}
} }

View File

@@ -1,78 +0,0 @@
package gitbucket.core.service
import gitbucket.core.model._
import gitbucket.core.model.Profile._
import profile.simple._
import org.specs2.mutable.Specification
import java.util.Date
class CommitStatusServiceSpec extends Specification with ServiceSpecBase with CommitStatusService
with RepositoryService with AccountService{
val now = new java.util.Date()
val fixture1 = CommitStatus(
userName = "root",
repositoryName = "repo",
commitId = "0e97b8f59f7cdd709418bb59de53f741fd1c1bd7",
context = "jenkins/test",
creator = "tester",
state = CommitState.PENDING,
targetUrl = Some("http://example.com/target"),
description = Some("description"),
updatedDate = now,
registeredDate = now)
def findById(id: Int)(implicit s:Session) = CommitStatuses.filter(_.byPrimaryKey(id)).firstOption
def generateFixture1(tester:Account)(implicit s:Session) = createCommitStatus(
userName = fixture1.userName,
repositoryName = fixture1.repositoryName,
sha = fixture1.commitId,
context = fixture1.context,
state = fixture1.state,
targetUrl = fixture1.targetUrl,
description = fixture1.description,
creator = tester,
now = fixture1.registeredDate)
"CommitStatusService" should {
"createCommitState can insert and update" in { withTestDB { implicit session =>
val tester = generateNewAccount(fixture1.creator)
createRepository(fixture1.repositoryName,fixture1.userName,None,false)
val id = generateFixture1(tester:Account)
getCommitStatus(fixture1.userName, fixture1.repositoryName, id) must_==
Some(fixture1.copy(commitStatusId=id))
// other one can update
val tester2 = generateNewAccount("tester2")
val time2 = new java.util.Date();
val id2 = createCommitStatus(
userName = fixture1.userName,
repositoryName = fixture1.repositoryName,
sha = fixture1.commitId,
context = fixture1.context,
state = CommitState.SUCCESS,
targetUrl = Some("http://example.com/target2"),
description = Some("description2"),
creator = tester2,
now = time2)
getCommitStatus(fixture1.userName, fixture1.repositoryName, id2) must_== Some(fixture1.copy(
commitStatusId = id,
creator = "tester2",
state = CommitState.SUCCESS,
targetUrl = Some("http://example.com/target2"),
description = Some("description2"),
updatedDate = time2))
}}
"getCommitStatus can find by commitId and context" in { withTestDB { implicit session =>
val tester = generateNewAccount(fixture1.creator)
createRepository(fixture1.repositoryName,fixture1.userName,None,false)
val id = generateFixture1(tester:Account)
getCommitStatus(fixture1.userName, fixture1.repositoryName, fixture1.commitId, fixture1.context) must_== Some(fixture1.copy(commitStatusId=id))
}}
"getCommitStatus can find by commitStatusId" in { withTestDB { implicit session =>
val tester = generateNewAccount(fixture1.creator)
createRepository(fixture1.repositoryName,fixture1.userName,None,false)
val id = generateFixture1(tester:Account)
getCommitStatus(fixture1.userName, fixture1.repositoryName, id) must_== Some(fixture1.copy(commitStatusId=id))
}}
}
}

View File

@@ -0,0 +1,74 @@
package gitbucket.core.service
import gitbucket.core.model._
import gitbucket.core.model.Profile._
import profile.simple._
import org.scalatest.FunSuite
class CommitStatusServiceSpec extends FunSuite with ServiceSpecBase with CommitStatusService
with RepositoryService with AccountService{
val now = new java.util.Date()
val fixture1 = CommitStatus(
userName = "root",
repositoryName = "repo",
commitId = "0e97b8f59f7cdd709418bb59de53f741fd1c1bd7",
context = "jenkins/test",
creator = "tester",
state = CommitState.PENDING,
targetUrl = Some("http://example.com/target"),
description = Some("description"),
updatedDate = now,
registeredDate = now)
def findById(id: Int)(implicit s:Session) = CommitStatuses.filter(_.byPrimaryKey(id)).firstOption
def generateFixture1(tester:Account)(implicit s:Session) = createCommitStatus(
userName = fixture1.userName,
repositoryName = fixture1.repositoryName,
sha = fixture1.commitId,
context = fixture1.context,
state = fixture1.state,
targetUrl = fixture1.targetUrl,
description = fixture1.description,
creator = tester,
now = fixture1.registeredDate)
test("createCommitState can insert and update") { withTestDB { implicit session =>
val tester = generateNewAccount(fixture1.creator)
createRepository(fixture1.repositoryName,fixture1.userName,None,false)
val id = generateFixture1(tester:Account)
assert(getCommitStatus(fixture1.userName, fixture1.repositoryName, id) == Some(fixture1.copy(commitStatusId=id)))
// other one can update
val tester2 = generateNewAccount("tester2")
val time2 = new java.util.Date();
val id2 = createCommitStatus(
userName = fixture1.userName,
repositoryName = fixture1.repositoryName,
sha = fixture1.commitId,
context = fixture1.context,
state = CommitState.SUCCESS,
targetUrl = Some("http://example.com/target2"),
description = Some("description2"),
creator = tester2,
now = time2)
assert(getCommitStatus(fixture1.userName, fixture1.repositoryName, id2) == Some(fixture1.copy(
commitStatusId = id,
creator = "tester2",
state = CommitState.SUCCESS,
targetUrl = Some("http://example.com/target2"),
description = Some("description2"),
updatedDate = time2)))
}}
test("getCommitStatus can find by commitId and context") { withTestDB { implicit session =>
val tester = generateNewAccount(fixture1.creator)
createRepository(fixture1.repositoryName,fixture1.userName,None,false)
val id = generateFixture1(tester:Account)
assert(getCommitStatus(fixture1.userName, fixture1.repositoryName, fixture1.commitId, fixture1.context) == Some(fixture1.copy(commitStatusId=id)))
}}
test("getCommitStatus can find by commitStatusId") { withTestDB { implicit session =>
val tester = generateNewAccount(fixture1.creator)
createRepository(fixture1.repositoryName,fixture1.userName,None,false)
val id = generateFixture1(tester:Account)
assert(getCommitStatus(fixture1.userName, fixture1.repositoryName, id) == Some(fixture1.copy(commitStatusId=id)))
}}
}

View File

@@ -2,49 +2,44 @@ package gitbucket.core.service
import gitbucket.core.model._ import gitbucket.core.model._
import gitbucket.core.service.IssuesService._ import gitbucket.core.service.IssuesService._
import org.scalatest.FunSuite
import org.specs2.mutable.Specification
import java.util.Date
class IssuesServiceSpec extends Specification with ServiceSpecBase { class IssuesServiceSpec extends FunSuite with ServiceSpecBase {
"IssuesService" should { test("getCommitStatues") { withTestDB { implicit session =>
"getCommitStatues" in { withTestDB { implicit session => val user1 = generateNewUserWithDBRepository("user1","repo1")
val user1 = generateNewUserWithDBRepository("user1","repo1")
def getCommitStatues = dummyService.getCommitStatues(List(("user1","repo1",1),("user1","repo1",2))) def getCommitStatues = dummyService.getCommitStatues(List(("user1","repo1",1),("user1","repo1",2)))
getCommitStatues must_== Map.empty assert(getCommitStatues == Map.empty)
val now = new java.util.Date() val now = new java.util.Date()
val issueId = generateNewIssue("user1","repo1") val issueId = generateNewIssue("user1","repo1")
issueId must_== 1 assert(issueId == 1)
getCommitStatues must_== Map.empty assert(getCommitStatues == Map.empty)
val cs = dummyService.createCommitStatus("user1","repo1","shasha", "default", CommitState.SUCCESS, Some("http://exmple.com/ci"), Some("exampleService"), now, user1) val cs = dummyService.createCommitStatus("user1","repo1","shasha", "default", CommitState.SUCCESS, Some("http://exmple.com/ci"), Some("exampleService"), now, user1)
getCommitStatues must_== Map.empty assert(getCommitStatues == Map.empty)
val (is2, pr2) = generateNewPullRequest("user1/repo1/master","user1/repo1/feature1") val (is2, pr2) = generateNewPullRequest("user1/repo1/master","user1/repo1/feature1")
pr2.issueId must_== 2 assert(pr2.issueId == 2)
// if there are no statuses, state is none // if there are no statuses, state is none
getCommitStatues must_== Map.empty assert(getCommitStatues == Map.empty)
// if there is a status, state is that // if there is a status, state is that
val cs2 = dummyService.createCommitStatus("user1","repo1","feature1", "default", CommitState.SUCCESS, Some("http://exmple.com/ci"), Some("exampleService"), now, user1) val cs2 = dummyService.createCommitStatus("user1","repo1","feature1", "default", CommitState.SUCCESS, Some("http://exmple.com/ci"), Some("exampleService"), now, user1)
getCommitStatues must_== Map(("user1","repo1",2) -> CommitStatusInfo(1,1,Some("default"),Some(CommitState.SUCCESS),Some("http://exmple.com/ci"),Some("exampleService"))) assert(getCommitStatues == Map(("user1","repo1",2) -> CommitStatusInfo(1,1,Some("default"),Some(CommitState.SUCCESS),Some("http://exmple.com/ci"),Some("exampleService"))))
// if there are two statuses, state is none // if there are two statuses, state is none
val cs3 = dummyService.createCommitStatus("user1","repo1","feature1", "pend", CommitState.PENDING, Some("http://exmple.com/ci"), Some("exampleService"), now, user1) val cs3 = dummyService.createCommitStatus("user1","repo1","feature1", "pend", CommitState.PENDING, Some("http://exmple.com/ci"), Some("exampleService"), now, user1)
getCommitStatues must_== Map(("user1","repo1",2) -> CommitStatusInfo(2,1,None,None,None,None)) assert(getCommitStatues == Map(("user1","repo1",2) -> CommitStatusInfo(2,1,None,None,None,None)))
// get only statuses in query issues // get only statuses in query issues
val (is3, pr3) = generateNewPullRequest("user1/repo1/master","user1/repo1/feature3") val (is3, pr3) = generateNewPullRequest("user1/repo1/master","user1/repo1/feature3")
val cs4 = dummyService.createCommitStatus("user1","repo1","feature3", "none", CommitState.PENDING, None, None, now, user1) val cs4 = dummyService.createCommitStatus("user1","repo1","feature3", "none", CommitState.PENDING, None, None, now, user1)
getCommitStatues must_== Map(("user1","repo1",2) -> CommitStatusInfo(2,1,None,None,None,None)) assert(getCommitStatues == Map(("user1","repo1",2) -> CommitStatusInfo(2,1,None,None,None,None)))
} } } }
}
} }

View File

@@ -1,12 +1,11 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model._ import gitbucket.core.model._
import org.scalatest.FunSpec
import org.specs2.mutable.Specification class LabelsServiceSpec extends FunSpec with ServiceSpecBase {
describe("getLabels") {
class LabelsServiceSpec extends Specification with ServiceSpecBase { it("should be empty when not have any labels") { withTestDB { implicit session =>
"getLabels" should {
"be empty when not have any labels" in { withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
generateNewUserWithDBRepository("user1", "repo2") generateNewUserWithDBRepository("user1", "repo2")
@@ -15,9 +14,9 @@ class LabelsServiceSpec extends Specification with ServiceSpecBase {
generateNewUserWithDBRepository("user2", "repo1") generateNewUserWithDBRepository("user2", "repo1")
dummyService.createLabel("user2", "repo1", "label1", "000000") dummyService.createLabel("user2", "repo1", "label1", "000000")
dummyService.getLabels("user1", "repo1") must haveSize(0) assert(dummyService.getLabels("user1", "repo1").isEmpty)
}} }}
"return contained labels" in { withTestDB { implicit session => it("should return contained labels") { withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
val labelId1 = dummyService.createLabel("user1", "repo1", "label1", "000000") val labelId1 = dummyService.createLabel("user1", "repo1", "label1", "000000")
val labelId2 = dummyService.createLabel("user1", "repo1", "label2", "ffffff") val labelId2 = dummyService.createLabel("user1", "repo1", "label2", "ffffff")
@@ -30,20 +29,22 @@ class LabelsServiceSpec extends Specification with ServiceSpecBase {
def getLabels = dummyService.getLabels("user1", "repo1") def getLabels = dummyService.getLabels("user1", "repo1")
getLabels must haveSize(2) assert(getLabels.length == 2)
getLabels must_== List( assert(getLabels == List(
Label("user1", "repo1", labelId1, "label1", "000000"), Label("user1", "repo1", labelId1, "label1", "000000"),
Label("user1", "repo1", labelId2, "label2", "ffffff")) Label("user1", "repo1", labelId2, "label2", "ffffff"))
)
}} }}
} }
"getLabel" should {
"return None when the label not exist" in { withTestDB { implicit session => describe("getLabel") {
it("should return None when the label not exist") { withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
dummyService.getLabel("user1", "repo1", 1) must beNone assert(dummyService.getLabel("user1", "repo1", 1) == None)
dummyService.getLabel("user1", "repo1", "label1") must beNone assert(dummyService.getLabel("user1", "repo1", "label1") == None)
}} }}
"return a label fetched by label id" in { withTestDB { implicit session => it("should return a label fetched by label id") { withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
val labelId1 = dummyService.createLabel("user1", "repo1", "label1", "000000") val labelId1 = dummyService.createLabel("user1", "repo1", "label1", "000000")
dummyService.createLabel("user1", "repo1", "label2", "ffffff") dummyService.createLabel("user1", "repo1", "label2", "ffffff")
@@ -55,9 +56,9 @@ class LabelsServiceSpec extends Specification with ServiceSpecBase {
dummyService.createLabel("user2", "repo1", "label1", "000000") dummyService.createLabel("user2", "repo1", "label1", "000000")
def getLabel = dummyService.getLabel("user1", "repo1", labelId1) def getLabel = dummyService.getLabel("user1", "repo1", labelId1)
getLabel must_== Some(Label("user1", "repo1", labelId1, "label1", "000000")) assert(getLabel == Some(Label("user1", "repo1", labelId1, "label1", "000000")))
}} }}
"return a label fetched by label name" in { withTestDB { implicit session => it("should return a label fetched by label name") { withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
val labelId1 = dummyService.createLabel("user1", "repo1", "label1", "000000") val labelId1 = dummyService.createLabel("user1", "repo1", "label1", "000000")
dummyService.createLabel("user1", "repo1", "label2", "ffffff") dummyService.createLabel("user1", "repo1", "label2", "ffffff")
@@ -69,11 +70,11 @@ class LabelsServiceSpec extends Specification with ServiceSpecBase {
dummyService.createLabel("user2", "repo1", "label1", "000000") dummyService.createLabel("user2", "repo1", "label1", "000000")
def getLabel = dummyService.getLabel("user1", "repo1", "label1") def getLabel = dummyService.getLabel("user1", "repo1", "label1")
getLabel must_== Some(Label("user1", "repo1", labelId1, "label1", "000000")) getLabel == Some(Label("user1", "repo1", labelId1, "label1", "000000"))
}} }}
} }
"createLabel" should { describe("createLabel") {
"return accurate label id" in { withTestDB { implicit session => it("should return accurate label id") { withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
generateNewUserWithDBRepository("user1", "repo2") generateNewUserWithDBRepository("user1", "repo2")
generateNewUserWithDBRepository("user2", "repo1") generateNewUserWithDBRepository("user2", "repo1")
@@ -81,13 +82,13 @@ class LabelsServiceSpec extends Specification with ServiceSpecBase {
dummyService.createLabel("user1", "repo2", "label1", "000000") dummyService.createLabel("user1", "repo2", "label1", "000000")
dummyService.createLabel("user2", "repo1", "label1", "000000") dummyService.createLabel("user2", "repo1", "label1", "000000")
val labelId = dummyService.createLabel("user1", "repo1", "label2", "000000") val labelId = dummyService.createLabel("user1", "repo1", "label2", "000000")
labelId must_== 4 assert(labelId == 4)
def getLabel = dummyService.getLabel("user1", "repo1", labelId) def getLabel = dummyService.getLabel("user1", "repo1", labelId)
getLabel must_== Some(Label("user1", "repo1", labelId, "label2", "000000")) assert(getLabel == Some(Label("user1", "repo1", labelId, "label2", "000000")))
}} }}
} }
"updateLabel" should { describe("updateLabel") {
"change target label" in { withTestDB { implicit session => it("should change target label") { withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
generateNewUserWithDBRepository("user1", "repo2") generateNewUserWithDBRepository("user1", "repo2")
generateNewUserWithDBRepository("user2", "repo1") generateNewUserWithDBRepository("user2", "repo1")
@@ -96,11 +97,11 @@ class LabelsServiceSpec extends Specification with ServiceSpecBase {
dummyService.createLabel("user2", "repo1", "label1", "000000") dummyService.createLabel("user2", "repo1", "label1", "000000")
dummyService.updateLabel("user1", "repo1", labelId, "updated-label", "ffffff") dummyService.updateLabel("user1", "repo1", labelId, "updated-label", "ffffff")
def getLabel = dummyService.getLabel("user1", "repo1", labelId) def getLabel = dummyService.getLabel("user1", "repo1", labelId)
getLabel must_== Some(Label("user1", "repo1", labelId, "updated-label", "ffffff")) assert(getLabel == Some(Label("user1", "repo1", labelId, "updated-label", "ffffff")))
}} }}
} }
"deleteLabel" should { describe("deleteLabel") {
"remove target label" in { withTestDB { implicit session => it("should remove target label") { withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
generateNewUserWithDBRepository("user1", "repo2") generateNewUserWithDBRepository("user1", "repo2")
generateNewUserWithDBRepository("user2", "repo1") generateNewUserWithDBRepository("user2", "repo1")
@@ -108,7 +109,7 @@ class LabelsServiceSpec extends Specification with ServiceSpecBase {
dummyService.createLabel("user1", "repo2", "label1", "000000") dummyService.createLabel("user1", "repo2", "label1", "000000")
dummyService.createLabel("user2", "repo1", "label1", "000000") dummyService.createLabel("user2", "repo1", "label1", "000000")
dummyService.deleteLabel("user1", "repo1", labelId) dummyService.deleteLabel("user1", "repo1", labelId)
dummyService.getLabel("user1", "repo1", labelId) must beNone assert(dummyService.getLabel("user1", "repo1", labelId) == None)
}} }}
} }
} }

View File

@@ -1,26 +1,17 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model._
import gitbucket.core.util.JGitUtil
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.GitSpecUtil._ import gitbucket.core.util.GitSpecUtil._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk._ import org.eclipse.jgit.revwalk._
import org.eclipse.jgit.treewalk._ import org.scalatest.FunSpec
import org.specs2.mutable.Specification
import java.io.File import java.io.File
import java.nio.file._
import java.util.Date
class MergeServiceSpec extends Specification { class MergeServiceSpec extends FunSpec {
sequential
val service = new MergeService{} val service = new MergeService{}
val branch = "master" val branch = "master"
val issueId = 10 val issueId = 10
@@ -36,95 +27,95 @@ class MergeServiceSpec extends Specification {
createFile(git, s"refs/heads/${branch}", "test.txt", "hoge2" ) createFile(git, s"refs/heads/${branch}", "test.txt", "hoge2" )
createFile(git, s"refs/pull/${issueId}/head", "test.txt", "hoge4" ) createFile(git, s"refs/pull/${issueId}/head", "test.txt", "hoge4" )
} }
"checkConflict, checkConflictCache" should { describe("checkConflict, checkConflictCache") {
"checkConflict false if not conflicted, and create cache" in { it("checkConflict false if not conflicted, and create cache") {
val repo1Dir = initRepository("user1","repo1") val repo1Dir = initRepository("user1","repo1")
service.checkConflictCache("user1", "repo1", branch, issueId) mustEqual None assert(service.checkConflictCache("user1", "repo1", branch, issueId) == None)
val conflicted = service.checkConflict("user1", "repo1", branch, issueId) val conflicted = service.checkConflict("user1", "repo1", branch, issueId)
service.checkConflictCache("user1", "repo1", branch, issueId) mustEqual Some(false) assert(service.checkConflictCache("user1", "repo1", branch, issueId) == Some(false))
conflicted mustEqual false assert(conflicted == false)
} }
"checkConflict true if not conflicted, and create cache" in { it("checkConflict true if not conflicted, and create cache") {
val repo2Dir = initRepository("user1","repo2") val repo2Dir = initRepository("user1","repo2")
using(Git.open(repo2Dir)){ git => using(Git.open(repo2Dir)){ git =>
createConfrict(git) createConfrict(git)
} }
service.checkConflictCache("user1", "repo2", branch, issueId) mustEqual None assert(service.checkConflictCache("user1", "repo2", branch, issueId) == None)
val conflicted = service.checkConflict("user1", "repo2", branch, issueId) val conflicted = service.checkConflict("user1", "repo2", branch, issueId)
conflicted mustEqual true assert(conflicted == true)
service.checkConflictCache("user1", "repo2", branch, issueId) mustEqual Some(true) assert(service.checkConflictCache("user1", "repo2", branch, issueId) == Some(true))
} }
} }
"checkConflictCache" should { describe("checkConflictCache") {
"merged cache invalid if origin branch moved" in { it("merged cache invalid if origin branch moved") {
val repo3Dir = initRepository("user1","repo3") val repo3Dir = initRepository("user1","repo3")
service.checkConflict("user1", "repo3", branch, issueId) mustEqual false assert(service.checkConflict("user1", "repo3", branch, issueId) == false)
service.checkConflictCache("user1", "repo3", branch, issueId) mustEqual Some(false) assert(service.checkConflictCache("user1", "repo3", branch, issueId) == Some(false))
using(Git.open(repo3Dir)){ git => using(Git.open(repo3Dir)){ git =>
createFile(git, s"refs/heads/${branch}", "test.txt", "hoge2" ) createFile(git, s"refs/heads/${branch}", "test.txt", "hoge2" )
} }
service.checkConflictCache("user1", "repo3", branch, issueId) mustEqual None assert(service.checkConflictCache("user1", "repo3", branch, issueId) == None)
} }
"merged cache invalid if request branch moved" in { it("merged cache invalid if request branch moved") {
val repo4Dir = initRepository("user1","repo4") val repo4Dir = initRepository("user1","repo4")
service.checkConflict("user1", "repo4", branch, issueId) mustEqual false assert(service.checkConflict("user1", "repo4", branch, issueId) == false)
service.checkConflictCache("user1", "repo4", branch, issueId) mustEqual Some(false) assert(service.checkConflictCache("user1", "repo4", branch, issueId) == Some(false))
using(Git.open(repo4Dir)){ git => using(Git.open(repo4Dir)){ git =>
createFile(git, s"refs/pull/${issueId}/head", "test.txt", "hoge4" ) createFile(git, s"refs/pull/${issueId}/head", "test.txt", "hoge4" )
} }
service.checkConflictCache("user1", "repo4", branch, issueId) mustEqual None assert(service.checkConflictCache("user1", "repo4", branch, issueId) == None)
} }
"merged cache invalid if origin branch moved" in { it("should merged cache invalid if origin branch moved") {
val repo5Dir = initRepository("user1","repo5") val repo5Dir = initRepository("user1","repo5")
service.checkConflict("user1", "repo5", branch, issueId) mustEqual false assert(service.checkConflict("user1", "repo5", branch, issueId) == false)
service.checkConflictCache("user1", "repo5", branch, issueId) mustEqual Some(false) assert(service.checkConflictCache("user1", "repo5", branch, issueId) == Some(false))
using(Git.open(repo5Dir)){ git => using(Git.open(repo5Dir)){ git =>
createFile(git, s"refs/heads/${branch}", "test.txt", "hoge2" ) createFile(git, s"refs/heads/${branch}", "test.txt", "hoge2" )
} }
service.checkConflictCache("user1", "repo5", branch, issueId) mustEqual None assert(service.checkConflictCache("user1", "repo5", branch, issueId) == None)
} }
"conflicted cache invalid if request branch moved" in { it("conflicted cache invalid if request branch moved") {
val repo6Dir = initRepository("user1","repo6") val repo6Dir = initRepository("user1","repo6")
using(Git.open(repo6Dir)){ git => using(Git.open(repo6Dir)){ git =>
createConfrict(git) createConfrict(git)
} }
service.checkConflict("user1", "repo6", branch, issueId) mustEqual true assert(service.checkConflict("user1", "repo6", branch, issueId) == true)
service.checkConflictCache("user1", "repo6", branch, issueId) mustEqual Some(true) assert(service.checkConflictCache("user1", "repo6", branch, issueId) == Some(true))
using(Git.open(repo6Dir)){ git => using(Git.open(repo6Dir)){ git =>
createFile(git, s"refs/pull/${issueId}/head", "test.txt", "hoge4" ) createFile(git, s"refs/pull/${issueId}/head", "test.txt", "hoge4" )
} }
service.checkConflictCache("user1", "repo6", branch, issueId) mustEqual None assert(service.checkConflictCache("user1", "repo6", branch, issueId) == None)
} }
"conflicted cache invalid if origin branch moved" in { it("conflicted cache invalid if origin branch moved") {
val repo7Dir = initRepository("user1","repo7") val repo7Dir = initRepository("user1","repo7")
using(Git.open(repo7Dir)){ git => using(Git.open(repo7Dir)){ git =>
createConfrict(git) createConfrict(git)
} }
service.checkConflict("user1", "repo7", branch, issueId) mustEqual true assert(service.checkConflict("user1", "repo7", branch, issueId) == true)
service.checkConflictCache("user1", "repo7", branch, issueId) mustEqual Some(true) assert(service.checkConflictCache("user1", "repo7", branch, issueId) == Some(true))
using(Git.open(repo7Dir)){ git => using(Git.open(repo7Dir)){ git =>
createFile(git, s"refs/heads/${branch}", "test.txt", "hoge4" ) createFile(git, s"refs/heads/${branch}", "test.txt", "hoge4" )
} }
service.checkConflictCache("user1", "repo7", branch, issueId) mustEqual None assert(service.checkConflictCache("user1", "repo7", branch, issueId) == None)
} }
} }
"mergePullRequest" should { describe("mergePullRequest") {
"can merge" in { it("can merge") {
val repo8Dir = initRepository("user1","repo8") val repo8Dir = initRepository("user1","repo8")
using(Git.open(repo8Dir)){ git => using(Git.open(repo8Dir)){ git =>
createFile(git, s"refs/pull/${issueId}/head", "test.txt", "hoge2" ) createFile(git, s"refs/pull/${issueId}/head", "test.txt", "hoge2" )
val committer = new PersonIdent("dummy2", "dummy2@example.com") val committer = new PersonIdent("dummy2", "dummy2@example.com")
getFile(git, branch, "test.txt").content.get mustEqual "hoge" assert(getFile(git, branch, "test.txt").content.get == "hoge")
val requestBranchId = git.getRepository.resolve(s"refs/pull/${issueId}/head") val requestBranchId = git.getRepository.resolve(s"refs/pull/${issueId}/head")
val masterId = git.getRepository.resolve(branch) val masterId = git.getRepository.resolve(branch)
service.mergePullRequest(git, branch, issueId, "merged", committer) service.mergePullRequest(git, branch, issueId, "merged", committer)
val lastCommitId = git.getRepository.resolve(branch); val lastCommitId = git.getRepository.resolve(branch)
val commit = using(new RevWalk(git.getRepository))(_.parseCommit(lastCommitId)) val commit = using(new RevWalk(git.getRepository))(_.parseCommit(lastCommitId))
commit.getCommitterIdent() mustEqual committer assert(commit.getCommitterIdent() == committer)
commit.getAuthorIdent() mustEqual committer assert(commit.getAuthorIdent() == committer)
commit.getFullMessage() mustEqual "merged" assert(commit.getFullMessage() == "merged")
commit.getParents.toSet mustEqual Set( requestBranchId, masterId ) assert(commit.getParents.toSet == Set( requestBranchId, masterId ))
getFile(git, branch, "test.txt").content.get mustEqual "hoge2" assert(getFile(git, branch, "test.txt").content.get == "hoge2")
} }
} }
} }

View File

@@ -1,134 +1,134 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.util.GitSpecUtil._ import gitbucket.core.util.GitSpecUtil._
import org.specs2.mutable.Specification
import org.eclipse.jgit.transport.{ReceivePack, ReceiveCommand} import org.eclipse.jgit.transport.{ReceivePack, ReceiveCommand}
import org.eclipse.jgit.lib.ObjectId import org.eclipse.jgit.lib.ObjectId
import gitbucket.core.model.CommitState import gitbucket.core.model.CommitState
import gitbucket.core.service.ProtectedBranchService.{ProtectedBranchReceiveHook, ProtectedBranchInfo} import gitbucket.core.service.ProtectedBranchService.{ProtectedBranchReceiveHook, ProtectedBranchInfo}
import scalaz._, Scalaz._ import scalaz._, Scalaz._
import org.scalatest.FunSpec
class ProtectedBranchServiceSpec extends Specification with ServiceSpecBase with ProtectedBranchService with CommitStatusService { class ProtectedBranchServiceSpec extends FunSpec with ServiceSpecBase with ProtectedBranchService with CommitStatusService {
val receiveHook = new ProtectedBranchReceiveHook() val receiveHook = new ProtectedBranchReceiveHook()
val now = new java.util.Date() val now = new java.util.Date()
val sha = "0c77148632618b59b6f70004e3084002be2b8804" val sha = "0c77148632618b59b6f70004e3084002be2b8804"
val sha2 = "0c77148632618b59b6f70004e3084002be2b8805" val sha2 = "0c77148632618b59b6f70004e3084002be2b8805"
"getProtectedBranchInfo" should { describe("getProtectedBranchInfo") {
"empty is disabled" in { it("should empty is disabled") {
withTestDB { implicit session => withTestDB { implicit session =>
getProtectedBranchInfo("user1", "repo1", "branch") must_== ProtectedBranchInfo.disabled("user1", "repo1") assert(getProtectedBranchInfo("user1", "repo1", "branch") == ProtectedBranchInfo.disabled("user1", "repo1"))
} }
} }
"enable and update and disable" in { it("should enable and update and disable") {
withTestDB { implicit session => withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
enableBranchProtection("user1", "repo1", "branch", false, Nil) enableBranchProtection("user1", "repo1", "branch", false, Nil)
getProtectedBranchInfo("user1", "repo1", "branch") must_== ProtectedBranchInfo("user1", "repo1", true, Nil, false) assert(getProtectedBranchInfo("user1", "repo1", "branch") == ProtectedBranchInfo("user1", "repo1", true, Nil, false))
enableBranchProtection("user1", "repo1", "branch", true, Seq("hoge","huge")) enableBranchProtection("user1", "repo1", "branch", true, Seq("hoge","huge"))
getProtectedBranchInfo("user1", "repo1", "branch") must_== ProtectedBranchInfo("user1", "repo1", true, Seq("hoge","huge"), true) assert(getProtectedBranchInfo("user1", "repo1", "branch") == ProtectedBranchInfo("user1", "repo1", true, Seq("hoge","huge"), true))
disableBranchProtection("user1", "repo1", "branch") disableBranchProtection("user1", "repo1", "branch")
getProtectedBranchInfo("user1", "repo1", "branch") must_== ProtectedBranchInfo.disabled("user1", "repo1") assert(getProtectedBranchInfo("user1", "repo1", "branch") == ProtectedBranchInfo.disabled("user1", "repo1"))
} }
} }
"empty contexts is no-include-administrators" in { it("should empty contexts is no-include-administrators") {
withTestDB { implicit session => withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
enableBranchProtection("user1", "repo1", "branch", false, Nil) enableBranchProtection("user1", "repo1", "branch", false, Nil)
getProtectedBranchInfo("user1", "repo1", "branch").includeAdministrators must_== false assert(getProtectedBranchInfo("user1", "repo1", "branch").includeAdministrators == false)
enableBranchProtection("user1", "repo1", "branch", true, Nil) enableBranchProtection("user1", "repo1", "branch", true, Nil)
getProtectedBranchInfo("user1", "repo1", "branch").includeAdministrators must_== false assert(getProtectedBranchInfo("user1", "repo1", "branch").includeAdministrators == false)
} }
} }
"getProtectedBranchList" in { it("getProtectedBranchList") {
withTestDB { implicit session => withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
enableBranchProtection("user1", "repo1", "branch", false, Nil) enableBranchProtection("user1", "repo1", "branch", false, Nil)
enableBranchProtection("user1", "repo1", "branch2", false, Seq("fuga")) enableBranchProtection("user1", "repo1", "branch2", false, Seq("fuga"))
enableBranchProtection("user1", "repo1", "branch3", true, Seq("hoge")) enableBranchProtection("user1", "repo1", "branch3", true, Seq("hoge"))
getProtectedBranchList("user1", "repo1").toSet must_== Set("branch", "branch2", "branch3") assert(getProtectedBranchList("user1", "repo1").toSet == Set("branch", "branch2", "branch3"))
} }
} }
"getBranchProtectedReason on force push from admin" in { it("getBranchProtectedReason on force push from admin") {
withTestDB { implicit session => withTestDB { implicit session =>
withTestRepository { git => withTestRepository { git =>
val rp = new ReceivePack(git.getRepository) <| { _.setAllowNonFastForwards(true) } val rp = new ReceivePack(git.getRepository) <| { _.setAllowNonFastForwards(true) }
val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE_NONFASTFORWARD) val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE_NONFASTFORWARD)
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== None assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1") == None)
enableBranchProtection("user1", "repo1", "branch", false, Nil) enableBranchProtection("user1", "repo1", "branch", false, Nil)
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== Some("Cannot force-push to a protected branch") assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1") == Some("Cannot force-push to a protected branch"))
} }
} }
} }
"getBranchProtectedReason on force push from othre" in { it("getBranchProtectedReason on force push from other") {
withTestDB { implicit session => withTestDB { implicit session =>
withTestRepository { git => withTestRepository { git =>
val rp = new ReceivePack(git.getRepository) <| { _.setAllowNonFastForwards(true) } val rp = new ReceivePack(git.getRepository) <| { _.setAllowNonFastForwards(true) }
val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE_NONFASTFORWARD) val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE_NONFASTFORWARD)
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
receiveHook.preReceive("user1", "repo1", rp, rc, "user2") must_== None assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2") == None)
enableBranchProtection("user1", "repo1", "branch", false, Nil) enableBranchProtection("user1", "repo1", "branch", false, Nil)
receiveHook.preReceive("user1", "repo1", rp, rc, "user2") must_== Some("Cannot force-push to a protected branch") assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2") == Some("Cannot force-push to a protected branch"))
} }
} }
} }
"getBranchProtectedReason check status on push from othre" in { it("getBranchProtectedReason check status on push from other") {
withTestDB { implicit session => withTestDB { implicit session =>
withTestRepository { git => withTestRepository { git =>
val rp = new ReceivePack(git.getRepository) <| { _.setAllowNonFastForwards(false) } val rp = new ReceivePack(git.getRepository) <| { _.setAllowNonFastForwards(false) }
val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE) val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE)
val user1 = generateNewUserWithDBRepository("user1", "repo1") val user1 = generateNewUserWithDBRepository("user1", "repo1")
receiveHook.preReceive("user1", "repo1", rp, rc, "user2") must_== None assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2") == None)
enableBranchProtection("user1", "repo1", "branch", false, Seq("must")) enableBranchProtection("user1", "repo1", "branch", false, Seq("must"))
receiveHook.preReceive("user1", "repo1", rp, rc, "user2") must_== Some("Required status check \"must\" is expected") assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2") == Some("Required status check \"must\" is expected"))
enableBranchProtection("user1", "repo1", "branch", false, Seq("must", "must2")) enableBranchProtection("user1", "repo1", "branch", false, Seq("must", "must2"))
receiveHook.preReceive("user1", "repo1", rp, rc, "user2") must_== Some("2 of 2 required status checks are expected") assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2") == Some("2 of 2 required status checks are expected"))
createCommitStatus("user1", "repo1", sha2, "context", CommitState.SUCCESS, None, None, now, user1) createCommitStatus("user1", "repo1", sha2, "context", CommitState.SUCCESS, None, None, now, user1)
receiveHook.preReceive("user1", "repo1", rp, rc, "user2") must_== Some("2 of 2 required status checks are expected") assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2") == Some("2 of 2 required status checks are expected"))
createCommitStatus("user1", "repo1", sha2, "must", CommitState.SUCCESS, None, None, now, user1) createCommitStatus("user1", "repo1", sha2, "must", CommitState.SUCCESS, None, None, now, user1)
receiveHook.preReceive("user1", "repo1", rp, rc, "user2") must_== Some("Required status check \"must2\" is expected") assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2") == Some("Required status check \"must2\" is expected"))
createCommitStatus("user1", "repo1", sha2, "must2", CommitState.SUCCESS, None, None, now, user1) createCommitStatus("user1", "repo1", sha2, "must2", CommitState.SUCCESS, None, None, now, user1)
receiveHook.preReceive("user1", "repo1", rp, rc, "user2") must_== None assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2") == None)
} }
} }
} }
"getBranchProtectedReason check status on push from admin" in { it("getBranchProtectedReason check status on push from admin") {
withTestDB { implicit session => withTestDB { implicit session =>
withTestRepository { git => withTestRepository { git =>
val rp = new ReceivePack(git.getRepository) <| { _.setAllowNonFastForwards(false) } val rp = new ReceivePack(git.getRepository) <| { _.setAllowNonFastForwards(false) }
val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE) val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE)
val user1 = generateNewUserWithDBRepository("user1", "repo1") val user1 = generateNewUserWithDBRepository("user1", "repo1")
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== None assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1") == None)
enableBranchProtection("user1", "repo1", "branch", false, Seq("must")) enableBranchProtection("user1", "repo1", "branch", false, Seq("must"))
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== None assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1") == None)
enableBranchProtection("user1", "repo1", "branch", true, Seq("must")) enableBranchProtection("user1", "repo1", "branch", true, Seq("must"))
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== Some("Required status check \"must\" is expected") assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1") == Some("Required status check \"must\" is expected"))
enableBranchProtection("user1", "repo1", "branch", false, Seq("must", "must2")) enableBranchProtection("user1", "repo1", "branch", false, Seq("must", "must2"))
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== None assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1") == None)
enableBranchProtection("user1", "repo1", "branch", true, Seq("must", "must2")) enableBranchProtection("user1", "repo1", "branch", true, Seq("must", "must2"))
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== Some("2 of 2 required status checks are expected") assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1") == Some("2 of 2 required status checks are expected"))
createCommitStatus("user1", "repo1", sha2, "context", CommitState.SUCCESS, None, None, now, user1) createCommitStatus("user1", "repo1", sha2, "context", CommitState.SUCCESS, None, None, now, user1)
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== Some("2 of 2 required status checks are expected") assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1") == Some("2 of 2 required status checks are expected"))
createCommitStatus("user1", "repo1", sha2, "must", CommitState.SUCCESS, None, None, now, user1) createCommitStatus("user1", "repo1", sha2, "must", CommitState.SUCCESS, None, None, now, user1)
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== Some("Required status check \"must2\" is expected") assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1") == Some("Required status check \"must2\" is expected"))
createCommitStatus("user1", "repo1", sha2, "must2", CommitState.SUCCESS, None, None, now, user1) createCommitStatus("user1", "repo1", sha2, "must2", CommitState.SUCCESS, None, None, now, user1)
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== None assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1") == None)
} }
} }
} }
} }
"ProtectedBranchInfo" should { describe("ProtectedBranchInfo") {
"administrator is owner" in { it("administrator is owner") {
withTestDB { implicit session => withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
val x = ProtectedBranchInfo("user1", "repo1", true, Nil, false) val x = ProtectedBranchInfo("user1", "repo1", true, Nil, false)
x.isAdministrator("user1") must_== true assert(x.isAdministrator("user1") == true)
x.isAdministrator("user2") must_== false assert(x.isAdministrator("user2") == false)
} }
} }
"administrator is manager" in { it("administrator is manager") {
withTestDB { implicit session => withTestDB { implicit session =>
val x = ProtectedBranchInfo("grp1", "repo1", true, Nil, false) val x = ProtectedBranchInfo("grp1", "repo1", true, Nil, false)
x.createGroup("grp1", None) x.createGroup("grp1", None)
@@ -137,49 +137,49 @@ class ProtectedBranchServiceSpec extends Specification with ServiceSpecBase with
generateNewAccount("user3") generateNewAccount("user3")
x.updateGroupMembers("grp1", List("user1"->true, "user2"->false)) x.updateGroupMembers("grp1", List("user1"->true, "user2"->false))
x.isAdministrator("user1") must_== true assert(x.isAdministrator("user1") == true)
x.isAdministrator("user2") must_== false assert(x.isAdministrator("user2") == false)
x.isAdministrator("user3") must_== false assert(x.isAdministrator("user3") == false)
} }
} }
"unSuccessedContexts" in { it("unSuccessedContexts") {
withTestDB { implicit session => withTestDB { implicit session =>
val user1 = generateNewUserWithDBRepository("user1", "repo1") val user1 = generateNewUserWithDBRepository("user1", "repo1")
val x = ProtectedBranchInfo("user1", "repo1", true, List("must"), false) val x = ProtectedBranchInfo("user1", "repo1", true, List("must"), false)
x.unSuccessedContexts(sha) must_== Set("must") assert(x.unSuccessedContexts(sha) == Set("must"))
createCommitStatus("user1", "repo1", sha, "context", CommitState.SUCCESS, None, None, now, user1) createCommitStatus("user1", "repo1", sha, "context", CommitState.SUCCESS, None, None, now, user1)
x.unSuccessedContexts(sha) must_== Set("must") assert(x.unSuccessedContexts(sha) == Set("must"))
createCommitStatus("user1", "repo1", sha, "must", CommitState.ERROR, None, None, now, user1) createCommitStatus("user1", "repo1", sha, "must", CommitState.ERROR, None, None, now, user1)
x.unSuccessedContexts(sha) must_== Set("must") assert(x.unSuccessedContexts(sha) == Set("must"))
createCommitStatus("user1", "repo1", sha, "must", CommitState.PENDING, None, None, now, user1) createCommitStatus("user1", "repo1", sha, "must", CommitState.PENDING, None, None, now, user1)
x.unSuccessedContexts(sha) must_== Set("must") assert(x.unSuccessedContexts(sha) == Set("must"))
createCommitStatus("user1", "repo1", sha, "must", CommitState.FAILURE, None, None, now, user1) createCommitStatus("user1", "repo1", sha, "must", CommitState.FAILURE, None, None, now, user1)
x.unSuccessedContexts(sha) must_== Set("must") assert(x.unSuccessedContexts(sha) == Set("must"))
createCommitStatus("user1", "repo1", sha, "must", CommitState.SUCCESS, None, None, now, user1) createCommitStatus("user1", "repo1", sha, "must", CommitState.SUCCESS, None, None, now, user1)
x.unSuccessedContexts(sha) must_== Set() assert(x.unSuccessedContexts(sha) == Set())
} }
} }
"unSuccessedContexts when empty" in { it("unSuccessedContexts when empty") {
withTestDB { implicit session => withTestDB { implicit session =>
val user1 = generateNewUserWithDBRepository("user1", "repo1") val user1 = generateNewUserWithDBRepository("user1", "repo1")
val x = ProtectedBranchInfo("user1", "repo1", true, Nil, false) val x = ProtectedBranchInfo("user1", "repo1", true, Nil, false)
val sha = "0c77148632618b59b6f70004e3084002be2b8804" val sha = "0c77148632618b59b6f70004e3084002be2b8804"
x.unSuccessedContexts(sha) must_== Nil assert(x.unSuccessedContexts(sha) == Set())
createCommitStatus("user1", "repo1", sha, "context", CommitState.SUCCESS, None, None, now, user1) createCommitStatus("user1", "repo1", sha, "context", CommitState.SUCCESS, None, None, now, user1)
x.unSuccessedContexts(sha) must_== Nil assert(x.unSuccessedContexts(sha) == Set())
} }
} }
"if disabled, needStatusCheck is false" in { it("if disabled, needStatusCheck is false") {
withTestDB { implicit session => withTestDB { implicit session =>
ProtectedBranchInfo("user1", "repo1", false, Seq("must"), true).needStatusCheck("user1") must_== false assert(ProtectedBranchInfo("user1", "repo1", false, Seq("must"), true).needStatusCheck("user1") == false)
} }
} }
"needStatusCheck includeAdministrators" in { it("needStatusCheck includeAdministrators") {
withTestDB { implicit session => withTestDB { implicit session =>
ProtectedBranchInfo("user1", "repo1", true, Seq("must"), false).needStatusCheck("user2") must_== true assert(ProtectedBranchInfo("user1", "repo1", true, Seq("must"), false).needStatusCheck("user2") == true)
ProtectedBranchInfo("user1", "repo1", true, Seq("must"), false).needStatusCheck("user1") must_== false assert(ProtectedBranchInfo("user1", "repo1", true, Seq("must"), false).needStatusCheck("user1") == false)
ProtectedBranchInfo("user1", "repo1", true, Seq("must"), true ).needStatusCheck("user2") must_== true assert(ProtectedBranchInfo("user1", "repo1", true, Seq("must"), true ).needStatusCheck("user2") == true)
ProtectedBranchInfo("user1", "repo1", true, Seq("must"), true ).needStatusCheck("user1") must_== true assert(ProtectedBranchInfo("user1", "repo1", true, Seq("must"), true ).needStatusCheck("user1") == true)
} }
} }
} }

View File

@@ -1,17 +1,17 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model._ import gitbucket.core.model._
import gitbucket.core.model.Profile._ import org.scalatest.FunSpec
import org.specs2.mutable.Specification class PullRequestServiceSpec extends FunSpec with ServiceSpecBase with PullRequestService with IssuesService {
class PullRequestServiceSpec extends Specification with ServiceSpecBase with PullRequestService with IssuesService {
def swap(r: (Issue, PullRequest)) = (r._2 -> r._1) def swap(r: (Issue, PullRequest)) = (r._2 -> r._1)
"PullRequestService.getPullRequestFromBranch" should {
""" describe("PullRequestService.getPullRequestFromBranch") {
it("""should
|return pull request if exists pull request from `branch` to `defaultBranch` and not closed. |return pull request if exists pull request from `branch` to `defaultBranch` and not closed.
|return pull request if exists pull request from `branch` to othre branch and not closed. |return pull request if exists pull request from `branch` to othre branch and not closed.
|return None if all pull request is closed""".stripMargin.trim in { withTestDB { implicit se => |return None if all pull request is closed""".stripMargin.trim) { withTestDB { implicit se =>
generateNewUserWithDBRepository("user1", "repo1") generateNewUserWithDBRepository("user1", "repo1")
generateNewUserWithDBRepository("user1", "repo2") generateNewUserWithDBRepository("user1", "repo2")
generateNewUserWithDBRepository("user2", "repo1") generateNewUserWithDBRepository("user2", "repo1")
@@ -22,12 +22,12 @@ class PullRequestServiceSpec extends Specification with ServiceSpecBase with Pul
val r1 = swap(generateNewPullRequest("user1/repo1/master2", "user1/repo1/head1")) val r1 = swap(generateNewPullRequest("user1/repo1/master2", "user1/repo1/head1"))
val r2 = swap(generateNewPullRequest("user1/repo1/master", "user1/repo1/head1")) val r2 = swap(generateNewPullRequest("user1/repo1/master", "user1/repo1/head1"))
val r3 = swap(generateNewPullRequest("user1/repo1/master4", "user1/repo1/head1")) val r3 = swap(generateNewPullRequest("user1/repo1/master4", "user1/repo1/head1"))
getPullRequestFromBranch("user1", "repo1", "head1", "master") must_== Some(r2) assert(getPullRequestFromBranch("user1", "repo1", "head1", "master") == Some(r2))
updateClosed("user1", "repo1", r2._1.issueId, true) updateClosed("user1", "repo1", r2._1.issueId, true)
getPullRequestFromBranch("user1", "repo1", "head1", "master").get must beOneOf(r1, r2) assert(Seq(r1, r2).contains(getPullRequestFromBranch("user1", "repo1", "head1", "master").get))
updateClosed("user1", "repo1", r1._1.issueId, true) updateClosed("user1", "repo1", r1._1.issueId, true)
updateClosed("user1", "repo1", r3._1.issueId, true) updateClosed("user1", "repo1", r3._1.issueId, true)
getPullRequestFromBranch("user1", "repo1", "head1", "master") must beNone assert(getPullRequestFromBranch("user1", "repo1", "head1", "master") == None)
} } } }
} }
} }

View File

@@ -1,39 +1,33 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model._ import gitbucket.core.model._
import org.specs2.mutable.Specification import org.scalatest.FunSuite
class RepositoryServiceSpec extends FunSuite with ServiceSpecBase with RepositoryService with AccountService{
test("renameRepository can rename CommitState, ProtectedBranches") { withTestDB { implicit session =>
val tester = generateNewAccount("tester")
createRepository("repo", "root", None, false)
val service = new CommitStatusService with ProtectedBranchService {}
val id = service.createCommitStatus(
userName = "root",
repositoryName = "repo",
sha = "0e97b8f59f7cdd709418bb59de53f741fd1c1bd7",
context = "jenkins/test",
state = CommitState.PENDING,
targetUrl = Some("http://example.com/target"),
description = Some("description"),
creator = tester,
now = new java.util.Date)
class RepositoryServiceSpec extends Specification with ServiceSpecBase with RepositoryService with AccountService{ service.enableBranchProtection("root", "repo", "branch", true, Seq("must1", "must2"))
"RepositoryService" should {
"renameRepository can rename CommitState, ProtectedBranches" in { withTestDB { implicit session =>
val tester = generateNewAccount("tester")
createRepository("repo","root",None,false)
val service = new CommitStatusService with ProtectedBranchService {}
val id = service.createCommitStatus(
userName = "root",
repositoryName = "repo",
sha = "0e97b8f59f7cdd709418bb59de53f741fd1c1bd7",
context = "jenkins/test",
state = CommitState.PENDING,
targetUrl = Some("http://example.com/target"),
description = Some("description"),
creator = tester,
now = new java.util.Date)
service.enableBranchProtection("root", "repo", "branch", true, Seq("must1", "must2"))
var orgPbi = service.getProtectedBranchInfo("root", "repo", "branch")
val org = service.getCommitStatus("root","repo", id).get
renameRepository("root","repo","tester","repo2") val orgPbi = service.getProtectedBranchInfo("root", "repo", "branch")
val org = service.getCommitStatus("root","repo", id).get
val neo = service.getCommitStatus("tester","repo2", org.commitId, org.context).get renameRepository("root","repo","tester","repo2")
neo must_==
org.copy( val neo = service.getCommitStatus("tester","repo2", org.commitId, org.context).get
commitStatusId=neo.commitStatusId, assert(neo == org.copy(commitStatusId = neo.commitStatusId, repositoryName = "repo2", userName = "tester"))
repositoryName="repo2", assert(service.getProtectedBranchInfo("tester", "repo2", "branch") == orgPbi.copy(owner = "tester", repository = "repo2"))
userName="tester") }}
service.getProtectedBranchInfo("tester", "repo2", "branch") must_==
orgPbi.copy(owner="tester", repository="repo2")
}}
}
} }

Some files were not shown because too many files have changed in this diff Show More