mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-02 19:45:57 +01:00
Merge branch 'master'
Conflicts: build.sbt src/main/scala/gitbucket/core/model/CommitStatus.scala src/main/scala/gitbucket/core/service/CommitStatusService.scala src/main/scala/gitbucket/core/service/CommitsService.scala src/main/scala/gitbucket/core/service/IssuesService.scala src/main/scala/gitbucket/core/service/RepositoryService.scala src/main/scala/gitbucket/core/util/Notifier.scala
This commit is contained in:
1
.github/CONTRIBUTING.md
vendored
1
.github/CONTRIBUTING.md
vendored
@@ -4,4 +4,3 @@
|
||||
- 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.
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -5,4 +5,4 @@
|
||||
- [] 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
|
||||
- [] [marked as closed using commit message](https://help.github.com/articles/closing-issues-via-commit-messages/) all issue ID that this PR should correct
|
||||
|
||||
@@ -8,4 +8,11 @@ before_script:
|
||||
- sudo apt-get install libaio1
|
||||
- sudo /etc/init.d/mysql stop
|
||||
- sudo /etc/init.d/postgresql stop
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.ivy2/cache
|
||||
- $HOME/.sbt/boot
|
||||
- $HOME/.sbt/launchers
|
||||
- $HOME/.coursier
|
||||
- $HOME/.embedmysql
|
||||
- $HOME/.embedpostgresql
|
||||
|
||||
61
README.md
61
README.md
@@ -2,30 +2,30 @@ GitBucket [](htt
|
||||
=========
|
||||
|
||||
GitBucket is a Git platform powered by Scala offering:
|
||||
- easy installation
|
||||
- high extensibility by plugins
|
||||
- API compatibility with Github
|
||||
- Easy installation
|
||||
- High extensibility by plugins
|
||||
- API compatibility with GitHub
|
||||
|
||||
Features
|
||||
--------
|
||||
The current version of GitBucket provides a basic features below:
|
||||
|
||||
- Public / Private Git repository (http and ssh access)
|
||||
- Repository viewer and online file editing
|
||||
- Wiki
|
||||
- Issues / Pull request
|
||||
- Email notification
|
||||
- Simple user and group management with LDAP integration
|
||||
- GitLFS support
|
||||
- Repository viewer includes online file editor
|
||||
- Issues, Pull request and Wiki for repositories
|
||||
- Activity timeline and email notification
|
||||
- Account and group management with LDAP integration
|
||||
- Plug-in system
|
||||
|
||||
If you want to try the development version of GitBucket, see [Developer's Guide](https://github.com/gitbucket/gitbucket/blob/master/doc/how_to_run.md).
|
||||
If you want to try the development version of GitBucket, see the [Developer's Guide](https://github.com/gitbucket/gitbucket/blob/master/doc/how_to_run.md).
|
||||
|
||||
Installation
|
||||
--------
|
||||
GitBucket requires **Java8**. You have to install beforehand when it's not installed.
|
||||
GitBucket requires **Java8**. You have to install it if it is not already installed.
|
||||
|
||||
1. Download latest **gitbucket.war** from [the release page](https://github.com/gitbucket/gitbucket/releases) and run it by `java -jar gitbucket.war`.
|
||||
2. Access `http://[hostname]:8080/` and logged in with **root** / **root**.
|
||||
1. Download the latest **gitbucket.war** from [the releases page](https://github.com/gitbucket/gitbucket/releases) and run it by `java -jar gitbucket.war`.
|
||||
2. Go to `http://[hostname]:8080/` and log in with **root** / **root**.
|
||||
|
||||
You can specify following options:
|
||||
|
||||
@@ -33,34 +33,47 @@ You can specify following options:
|
||||
- `--prefix=[CONTEXTPATH]`
|
||||
- `--host=[HOSTNAME]`
|
||||
- `--gitbucket.home=[DATA_DIR]`
|
||||
- `--temp_dir=[TEMP_DIR]`
|
||||
|
||||
Of course, you can also deploy gitbucket.war to a servlet container which supports Servlet 3.0 (like Jetty, Tomcat, JBoss, etc)
|
||||
`TEMP_DIR` is used as the [temporary directory for the jetty application context](https://www.eclipse.org/jetty/documentation/9.3.x/ref-temporary-directories.html). This is the directory into which the `gitbucket.war` file is unpacked, the source files are compiled, etc. If given this parameter **must** match the path of an existing directory or the application will quit reporting an error; if not given the path used will be a `tmp` directory inside the gitbucket home.
|
||||
|
||||
About installation on Mac or Windows Server (with IIS), configuration of Apache or Nginx and also integration with other tools or services such as Jenkins or Slack, see [Wiki](https://github.com/gitbucket/gitbucket/wiki).
|
||||
You can also deploy `gitbucket.war` to a servlet container which supports Servlet 3.0 (like Jetty, Tomcat, JBoss, etc)
|
||||
|
||||
To upgrade GitBucket, only replace gitbucket.war after stop GitBucket. All GitBucket data is stored in `HOME/.gitbucket` in default. So if you want to back up GitBucket data, copy this directory to the other disk.
|
||||
For more information about installation on Mac or Windows Server (with IIS), or configuration of Apache or Nginx and also integration with other tools or services such as Jenkins or Slack, see [Wiki](https://github.com/gitbucket/gitbucket/wiki).
|
||||
|
||||
Plug-ins
|
||||
To upgrade GitBucket, replace `gitbucket.war` with the new version, after stopping GitBucket. All GitBucket data is stored in `HOME/.gitbucket` by default. So if you want to back up GitBucket's data, copy this directory to the backup location.
|
||||
|
||||
Plugins
|
||||
--------
|
||||
GitBucket has the plug-in system to extend GitBucket from outside of GitBucket. We are providing some official plug-ins:
|
||||
GitBucket has a plug-in system to allow extensions to GitBucket. We provide some official plug-ins:
|
||||
|
||||
- [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin)
|
||||
- [gitbucket-emoji-plugin](https://github.com/gitbucket/gitbucket-emoji-plugin)
|
||||
|
||||
You can find more plugins made by community at [gitbucket community plugins](http://gitbucket-plugins.github.io/).
|
||||
You can find more plugins made by the community at [GitBucket community plugins](http://gitbucket-plugins.github.io/).
|
||||
|
||||
Support
|
||||
--------
|
||||
|
||||
- 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 API compatibility with GitHub, so we might reject if your request is against it.
|
||||
- If you have any questions about GitBucket, send it to the [gitter room](https://gitter.im/gitbucket/gitbucket) before opening an issue.
|
||||
- Make sure check whether there is the same question or request in the past.
|
||||
- When raise a new issue, write at least the subject in **English**.
|
||||
- We can also provide support in Japanese at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
|
||||
- The first priority of GitBucket is easy installation and API compatibility with GitHub, so we might reject if your request is against it.
|
||||
|
||||
Release Notes
|
||||
-------------
|
||||
## 4.8 - 23 Dec 2016
|
||||
### 4.9 - 29 Jan 2017
|
||||
- GitLFS support
|
||||
- Template for issues and pull requests
|
||||
- Manual label color editing
|
||||
- Account description
|
||||
- `--tmp-dir` option for standalone mode
|
||||
- More APIs for issues
|
||||
- [List issues for a repository](https://developer.github.com/v3/issues/#list-issues-for-a-repository)
|
||||
- [Create an issue](https://developer.github.com/v3/issues/#create-an-issue)
|
||||
|
||||
### 4.8 - 23 Dec 2016
|
||||
- Search for repository names from the global header
|
||||
- Filter repositories on the sidebar of the dashboard
|
||||
- Search issues and wiki
|
||||
|
||||
19
build.sbt
19
build.sbt
@@ -1,6 +1,6 @@
|
||||
val Organization = "io.github.gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val GitBucketVersion = "4.8"
|
||||
val GitBucketVersion = "4.9.0"
|
||||
val ScalatraVersion = "2.5.0"
|
||||
val JettyVersion = "9.3.9.v20160517"
|
||||
|
||||
@@ -22,8 +22,8 @@ resolvers ++= Seq(
|
||||
)
|
||||
libraryDependencies ++= Seq(
|
||||
"org.scala-lang.modules" %% "scala-java8-compat" % "0.8.0",
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.2.201602141800-r",
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.1.2.201602141800-r",
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.6.0.201612231935-r",
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.6.0.201612231935-r",
|
||||
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
||||
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
|
||||
"org.json4s" %% "json4s-jackson" % "3.5.0",
|
||||
@@ -37,7 +37,7 @@ libraryDependencies ++= Seq(
|
||||
"org.apache.sshd" % "apache-sshd" % "1.2.0",
|
||||
"org.apache.tika" % "tika-core" % "1.13",
|
||||
"com.github.takezoe" %% "blocking-slick" % "0.0.3",
|
||||
"joda-time" % "joda-time" % "2.9.6",
|
||||
"joda-time" % "joda-time" % "2.9.6",
|
||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||
"com.h2database" % "h2" % "1.4.192",
|
||||
"mysql" % "mysql-connector-java" % "5.1.39",
|
||||
@@ -47,14 +47,15 @@ libraryDependencies ++= Seq(
|
||||
"com.typesafe" % "config" % "1.3.0",
|
||||
"com.typesafe.akka" %% "akka-actor" % "2.4.12",
|
||||
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.0.0",
|
||||
"com.github.bkromhout" % "java-diff-utils" % "2.1.1",
|
||||
"org.cache2k" % "cache2k-all" % "1.0.0.CR1",
|
||||
"com.github.bkromhout" % "java-diff-utils" % "2.1.1",
|
||||
"org.cache2k" % "cache2k-all" % "1.0.0.CR1",
|
||||
"com.enragedginger" %% "akka-quartz-scheduler" % "1.6.0-akka-2.4.x" exclude("c3p0","c3p0"),
|
||||
"net.coobird" % "thumbnailator" % "0.4.8",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
|
||||
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
|
||||
"junit" % "junit" % "4.12" % "test",
|
||||
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
|
||||
"com.wix" % "wix-embedded-mysql" % "1.0.3" % "test",
|
||||
"com.wix" % "wix-embedded-mysql" % "2.1.4" % "test",
|
||||
"ru.yandex.qatools.embed" % "postgresql-embedded" % "1.14" % "test"
|
||||
)
|
||||
|
||||
@@ -166,9 +167,9 @@ executableKey := {
|
||||
log info s"built executable webapp ${outputFile}"
|
||||
outputFile
|
||||
}
|
||||
publishTo <<= version { (v: String) =>
|
||||
publishTo := {
|
||||
val nexus = "https://oss.sonatype.org/"
|
||||
if (v.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots")
|
||||
if (version.value.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots")
|
||||
else Some("releases" at nexus + "service/local/staging/deploy/maven2")
|
||||
}
|
||||
publishMavenStyle := true
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
RPM spec file and init script for Red Hat Enterprise Linux 6.x.
|
||||
|
||||
To create RPM:
|
||||
|
||||
1. Edit `../../gitbucket.conf` to suit.
|
||||
2. Edit `gitbucket.init` to suit.
|
||||
3. Edit `gitbucket.spec` to suit.
|
||||
|
||||
@@ -5,3 +5,4 @@ addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.3")
|
||||
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.1")
|
||||
addSbtPlugin("fi.gekkio.sbtplugins" % "sbt-jrebel-plugin" % "0.10.0")
|
||||
addSbtPlugin("com.typesafe.sbt" % "sbt-pgp" % "0.8.3")
|
||||
addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-M15")
|
||||
|
||||
1
project/project/plugins.sbt
Normal file
1
project/project/plugins.sbt
Normal file
@@ -0,0 +1 @@
|
||||
addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-M15")
|
||||
@@ -12,6 +12,7 @@ public class JettyLauncher {
|
||||
int port = 8080;
|
||||
InetSocketAddress address = null;
|
||||
String contextPath = "/";
|
||||
String tmpDirPath="";
|
||||
boolean forceHttps = false;
|
||||
|
||||
for(String arg: args) {
|
||||
@@ -24,8 +25,13 @@ public class JettyLauncher {
|
||||
port = Integer.parseInt(dim[1]);
|
||||
} else if(dim[0].equals("--prefix")) {
|
||||
contextPath = dim[1];
|
||||
if(!contextPath.startsWith("/")){
|
||||
contextPath = "/" + contextPath;
|
||||
}
|
||||
} else if(dim[0].equals("--gitbucket.home")){
|
||||
System.setProperty("gitbucket.home", dim[1]);
|
||||
} else if(dim[0].equals("--temp_dir")){
|
||||
tmpDirPath = dim[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,9 +56,21 @@ public class JettyLauncher {
|
||||
|
||||
WebAppContext context = new WebAppContext();
|
||||
|
||||
File tmpDir = new File(getGitBucketHome(), "tmp");
|
||||
if(!tmpDir.exists()){
|
||||
tmpDir.mkdirs();
|
||||
File tmpDir;
|
||||
if(tmpDirPath.equals("")){
|
||||
tmpDir = new File(getGitBucketHome(), "tmp");
|
||||
if(!tmpDir.exists()){
|
||||
tmpDir.mkdirs();
|
||||
}
|
||||
} else {
|
||||
tmpDir = new File(tmpDirPath);
|
||||
if(!tmpDir.exists()){
|
||||
throw new java.io.FileNotFoundException(
|
||||
String.format("temp_dir \"%s\" not found", tmpDirPath));
|
||||
} else if(!tmpDir.isDirectory()) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("temp_dir \"%s\" is not a directory", tmpDirPath));
|
||||
}
|
||||
}
|
||||
context.setTempDirectory(tmpDir);
|
||||
|
||||
|
||||
6
src/main/resources/update/gitbucket-core_4.9.xml
Normal file
6
src/main/resources/update/gitbucket-core_4.9.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<changeSet>
|
||||
<addColumn tableName="ACCOUNT">
|
||||
<column name="DESCRIPTION" type="text" nullable="true" />
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
@@ -24,5 +24,8 @@ object GitBucketCoreModule extends Module("gitbucket-core",
|
||||
new SqlMigration("update/gitbucket-core_4.7.sql")
|
||||
),
|
||||
new Version("4.7.1"),
|
||||
new Version("4.8")
|
||||
new Version("4.8"),
|
||||
new Version("4.9.0",
|
||||
new LiquibaseMigration("update/gitbucket-core_4.9.xml")
|
||||
)
|
||||
)
|
||||
|
||||
11
src/main/scala/gitbucket/core/api/CreateAnIssue.scala
Normal file
11
src/main/scala/gitbucket/core/api/CreateAnIssue.scala
Normal file
@@ -0,0 +1,11 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/issues/#create-an-issue
|
||||
*/
|
||||
case class CreateAnIssue(
|
||||
title: String,
|
||||
body: Option[String],
|
||||
assignees: List[String],
|
||||
milestone: Option[Int],
|
||||
labels: List[String])
|
||||
@@ -29,10 +29,10 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
with AccessTokenService with WebHookService with RepositoryCreationService =>
|
||||
|
||||
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
|
||||
url: Option[String], fileId: Option[String])
|
||||
description: Option[String], url: Option[String], fileId: Option[String])
|
||||
|
||||
case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String,
|
||||
url: Option[String], fileId: Option[String], clearImage: Boolean)
|
||||
description: Option[String], url: Option[String], fileId: Option[String], clearImage: Boolean)
|
||||
|
||||
case class SshKeyForm(title: String, publicKey: String)
|
||||
|
||||
@@ -43,6 +43,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
"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()))),
|
||||
"description" -> trim(label("bio" , optional(text()))),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text())))
|
||||
)(AccountNewForm.apply)
|
||||
@@ -51,6 +52,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
"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")))),
|
||||
"description" -> trim(label("bio" , optional(text()))),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" , boolean()))
|
||||
@@ -65,11 +67,12 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
"note" -> trim(label("Token", text(required, maxlength(100))))
|
||||
)(PersonalTokenForm.apply)
|
||||
|
||||
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)
|
||||
case class NewGroupForm(groupName: String, description: Option[String], url: Option[String], fileId: Option[String], members: String)
|
||||
case class EditGroupForm(groupName: String, description: Option[String], url: Option[String], fileId: Option[String], members: String, clearImage: Boolean)
|
||||
|
||||
val newGroupForm = mapping(
|
||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
|
||||
"description" -> trim(label("Group description", optional(text()))),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"members" -> trim(label("Members" ,text(required, members)))
|
||||
@@ -77,6 +80,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
val editGroupForm = mapping(
|
||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
|
||||
"description" -> trim(label("Group description", optional(text()))),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"members" -> trim(label("Members" ,text(required, members))),
|
||||
@@ -167,6 +171,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
password = form.password.map(sha1).getOrElse(account.password),
|
||||
fullName = form.fullName,
|
||||
mailAddress = form.mailAddress,
|
||||
description = form.description,
|
||||
url = form.url))
|
||||
|
||||
updateImage(userName, form.fileId, form.clearImage)
|
||||
@@ -266,7 +271,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
post("/register", newForm){ form =>
|
||||
if(context.settings.allowAccountRegistration){
|
||||
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url)
|
||||
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.description, form.url)
|
||||
updateImage(form.userName, form.fileId, false)
|
||||
redirect("/signin")
|
||||
} else NotFound()
|
||||
@@ -277,7 +282,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
})
|
||||
|
||||
post("/groups/new", newGroupForm)(usersOnly { form =>
|
||||
createGroup(form.groupName, form.url)
|
||||
createGroup(form.groupName, form.description, form.url)
|
||||
updateGroupMembers(form.groupName, form.members.split(",").map {
|
||||
_.split(":") match {
|
||||
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
||||
@@ -315,7 +320,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
}
|
||||
}.toList){ case (groupName, members) =>
|
||||
getAccountByUserName(groupName, true).map { account =>
|
||||
updateGroup(groupName, form.url, false)
|
||||
updateGroup(groupName, form.description, form.url, false)
|
||||
|
||||
// Update GROUP_MEMBER
|
||||
updateGroupMembers(form.groupName, members)
|
||||
|
||||
@@ -21,10 +21,12 @@ class ApiController extends ApiControllerBase
|
||||
with ProtectedBranchService
|
||||
with IssuesService
|
||||
with LabelsService
|
||||
with MilestonesService
|
||||
with PullRequestService
|
||||
with CommitsService
|
||||
with CommitStatusService
|
||||
with RepositoryCreationService
|
||||
with IssueCreationService
|
||||
with HandleCommentService
|
||||
with WebHookService
|
||||
with WebHookPullRequestService
|
||||
@@ -44,9 +46,11 @@ trait ApiControllerBase extends ControllerBase {
|
||||
with ProtectedBranchService
|
||||
with IssuesService
|
||||
with LabelsService
|
||||
with MilestonesService
|
||||
with PullRequestService
|
||||
with CommitStatusService
|
||||
with RepositoryCreationService
|
||||
with IssueCreationService
|
||||
with HandleCommentService
|
||||
with OwnerAuthenticator
|
||||
with UsersAuthenticator
|
||||
@@ -55,6 +59,13 @@ trait ApiControllerBase extends ControllerBase {
|
||||
with ReadableUsersAuthenticator
|
||||
with WritableUsersAuthenticator =>
|
||||
|
||||
/**
|
||||
* 404 for non-implemented api
|
||||
*/
|
||||
get("/api/v3/*") {
|
||||
NotFound()
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/#root-endpoint
|
||||
*/
|
||||
@@ -175,7 +186,7 @@ trait ApiControllerBase extends ControllerBase {
|
||||
using(Git.open(getRepositoryDir(params("owner"), params("repo")))) { git =>
|
||||
//JsonFormat( (revstr, git.getRepository().resolve(revstr)) )
|
||||
// getRef is deprecated by jgit-4.2. use exactRef() or findRef()
|
||||
val sha = git.getRepository().getRef(revstr).getObjectId().name()
|
||||
val sha = git.getRepository().exactRef(revstr).getObjectId().name()
|
||||
JsonFormat(ApiRef(revstr, ApiObject(sha)))
|
||||
}
|
||||
})
|
||||
@@ -283,6 +294,32 @@ trait ApiControllerBase extends ControllerBase {
|
||||
org.scalatra.NotFound(ApiError("Rate limiting is not enabled."))
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/issues/#list-issues-for-a-repository
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/issues")(referrersOnly { repository =>
|
||||
val page = IssueSearchCondition.page(request)
|
||||
// TODO: more api spec condition
|
||||
val condition = IssueSearchCondition(request)
|
||||
val baseOwner = getAccountByUserName(repository.owner).get
|
||||
|
||||
val issues: List[(Issue, Account)] =
|
||||
searchIssueByApi(
|
||||
condition = condition,
|
||||
offset = (page - 1) * PullRequestLimit,
|
||||
limit = PullRequestLimit,
|
||||
repos = repository.owner -> repository.name
|
||||
)
|
||||
|
||||
JsonFormat(issues.map { case (issue, issueUser) =>
|
||||
ApiIssue(
|
||||
issue = issue,
|
||||
repositoryName = RepositoryName(repository),
|
||||
user = ApiUser(issueUser)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/issues/#get-a-single-issue
|
||||
*/
|
||||
@@ -296,6 +333,29 @@ trait ApiControllerBase extends ControllerBase {
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/issues/#create-an-issue
|
||||
*/
|
||||
post("/api/v3/repos/:owner/:repository/issues")(readableUsersOnly { repository =>
|
||||
if(isIssueEditable(repository)){ // TODO Should this check is provided by authenticator?
|
||||
(for{
|
||||
data <- extractFromJsonBody[CreateAnIssue]
|
||||
loginAccount <- context.loginAccount
|
||||
} yield {
|
||||
val milestone = data.milestone.flatMap(getMilestone(repository.owner, repository.name, _))
|
||||
val issue = createIssue(
|
||||
repository,
|
||||
data.title,
|
||||
data.body,
|
||||
data.assignees.headOption,
|
||||
milestone.map(_.milestoneId),
|
||||
data.labels,
|
||||
loginAccount)
|
||||
JsonFormat(ApiIssue(issue, RepositoryName(repository), ApiUser(loginAccount)))
|
||||
}) getOrElse NotFound()
|
||||
} else Unauthorized()
|
||||
})
|
||||
|
||||
/**
|
||||
* https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue
|
||||
*/
|
||||
|
||||
@@ -9,7 +9,6 @@ import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util._
|
||||
|
||||
import io.github.gitbucket.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.json4s._
|
||||
import org.scalatra._
|
||||
import org.scalatra.i18n._
|
||||
@@ -20,6 +19,8 @@ import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
import net.coobird.thumbnailator.Thumbnails
|
||||
|
||||
|
||||
/**
|
||||
* Provides generic features for controller implementations.
|
||||
@@ -57,7 +58,7 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
// Redirect to dashboard
|
||||
httpResponse.sendRedirect(baseUrl + "/")
|
||||
}
|
||||
} else if(path.startsWith("/git/")){
|
||||
} else if(path.startsWith("/git/") || path.startsWith("/git-lfs/")){
|
||||
// Git repository
|
||||
chain.doFilter(request, response)
|
||||
} else {
|
||||
@@ -225,10 +226,13 @@ trait AccountManagementControllerBase extends ControllerBase {
|
||||
} else {
|
||||
fileId.map { fileId =>
|
||||
val filename = "avatar." + FileUtil.getExtension(session.getAndRemove(Keys.Session.Upload(fileId)).get)
|
||||
FileUtils.moveFile(
|
||||
new java.io.File(getTemporaryDir(session.getId), fileId),
|
||||
new java.io.File(getUserUploadDir(userName), filename)
|
||||
)
|
||||
val uploadDir = getUserUploadDir(userName)
|
||||
if(!uploadDir.exists){
|
||||
uploadDir.mkdirs()
|
||||
}
|
||||
Thumbnails.of(new java.io.File(getTemporaryDir(session.getId), fileId))
|
||||
.size(324, 324)
|
||||
.toFile(new java.io.File(uploadDir, filename))
|
||||
updateAvatarImage(userName, Some(filename))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import gitbucket.core.util.Implicits._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.dircache.DirCache
|
||||
import org.eclipse.jgit.lib.{FileMode, Constants}
|
||||
import org.scalatra
|
||||
import org.scalatra._
|
||||
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem}
|
||||
import org.apache.commons.io.{IOUtils, FileUtils}
|
||||
|
||||
@@ -5,7 +5,7 @@ import gitbucket.core.model.Account
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.{Keys, LDAPUtil, ReferrerAuthenticator, StringUtil, UsersAuthenticator}
|
||||
import gitbucket.core.util.{Keys, LDAPUtil, ReferrerAuthenticator, UsersAuthenticator}
|
||||
import io.github.gitbucket.scalatra.forms._
|
||||
import org.scalatra.Ok
|
||||
|
||||
@@ -49,13 +49,18 @@ trait IndexControllerBase extends ControllerBase {
|
||||
if(redirect.isDefined && redirect.get.startsWith("/")){
|
||||
flash += Keys.Flash.Redirect -> redirect.get
|
||||
}
|
||||
gitbucket.core.html.signin()
|
||||
gitbucket.core.html.signin(flash.get("userName"), flash.get("password"), flash.get("error"))
|
||||
}
|
||||
|
||||
post("/signin", signinForm){ form =>
|
||||
authenticate(context.settings, form.userName, form.password) match {
|
||||
case Some(account) => signin(account)
|
||||
case None => redirect("/signin")
|
||||
case None => {
|
||||
flash += "userName" -> form.userName
|
||||
flash += "password" -> form.password
|
||||
flash += "error" -> "Sorry, your Username and/or Password is incorrect. Please try again."
|
||||
redirect("/signin")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.issues.html
|
||||
import gitbucket.core.service.IssuesService._
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.Implicits._
|
||||
@@ -10,7 +9,7 @@ import gitbucket.core.util._
|
||||
import gitbucket.core.view
|
||||
import gitbucket.core.view.Markdown
|
||||
import io.github.gitbucket.scalatra.forms._
|
||||
import org.scalatra.Ok
|
||||
import org.scalatra.{BadRequest, Ok}
|
||||
|
||||
|
||||
class IssuesController extends IssuesControllerBase
|
||||
@@ -21,6 +20,7 @@ class IssuesController extends IssuesControllerBase
|
||||
with MilestonesService
|
||||
with ActivityService
|
||||
with HandleCommentService
|
||||
with IssueCreationService
|
||||
with ReadableUsersAuthenticator
|
||||
with ReferrerAuthenticator
|
||||
with WritableUsersAuthenticator
|
||||
@@ -36,6 +36,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
with MilestonesService
|
||||
with ActivityService
|
||||
with HandleCommentService
|
||||
with IssueCreationService
|
||||
with ReadableUsersAuthenticator
|
||||
with ReferrerAuthenticator
|
||||
with WritableUsersAuthenticator
|
||||
@@ -91,67 +92,39 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
getAssignableUserNames(owner, name),
|
||||
getMilestonesWithIssueCount(owner, name),
|
||||
getLabels(owner, name),
|
||||
isEditable(repository),
|
||||
isManageable(repository),
|
||||
isIssueEditable(repository),
|
||||
isIssueManageable(repository),
|
||||
repository)
|
||||
} getOrElse NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
|
||||
if(isEditable(repository)){ // TODO Should this check is provided by authenticator?
|
||||
if(isIssueEditable(repository)){ // TODO Should this check is provided by authenticator?
|
||||
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||
html.create(
|
||||
getAssignableUserNames(owner, name),
|
||||
getMilestones(owner, name),
|
||||
getLabels(owner, name),
|
||||
isManageable(repository),
|
||||
isIssueManageable(repository),
|
||||
getContentTemplate(repository, "ISSUE_TEMPLATE"),
|
||||
repository)
|
||||
}
|
||||
} else Unauthorized()
|
||||
})
|
||||
|
||||
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
|
||||
if(isEditable(repository)){ // TODO Should this check is provided by authenticator?
|
||||
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||
val manageable = isManageable(repository)
|
||||
val userName = context.loginAccount.get.userName
|
||||
if(isIssueEditable(repository)){ // TODO Should this check is provided by authenticator?
|
||||
val issue = createIssue(
|
||||
repository,
|
||||
form.title,
|
||||
form.content,
|
||||
form.assignedUserName,
|
||||
form.milestoneId,
|
||||
form.labelNames.toArray.flatMap(_.split(",")),
|
||||
context.loginAccount.get)
|
||||
|
||||
// insert issue
|
||||
val issueId = createIssue(owner, name, userName, form.title, form.content,
|
||||
if (manageable) form.assignedUserName else None,
|
||||
if (manageable) form.milestoneId else None)
|
||||
|
||||
// insert labels
|
||||
if (manageable) {
|
||||
form.labelNames.map { value =>
|
||||
val labels = getLabels(owner, name)
|
||||
value.split(",").foreach { labelName =>
|
||||
labels.find(_.labelName == labelName).map { label =>
|
||||
registerIssueLabel(owner, name, issueId, label.labelId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// record activity
|
||||
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
|
||||
|
||||
getIssue(owner, name, issueId.toString).foreach { issue =>
|
||||
// extract references and create refer comment
|
||||
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)
|
||||
|
||||
// call web hooks
|
||||
callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issue, form.content.getOrElse("")) {
|
||||
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||
}
|
||||
}
|
||||
|
||||
redirect(s"/${owner}/${name}/issues/${issueId}")
|
||||
}
|
||||
redirect(s"/${issue.userName}/${issue.repositoryName}/issues/${issue.issueId}")
|
||||
} else Unauthorized()
|
||||
})
|
||||
|
||||
@@ -327,7 +300,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
handleComment(issue, None, repository, Some("close"))
|
||||
}
|
||||
}
|
||||
case _ => // TODO BadRequest
|
||||
case _ => BadRequest()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -398,27 +371,8 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
countIssue(condition.copy(state = "closed"), false, owner -> repoName),
|
||||
condition,
|
||||
repository,
|
||||
isEditable(repository),
|
||||
isManageable(repository))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether an logged-in user can manage issues.
|
||||
*/
|
||||
private def isManageable(repository: RepositoryInfo)(implicit context: Context): Boolean = {
|
||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether an logged-in user can post issues.
|
||||
*/
|
||||
private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = {
|
||||
repository.repository.options.issuesOption match {
|
||||
case "ALL" => !repository.repository.isPrivate && context.loginAccount.isDefined
|
||||
case "PUBLIC" => hasGuestRole(repository.owner, repository.name, context.loginAccount)
|
||||
case "PRIVATE" => hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||
case "DISABLE" => false
|
||||
isIssueEditable(repository),
|
||||
isIssueManageable(repository))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,5 +382,4 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
private def isEditableContent(owner: String, repository: String, author: String)(implicit context: Context): Boolean = {
|
||||
hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -368,6 +368,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
forkedId,
|
||||
oldId.getName,
|
||||
newId.getName,
|
||||
getContentTemplate(originRepository, "PULL_REQUEST_TEMPLATE"),
|
||||
forkedRepository,
|
||||
originRepository,
|
||||
forkedRepository,
|
||||
@@ -424,7 +425,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
if(editable) {
|
||||
val loginUserName = context.loginAccount.get.userName
|
||||
|
||||
val issueId = createIssue(
|
||||
val issueId = insertIssue(
|
||||
owner = repository.owner,
|
||||
repository = repository.name,
|
||||
loginUser = loginUserName,
|
||||
|
||||
@@ -338,6 +338,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name))
|
||||
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
|
||||
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
|
||||
FileUtils.deleteDirectory(getLfsDir(repository.owner, repository.name))
|
||||
}
|
||||
redirect(s"/${repository.owner}")
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||
import java.io.FileInputStream
|
||||
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
|
||||
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.repo.html
|
||||
@@ -16,9 +17,8 @@ import gitbucket.core.model.{Account, WebHook}
|
||||
import gitbucket.core.service.WebHookService._
|
||||
import gitbucket.core.view
|
||||
import gitbucket.core.view.helpers
|
||||
|
||||
import io.github.gitbucket.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.eclipse.jgit.api.{ArchiveCommand, Git}
|
||||
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
|
||||
import org.eclipse.jgit.dircache.DirCache
|
||||
@@ -255,13 +255,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
val (id, path) = repository.splitPath(multiParams("splat").head)
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
|
||||
getPathObjectId(git, path, revCommit).flatMap { objectId =>
|
||||
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
|
||||
contentType = FileUtil.getMimeType(path)
|
||||
response.setContentLength(loader.getSize.toInt)
|
||||
loader.copyTo(response.outputStream)
|
||||
()
|
||||
}
|
||||
|
||||
getPathObjectId(git, path, revCommit).map { objectId =>
|
||||
responseRawFile(git, objectId, path, repository)
|
||||
} getOrElse NotFound()
|
||||
}
|
||||
})
|
||||
@@ -277,23 +273,62 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
getPathObjectId(git, path, revCommit).map { objectId =>
|
||||
if(raw){
|
||||
// Download (This route is left for backword compatibility)
|
||||
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
|
||||
contentType = FileUtil.getMimeType(path)
|
||||
response.setContentLength(loader.getSize.toInt)
|
||||
loader.copyTo(response.outputStream)
|
||||
()
|
||||
} getOrElse NotFound()
|
||||
responseRawFile(git, objectId, path, repository)
|
||||
} else {
|
||||
html.blob(id, repository, path.split("/").toList,
|
||||
JGitUtil.getContentInfo(git, path, objectId),
|
||||
new JGitUtil.CommitInfo(JGitUtil.getLastModifiedCommit(git, revCommit, path)),
|
||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
|
||||
request.paths(2) == "blame")
|
||||
request.paths(2) == "blame",
|
||||
isLfsFile(git, objectId))
|
||||
}
|
||||
} getOrElse NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
private def isLfsFile(git: Git, objectId: ObjectId): Boolean = {
|
||||
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
|
||||
if(loader.isLarge){
|
||||
false
|
||||
} else {
|
||||
new String(loader.getCachedBytes, "UTF-8").startsWith("version https://git-lfs.github.com/spec/v1")
|
||||
}
|
||||
}.getOrElse(false)
|
||||
}
|
||||
|
||||
private def responseRawFile(git: Git, objectId: ObjectId, path: String,
|
||||
repository: RepositoryService.RepositoryInfo): Unit = {
|
||||
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
|
||||
contentType = FileUtil.getMimeType(path)
|
||||
|
||||
if(loader.isLarge){
|
||||
response.setContentLength(loader.getSize.toInt)
|
||||
loader.copyTo(response.outputStream)
|
||||
} else {
|
||||
val bytes = loader.getCachedBytes
|
||||
val text = new String(bytes, "UTF-8")
|
||||
|
||||
if(text.startsWith("version https://git-lfs.github.com/spec/v1")){
|
||||
// LFS objects
|
||||
val attrs = text.split("\n").map { line =>
|
||||
val dim = line.split(" ")
|
||||
dim(0) -> dim(1)
|
||||
}.toMap
|
||||
|
||||
response.setContentLength(attrs("size").toInt)
|
||||
val oid = attrs("oid").split(":")(1)
|
||||
|
||||
using(new FileInputStream(FileUtil.getLfsFilePath(repository.owner, repository.name, oid))){ in =>
|
||||
IOUtils.copy(in, response.getOutputStream)
|
||||
}
|
||||
} else {
|
||||
response.setContentLength(loader.getSize.toInt)
|
||||
response.getOutputStream.write(bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get("/:owner/:repository/blame/*"){
|
||||
blobRoute.action()
|
||||
}
|
||||
@@ -683,11 +718,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
|
||||
private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): Unit = {
|
||||
val revision = name.stripSuffix(suffix)
|
||||
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
|
||||
if(workDir.exists) {
|
||||
FileUtils.deleteDirectory(workDir)
|
||||
}
|
||||
workDir.mkdirs
|
||||
|
||||
val filename = repository.name + "-" +
|
||||
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix
|
||||
@@ -701,7 +731,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
|
||||
git.archive
|
||||
.setFormat(suffix.tail)
|
||||
.setTree(revCommit.getTree)
|
||||
.setTree(revCommit)
|
||||
.setOutputStream(response.getOutputStream)
|
||||
.call()
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
"user" -> trim(label("SMTP User", optional(text()))),
|
||||
"password" -> trim(label("SMTP Password", optional(text()))),
|
||||
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
|
||||
"starttls" -> trim(label("Enable STARTTLS", optional(boolean()))),
|
||||
"fromAddress" -> trim(label("FROM Address", optional(text()))),
|
||||
"fromName" -> trim(label("FROM Name", optional(text())))
|
||||
)(Smtp.apply)),
|
||||
@@ -77,6 +78,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
"user" -> trim(label("SMTP User", optional(text()))),
|
||||
"password" -> trim(label("SMTP Password", optional(text()))),
|
||||
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
|
||||
"starttls" -> trim(label("Enable STARTTLS", optional(boolean()))),
|
||||
"fromAddress" -> trim(label("FROM Address", optional(text()))),
|
||||
"fromName" -> trim(label("FROM Name", optional(text())))
|
||||
)(Smtp.apply),
|
||||
@@ -89,16 +91,16 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
case class NewUserForm(userName: String, password: String, fullName: String,
|
||||
mailAddress: String, isAdmin: Boolean,
|
||||
url: Option[String], fileId: Option[String])
|
||||
description: Option[String], url: Option[String], fileId: Option[String])
|
||||
|
||||
case class EditUserForm(userName: String, password: Option[String], fullName: String,
|
||||
mailAddress: String, isAdmin: Boolean, url: Option[String],
|
||||
mailAddress: String, isAdmin: Boolean, description: Option[String], url: Option[String],
|
||||
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
|
||||
|
||||
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
case class NewGroupForm(groupName: String, description: Option[String], url: Option[String], fileId: Option[String],
|
||||
members: String)
|
||||
|
||||
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
case class EditGroupForm(groupName: String, description: Option[String], url: Option[String], fileId: Option[String],
|
||||
members: String, clearImage: Boolean, isRemoved: Boolean)
|
||||
|
||||
|
||||
@@ -108,6 +110,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
"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())),
|
||||
"description" -> trim(label("bio" ,optional(text()))),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text())))
|
||||
)(NewUserForm.apply)
|
||||
@@ -118,6 +121,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
"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())),
|
||||
"description" -> trim(label("bio" ,optional(text()))),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||
@@ -126,6 +130,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
val newGroupForm = mapping(
|
||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
|
||||
"description" -> trim(label("Group description", optional(text()))),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"members" -> trim(label("Members" ,text(required, members)))
|
||||
@@ -133,6 +138,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
val editGroupForm = mapping(
|
||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
|
||||
"description" -> trim(label("Group description", optional(text()))),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"members" -> trim(label("Members" ,text(required, members))),
|
||||
@@ -164,7 +170,8 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
post("/admin/system/sendmail", sendMailForm)(adminOnly { form =>
|
||||
try {
|
||||
new Mailer(form.smtp).send(form.testAddress,
|
||||
"Test message from GitBucket", "This is a test message from GitBucket.")
|
||||
"Test message from GitBucket", "This is a test message from GitBucket.",
|
||||
context.loginAccount.get)
|
||||
|
||||
"Test mail has been sent to: " + form.testAddress
|
||||
|
||||
@@ -193,7 +200,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
})
|
||||
|
||||
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
|
||||
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url)
|
||||
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.description, form.url)
|
||||
updateImage(form.userName, form.fileId, false)
|
||||
redirect("/admin/users")
|
||||
})
|
||||
@@ -227,6 +234,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
fullName = form.fullName,
|
||||
mailAddress = form.mailAddress,
|
||||
isAdmin = form.isAdmin,
|
||||
description = form.description,
|
||||
url = form.url,
|
||||
isRemoved = form.isRemoved))
|
||||
|
||||
@@ -241,7 +249,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
})
|
||||
|
||||
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
|
||||
createGroup(form.groupName, form.url)
|
||||
createGroup(form.groupName, form.description, form.url)
|
||||
updateGroupMembers(form.groupName, form.members.split(",").map {
|
||||
_.split(":") match {
|
||||
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
||||
@@ -264,7 +272,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
}
|
||||
}.toList){ case (groupName, members) =>
|
||||
getAccountByUserName(groupName, true).map { account =>
|
||||
updateGroup(groupName, form.url, form.isRemoved)
|
||||
updateGroup(groupName, form.url, form.description, form.isRemoved)
|
||||
|
||||
if(form.isRemoved){
|
||||
// Remove from GROUP_MEMBER
|
||||
|
||||
@@ -19,7 +19,8 @@ trait AccountComponent { self: Profile =>
|
||||
val image = column[String]("IMAGE")
|
||||
val groupAccount = column[Boolean]("GROUP_ACCOUNT")
|
||||
val removed = column[Boolean]("REMOVED")
|
||||
def * = (userName, fullName, mailAddress, password, isAdmin, url.?, registeredDate, updatedDate, lastLoginDate.?, image.?, groupAccount, removed) <> (Account.tupled, Account.unapply)
|
||||
val description = column[String]("DESCRIPTION")
|
||||
def * = (userName, fullName, mailAddress, password, isAdmin, url.?, registeredDate, updatedDate, lastLoginDate.?, image.?, groupAccount, removed, description.?) <> (Account.tupled, Account.unapply)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,5 +36,6 @@ case class Account(
|
||||
lastLoginDate: Option[java.util.Date],
|
||||
image: Option[String],
|
||||
isGroupAccount: Boolean,
|
||||
isRemoved: Boolean
|
||||
isRemoved: Boolean,
|
||||
description: Option[String]
|
||||
)
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
//import scala.slick.lifted.MappedTo
|
||||
//import scala.slick.jdbc._
|
||||
|
||||
trait CommitStatusComponent extends TemplateComponent { self: Profile =>
|
||||
import profile.api._
|
||||
import self._
|
||||
|
||||
@@ -14,13 +14,20 @@ trait AccountService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[AccountService])
|
||||
|
||||
def authenticate(settings: SystemSettings, userName: String, password: String)(implicit s: Session): Option[Account] =
|
||||
if(settings.ldapAuthentication){
|
||||
def authenticate(settings: SystemSettings, userName: String, password: String)(implicit s: Session): Option[Account] = {
|
||||
val account = if (settings.ldapAuthentication) {
|
||||
ldapAuthentication(settings, userName, password)
|
||||
} else {
|
||||
defaultAuthentication(userName, password)
|
||||
}
|
||||
|
||||
if(account.isEmpty){
|
||||
logger.info(s"Failed to authenticate: $userName")
|
||||
}
|
||||
|
||||
account
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate by internal database.
|
||||
*/
|
||||
@@ -61,14 +68,14 @@ trait AccountService {
|
||||
defaultAuthentication(userName, password)
|
||||
}
|
||||
case None => {
|
||||
createAccount(ldapUserInfo.userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None)
|
||||
createAccount(ldapUserInfo.userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None, None)
|
||||
getAccountByUserName(ldapUserInfo.userName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case Left(errorMessage) => {
|
||||
logger.info(s"LDAP Authentication Failed: ${errorMessage}")
|
||||
logger.info(s"LDAP error: ${errorMessage}")
|
||||
defaultAuthentication(userName, password)
|
||||
}
|
||||
}
|
||||
@@ -103,7 +110,7 @@ trait AccountService {
|
||||
} else false
|
||||
}
|
||||
|
||||
def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String])
|
||||
def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, description: Option[String], url: Option[String])
|
||||
(implicit s: Session): Unit =
|
||||
Accounts insert Account(
|
||||
userName = userName,
|
||||
@@ -117,12 +124,13 @@ trait AccountService {
|
||||
lastLoginDate = None,
|
||||
image = None,
|
||||
isGroupAccount = false,
|
||||
isRemoved = false)
|
||||
isRemoved = false,
|
||||
description = description)
|
||||
|
||||
def updateAccount(account: Account)(implicit s: Session): Unit =
|
||||
Accounts
|
||||
.filter { a => a.userName === account.userName.bind }
|
||||
.map { a => (a.password, a.fullName, a.mailAddress, a.isAdmin, a.url.?, a.registeredDate, a.updatedDate, a.lastLoginDate.?, a.removed) }
|
||||
.map { a => (a.password, a.fullName, a.mailAddress, a.isAdmin, a.url.?, a.registeredDate, a.updatedDate, a.lastLoginDate.?, a.removed, a.description.?) }
|
||||
.update (
|
||||
account.password,
|
||||
account.fullName,
|
||||
@@ -132,7 +140,8 @@ trait AccountService {
|
||||
account.registeredDate,
|
||||
currentDate,
|
||||
account.lastLoginDate,
|
||||
account.isRemoved)
|
||||
account.isRemoved,
|
||||
account.description)
|
||||
|
||||
def updateAvatarImage(userName: String, image: Option[String])(implicit s: Session): Unit =
|
||||
Accounts.filter(_.userName === userName.bind).map(_.image.?).update(image)
|
||||
@@ -140,7 +149,7 @@ trait AccountService {
|
||||
def updateLastLoginDate(userName: String)(implicit s: Session): Unit =
|
||||
Accounts.filter(_.userName === userName.bind).map(_.lastLoginDate).update(currentDate)
|
||||
|
||||
def createGroup(groupName: String, url: Option[String])(implicit s: Session): Unit =
|
||||
def createGroup(groupName: String, description: Option[String], url: Option[String])(implicit s: Session): Unit =
|
||||
Accounts insert Account(
|
||||
userName = groupName,
|
||||
password = "",
|
||||
@@ -153,10 +162,13 @@ trait AccountService {
|
||||
lastLoginDate = None,
|
||||
image = None,
|
||||
isGroupAccount = true,
|
||||
isRemoved = false)
|
||||
isRemoved = false,
|
||||
description = description)
|
||||
|
||||
def updateGroup(groupName: String, url: Option[String], removed: Boolean)(implicit s: Session): Unit =
|
||||
Accounts.filter(_.userName === groupName.bind).map(t => t.url.? -> t.removed).update(url, removed)
|
||||
def updateGroup(groupName: String, description: Option[String], url: Option[String], removed: Boolean)(implicit s: Session): Unit =
|
||||
Accounts.filter(_.userName === groupName.bind)
|
||||
.map(t => (t.url.?, t.description.?, t.removed))
|
||||
.update(url, description, removed)
|
||||
|
||||
def updateGroupMembers(groupName: String, members: List[(String, Boolean)])(implicit s: Session): Unit = {
|
||||
GroupMembers.filter(_.groupName === groupName.bind).delete
|
||||
|
||||
@@ -17,76 +17,77 @@ trait HandleCommentService {
|
||||
*/
|
||||
def handleComment(issue: Issue, content: Option[String], repository: RepositoryService.RepositoryInfo, actionOpt: Option[String])
|
||||
(implicit context: Context, s: Session) = {
|
||||
context.loginAccount.flatMap { loginAccount =>
|
||||
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||
val userName = loginAccount.userName
|
||||
|
||||
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||
val userName = context.loginAccount.get.userName
|
||||
|
||||
val (action, recordActivity) = actionOpt
|
||||
.collect {
|
||||
case "close" if(!issue.closed) => true ->
|
||||
(Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
|
||||
case "reopen" if(issue.closed) => false ->
|
||||
(Some("reopen") -> Some(recordReopenIssueActivity _))
|
||||
}
|
||||
.map { case (closed, t) =>
|
||||
updateClosed(owner, name, issue.issueId, closed)
|
||||
t
|
||||
}
|
||||
.getOrElse(None -> None)
|
||||
|
||||
val commentId = (content, action) match {
|
||||
case (None, None) => None
|
||||
case (None, Some(action)) => Some(createComment(owner, name, userName, issue.issueId, action.capitalize, action))
|
||||
case (Some(content), _) => Some(createComment(owner, name, userName, issue.issueId, content, action.map(_+ "_comment").getOrElse("comment")))
|
||||
}
|
||||
|
||||
// record comment activity if comment is entered
|
||||
content foreach {
|
||||
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
|
||||
(owner, name, userName, issue.issueId, _)
|
||||
}
|
||||
recordActivity foreach ( _ (owner, name, userName, issue.issueId, issue.title) )
|
||||
|
||||
// extract references and create refer comment
|
||||
content.map { content =>
|
||||
createReferComment(owner, name, issue, content, context.loginAccount.get)
|
||||
}
|
||||
|
||||
// call web hooks
|
||||
action match {
|
||||
case None => commentId.map { commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) }
|
||||
case Some(act) => {
|
||||
val webHookAction = act match {
|
||||
case "open" => "opened"
|
||||
case "reopen" => "reopened"
|
||||
case "close" => "closed"
|
||||
case _ => act
|
||||
val (action, recordActivity) = actionOpt
|
||||
.collect {
|
||||
case "close" if(!issue.closed) => true ->
|
||||
(Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
|
||||
case "reopen" if(issue.closed) => false ->
|
||||
(Some("reopen") -> Some(recordReopenIssueActivity _))
|
||||
}
|
||||
if (issue.isPullRequest) {
|
||||
callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get)
|
||||
} else {
|
||||
callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get)
|
||||
.map { case (closed, t) =>
|
||||
updateClosed(owner, name, issue.issueId, closed)
|
||||
t
|
||||
}
|
||||
}
|
||||
}
|
||||
.getOrElse(None -> None)
|
||||
|
||||
// notifications
|
||||
Notifier() match {
|
||||
case f =>
|
||||
content foreach {
|
||||
f.toNotify(repository, issue, _){
|
||||
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
|
||||
if(issue.isPullRequest) "pull" else "issues"}/${issue.issueId}#comment-${commentId.get}")
|
||||
val commentId = (content, action) match {
|
||||
case (None, None) => None
|
||||
case (None, Some(action)) => Some(createComment(owner, name, userName, issue.issueId, action.capitalize, action))
|
||||
case (Some(content), _) => Some(createComment(owner, name, userName, issue.issueId, content, action.map(_+ "_comment").getOrElse("comment")))
|
||||
}
|
||||
|
||||
// record comment activity if comment is entered
|
||||
content foreach {
|
||||
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
|
||||
(owner, name, userName, issue.issueId, _)
|
||||
}
|
||||
recordActivity foreach ( _ (owner, name, userName, issue.issueId, issue.title) )
|
||||
|
||||
// extract references and create refer comment
|
||||
content.map { content =>
|
||||
createReferComment(owner, name, issue, content, loginAccount)
|
||||
}
|
||||
|
||||
// call web hooks
|
||||
action match {
|
||||
case None => commentId.map { commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, loginAccount) }
|
||||
case Some(act) => {
|
||||
val webHookAction = act match {
|
||||
case "open" => "opened"
|
||||
case "reopen" => "reopened"
|
||||
case "close" => "closed"
|
||||
case _ => act
|
||||
}
|
||||
if (issue.isPullRequest) {
|
||||
callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, loginAccount)
|
||||
} else {
|
||||
callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, loginAccount)
|
||||
}
|
||||
}
|
||||
action foreach {
|
||||
f.toNotify(repository, issue, _){
|
||||
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issue.issueId}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commentId.map( issue -> _ )
|
||||
// notifications
|
||||
Notifier() match {
|
||||
case f =>
|
||||
content foreach {
|
||||
f.toNotify(repository, issue, _){
|
||||
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
|
||||
if(issue.isPullRequest) "pull" else "issues"}/${issue.issueId}#comment-${commentId.get}")
|
||||
}
|
||||
}
|
||||
action foreach {
|
||||
f.toNotify(repository, issue, _){
|
||||
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issue.issueId}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commentId.map( issue -> _ )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.model.{Account, Issue}
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.util.Notifier
|
||||
import gitbucket.core.util.Implicits._
|
||||
|
||||
// TODO: Merged with IssuesService?
|
||||
trait IssueCreationService {
|
||||
|
||||
self: RepositoryService with WebHookIssueCommentService with LabelsService with IssuesService with ActivityService =>
|
||||
|
||||
def createIssue(repository: RepositoryInfo, title:String, body:Option[String],
|
||||
assignee: Option[String], milestoneId: Option[Int], labelNames: Seq[String],
|
||||
loginAccount: Account)(implicit context: Context, s: Session) : Issue = {
|
||||
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
val userName = loginAccount.userName
|
||||
val manageable = isIssueManageable(repository)
|
||||
|
||||
// insert issue
|
||||
val issueId = insertIssue(owner, name, userName, title, body,
|
||||
if (manageable) assignee else None,
|
||||
if (manageable) milestoneId else None)
|
||||
val issue: Issue = getIssue(owner, name, issueId.toString).get
|
||||
|
||||
// insert labels
|
||||
if (manageable) {
|
||||
val labels = getLabels(owner, name)
|
||||
labelNames.map { labelName =>
|
||||
labels.find(_.labelName == labelName).map { label =>
|
||||
registerIssueLabel(owner, name, issueId, label.labelId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// record activity
|
||||
recordCreateIssueActivity(owner, name, userName, issueId, title)
|
||||
|
||||
// extract references and create refer comment
|
||||
createReferComment(owner, name, issue, title + " " + body.getOrElse(""), loginAccount)
|
||||
|
||||
// call web hooks
|
||||
callIssuesWebHook("opened", repository, issue, context.baseUrl, loginAccount)
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issue, body.getOrElse("")) {
|
||||
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||
}
|
||||
issue
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether an logged-in user can manage issues.
|
||||
*/
|
||||
protected def isIssueManageable(repository: RepositoryInfo)(implicit context: Context, s: Session): Boolean = {
|
||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether an logged-in user can post issues.
|
||||
*/
|
||||
protected def isIssueEditable(repository: RepositoryInfo)(implicit context: Context, s: Session): Boolean = {
|
||||
repository.repository.options.issuesOption match {
|
||||
case "ALL" => !repository.repository.isPrivate && context.loginAccount.isDefined
|
||||
case "PUBLIC" => hasGuestRole(repository.owner, repository.name, context.loginAccount)
|
||||
case "PRIVATE" => hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||
case "DISABLE" => false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ trait IssuesService {
|
||||
else None
|
||||
|
||||
def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) =
|
||||
IssueComments filter (_.byIssue(owner, repository, issueId)) list
|
||||
IssueComments filter (_.byIssue(owner, repository, issueId)) sortBy(_.commentId asc) list
|
||||
|
||||
/** @return IssueComment and commentedUser and Issue */
|
||||
def getCommentsForApi(owner: String, repository: String, issueId: Int)(implicit s: Session): List[(IssueComment, Account, Issue)] =
|
||||
@@ -149,7 +149,19 @@ trait IssuesService {
|
||||
}
|
||||
|
||||
/** for api
|
||||
*
|
||||
* @return (issue, issueUser, commentCount)
|
||||
*/
|
||||
def searchIssueByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)
|
||||
(implicit s: Session): List[(Issue, Account)] = {
|
||||
// get issues and comment count and labels
|
||||
searchIssueQueryBase(condition, false, offset, limit, repos)
|
||||
.join(Accounts).on { case (((t1, t2), i), t3) => t3.userName === t1.openedUserName }
|
||||
.sortBy { case (((t1, t2), i), t3) => i asc }
|
||||
.map { case (((t1, t2), i), t3) => (t1, t3) }
|
||||
.list
|
||||
}
|
||||
|
||||
/** for api
|
||||
* @return (issue, issueUser, commentCount, pullRequest, headRepo, headOwner)
|
||||
*/
|
||||
def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)
|
||||
@@ -232,7 +244,7 @@ trait IssuesService {
|
||||
} exists), condition.mentioned.isDefined)
|
||||
}
|
||||
|
||||
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
|
||||
def insertIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
|
||||
assignedUserName: Option[String], milestoneId: Option[Int],
|
||||
isPullRequest: Boolean = false)(implicit s: Session) =
|
||||
// next id number
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.util.JGitUtil
|
||||
import gitbucket.core.model.{Collaborator, Repository, RepositoryOptions, Account, Role}
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.model.{Account, Collaborator, Repository, RepositoryOptions, Role}
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.model.Profile.profile._
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.model.Profile.dateColumnType
|
||||
import gitbucket.core.util.JGitUtil.FileInfo
|
||||
import org.eclipse.jgit.api.Git
|
||||
|
||||
trait RepositoryService { self: AccountService =>
|
||||
import RepositoryService._
|
||||
@@ -423,6 +426,36 @@ trait RepositoryService { self: AccountService =>
|
||||
}
|
||||
.sortBy(_.userName asc).map(t => t.userName -> t.repositoryName).list
|
||||
|
||||
private val templateExtensions = Seq("md", "markdown")
|
||||
|
||||
/**
|
||||
* Returns content of template set per repository.
|
||||
*
|
||||
* @param repository the repository information
|
||||
* @param fileBaseName the file basename without extension of template
|
||||
* @return The content of template if the repository has it, otherwise empty string.
|
||||
*/
|
||||
def getContentTemplate(repository: RepositoryInfo, fileBaseName: String)(implicit s: Session): String = {
|
||||
val withExtFilenames = templateExtensions.map(extension => s"${fileBaseName.toLowerCase()}.${extension}")
|
||||
|
||||
def choiceTemplate(files: List[FileInfo]): Option[FileInfo] =
|
||||
files.find { f =>
|
||||
f.name.toLowerCase() == fileBaseName
|
||||
}.orElse {
|
||||
files.find(f => withExtFilenames.contains(f.name.toLowerCase()))
|
||||
}
|
||||
|
||||
// Get template file from project root. When didn't find, will lookup default folder.
|
||||
using(Git.open(Directory.getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
choiceTemplate(JGitUtil.getFileList(git, repository.repository.defaultBranch, ".")).orElse {
|
||||
choiceTemplate(JGitUtil.getFileList(git, repository.repository.defaultBranch, ".gitbucket"))
|
||||
}.map { file =>
|
||||
JGitUtil.getContentFromId(git, file.id, true).collect {
|
||||
case bytes if FileUtil.isText(bytes) => StringUtil.convertFromByteArray(bytes)
|
||||
}
|
||||
} getOrElse None
|
||||
} getOrElse ""
|
||||
}
|
||||
}
|
||||
|
||||
object RepositoryService {
|
||||
|
||||
@@ -32,6 +32,7 @@ trait SystemSettingsService {
|
||||
smtp.user.foreach(props.setProperty(SmtpUser, _))
|
||||
smtp.password.foreach(props.setProperty(SmtpPassword, _))
|
||||
smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
|
||||
smtp.starttls.foreach(x => props.setProperty(SmtpStarttls, x.toString))
|
||||
smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _))
|
||||
smtp.fromName.foreach(props.setProperty(SmtpFromName, _))
|
||||
}
|
||||
@@ -87,6 +88,7 @@ trait SystemSettingsService {
|
||||
getOptionValue(props, SmtpUser, None),
|
||||
getOptionValue(props, SmtpPassword, None),
|
||||
getOptionValue[Boolean](props, SmtpSsl, None),
|
||||
getOptionValue[Boolean](props, SmtpStarttls, None),
|
||||
getOptionValue(props, SmtpFromAddress, None),
|
||||
getOptionValue(props, SmtpFromName, None)))
|
||||
} else {
|
||||
@@ -168,6 +170,7 @@ object SystemSettingsService {
|
||||
user: Option[String],
|
||||
password: Option[String],
|
||||
ssl: Option[Boolean],
|
||||
starttls: Option[Boolean],
|
||||
fromAddress: Option[String],
|
||||
fromName: Option[String])
|
||||
|
||||
@@ -176,6 +179,9 @@ object SystemSettingsService {
|
||||
port:Int,
|
||||
genericUser:String)
|
||||
|
||||
case class Lfs(
|
||||
serverUrl: Option[String])
|
||||
|
||||
val DefaultSshPort = 29418
|
||||
val DefaultSmtpPort = 25
|
||||
val DefaultLdapPort = 389
|
||||
@@ -197,6 +203,7 @@ object SystemSettingsService {
|
||||
private val SmtpUser = "smtp.user"
|
||||
private val SmtpPassword = "smtp.password"
|
||||
private val SmtpSsl = "smtp.ssl"
|
||||
private val SmtpStarttls = "smtp.starttls"
|
||||
private val SmtpFromAddress = "smtp.from_address"
|
||||
private val SmtpFromName = "smtp.from_name"
|
||||
private val LdapAuthentication = "ldap_authentication"
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import java.util.Date
|
||||
|
||||
import fr.brouillard.oss.security.xhub.XHub
|
||||
import fr.brouillard.oss.security.xhub.XHub.{XHubConverter, XHubDigest}
|
||||
import gitbucket.core.api._
|
||||
|
||||
@@ -7,8 +7,6 @@ import gitbucket.core.model.Account
|
||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||
import gitbucket.core.service.{AccessTokenService, AccountService, SystemSettingsService}
|
||||
import gitbucket.core.util.{AuthUtil, Keys}
|
||||
import org.scalatra.servlet.ServletApiImplicits._
|
||||
import org.scalatra._
|
||||
|
||||
|
||||
class ApiAuthenticationFilter extends Filter with AccessTokenService with AccountService with SystemSettingsService {
|
||||
|
||||
@@ -6,6 +6,7 @@ import gitbucket.core.plugin.{GitRepositoryFilter, GitRepositoryRouting, PluginR
|
||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||
import gitbucket.core.service.{RepositoryService, AccountService, SystemSettingsService}
|
||||
import gitbucket.core.util.{Keys, Implicits, AuthUtil}
|
||||
import gitbucket.core.model.Profile.profile._
|
||||
import org.slf4j.LoggerFactory
|
||||
import Implicits._
|
||||
|
||||
@@ -70,42 +71,47 @@ class GitAuthenticationFilter extends Filter with RepositoryService with Account
|
||||
|
||||
private def defaultRepository(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain,
|
||||
settings: SystemSettings, isUpdating: Boolean): Unit = {
|
||||
implicit val r = request
|
||||
|
||||
request.paths match {
|
||||
val action = request.paths match {
|
||||
case Array(_, repositoryOwner, repositoryName, _*) =>
|
||||
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", "")) match {
|
||||
case Some(repository) => {
|
||||
if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){
|
||||
chain.doFilter(request, response)
|
||||
} else {
|
||||
val passed = for {
|
||||
auth <- Option(request.getHeader("Authorization"))
|
||||
Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2)
|
||||
account <- authenticate(settings, username, password)
|
||||
} yield if(isUpdating || repository.repository.isPrivate){
|
||||
if(hasDeveloperRole(repository.owner, repository.name, Some(account))){
|
||||
Database() withSession { implicit session =>
|
||||
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", "")) match {
|
||||
case Some(repository) => {
|
||||
val execute = if (!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess) {
|
||||
// Authentication is not required
|
||||
true
|
||||
} else {
|
||||
// Authentication is required
|
||||
val passed = for {
|
||||
auth <- Option(request.getHeader("Authorization"))
|
||||
Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2)
|
||||
account <- authenticate(settings, username, password)
|
||||
} yield if (isUpdating || repository.repository.isPrivate) {
|
||||
if (hasDeveloperRole(repository.owner, repository.name, Some(account))) {
|
||||
request.setAttribute(Keys.Request.UserName, account.userName)
|
||||
true
|
||||
} else false
|
||||
} else true
|
||||
passed.getOrElse(false)
|
||||
}
|
||||
|
||||
if(passed.getOrElse(false)){
|
||||
chain.doFilter(request, response)
|
||||
if (execute) {
|
||||
() => chain.doFilter(request, response)
|
||||
} else {
|
||||
AuthUtil.requireAuth(response)
|
||||
() => AuthUtil.requireAuth(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
case None => {
|
||||
logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.")
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND)
|
||||
case None => () => {
|
||||
logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.")
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND)
|
||||
}
|
||||
}
|
||||
}
|
||||
case _ => {
|
||||
case _ => () => {
|
||||
logger.debug(s"Not enough path arguments: ${request.paths}")
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
action()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package gitbucket.core.servlet
|
||||
|
||||
import java.io.{File, FileInputStream, FileOutputStream}
|
||||
import java.text.MessageFormat
|
||||
import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse}
|
||||
|
||||
import gitbucket.core.util.{FileUtil, StringUtil}
|
||||
import org.apache.commons.io.{FileUtils, IOUtils}
|
||||
import org.json4s.jackson.Serialization._
|
||||
import org.apache.http.HttpStatus
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
|
||||
/**
|
||||
* Provides GitLFS Transfer API
|
||||
* https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md
|
||||
*/
|
||||
class GitLfsTransferServlet extends HttpServlet {
|
||||
|
||||
private implicit val jsonFormats = gitbucket.core.api.JsonFormat.jsonFormats
|
||||
private val LongObjectIdLength = 32
|
||||
private val LongObjectIdStringLength = LongObjectIdLength * 2
|
||||
|
||||
override protected def doGet(req: HttpServletRequest, res: HttpServletResponse): Unit = {
|
||||
for {
|
||||
(owner, repository, oid) <- getPathInfo(req, res) if checkToken(req, oid)
|
||||
} yield {
|
||||
val file = new File(FileUtil.getLfsFilePath(owner, repository, oid))
|
||||
if(file.exists()){
|
||||
res.setStatus(HttpStatus.SC_OK)
|
||||
res.setContentType("application/octet-stream")
|
||||
res.setContentLength(file.length.toInt)
|
||||
using(new FileInputStream(file), res.getOutputStream){ (in, out) =>
|
||||
IOUtils.copy(in, out)
|
||||
out.flush()
|
||||
}
|
||||
} else {
|
||||
sendError(res, HttpStatus.SC_NOT_FOUND,
|
||||
MessageFormat.format("Object ''{0}'' not found", oid))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override protected def doPut(req: HttpServletRequest, res: HttpServletResponse): Unit = {
|
||||
for {
|
||||
(owner, repository, oid) <- getPathInfo(req, res) if checkToken(req, oid)
|
||||
} yield {
|
||||
val file = new File(FileUtil.getLfsFilePath(owner, repository, oid))
|
||||
FileUtils.forceMkdir(file.getParentFile)
|
||||
using(req.getInputStream, new FileOutputStream(file)){ (in, out) =>
|
||||
IOUtils.copy(in, out)
|
||||
}
|
||||
res.setStatus(HttpStatus.SC_OK)
|
||||
}
|
||||
}
|
||||
|
||||
private def checkToken(req: HttpServletRequest, oid: String): Boolean = {
|
||||
val token = req.getHeader("Authorization")
|
||||
if(token != null){
|
||||
val Array(expireAt, targetOid) = StringUtil.decodeBlowfish(token).split(" ")
|
||||
oid == targetOid && expireAt.toLong > System.currentTimeMillis
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private def getPathInfo(req: HttpServletRequest, res: HttpServletResponse): Option[(String, String, String)] = {
|
||||
req.getRequestURI.substring(1).split("/").reverse match {
|
||||
case Array(oid, repository, owner, _*) => Some((owner, repository, oid))
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
private def sendError(res: HttpServletResponse, status: Int, message: String): Unit = {
|
||||
res.setStatus(status)
|
||||
using(res.getWriter()){ out =>
|
||||
out.write(write(GitLfs.Error(message)))
|
||||
out.flush()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package gitbucket.core.servlet
|
||||
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
|
||||
import gitbucket.core.api
|
||||
import gitbucket.core.model.{Session, WebHook}
|
||||
import gitbucket.core.model.WebHook
|
||||
import gitbucket.core.model.Profile.profile._
|
||||
import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
|
||||
import gitbucket.core.service.IssuesService.IssueSearchCondition
|
||||
import gitbucket.core.service.WebHookService._
|
||||
@@ -11,16 +13,16 @@ import gitbucket.core.service._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util._
|
||||
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.http.server.GitServlet
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.eclipse.jgit.transport._
|
||||
import org.eclipse.jgit.transport.resolver._
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import javax.servlet.ServletConfig
|
||||
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
|
||||
|
||||
import org.json4s.jackson.Serialization._
|
||||
|
||||
|
||||
/**
|
||||
@@ -32,6 +34,7 @@ import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||
class GitRepositoryServlet extends GitServlet with SystemSettingsService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet])
|
||||
private implicit val jsonFormats = gitbucket.core.api.JsonFormat.jsonFormats
|
||||
|
||||
override def init(config: ServletConfig): Unit = {
|
||||
setReceivePackFactory(new GitBucketReceivePackFactory())
|
||||
@@ -45,15 +48,75 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
|
||||
override def service(req: HttpServletRequest, res: HttpServletResponse): Unit = {
|
||||
val agent = req.getHeader("USER-AGENT")
|
||||
val index = req.getRequestURI.indexOf(".git")
|
||||
if(index >= 0 && (agent == null || agent.toLowerCase.indexOf("git/") < 0)){
|
||||
if(index >= 0 && (agent == null || agent.toLowerCase.indexOf("git") < 0)){
|
||||
// redirect for browsers
|
||||
val paths = req.getRequestURI.substring(0, index).split("/")
|
||||
res.sendRedirect(baseUrl(req) + "/" + paths.dropRight(1).last + "/" + paths.last)
|
||||
|
||||
} else if(req.getMethod.toUpperCase == "POST" && req.getRequestURI.endsWith("/info/lfs/objects/batch")){
|
||||
serviceGitLfsBatchAPI(req, res)
|
||||
|
||||
} else {
|
||||
// response for git client
|
||||
super.service(req, res)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides GitLFS Batch API
|
||||
* https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
|
||||
*/
|
||||
protected def serviceGitLfsBatchAPI(req: HttpServletRequest, res: HttpServletResponse): Unit = {
|
||||
val batchRequest = read[GitLfs.BatchRequest](req.getInputStream)
|
||||
val settings = loadSystemSettings()
|
||||
|
||||
settings.baseUrl match {
|
||||
case None => {
|
||||
throw new IllegalStateException("lfs.server_url is not configured.")
|
||||
}
|
||||
case Some(baseUrl) => {
|
||||
val index = req.getRequestURI.indexOf(".git")
|
||||
if(index >= 0){
|
||||
req.getRequestURI.substring(0, index).split("/").reverse match {
|
||||
case Array(repository, owner, _*) =>
|
||||
val timeout = System.currentTimeMillis + (60000 * 10) // 10 min.
|
||||
val batchResponse = batchRequest.operation match {
|
||||
case "upload" =>
|
||||
GitLfs.BatchUploadResponse("basic", batchRequest.objects.map { requestObject =>
|
||||
GitLfs.BatchResponseObject(requestObject.oid, requestObject.size, true,
|
||||
GitLfs.Actions(
|
||||
upload = Some(GitLfs.Action(
|
||||
href = baseUrl + "/git-lfs/" + owner + "/" + repository + "/" + requestObject.oid,
|
||||
header = Map("Authorization" -> StringUtil.encodeBlowfish(timeout + " " + requestObject.oid)),
|
||||
expires_at = new Date(timeout)
|
||||
))
|
||||
)
|
||||
)
|
||||
})
|
||||
case "download" =>
|
||||
GitLfs.BatchUploadResponse("basic", batchRequest.objects.map { requestObject =>
|
||||
GitLfs.BatchResponseObject(requestObject.oid, requestObject.size, true,
|
||||
GitLfs.Actions(
|
||||
download = Some(GitLfs.Action(
|
||||
href = baseUrl + "/git-lfs/" + owner + "/" + repository + "/" + requestObject.oid,
|
||||
header = Map("Authorization" -> StringUtil.encodeBlowfish(timeout + " " + requestObject.oid)),
|
||||
expires_at = new Date(timeout)
|
||||
))
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
res.setContentType("application/vnd.git-lfs+json")
|
||||
using(res.getWriter){ out =>
|
||||
out.print(write(batchResponse))
|
||||
out.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GitBucketRepositoryResolver(parent: FileResolver[HttpServletRequest]) extends RepositoryResolver[HttpServletRequest] {
|
||||
@@ -107,7 +170,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: Session)
|
||||
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)/*(implicit session: Session)*/
|
||||
extends PostReceiveHook with PreReceiveHook
|
||||
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService
|
||||
with WebHookPullRequestService with CommitsService {
|
||||
@@ -116,119 +179,165 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
private var existIds: Seq[String] = Nil
|
||||
|
||||
def onPreReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
|
||||
try {
|
||||
commands.asScala.foreach { command =>
|
||||
// call pre-commit hook
|
||||
PluginRegistry().getReceiveHooks
|
||||
.flatMap(_.preReceive(owner, repository, receivePack, command, pusher))
|
||||
.headOption.foreach { error =>
|
||||
command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, error)
|
||||
Database() withTransaction { implicit session =>
|
||||
try {
|
||||
commands.asScala.foreach { command =>
|
||||
// call pre-commit hook
|
||||
PluginRegistry().getReceiveHooks
|
||||
.flatMap(_.preReceive(owner, repository, receivePack, command, pusher))
|
||||
.headOption.foreach { error =>
|
||||
command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, error)
|
||||
}
|
||||
}
|
||||
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
|
||||
existIds = JGitUtil.getAllCommitIds(git)
|
||||
}
|
||||
} catch {
|
||||
case ex: Exception => {
|
||||
logger.error(ex.toString, ex)
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
|
||||
existIds = JGitUtil.getAllCommitIds(git)
|
||||
}
|
||||
} catch {
|
||||
case ex: Exception => {
|
||||
logger.error(ex.toString, ex)
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
|
||||
try {
|
||||
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
|
||||
JGitUtil.removeCache(git)
|
||||
Database() withTransaction { implicit session =>
|
||||
try {
|
||||
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
|
||||
JGitUtil.removeCache(git)
|
||||
|
||||
val pushedIds = scala.collection.mutable.Set[String]()
|
||||
commands.asScala.foreach { command =>
|
||||
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
|
||||
implicit val apiContext = api.JsonFormat.Context(baseUrl)
|
||||
val refName = command.getRefName.split("/")
|
||||
val branchName = refName.drop(2).mkString("/")
|
||||
val commits = if (refName(1) == "tags") {
|
||||
Nil
|
||||
} else {
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.DELETE => Nil
|
||||
case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve all issue count in the repository
|
||||
val issueCount =
|
||||
countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
|
||||
countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
|
||||
|
||||
val repositoryInfo = getRepository(owner, repository).get
|
||||
|
||||
// Extract new commit and apply issue comment
|
||||
val defaultBranch = repositoryInfo.repository.defaultBranch
|
||||
val newCommits = commits.flatMap { commit =>
|
||||
if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) {
|
||||
if (issueCount > 0) {
|
||||
pushedIds.add(commit.id)
|
||||
createIssueComment(owner, repository, commit)
|
||||
// close issues
|
||||
if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
|
||||
closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository)
|
||||
}
|
||||
val pushedIds = scala.collection.mutable.Set[String]()
|
||||
commands.asScala.foreach { command =>
|
||||
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
|
||||
implicit val apiContext = api.JsonFormat.Context(baseUrl)
|
||||
val refName = command.getRefName.split("/")
|
||||
val branchName = refName.drop(2).mkString("/")
|
||||
val commits = if (refName(1) == "tags") {
|
||||
Nil
|
||||
} else {
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.DELETE => Nil
|
||||
case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
|
||||
}
|
||||
Some(commit)
|
||||
} else None
|
||||
}
|
||||
|
||||
// record activity
|
||||
if(refName(1) == "heads"){
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, pusher, branchName)
|
||||
case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, pusher, branchName, newCommits)
|
||||
case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, pusher, branchName)
|
||||
case _ =>
|
||||
}
|
||||
} else if(refName(1) == "tags"){
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, pusher, branchName, newCommits)
|
||||
case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, pusher, branchName, newCommits)
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
if(refName(1) == "heads"){
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.CREATE |
|
||||
ReceiveCommand.Type.UPDATE |
|
||||
ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
|
||||
updatePullRequests(owner, repository, branchName)
|
||||
getAccountByUserName(pusher).map{ pusherAccount =>
|
||||
callPullRequestWebHookByRequestBranch("synchronize", repositoryInfo, branchName, baseUrl, pusherAccount)
|
||||
// Retrieve all issue count in the repository
|
||||
val issueCount =
|
||||
countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
|
||||
countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
|
||||
|
||||
val repositoryInfo = getRepository(owner, repository).get
|
||||
|
||||
// Extract new commit and apply issue comment
|
||||
val defaultBranch = repositoryInfo.repository.defaultBranch
|
||||
val newCommits = commits.flatMap { commit =>
|
||||
if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) {
|
||||
if (issueCount > 0) {
|
||||
pushedIds.add(commit.id)
|
||||
createIssueComment(owner, repository, commit)
|
||||
// close issues
|
||||
if (refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE) {
|
||||
closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository)
|
||||
}
|
||||
}
|
||||
case _ =>
|
||||
Some(commit)
|
||||
} else None
|
||||
}
|
||||
}
|
||||
|
||||
// call web hook
|
||||
callWebHookOf(owner, repository, WebHook.Push){
|
||||
for(pusherAccount <- getAccountByUserName(pusher);
|
||||
ownerAccount <- getAccountByUserName(owner)) yield {
|
||||
WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount,
|
||||
newId = command.getNewId(), oldId = command.getOldId())
|
||||
// record activity
|
||||
if (refName(1) == "heads") {
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, pusher, branchName)
|
||||
case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, pusher, branchName, newCommits)
|
||||
case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, pusher, branchName)
|
||||
case _ =>
|
||||
}
|
||||
} else if (refName(1) == "tags") {
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, pusher, branchName, newCommits)
|
||||
case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, pusher, branchName, newCommits)
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// call post-commit hook
|
||||
PluginRegistry().getReceiveHooks.foreach(_.postReceive(owner, repository, receivePack, command, pusher))
|
||||
if (refName(1) == "heads") {
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.CREATE |
|
||||
ReceiveCommand.Type.UPDATE |
|
||||
ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
|
||||
updatePullRequests(owner, repository, branchName)
|
||||
getAccountByUserName(pusher).map { pusherAccount =>
|
||||
callPullRequestWebHookByRequestBranch("synchronize", repositoryInfo, branchName, baseUrl, pusherAccount)
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
// call web hook
|
||||
callWebHookOf(owner, repository, WebHook.Push) {
|
||||
for (pusherAccount <- getAccountByUserName(pusher);
|
||||
ownerAccount <- getAccountByUserName(owner)) yield {
|
||||
WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount,
|
||||
newId = command.getNewId(), oldId = command.getOldId())
|
||||
}
|
||||
}
|
||||
|
||||
// call post-commit hook
|
||||
PluginRegistry().getReceiveHooks.foreach(_.postReceive(owner, repository, receivePack, command, pusher))
|
||||
}
|
||||
}
|
||||
// update repository last modified time.
|
||||
updateLastActivityDate(owner, repository)
|
||||
} catch {
|
||||
case ex: Exception => {
|
||||
logger.error(ex.toString, ex)
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
// update repository last modified time.
|
||||
updateLastActivityDate(owner, repository)
|
||||
} catch {
|
||||
case ex: Exception => {
|
||||
logger.error(ex.toString, ex)
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object GitLfs {
|
||||
|
||||
case class BatchRequest(
|
||||
operation: String,
|
||||
transfers: Seq[String],
|
||||
objects: Seq[BatchRequestObject]
|
||||
)
|
||||
|
||||
case class BatchRequestObject(
|
||||
oid: String,
|
||||
size: Long
|
||||
)
|
||||
|
||||
case class BatchUploadResponse(
|
||||
transfer: String,
|
||||
objects: Seq[BatchResponseObject]
|
||||
)
|
||||
|
||||
case class BatchResponseObject(
|
||||
oid: String,
|
||||
size: Long,
|
||||
authenticated: Boolean,
|
||||
actions: Actions
|
||||
)
|
||||
|
||||
case class Actions(
|
||||
download: Option[Action] = None,
|
||||
upload: Option[Action] = None
|
||||
)
|
||||
|
||||
case class Action(
|
||||
href: String,
|
||||
header: Map[String, String] = Map.empty,
|
||||
expires_at: Date
|
||||
)
|
||||
|
||||
case class Error(
|
||||
message: String
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -22,8 +22,9 @@ class TransactionFilter extends Filter {
|
||||
def destroy(): Unit = {}
|
||||
|
||||
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
|
||||
if(req.asInstanceOf[HttpServletRequest].getServletPath().startsWith("/assets/")){
|
||||
// assets don't need transaction
|
||||
val servletPath = req.asInstanceOf[HttpServletRequest].getServletPath()
|
||||
if(servletPath.startsWith("/assets/") || servletPath == "/git" || servletPath == "/git-lfs"){
|
||||
// assets and git-lfs don't need transaction
|
||||
chain.doFilter(req, res)
|
||||
} else {
|
||||
Database() withTransaction { session =>
|
||||
|
||||
@@ -31,24 +31,22 @@ abstract class GitCommand extends Command with SessionAware {
|
||||
@volatile protected var callback: ExitCallback = null
|
||||
@volatile private var authUser:Option[String] = None
|
||||
|
||||
protected def runTask(authUser: String)(implicit session: Session): Unit
|
||||
protected def runTask(authUser: String): Unit
|
||||
|
||||
private def newTask(): Runnable = new Runnable {
|
||||
override def run(): Unit = {
|
||||
authUser match {
|
||||
case Some(authUser) =>
|
||||
Database() withTransaction { implicit session =>
|
||||
try {
|
||||
runTask(authUser)
|
||||
callback.onExit(0)
|
||||
} catch {
|
||||
case e: RepositoryNotFoundException =>
|
||||
logger.info(e.getMessage)
|
||||
callback.onExit(1, "Repository Not Found")
|
||||
case e: Throwable =>
|
||||
logger.error(e.getMessage, e)
|
||||
callback.onExit(1)
|
||||
}
|
||||
try {
|
||||
runTask(authUser)
|
||||
callback.onExit(0)
|
||||
} catch {
|
||||
case e: RepositoryNotFoundException =>
|
||||
logger.info(e.getMessage)
|
||||
callback.onExit(1, "Repository Not Found")
|
||||
case e: Throwable =>
|
||||
logger.error(e.getMessage, e)
|
||||
callback.onExit(1)
|
||||
}
|
||||
case None =>
|
||||
val message = "User not authenticated"
|
||||
@@ -103,14 +101,18 @@ abstract class DefaultGitCommand(val owner: String, val repoName: String) extend
|
||||
class DefaultGitUploadPack(owner: String, repoName: String) extends DefaultGitCommand(owner, repoName)
|
||||
with RepositoryService with AccountService {
|
||||
|
||||
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
||||
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).foreach { repositoryInfo =>
|
||||
if(!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)){
|
||||
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
|
||||
val repository = git.getRepository
|
||||
val upload = new UploadPack(repository)
|
||||
upload.upload(in, out, err)
|
||||
}
|
||||
override protected def runTask(user: String): Unit = {
|
||||
val execute = Database() withSession { implicit session =>
|
||||
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).map { repositoryInfo =>
|
||||
!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)
|
||||
}.getOrElse(false)
|
||||
}
|
||||
|
||||
if(execute){
|
||||
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
|
||||
val repository = git.getRepository
|
||||
val upload = new UploadPack(repository)
|
||||
upload.upload(in, out, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,19 +121,23 @@ class DefaultGitUploadPack(owner: String, repoName: String) extends DefaultGitCo
|
||||
class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) extends DefaultGitCommand(owner, repoName)
|
||||
with RepositoryService with AccountService {
|
||||
|
||||
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
||||
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).foreach { repositoryInfo =>
|
||||
if(isWritableUser(user, repositoryInfo)){
|
||||
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
|
||||
val repository = git.getRepository
|
||||
val receive = new ReceivePack(repository)
|
||||
if(!repoName.endsWith(".wiki")){
|
||||
val hook = new CommitLogHook(owner, repoName, user, baseUrl)
|
||||
receive.setPreReceiveHook(hook)
|
||||
receive.setPostReceiveHook(hook)
|
||||
}
|
||||
receive.receive(in, out, err)
|
||||
override protected def runTask(user: String): Unit = {
|
||||
val execute = Database() withSession { implicit session =>
|
||||
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).map { repositoryInfo =>
|
||||
isWritableUser(user, repositoryInfo)
|
||||
}.getOrElse(false)
|
||||
}
|
||||
|
||||
if(execute) {
|
||||
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
|
||||
val repository = git.getRepository
|
||||
val receive = new ReceivePack(repository)
|
||||
if (!repoName.endsWith(".wiki")) {
|
||||
val hook = new CommitLogHook(owner, repoName, user, baseUrl)
|
||||
receive.setPreReceiveHook(hook)
|
||||
receive.setPostReceiveHook(hook)
|
||||
}
|
||||
receive.receive(in, out, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,8 +146,12 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) ex
|
||||
class PluginGitUploadPack(repoName: String, routing: GitRepositoryRouting) extends GitCommand
|
||||
with SystemSettingsService {
|
||||
|
||||
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
||||
if(routing.filter.filter("/" + repoName, Some(user), loadSystemSettings(), false)){
|
||||
override protected def runTask(user: String): Unit = {
|
||||
val execute = Database() withSession { implicit session =>
|
||||
routing.filter.filter("/" + repoName, Some(user), loadSystemSettings(), false)
|
||||
}
|
||||
|
||||
if(execute){
|
||||
val path = routing.urlPattern.r.replaceFirstIn(repoName, routing.localPath)
|
||||
using(Git.open(new File(Directory.GitBucketHome, path))){ git =>
|
||||
val repository = git.getRepository
|
||||
@@ -155,8 +165,11 @@ class PluginGitUploadPack(repoName: String, routing: GitRepositoryRouting) exten
|
||||
class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting) extends GitCommand
|
||||
with SystemSettingsService {
|
||||
|
||||
override protected def runTask(user: String)(implicit session: Session): Unit = {
|
||||
if(routing.filter.filter("/" + repoName, Some(user), loadSystemSettings(), true)){
|
||||
override protected def runTask(user: String): Unit = {
|
||||
val execute = Database() withSession { implicit session =>
|
||||
routing.filter.filter("/" + repoName, Some(user), loadSystemSettings(), true)
|
||||
}
|
||||
if(execute){
|
||||
val path = routing.urlPattern.r.replaceFirstIn(repoName, routing.localPath)
|
||||
using(Git.open(new File(Directory.GitBucketHome, path))){ git =>
|
||||
val repository = git.getRepository
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package gitbucket.core.ssh
|
||||
|
||||
import gitbucket.core.service.SystemSettingsService
|
||||
import gitbucket.core.service.SystemSettingsService.SshAddress
|
||||
import org.apache.sshd.common.Factory
|
||||
import org.apache.sshd.server.{Environment, ExitCallback, Command}
|
||||
|
||||
@@ -20,6 +20,20 @@ object ControlUtil {
|
||||
}
|
||||
}
|
||||
|
||||
def using[A <% { def close(): Unit }, B <% { def close(): Unit }, C](resource1: A, resource2: B)(f: (A, B) => C): C =
|
||||
try f(resource1, resource2) finally {
|
||||
if(resource1 != null){
|
||||
ignoring(classOf[Throwable]) {
|
||||
resource1.close()
|
||||
}
|
||||
}
|
||||
if(resource2 != null){
|
||||
ignoring(classOf[Throwable]) {
|
||||
resource2.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def using[T](git: Git)(f: Git => T): T =
|
||||
try f(git) finally git.getRepository.close()
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package gitbucket.core.util
|
||||
|
||||
import java.io.File
|
||||
import ControlUtil._
|
||||
import org.apache.commons.io.FileUtils
|
||||
|
||||
/**
|
||||
* Provides directories used by GitBucket.
|
||||
* Provides directory locations used by GitBucket.
|
||||
*/
|
||||
object Directory {
|
||||
|
||||
@@ -50,6 +48,12 @@ object Directory {
|
||||
def getAttachedDir(owner: String, repository: String): File =
|
||||
new File(s"${RepositoryHome}/${owner}/${repository}/comments")
|
||||
|
||||
/**
|
||||
* Directory for files which are attached to issue.
|
||||
*/
|
||||
def getLfsDir(owner: String, repository: String): File =
|
||||
new File(s"${RepositoryHome}/${owner}/${repository}/lfs")
|
||||
|
||||
/**
|
||||
* Directory for uploaded files by the specified user.
|
||||
*/
|
||||
@@ -72,12 +76,6 @@ object Directory {
|
||||
*/
|
||||
def getPluginCacheDir(): File = new File(s"${TemporaryHome}/_plugins")
|
||||
|
||||
/**
|
||||
* Temporary directory which is used to create an archive to download repository contents.
|
||||
*/
|
||||
def getDownloadWorkDir(owner: String, repository: String, sessionId: String): File =
|
||||
new File(getTemporaryDir(owner, repository), s"download/${sessionId}")
|
||||
|
||||
/**
|
||||
* Substance directory of the wiki repository.
|
||||
*/
|
||||
|
||||
@@ -62,4 +62,8 @@ object FileUtil {
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"text/plain")
|
||||
|
||||
def getLfsFilePath(owner: String, repository: String, oid: String): String =
|
||||
Directory.getLfsDir(owner, repository) + "/" + oid
|
||||
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ object Implicits {
|
||||
|
||||
implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context = JsonFormat.Context(context.baseUrl)
|
||||
|
||||
implicit class RichSeq[A](seq: Seq[A]) {
|
||||
implicit class RichSeq[A](private val seq: Seq[A]) extends AnyVal {
|
||||
|
||||
def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition)
|
||||
|
||||
@@ -40,7 +40,7 @@ object Implicits {
|
||||
}
|
||||
}
|
||||
|
||||
implicit class RichString(value: String){
|
||||
implicit class RichString(private val value: String) extends AnyVal {
|
||||
def replaceBy(regex: Regex)(replace: Regex.MatchData => Option[String]): String = {
|
||||
val sb = new StringBuilder()
|
||||
var i = 0
|
||||
@@ -63,7 +63,7 @@ object Implicits {
|
||||
}
|
||||
}
|
||||
|
||||
implicit class RichRequest(request: HttpServletRequest){
|
||||
implicit class RichRequest(private val request: HttpServletRequest) extends AnyVal {
|
||||
|
||||
def paths: Array[String] = (request.getRequestURI.substring(request.getContextPath.length + 1) match{
|
||||
case path if path.startsWith("api/v3/repos/") => path.substring(13/* "/api/v3/repos".length */)
|
||||
@@ -84,7 +84,7 @@ object Implicits {
|
||||
}
|
||||
}
|
||||
|
||||
implicit class RichSession(session: HttpSession){
|
||||
implicit class RichSession(private val session: HttpSession) extends AnyVal {
|
||||
def getAndRemove[T](key: String): Option[T] = {
|
||||
val value = session.getAttribute(key).asInstanceOf[T]
|
||||
if(value == null){
|
||||
|
||||
@@ -16,7 +16,7 @@ import scala.collection.mutable.ListBuffer
|
||||
*/
|
||||
object JDBCUtil {
|
||||
|
||||
implicit class RichConnection(conn: Connection){
|
||||
implicit class RichConnection(private val conn: Connection) extends AnyVal {
|
||||
|
||||
def update(sql: String, params: Any*): Int = {
|
||||
execute(sql, params: _*){ stmt =>
|
||||
@@ -214,8 +214,6 @@ object JDBCUtil {
|
||||
tsort(edges).toSeq
|
||||
}
|
||||
|
||||
case class TableDependency(tableName: String, children: Seq[String])
|
||||
|
||||
|
||||
def tsort[A](edges: Traversable[(A, A)]): Iterable[A] = {
|
||||
@tailrec
|
||||
@@ -236,4 +234,6 @@ object JDBCUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private case class TableDependency(tableName: String, children: Seq[String])
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package gitbucket.core.util
|
||||
|
||||
import gitbucket.core.model.{Session, Issue}
|
||||
import gitbucket.core.model.{Session, Issue, Account}
|
||||
import gitbucket.core.model.Profile.profile._
|
||||
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, SystemSettingsService}
|
||||
import gitbucket.core.servlet.Database
|
||||
@@ -11,16 +11,16 @@ import scala.util.{Success, Failure}
|
||||
import ExecutionContext.Implicits.global
|
||||
import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import gitbucket.core.controller.Context
|
||||
import SystemSettingsService.Smtp
|
||||
import ControlUtil.defining
|
||||
|
||||
trait Notifier extends RepositoryService with AccountService with IssuesService {
|
||||
|
||||
def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)
|
||||
(msg: String => String)(implicit context: Context): Unit
|
||||
|
||||
protected def recipients(issue: Issue)(notify: String => Unit)(implicit session: Session, context: Context) =
|
||||
protected def recipients(issue: Issue, loginAccount: Account)(notify: String => Unit)(implicit session: Session) =
|
||||
(
|
||||
// individual repository's owner
|
||||
issue.userName ::
|
||||
@@ -33,9 +33,13 @@ trait Notifier extends RepositoryService with AccountService with IssuesService
|
||||
getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName)
|
||||
)
|
||||
.distinct
|
||||
.withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded
|
||||
.foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) filterNot (LDAPUtil.isDummyMailAddress(_)) foreach (x => notify(x.mailAddress)) )
|
||||
|
||||
.withFilter ( _ != loginAccount.userName ) // the operation in person is excluded
|
||||
.foreach (
|
||||
getAccountByUserName(_)
|
||||
.filterNot (_.isGroupAccount)
|
||||
.filterNot (LDAPUtil.isDummyMailAddress(_))
|
||||
.foreach (x => notify(x.mailAddress))
|
||||
)
|
||||
}
|
||||
|
||||
object Notifier {
|
||||
@@ -72,35 +76,36 @@ class Mailer(private val smtp: Smtp) extends Notifier {
|
||||
private val logger = LoggerFactory.getLogger(classOf[Mailer])
|
||||
|
||||
def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)
|
||||
(msg: String => String)(implicit context: Context) = {
|
||||
val database = Database()
|
||||
(msg: String => String)(implicit context: Context): Unit = {
|
||||
context.loginAccount.foreach { loginAccount =>
|
||||
val database = Database()
|
||||
|
||||
val f = Future {
|
||||
database withSession { implicit session =>
|
||||
defining(
|
||||
s"[${r.name}] ${issue.title} (#${issue.issueId})" ->
|
||||
msg(Markdown.toHtml(
|
||||
markdown = content,
|
||||
repository = r,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableAnchor = false,
|
||||
enableLineBreaks = false
|
||||
))) { case (subject, msg) =>
|
||||
recipients(issue) { to =>
|
||||
send(to, subject, msg)
|
||||
}
|
||||
val f = Future {
|
||||
database withSession { implicit session =>
|
||||
defining(
|
||||
s"[${r.name}] ${issue.title} (#${issue.issueId})" ->
|
||||
msg(Markdown.toHtml(
|
||||
markdown = content,
|
||||
repository = r,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableAnchor = false,
|
||||
enableLineBreaks = false
|
||||
))
|
||||
) { case (subject, msg) =>
|
||||
recipients(issue, loginAccount) { to => send(to, subject, msg, loginAccount) }
|
||||
}
|
||||
}
|
||||
"Notifications Successful."
|
||||
}
|
||||
f.onComplete {
|
||||
case Success(s) => logger.debug(s)
|
||||
case Failure(t) => logger.error("Notifications Failed.", t)
|
||||
}
|
||||
"Notifications Successful."
|
||||
}
|
||||
f.onComplete {
|
||||
case Success(s) => logger.debug(s)
|
||||
case Failure(t) => logger.error("Notifications Failed.", t)
|
||||
}
|
||||
}
|
||||
|
||||
def send(to: String, subject: String, msg: String)(implicit context: Context): Unit = {
|
||||
def send(to: String, subject: String, msg: String, loginAccount: Account): Unit = {
|
||||
val email = new HtmlEmail
|
||||
email.setHostName(smtp.host)
|
||||
email.setSmtpPort(smtp.port.get)
|
||||
@@ -113,9 +118,13 @@ class Mailer(private val smtp: Smtp) extends Notifier {
|
||||
email.setSslSmtpPort(smtp.port.get.toString)
|
||||
}
|
||||
}
|
||||
smtp.starttls.foreach { starttls =>
|
||||
email.setStartTLSEnabled(starttls)
|
||||
email.setStartTLSRequired(starttls)
|
||||
}
|
||||
smtp.fromAddress
|
||||
.map (_ -> smtp.fromName.getOrElse(context.loginAccount.get.userName))
|
||||
.orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName))
|
||||
.map (_ -> smtp.fromName.getOrElse(loginAccount.userName))
|
||||
.orElse (Some("notifications@gitbucket.com" -> loginAccount.userName))
|
||||
.foreach { case (address, name) =>
|
||||
email.setFrom(address, name)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
package gitbucket.core.util
|
||||
|
||||
import java.net.{URLDecoder, URLEncoder}
|
||||
|
||||
import org.mozilla.universalchardet.UniversalDetector
|
||||
import ControlUtil._
|
||||
import org.apache.commons.io.input.BOMInputStream
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.codec.binary.Base64
|
||||
|
||||
import scala.util.control.Exception._
|
||||
|
||||
object StringUtil {
|
||||
|
||||
private lazy val BlowfishKey = {
|
||||
// last 4 numbers in current timestamp
|
||||
val time = System.currentTimeMillis.toString
|
||||
time.substring(time.length - 4)
|
||||
}
|
||||
|
||||
def sha1(value: String): String =
|
||||
defining(java.security.MessageDigest.getInstance("SHA-1")){ md =>
|
||||
md.update(value.getBytes)
|
||||
@@ -21,6 +30,20 @@ object StringUtil {
|
||||
md.digest.map(b => "%02x".format(b)).mkString
|
||||
}
|
||||
|
||||
def encodeBlowfish(value: String): String = {
|
||||
val spec = new javax.crypto.spec.SecretKeySpec(BlowfishKey.getBytes(), "Blowfish")
|
||||
val cipher = javax.crypto.Cipher.getInstance("Blowfish")
|
||||
cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, spec)
|
||||
new String(Base64.encodeBase64(cipher.doFinal(value.getBytes("UTF-8"))), "UTF-8")
|
||||
}
|
||||
|
||||
def decodeBlowfish(value: String): String = {
|
||||
val spec = new javax.crypto.spec.SecretKeySpec(BlowfishKey.getBytes(), "Blowfish")
|
||||
val cipher = javax.crypto.Cipher.getInstance("Blowfish")
|
||||
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, spec)
|
||||
new String(cipher.doFinal(Base64.decodeBase64(value)), "UTF-8")
|
||||
}
|
||||
|
||||
def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8").replace("+", "%20")
|
||||
|
||||
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")
|
||||
|
||||
@@ -161,7 +161,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
}
|
||||
|
||||
import scala.util.matching.Regex._
|
||||
implicit class RegexReplaceString(s: String) {
|
||||
implicit class RegexReplaceString(private val s: String) extends AnyVal {
|
||||
def replaceAll(pattern: String, replacer: (Match) => String): String = {
|
||||
pattern.r.replaceAllIn(s, (m: Match) => replacer(m).replace("$", "\\$"))
|
||||
}
|
||||
@@ -297,7 +297,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
/**
|
||||
* Implicit conversion to add mkHtml() to Seq[Html].
|
||||
*/
|
||||
implicit class RichHtmlSeq(seq: Seq[Html]) {
|
||||
implicit class RichHtmlSeq(private val seq: Seq[Html]) extends AnyVal {
|
||||
def mkHtml(separator: String) = Html(seq.mkString(separator))
|
||||
def mkHtml(separator: scala.xml.Elem) = Html(seq.mkString(separator.toString))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
@if(personalTokens.isEmpty && gneratedToken.isEmpty){
|
||||
No tokens.
|
||||
} else {
|
||||
Tokens you have generated that can be used to access the GitBucket API.
|
||||
Tokens you have generated which can be used to access the GitBucket API.
|
||||
<hr style="margin-top: 10px;">
|
||||
}
|
||||
@gneratedToken.map { case (token, tokenString) =>
|
||||
@@ -42,7 +42,7 @@
|
||||
<label for="note" class="strong">Token description</label>
|
||||
<div><span id="error-note" class="error"></span></div>
|
||||
<input type="text" name="note" id="note" class="form-control"/>
|
||||
<p class="muted">What's this token for?</p>
|
||||
<p class="muted">What is this token for?</p>
|
||||
</fieldset>
|
||||
<input type="submit" class="btn btn-success" value="Generate token"/>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,11 @@
|
||||
<input type="text" name="url" id="url" class="form-control" value="@account.url"/>
|
||||
<span id="error-url" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<label for="description" class="strong">Bio (optional):</label>
|
||||
<textarea name="description" id="description" class="form-control">@account.description</textarea>
|
||||
<span id="error-description" class="error"></span>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<fieldset class="form-group">
|
||||
|
||||
@@ -21,6 +21,12 @@
|
||||
</div>
|
||||
<input type="text" name="url" id="url" class="form-control" value="@account.map(_.url)"/>
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<label for="groupDescription" class="strong">Description (Optional)</label>
|
||||
<div>
|
||||
<textarea name="description" id="description" class="form-control">@account.map(_.description)</textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<label for="avatar" class="strong">Image (Optional)</label>
|
||||
@gitbucket.core.helper.html.uploadavatar(account)
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding-left: 10px; padding-right: 10px;">
|
||||
@account.description.map{ description =>
|
||||
<p style="color: white;">@description</p>
|
||||
}
|
||||
@if(account.url.isDefined){
|
||||
<p style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<i class="octicon octicon-home"></i> <a href="@account.url">@account.url</a>
|
||||
|
||||
@@ -6,7 +6,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
||||
<div class="content body">
|
||||
<h2>Create a new repository</h2>
|
||||
<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.
|
||||
</p>
|
||||
<form id="form" method="post" action="@context.path/new" validate="true">
|
||||
<fieldset class="border-top form-group">
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
<input type="text" name="url" id="url" class="form-control" value=""/>
|
||||
<span id="error-url" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="description" class="strong">Bio (optional):</label>
|
||||
<textarea name="description" id="description" class="form-control"></textarea>
|
||||
<span id="error-description" class="error"></span>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<fieldset>
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
</fieldset>
|
||||
<p class="muted">
|
||||
The base URL is used for redirect, notification email, git repository URL box and more.
|
||||
If the base URL is empty, GitBucket generates URL from request information.
|
||||
You can use this property to adjust URL difference between the reverse proxy and GitBucket.
|
||||
If the base URL is empty, GitBucket generates URL from the request information.
|
||||
You can use this property to adjust to URL differences between the reverse proxy and GitBucket.
|
||||
</p>
|
||||
<!--====================================================================-->
|
||||
<!-- Information -->
|
||||
@@ -59,19 +59,19 @@
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="allowAccountRegistration" value="false"@if(!context.settings.allowAccountRegistration){ checked}>
|
||||
<span class="strong">Deny</span> - <span class="normal">Only administrators can create accounts.</span>
|
||||
<span class="strong">Deny</span> <span class="normal">- Only administrators can create accounts.</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<hr>
|
||||
<label class="strong">Default option to create a new repository</label>
|
||||
<label class="strong">Default permissions when creating a new repository</label>
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="isCreateRepoOptionPublic" value="true"@if(context.settings.isCreateRepoOptionPublic){ checked}>
|
||||
<span class="strong">Public</span> <span class="normal">- All users and guests can read that repository.</span>
|
||||
<span class="strong">Public</span> <span class="normal">- All users and guests can read the repository.</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="isCreateRepoOptionPublic" value="false"@if(!context.settings.isCreateRepoOptionPublic){ checked}>
|
||||
<span class="strong">Private</span> <span class="normal">- Only collaborators can read that repository.</span>
|
||||
<span class="strong">Private</span> <span class="normal">- Only collaborators can read the repository.</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
@@ -82,7 +82,7 @@
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="allowAnonymousAccess" value="true"@if(context.settings.allowAnonymousAccess){ checked}>
|
||||
<span class="strong">Allow</span> <span class="normal">- Anyone can view public repositories, user/group profiles.</span>
|
||||
<span class="strong">Allow</span> <span class="normal">- Anyone can view public repositories and user/group profiles.</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="allowAnonymousAccess" value="false"@if(!context.settings.allowAnonymousAccess){ checked}>
|
||||
@@ -93,7 +93,7 @@
|
||||
<!-- Activity -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><span class="strong">Limit of activity logs</span> (Unlimited if it's not specified or zero)</label>
|
||||
<label><span class="strong">Limit of activity logs</span> (Unlimited if it is not specified or zero)</label>
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="activityLogLimit">Limit</label>
|
||||
@@ -111,7 +111,7 @@
|
||||
<fieldset>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="gravatar"@if(context.settings.gravatar){ checked}/>
|
||||
Use Gravatar for Profile-Images
|
||||
Use Gravatar for profile images
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
@@ -123,27 +123,25 @@
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="ssh" name="ssh"@if(context.settings.ssh){ checked}/>
|
||||
Enable SSH access to git repository
|
||||
<span class="muted normal">(Both SSH host and Base URL are required if SSH access is enabled)</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<div class="ssh">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="sshHost">SSH Host</label>
|
||||
<label class="control-label col-md-3" for="sshHost">SSH host</label>
|
||||
<div class="col-md-9">
|
||||
<input type="text" id="sshHost" name="sshHost" class="form-control" value="@context.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>
|
||||
<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="@context.settings.sshPort"/>
|
||||
<span id="error-sshPort" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="muted">
|
||||
Both of SSH host and Base URL are required if SSH access is enabled.
|
||||
</p>
|
||||
<!--====================================================================-->
|
||||
<!-- Authentication -->
|
||||
<!--====================================================================-->
|
||||
@@ -157,14 +155,14 @@
|
||||
</fieldset>
|
||||
<div class="ldap">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="ldapHost">LDAP Host</label>
|
||||
<label class="control-label col-md-3" for="ldapHost">LDAP host</label>
|
||||
<div class="col-md-9">
|
||||
<input type="text" id="ldapHost" name="ldap.host" class="form-control" value="@context.settings.ldap.map(_.host)"/>
|
||||
<span id="error-ldap_host" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="ldapPort">LDAP Port</label>
|
||||
<label class="control-label col-md-3" for="ldapPort">LDAP port</label>
|
||||
<div class="col-md-9">
|
||||
<input type="text" id="ldapPort" name="ldap.port" class="form-control input-mini" value="@context.settings.ldap.map(_.port)"/>
|
||||
<span id="error-ldap_port" class="error"></span>
|
||||
@@ -178,7 +176,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="ldapBindPassword">Bind Password</label>
|
||||
<label class="control-label col-md-3" for="ldapBindPassword">Bind password</label>
|
||||
<div class="col-md-9">
|
||||
<input type="password" id="ldapBindPassword" name="ldap.bindPassword" class="form-control" value="@context.settings.ldap.map(_.bindPassword)"/>
|
||||
<span id="error-ldap_bindPassword" class="error"></span>
|
||||
@@ -259,49 +257,56 @@
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="useSMTP" name="useSMTP" @if(context.settings.useSMTP){ checked}/>
|
||||
SMTP
|
||||
<span class="muted normal">(Enable notification as well as SMTP configuration if you want to send notification email too)</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<div class="useSMTP">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="smtpHost">SMTP Host</label>
|
||||
<label class="control-label col-md-3" for="smtpHost">SMTP host</label>
|
||||
<div class="col-md-9">
|
||||
<input type="text" id="smtpHost" name="smtp.host" class="form-control" value="@context.settings.smtp.map(_.host)"/>
|
||||
<span id="error-smtp_host" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="smtpPort">SMTP Port</label>
|
||||
<label class="control-label col-md-3" for="smtpPort">SMTP port</label>
|
||||
<div class="col-md-9">
|
||||
<input type="text" id="smtpPort" name="smtp.port" class="form-control input-mini" value="@context.settings.smtp.map(_.port)"/>
|
||||
<span id="error-smtp_port" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="smtpUser">SMTP User</label>
|
||||
<label class="control-label col-md-3" for="smtpUser">SMTP user</label>
|
||||
<div class="col-md-9">
|
||||
<input type="text" id="smtpUser" name="smtp.user" class="form-control" value="@context.settings.smtp.map(_.user)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="smtpPassword">SMTP Password</label>
|
||||
<label class="control-label col-md-3" for="smtpPassword">SMTP password</label>
|
||||
<div class="col-md-9">
|
||||
<input type="password" id="smtpPassword" name="smtp.password" class="form-control" value="@context.settings.smtp.map(_.password)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="smtpPassword">Enable SSL</label>
|
||||
<label class="control-label col-md-3" for="smtpSsl">Enable SSL</label>
|
||||
<div class="col-md-9">
|
||||
<input type="checkbox" id="smtpSsl" name="smtp.ssl"@if(context.settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="fromAddress">FROM Address</label>
|
||||
<label class="control-label col-md-3" for="smtpStarttls">Enable STARTTLS</label>
|
||||
<div class="col-md-9">
|
||||
<input type="checkbox" id="smtpStarttls" name="smtp.starttls"@if(context.settings.smtp.flatMap(_.starttls).getOrElse(false)){ checked}/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="fromAddress">FROM address</label>
|
||||
<div class="col-md-9">
|
||||
<input type="text" id="fromAddress" name="smtp.fromAddress" class="form-control" value="@context.settings.smtp.map(_.fromAddress)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="fromName">FROM Name</label>
|
||||
<label class="control-label col-md-3" for="fromName">FROM name</label>
|
||||
<div class="col-md-9">
|
||||
<input type="text" id="fromName" name="smtp.fromName" class="form-control" value="@context.settings.smtp.map(_.fromName)"/>
|
||||
</div>
|
||||
@@ -311,11 +316,23 @@
|
||||
<input type="text" id="testAddress" size="30"/>
|
||||
<input type="button" id="sendTestMail" value="Send"/>
|
||||
</div>
|
||||
|
||||
<p class="muted">
|
||||
Enable notification not only SMTP configuration if you want to send notification email.
|
||||
</p>
|
||||
</div>
|
||||
<!--====================================================================-->
|
||||
<!-- GitLFS -->
|
||||
<!--====================================================================-->
|
||||
@*
|
||||
<hr>
|
||||
<label class="strong">
|
||||
GitLFS <span class="muted normal">(Enter the LFS server url to enable GitLFS support)</span>
|
||||
</label>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="smtpHost">LFS server url</label>
|
||||
<div class="col-md-9">
|
||||
<input type="text" id="lfsServerUrl" name="lfs.serverUrl" class="form-control" value="@context.settings.lfs.serverUrl"/>
|
||||
<span id="error-lfs_serverUrl" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
*@
|
||||
</div>
|
||||
</div>
|
||||
<div class="align-right" style="margin-top: 20px;">
|
||||
@@ -332,6 +349,7 @@ $(function(){
|
||||
var user = $('#smtpUser' ).val();
|
||||
var password = $('#smtpPassword').val();
|
||||
var ssl = $('#smtpSsl' ).prop('checked');
|
||||
var starttls = $('#smtpStarttls').prop('checked');
|
||||
var fromAddress = $('#fromAddress' ).val();
|
||||
var fromName = $('#fromName' ).val();
|
||||
var testAddress = $('#testAddress' ).val();
|
||||
@@ -349,6 +367,7 @@ $(function(){
|
||||
'smtp.user': user,
|
||||
'smtp.password': password,
|
||||
'smtp.ssl': ssl,
|
||||
'smtp.starttls': starttls,
|
||||
'smtp.fromAddress': fromAddress,
|
||||
'smtp.fromName': fromName,
|
||||
'testAddress': testAddress
|
||||
|
||||
@@ -66,6 +66,13 @@
|
||||
</div>
|
||||
<input type="text" name="url" id="url" class="form-control" value="@account.map(_.url)"/>
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<label class="strong">Bio (Optional):</label>
|
||||
<div>
|
||||
<span id="error-description" class="error"></span>
|
||||
</div>
|
||||
<textarea name="description" id="description" class="form-control">@account.map(_.description)</textarea>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<fieldset class="form-group">
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
</div>
|
||||
<input type="text" name="url" id="url" class="form-control" value="@account.map(_.url)"/>
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<label class="strong">Description (Optional)</label>
|
||||
<textarea name="description" id="description" class="form-control">@account.map(_.description)</textarea>
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<label for="avatar" class="strong">Image (Optional)</label>
|
||||
@gitbucket.core.helper.html.uploadavatar(account)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
milestones: List[gitbucket.core.model.Milestone],
|
||||
labels: List[gitbucket.core.model.Label],
|
||||
isManageable: Boolean,
|
||||
content: String,
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@@ -13,7 +14,7 @@
|
||||
<input type="text" id="issue-title" name="title" class="form-control" value="" placeholder="Title" style="margin-bottom: 6px;" autofocus/>
|
||||
@gitbucket.core.helper.html.preview(
|
||||
repository = repository,
|
||||
content = "",
|
||||
content = content,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableLineBreaks = true,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<form class="form-inline">
|
||||
<input type="text" id="labelName-@labelId" style="width: 300px; float: left; margin-right: 4px;" class="form-control" value="@label.map(_.labelName)"@if(labelId == "new"){ placeholder="New label name"}/>
|
||||
<div id="label-color-@labelId" class="input-group color bscp" data-color="#@label.map(_.color).getOrElse("888888")" data-color-format="hex" style="width: 100px; float: left;">
|
||||
<input type="text" class="form-control" id="labelColor-@labelId" value="#@label.map(_.color).getOrElse("888888")" readonly style="width: 100px;">
|
||||
<input type="text" class="form-control" id="labelColor-@labelId" value="#@label.map(_.color).getOrElse("888888")" style="width: 100px;">
|
||||
<span class="input-group-addon"><i style="background-color: #@label.map(_.color).getOrElse("888888");"></i></span>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="@helpers.assets/vendors/bootstrap-3.3.6/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="@helpers.assets/vendors/octicons-4.2.0/octicons.css" rel="stylesheet">
|
||||
<link href="@helpers.assets/vendors/datepicker/css/bootstrap-datetimepicker.min.css" rel="stylesheet">
|
||||
<link href="@helpers.assets/vendors/bootstrap-datetimepicker-4.17.44/css/bootstrap-datetimepicker.min.css" rel="stylesheet">
|
||||
<link href="@helpers.assets/vendors/colorpicker/css/bootstrap-colorpicker.css" rel="stylesheet">
|
||||
<link href="@helpers.assets/vendors/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
|
||||
<link href="@helpers.assets/vendors/facebox/facebox.css" rel="stylesheet"/>
|
||||
@@ -25,8 +25,8 @@
|
||||
<script src="@helpers.assets/common/js/gitbucket.js"></script>
|
||||
<script src="@helpers.assets/vendors/bootstrap-3.3.6/js/bootstrap.js"></script>
|
||||
<script src="@helpers.assets/vendors/bootstrap3-typeahead/bootstrap3-typeahead.js"></script>
|
||||
<script src="@helpers.assets/vendors/datepicker/js/moment.js"></script>
|
||||
<script src="@helpers.assets/vendors/datepicker/js/bootstrap-datetimepicker.min.js"></script>
|
||||
<script src="@helpers.assets/vendors/bootstrap-datetimepicker-4.17.44/js/moment.min.js"></script>
|
||||
<script src="@helpers.assets/vendors/bootstrap-datetimepicker-4.17.44/js/bootstrap-datetimepicker.min.js"></script>
|
||||
<script src="@helpers.assets/vendors/colorpicker/js/bootstrap-colorpicker.js"></script>
|
||||
<script src="@helpers.assets/vendors/google-code-prettify/prettify.js"></script>
|
||||
<script src="@helpers.assets/vendors/elastic/jquery.elastic.source.js"></script>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
forkedId: String,
|
||||
sourceId: String,
|
||||
commitId: String,
|
||||
content: String,
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
originRepository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
forkedRepository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
@@ -59,7 +60,7 @@
|
||||
<input type="text" name="title" value="@title" class="form-control" style="margin-bottom: 6px;" placeholder="Title"/>
|
||||
@gitbucket.core.helper.html.preview(
|
||||
repository = repository,
|
||||
content = "",
|
||||
content = content,
|
||||
enableWikiLink = false,
|
||||
enableRefsLink = true,
|
||||
enableLineBreaks = true,
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<div>
|
||||
<span class="strong">Pull request successfully merged and closed</span>
|
||||
</div>
|
||||
<span class="small muted">You're all set-the <span class="label label-info monospace">@pullreq.requestBranch</span> branch can be safely deleted.</span>
|
||||
<span class="small muted">You're all set. The <span class="label label-info monospace">@pullreq.requestBranch</span> branch can now be safely deleted.</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
content: gitbucket.core.util.JGitUtil.ContentInfo,
|
||||
latestCommit: gitbucket.core.util.JGitUtil.CommitInfo,
|
||||
hasWritePermission: Boolean,
|
||||
isBlame: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||
isBlame: Boolean,
|
||||
isLfsFile: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main(s"${(repository.name :: pathList).mkString("/")} at ${helpers.encodeRefName(branch)} - ${repository.owner}/${repository.name}", Some(repository)) {
|
||||
@gitbucket.core.html.menu("files", repository){
|
||||
@@ -45,6 +46,9 @@
|
||||
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName(branch)/@pathList.take(i + 1).mkString("/")">@section</a> /
|
||||
}
|
||||
}
|
||||
@if(isLfsFile){
|
||||
<span class="label label-info">LFS</span>
|
||||
}
|
||||
</div>
|
||||
<div class="box-header">
|
||||
@helpers.avatar(latestCommit, 28)
|
||||
@@ -92,12 +96,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
<script src="@helpers.assets/vendors/jquery/jquery.ba-hashchange.js"></script>
|
||||
<script>
|
||||
$(window).load(function(){
|
||||
$(window).hashchange(function(){
|
||||
updateHighlighting();
|
||||
|
||||
window.onhashchange = function(){
|
||||
updateHighlighting();
|
||||
}).hashchange();
|
||||
}
|
||||
|
||||
var pre = $('pre.prettyprint');
|
||||
function updateSourceLineNum(){
|
||||
@@ -112,7 +117,6 @@ $(window).load(function(){
|
||||
top : pos.top + 'px',
|
||||
left : pos.left + 'px'
|
||||
}).click(function(e){
|
||||
$(window).hashchange(function(){})
|
||||
var pos = $(this).data("pos");
|
||||
if(!pos){
|
||||
pos = $('ol.linenums li').map(function(){ return { id: $(this).attr("id"), top: $(this).position().top} }).toArray();
|
||||
@@ -178,7 +182,7 @@ $(window).load(function(){
|
||||
var h = $(e).height();
|
||||
if(blame == index[i]){
|
||||
lastDiv.css("min-height",(p.top + h + 1) - lastDiv.position().top);
|
||||
}else{
|
||||
} else {
|
||||
$(e).addClass('blame-sep')
|
||||
blame = index[i];
|
||||
var sha = $('<div class="blame-sha">')
|
||||
|
||||
@@ -168,7 +168,9 @@
|
||||
}
|
||||
</td>
|
||||
<td class="ellipsis-cell" style="width: 70%;">
|
||||
<a href="@helpers.url(repository)/commit/@file.commitId" class="commit-message" title="@file.message">@helpers.link(file.message, repository)</a>
|
||||
<span>
|
||||
<a href="@helpers.url(repository)/commit/@file.commitId" class="commit-message" title="@file.message">@helpers.link(file.message, repository)</a>
|
||||
</span>
|
||||
</td>
|
||||
<td style="width: 10%; min-width: 125px; text-align: right;">@gitbucket.core.helper.html.datetimeago(file.time, false)</td>
|
||||
</tr>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<a href="@sshUrl" class="git-protocol-selector">SSH</a>
|
||||
}
|
||||
</div>
|
||||
<h3 style="margin-top: 30px;">Create a new repository on the command line</h3>
|
||||
<h3 style="margin-top: 30px;">Create a new repository from the command line</h3>
|
||||
@helpers.pre {
|
||||
touch README.md
|
||||
git init
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
@if(files.isEmpty) {
|
||||
<h4>We couldn't find any code matching '@query'</h4>
|
||||
} else {
|
||||
<h4>We've found @files.size code @helpers.plural(files.size, "result")</h4>
|
||||
<h4>We have found @files.size code @helpers.plural(files.size, "result")</h4>
|
||||
}
|
||||
}
|
||||
@files.drop((page - 1) * RepositorySearchService.CodeLimit).take(RepositorySearchService.CodeLimit).map { file =>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<input type="submit" value="Search" class="btn btn-default"/>
|
||||
</form>
|
||||
@if(repositories.isEmpty) {
|
||||
<h4>We couldn't find any repository matching '@query'</h4>
|
||||
<h4>We couldn't find any repositories matching '@query'</h4>
|
||||
} else {
|
||||
<h4>We've found @repositories.size @helpers.plural(repositories.size, "repository", "repositories")</h4>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@gitbucket.core.search.html.menu("wiki", query, repository){
|
||||
@if(query.nonEmpty) {
|
||||
@if(wikis.isEmpty) {
|
||||
<h4>We couldn't find any code matching '@query'</h4>
|
||||
<h4>We could not find any code matching '@query'</h4>
|
||||
} else {
|
||||
<h4>We've found @wikis.size @helpers.plural(wikis.size, "page")</h4>
|
||||
}
|
||||
|
||||
@@ -176,6 +176,15 @@ $(function(){
|
||||
$("#res-headers").html(headers(e.responce));
|
||||
$("#res-body").text(e.responce && e.responce.body ? e.responce.body : "");
|
||||
},
|
||||
error:function (e) {
|
||||
if(e) {
|
||||
console.log(e.responseText, e);
|
||||
alert("request error ( http status " + e.status + " error on gitbugket or browser to gitbucket. show details on your javascript console )");
|
||||
}else{
|
||||
alert("unknown javascript error (please report to gitbucket team)");
|
||||
}
|
||||
$("#test-report-modal").modal('hide')
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<input type="checkbox" id="allowFork" name="allowFork"@if(repository.repository.options.allowFork){ checked}/>
|
||||
Forks<br>
|
||||
<div class="normal muted">
|
||||
Allow repository forking to users who can access this repository.
|
||||
Allow users who can access this repository to fork it.
|
||||
</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
@@ -56,7 +56,7 @@
|
||||
<fieldset class="form-group">
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="issuesOption" value="DISABLE" @if(repository.repository.options.issuesOption == "DISABLE"){ checked}> Disables issues tracking system
|
||||
<input type="radio" name="issuesOption" value="DISABLE" @if(repository.repository.options.issuesOption == "DISABLE"){ checked}> Disable issues tracking system
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
@@ -75,7 +75,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<label for="externalIssuesUrl" class="strong">External URL:
|
||||
<span class="normal muted">(Put if you have the external issue tracking system for this project)</span>
|
||||
<span class="normal muted">(Fill if you use an external issue tracking system for this project)</span>
|
||||
</label>
|
||||
<input type="text" class="form-control" id="externalIssuesUrl" name="externalIssuesUrl" value="@repository.repository.options.externalIssuesUrl"/>
|
||||
</fieldset>
|
||||
@@ -87,7 +87,7 @@
|
||||
<fieldset class="form-group">
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="wikiOption" value="DISABLE" @if(repository.repository.options.wikiOption == "DISABLE"){ checked}> Disables wiki
|
||||
<input type="radio" name="wikiOption" value="DISABLE" @if(repository.repository.options.wikiOption == "DISABLE"){ checked}> Disable wiki
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
@@ -106,7 +106,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<label for="externalWikiUrl" class="strong">External URL:
|
||||
<span class="normal muted">(Put if you have the external Wiki for this project)</span>
|
||||
<span class="normal muted">(Fill if you use an external wiki for this project)</span>
|
||||
</label>
|
||||
<input type="text" class="form-control" id="externalWikiUrl" name="externalWikiUrl" value="@repository.repository.options.externalWikiUrl"/>
|
||||
</fieldset>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
@()(implicit context: gitbucket.core.controller.Context)
|
||||
@(userName: Option[Any] = None,
|
||||
password: Option[Any] = None,
|
||||
error: Option[Any] = None)(implicit context: gitbucket.core.controller.Context)
|
||||
@gitbucket.core.html.main("Sign in"){
|
||||
<div class="content-wrapper main-center">
|
||||
<div class="content body">
|
||||
@@ -9,7 +11,8 @@
|
||||
@Html(information)
|
||||
</div>
|
||||
}
|
||||
@gitbucket.core.html.signinform(context.settings)
|
||||
@gitbucket.core.helper.html.error(error)
|
||||
@gitbucket.core.html.signinform(context.settings, userName, password)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
@(systemSettings: gitbucket.core.service.SystemSettingsService.SystemSettings)(implicit context: gitbucket.core.controller.Context)
|
||||
@(systemSettings: gitbucket.core.service.SystemSettingsService.SystemSettings,
|
||||
userName: Option[Any] = None,
|
||||
password: Option[Any] = None)(implicit context: gitbucket.core.controller.Context)
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading strong">Sign in</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
@@ -7,12 +9,12 @@
|
||||
<div class="form-group">
|
||||
<label for="userName">Username:</label>
|
||||
<span id="error-userName" class="error"></span>
|
||||
<input type="text" name="userName" id="userName" class="form-control" autofocus/>
|
||||
<input type="text" name="userName" id="userName" class="form-control" autofocus value="@userName"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password:</label>
|
||||
<span id="error-password" class="error"></span>
|
||||
<input type="password" name="password" id="password" class="form-control"/>
|
||||
<input type="password" name="password" id="password" class="form-control" value="@password"/>
|
||||
</div>
|
||||
<input type="submit" class="btn btn-success" value="Sign in"/>
|
||||
@if(systemSettings.allowAccountRegistration){
|
||||
|
||||
@@ -46,6 +46,17 @@
|
||||
<url-pattern>/git/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>GitLfsTransferServlet</servlet-name>
|
||||
<servlet-class>gitbucket.core.servlet.GitLfsTransferServlet</servlet-class>
|
||||
</servlet>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>GitLfsTransferServlet</servlet-name>
|
||||
<url-pattern>/git-lfs/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- Supply assets which are provided by plugins -->
|
||||
<!-- ===================================================================== -->
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
551
src/main/webapp/assets/vendors/bootstrap-datetimepicker-4.17.44/js/moment.min.js
vendored
Normal file
551
src/main/webapp/assets/vendors/bootstrap-datetimepicker-4.17.44/js/moment.min.js
vendored
Normal file
@@ -0,0 +1,551 @@
|
||||
//! moment.js
|
||||
//! version : 2.17.1
|
||||
//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
|
||||
//! license : MIT
|
||||
//! momentjs.com
|
||||
!function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.moment=b()}(this,function(){"use strict";function a(){return od.apply(null,arguments)}
|
||||
// This is done to register the method called with moment()
|
||||
// without creating circular dependencies.
|
||||
function b(a){od=a}function c(a){return a instanceof Array||"[object Array]"===Object.prototype.toString.call(a)}function d(a){
|
||||
// IE8 will treat undefined and null as object if it wasn't for
|
||||
// input != null
|
||||
return null!=a&&"[object Object]"===Object.prototype.toString.call(a)}function e(a){var b;for(b in a)
|
||||
// even if its not own property I'd still call it non-empty
|
||||
return!1;return!0}function f(a){return"number"==typeof a||"[object Number]"===Object.prototype.toString.call(a)}function g(a){return a instanceof Date||"[object Date]"===Object.prototype.toString.call(a)}function h(a,b){var c,d=[];for(c=0;c<a.length;++c)d.push(b(a[c],c));return d}function i(a,b){return Object.prototype.hasOwnProperty.call(a,b)}function j(a,b){for(var c in b)i(b,c)&&(a[c]=b[c]);return i(b,"toString")&&(a.toString=b.toString),i(b,"valueOf")&&(a.valueOf=b.valueOf),a}function k(a,b,c,d){return rb(a,b,c,d,!0).utc()}function l(){
|
||||
// We need to deep clone this object.
|
||||
return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],meridiem:null}}function m(a){return null==a._pf&&(a._pf=l()),a._pf}function n(a){if(null==a._isValid){var b=m(a),c=qd.call(b.parsedDateParts,function(a){return null!=a}),d=!isNaN(a._d.getTime())&&b.overflow<0&&!b.empty&&!b.invalidMonth&&!b.invalidWeekday&&!b.nullInput&&!b.invalidFormat&&!b.userInvalidated&&(!b.meridiem||b.meridiem&&c);if(a._strict&&(d=d&&0===b.charsLeftOver&&0===b.unusedTokens.length&&void 0===b.bigHour),null!=Object.isFrozen&&Object.isFrozen(a))return d;a._isValid=d}return a._isValid}function o(a){var b=k(NaN);return null!=a?j(m(b),a):m(b).userInvalidated=!0,b}function p(a){return void 0===a}function q(a,b){var c,d,e;if(p(b._isAMomentObject)||(a._isAMomentObject=b._isAMomentObject),p(b._i)||(a._i=b._i),p(b._f)||(a._f=b._f),p(b._l)||(a._l=b._l),p(b._strict)||(a._strict=b._strict),p(b._tzm)||(a._tzm=b._tzm),p(b._isUTC)||(a._isUTC=b._isUTC),p(b._offset)||(a._offset=b._offset),p(b._pf)||(a._pf=m(b)),p(b._locale)||(a._locale=b._locale),rd.length>0)for(c in rd)d=rd[c],e=b[d],p(e)||(a[d]=e);return a}
|
||||
// Moment prototype object
|
||||
function r(b){q(this,b),this._d=new Date(null!=b._d?b._d.getTime():NaN),this.isValid()||(this._d=new Date(NaN)),
|
||||
// Prevent infinite loop in case updateOffset creates new moment
|
||||
// objects.
|
||||
sd===!1&&(sd=!0,a.updateOffset(this),sd=!1)}function s(a){return a instanceof r||null!=a&&null!=a._isAMomentObject}function t(a){return a<0?Math.ceil(a)||0:Math.floor(a)}function u(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=t(b)),c}
|
||||
// compare two arrays, return the number of differences
|
||||
function v(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;d<e;d++)(c&&a[d]!==b[d]||!c&&u(a[d])!==u(b[d]))&&g++;return g+f}function w(b){a.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+b)}function x(b,c){var d=!0;return j(function(){if(null!=a.deprecationHandler&&a.deprecationHandler(null,b),d){for(var e,f=[],g=0;g<arguments.length;g++){if(e="","object"==typeof arguments[g]){e+="\n["+g+"] ";for(var h in arguments[0])e+=h+": "+arguments[0][h]+", ";e=e.slice(0,-2)}else e=arguments[g];f.push(e)}w(b+"\nArguments: "+Array.prototype.slice.call(f).join("")+"\n"+(new Error).stack),d=!1}return c.apply(this,arguments)},c)}function y(b,c){null!=a.deprecationHandler&&a.deprecationHandler(b,c),td[b]||(w(c),td[b]=!0)}function z(a){return a instanceof Function||"[object Function]"===Object.prototype.toString.call(a)}function A(a){var b,c;for(c in a)b=a[c],z(b)?this[c]=b:this["_"+c]=b;this._config=a,
|
||||
// Lenient ordinal parsing accepts just a number in addition to
|
||||
// number + (possibly) stuff coming from _ordinalParseLenient.
|
||||
this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function B(a,b){var c,e=j({},a);for(c in b)i(b,c)&&(d(a[c])&&d(b[c])?(e[c]={},j(e[c],a[c]),j(e[c],b[c])):null!=b[c]?e[c]=b[c]:delete e[c]);for(c in a)i(a,c)&&!i(b,c)&&d(a[c])&&(
|
||||
// make sure changes to properties don't modify parent config
|
||||
e[c]=j({},e[c]));return e}function C(a){null!=a&&this.set(a)}function D(a,b,c){var d=this._calendar[a]||this._calendar.sameElse;return z(d)?d.call(b,c):d}function E(a){var b=this._longDateFormat[a],c=this._longDateFormat[a.toUpperCase()];return b||!c?b:(this._longDateFormat[a]=c.replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a])}function F(){return this._invalidDate}function G(a){return this._ordinal.replace("%d",a)}function H(a,b,c,d){var e=this._relativeTime[c];return z(e)?e(a,b,c,d):e.replace(/%d/i,a)}function I(a,b){var c=this._relativeTime[a>0?"future":"past"];return z(c)?c(b):c.replace(/%s/i,b)}function J(a,b){var c=a.toLowerCase();Dd[c]=Dd[c+"s"]=Dd[b]=a}function K(a){return"string"==typeof a?Dd[a]||Dd[a.toLowerCase()]:void 0}function L(a){var b,c,d={};for(c in a)i(a,c)&&(b=K(c),b&&(d[b]=a[c]));return d}function M(a,b){Ed[a]=b}function N(a){var b=[];for(var c in a)b.push({unit:c,priority:Ed[c]});return b.sort(function(a,b){return a.priority-b.priority}),b}function O(b,c){return function(d){return null!=d?(Q(this,b,d),a.updateOffset(this,c),this):P(this,b)}}function P(a,b){return a.isValid()?a._d["get"+(a._isUTC?"UTC":"")+b]():NaN}function Q(a,b,c){a.isValid()&&a._d["set"+(a._isUTC?"UTC":"")+b](c)}
|
||||
// MOMENTS
|
||||
function R(a){return a=K(a),z(this[a])?this[a]():this}function S(a,b){if("object"==typeof a){a=L(a);for(var c=N(a),d=0;d<c.length;d++)this[c[d].unit](a[c[d].unit])}else if(a=K(a),z(this[a]))return this[a](b);return this}function T(a,b,c){var d=""+Math.abs(a),e=b-d.length,f=a>=0;return(f?c?"+":"":"-")+Math.pow(10,Math.max(0,e)).toString().substr(1)+d}
|
||||
// token: 'M'
|
||||
// padded: ['MM', 2]
|
||||
// ordinal: 'Mo'
|
||||
// callback: function () { this.month() + 1 }
|
||||
function U(a,b,c,d){var e=d;"string"==typeof d&&(e=function(){return this[d]()}),a&&(Id[a]=e),b&&(Id[b[0]]=function(){return T(e.apply(this,arguments),b[1],b[2])}),c&&(Id[c]=function(){return this.localeData().ordinal(e.apply(this,arguments),a)})}function V(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function W(a){var b,c,d=a.match(Fd);for(b=0,c=d.length;b<c;b++)Id[d[b]]?d[b]=Id[d[b]]:d[b]=V(d[b]);return function(b){var e,f="";for(e=0;e<c;e++)f+=d[e]instanceof Function?d[e].call(b,a):d[e];return f}}
|
||||
// format date using native date object
|
||||
function X(a,b){return a.isValid()?(b=Y(b,a.localeData()),Hd[b]=Hd[b]||W(b),Hd[b](a)):a.localeData().invalidDate()}function Y(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(Gd.lastIndex=0;d>=0&&Gd.test(a);)a=a.replace(Gd,c),Gd.lastIndex=0,d-=1;return a}function Z(a,b,c){$d[a]=z(b)?b:function(a,d){return a&&c?c:b}}function $(a,b){return i($d,a)?$d[a](b._strict,b._locale):new RegExp(_(a))}
|
||||
// Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
|
||||
function _(a){return aa(a.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e}))}function aa(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function ba(a,b){var c,d=b;for("string"==typeof a&&(a=[a]),f(b)&&(d=function(a,c){c[b]=u(a)}),c=0;c<a.length;c++)_d[a[c]]=d}function ca(a,b){ba(a,function(a,c,d,e){d._w=d._w||{},b(a,d._w,d,e)})}function da(a,b,c){null!=b&&i(_d,a)&&_d[a](b,c._a,c,a)}function ea(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function fa(a,b){return a?c(this._months)?this._months[a.month()]:this._months[(this._months.isFormat||ke).test(b)?"format":"standalone"][a.month()]:this._months}function ga(a,b){return a?c(this._monthsShort)?this._monthsShort[a.month()]:this._monthsShort[ke.test(b)?"format":"standalone"][a.month()]:this._monthsShort}function ha(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._monthsParse)for(
|
||||
// this is not used
|
||||
this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],d=0;d<12;++d)f=k([2e3,d]),this._shortMonthsParse[d]=this.monthsShort(f,"").toLocaleLowerCase(),this._longMonthsParse[d]=this.months(f,"").toLocaleLowerCase();return c?"MMM"===b?(e=je.call(this._shortMonthsParse,g),e!==-1?e:null):(e=je.call(this._longMonthsParse,g),e!==-1?e:null):"MMM"===b?(e=je.call(this._shortMonthsParse,g),e!==-1?e:(e=je.call(this._longMonthsParse,g),e!==-1?e:null)):(e=je.call(this._longMonthsParse,g),e!==-1?e:(e=je.call(this._shortMonthsParse,g),e!==-1?e:null))}function ia(a,b,c){var d,e,f;if(this._monthsParseExact)return ha.call(this,a,b,c);
|
||||
// TODO: add sorting
|
||||
// Sorting makes sure if one month (or abbr) is a prefix of another
|
||||
// see sorting in computeMonthsParse
|
||||
for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),d=0;d<12;d++){
|
||||
// test the regex
|
||||
if(
|
||||
// make the regex if we don't have it already
|
||||
e=k([2e3,d]),c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp("^"+this.months(e,"").replace(".","")+"$","i"),this._shortMonthsParse[d]=new RegExp("^"+this.monthsShort(e,"").replace(".","")+"$","i")),c||this._monthsParse[d]||(f="^"+this.months(e,"")+"|^"+this.monthsShort(e,""),this._monthsParse[d]=new RegExp(f.replace(".",""),"i")),c&&"MMMM"===b&&this._longMonthsParse[d].test(a))return d;if(c&&"MMM"===b&&this._shortMonthsParse[d].test(a))return d;if(!c&&this._monthsParse[d].test(a))return d}}
|
||||
// MOMENTS
|
||||
function ja(a,b){var c;if(!a.isValid())
|
||||
// No op
|
||||
return a;if("string"==typeof b)if(/^\d+$/.test(b))b=u(b);else
|
||||
// TODO: Another silent failure?
|
||||
if(b=a.localeData().monthsParse(b),!f(b))return a;return c=Math.min(a.date(),ea(a.year(),b)),a._d["set"+(a._isUTC?"UTC":"")+"Month"](b,c),a}function ka(b){return null!=b?(ja(this,b),a.updateOffset(this,!0),this):P(this,"Month")}function la(){return ea(this.year(),this.month())}function ma(a){return this._monthsParseExact?(i(this,"_monthsRegex")||oa.call(this),a?this._monthsShortStrictRegex:this._monthsShortRegex):(i(this,"_monthsShortRegex")||(this._monthsShortRegex=ne),this._monthsShortStrictRegex&&a?this._monthsShortStrictRegex:this._monthsShortRegex)}function na(a){return this._monthsParseExact?(i(this,"_monthsRegex")||oa.call(this),a?this._monthsStrictRegex:this._monthsRegex):(i(this,"_monthsRegex")||(this._monthsRegex=oe),this._monthsStrictRegex&&a?this._monthsStrictRegex:this._monthsRegex)}function oa(){function a(a,b){return b.length-a.length}var b,c,d=[],e=[],f=[];for(b=0;b<12;b++)
|
||||
// make the regex if we don't have it already
|
||||
c=k([2e3,b]),d.push(this.monthsShort(c,"")),e.push(this.months(c,"")),f.push(this.months(c,"")),f.push(this.monthsShort(c,""));for(
|
||||
// Sorting makes sure if one month (or abbr) is a prefix of another it
|
||||
// will match the longer piece.
|
||||
d.sort(a),e.sort(a),f.sort(a),b=0;b<12;b++)d[b]=aa(d[b]),e[b]=aa(e[b]);for(b=0;b<24;b++)f[b]=aa(f[b]);this._monthsRegex=new RegExp("^("+f.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+e.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+d.join("|")+")","i")}
|
||||
// HELPERS
|
||||
function pa(a){return qa(a)?366:365}function qa(a){return a%4===0&&a%100!==0||a%400===0}function ra(){return qa(this.year())}function sa(a,b,c,d,e,f,g){
|
||||
//can't just apply() to create a date:
|
||||
//http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
|
||||
var h=new Date(a,b,c,d,e,f,g);
|
||||
//the date constructor remaps years 0-99 to 1900-1999
|
||||
return a<100&&a>=0&&isFinite(h.getFullYear())&&h.setFullYear(a),h}function ta(a){var b=new Date(Date.UTC.apply(null,arguments));
|
||||
//the Date.UTC function remaps years 0-99 to 1900-1999
|
||||
return a<100&&a>=0&&isFinite(b.getUTCFullYear())&&b.setUTCFullYear(a),b}
|
||||
// start-of-first-week - start-of-year
|
||||
function ua(a,b,c){var// first-week day -- which january is always in the first week (4 for iso, 1 for other)
|
||||
d=7+b-c,
|
||||
// first-week day local weekday -- which local weekday is fwd
|
||||
e=(7+ta(a,0,d).getUTCDay()-b)%7;return-e+d-1}
|
||||
//http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
|
||||
function va(a,b,c,d,e){var f,g,h=(7+c-d)%7,i=ua(a,d,e),j=1+7*(b-1)+h+i;return j<=0?(f=a-1,g=pa(f)+j):j>pa(a)?(f=a+1,g=j-pa(a)):(f=a,g=j),{year:f,dayOfYear:g}}function wa(a,b,c){var d,e,f=ua(a.year(),b,c),g=Math.floor((a.dayOfYear()-f-1)/7)+1;return g<1?(e=a.year()-1,d=g+xa(e,b,c)):g>xa(a.year(),b,c)?(d=g-xa(a.year(),b,c),e=a.year()+1):(e=a.year(),d=g),{week:d,year:e}}function xa(a,b,c){var d=ua(a,b,c),e=ua(a+1,b,c);return(pa(a)-d+e)/7}
|
||||
// HELPERS
|
||||
// LOCALES
|
||||
function ya(a){return wa(a,this._week.dow,this._week.doy).week}function za(){return this._week.dow}function Aa(){return this._week.doy}
|
||||
// MOMENTS
|
||||
function Ba(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")}function Ca(a){var b=wa(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")}
|
||||
// HELPERS
|
||||
function Da(a,b){return"string"!=typeof a?a:isNaN(a)?(a=b.weekdaysParse(a),"number"==typeof a?a:null):parseInt(a,10)}function Ea(a,b){return"string"==typeof a?b.weekdaysParse(a)%7||7:isNaN(a)?null:a}function Fa(a,b){return a?c(this._weekdays)?this._weekdays[a.day()]:this._weekdays[this._weekdays.isFormat.test(b)?"format":"standalone"][a.day()]:this._weekdays}function Ga(a){return a?this._weekdaysShort[a.day()]:this._weekdaysShort}function Ha(a){return a?this._weekdaysMin[a.day()]:this._weekdaysMin}function Ia(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],d=0;d<7;++d)f=k([2e3,1]).day(d),this._minWeekdaysParse[d]=this.weekdaysMin(f,"").toLocaleLowerCase(),this._shortWeekdaysParse[d]=this.weekdaysShort(f,"").toLocaleLowerCase(),this._weekdaysParse[d]=this.weekdays(f,"").toLocaleLowerCase();return c?"dddd"===b?(e=je.call(this._weekdaysParse,g),e!==-1?e:null):"ddd"===b?(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:null):(e=je.call(this._minWeekdaysParse,g),e!==-1?e:null):"dddd"===b?(e=je.call(this._weekdaysParse,g),e!==-1?e:(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:(e=je.call(this._minWeekdaysParse,g),e!==-1?e:null))):"ddd"===b?(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:(e=je.call(this._weekdaysParse,g),e!==-1?e:(e=je.call(this._minWeekdaysParse,g),e!==-1?e:null))):(e=je.call(this._minWeekdaysParse,g),e!==-1?e:(e=je.call(this._weekdaysParse,g),e!==-1?e:(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:null)))}function Ja(a,b,c){var d,e,f;if(this._weekdaysParseExact)return Ia.call(this,a,b,c);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),d=0;d<7;d++){
|
||||
// test the regex
|
||||
if(
|
||||
// make the regex if we don't have it already
|
||||
e=k([2e3,1]).day(d),c&&!this._fullWeekdaysParse[d]&&(this._fullWeekdaysParse[d]=new RegExp("^"+this.weekdays(e,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[d]=new RegExp("^"+this.weekdaysShort(e,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[d]=new RegExp("^"+this.weekdaysMin(e,"").replace(".",".?")+"$","i")),this._weekdaysParse[d]||(f="^"+this.weekdays(e,"")+"|^"+this.weekdaysShort(e,"")+"|^"+this.weekdaysMin(e,""),this._weekdaysParse[d]=new RegExp(f.replace(".",""),"i")),c&&"dddd"===b&&this._fullWeekdaysParse[d].test(a))return d;if(c&&"ddd"===b&&this._shortWeekdaysParse[d].test(a))return d;if(c&&"dd"===b&&this._minWeekdaysParse[d].test(a))return d;if(!c&&this._weekdaysParse[d].test(a))return d}}
|
||||
// MOMENTS
|
||||
function Ka(a){if(!this.isValid())return null!=a?this:NaN;var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=Da(a,this.localeData()),this.add(a-b,"d")):b}function La(a){if(!this.isValid())return null!=a?this:NaN;var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")}function Ma(a){if(!this.isValid())return null!=a?this:NaN;
|
||||
// behaves the same as moment#day except
|
||||
// as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
|
||||
// as a setter, sunday should belong to the previous week.
|
||||
if(null!=a){var b=Ea(a,this.localeData());return this.day(this.day()%7?b:b-7)}return this.day()||7}function Na(a){return this._weekdaysParseExact?(i(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysStrictRegex:this._weekdaysRegex):(i(this,"_weekdaysRegex")||(this._weekdaysRegex=ue),this._weekdaysStrictRegex&&a?this._weekdaysStrictRegex:this._weekdaysRegex)}function Oa(a){return this._weekdaysParseExact?(i(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(i(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=ve),this._weekdaysShortStrictRegex&&a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)}function Pa(a){return this._weekdaysParseExact?(i(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(i(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=we),this._weekdaysMinStrictRegex&&a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)}function Qa(){function a(a,b){return b.length-a.length}var b,c,d,e,f,g=[],h=[],i=[],j=[];for(b=0;b<7;b++)
|
||||
// make the regex if we don't have it already
|
||||
c=k([2e3,1]).day(b),d=this.weekdaysMin(c,""),e=this.weekdaysShort(c,""),f=this.weekdays(c,""),g.push(d),h.push(e),i.push(f),j.push(d),j.push(e),j.push(f);for(
|
||||
// Sorting makes sure if one weekday (or abbr) is a prefix of another it
|
||||
// will match the longer piece.
|
||||
g.sort(a),h.sort(a),i.sort(a),j.sort(a),b=0;b<7;b++)h[b]=aa(h[b]),i[b]=aa(i[b]),j[b]=aa(j[b]);this._weekdaysRegex=new RegExp("^("+j.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+h.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+g.join("|")+")","i")}
|
||||
// FORMATTING
|
||||
function Ra(){return this.hours()%12||12}function Sa(){return this.hours()||24}function Ta(a,b){U(a,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),b)})}
|
||||
// PARSING
|
||||
function Ua(a,b){return b._meridiemParse}
|
||||
// LOCALES
|
||||
function Va(a){
|
||||
// IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
|
||||
// Using charAt should be more compatible.
|
||||
return"p"===(a+"").toLowerCase().charAt(0)}function Wa(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"}function Xa(a){return a?a.toLowerCase().replace("_","-"):a}
|
||||
// pick the locale from the array
|
||||
// try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
|
||||
// substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
|
||||
function Ya(a){for(var b,c,d,e,f=0;f<a.length;){for(e=Xa(a[f]).split("-"),b=e.length,c=Xa(a[f+1]),c=c?c.split("-"):null;b>0;){if(d=Za(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&v(e,c,!0)>=b-1)
|
||||
//the next array item is better than a shallower substring of this one
|
||||
break;b--}f++}return null}function Za(a){var b=null;
|
||||
// TODO: Find a better way to register and load all the locales in Node
|
||||
if(!Be[a]&&"undefined"!=typeof module&&module&&module.exports)try{b=xe._abbr,require("./locale/"+a),
|
||||
// because defineLocale currently also sets the global locale, we
|
||||
// want to undo that for lazy loaded locales
|
||||
$a(b)}catch(a){}return Be[a]}
|
||||
// This function will load locale and then set the global locale. If
|
||||
// no arguments are passed in, it will simply return the current global
|
||||
// locale key.
|
||||
function $a(a,b){var c;
|
||||
// moment.duration._locale = moment._locale = data;
|
||||
return a&&(c=p(b)?bb(a):_a(a,b),c&&(xe=c)),xe._abbr}function _a(a,b){if(null!==b){var c=Ae;if(b.abbr=a,null!=Be[a])y("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),c=Be[a]._config;else if(null!=b.parentLocale){if(null==Be[b.parentLocale])return Ce[b.parentLocale]||(Ce[b.parentLocale]=[]),Ce[b.parentLocale].push({name:a,config:b}),null;c=Be[b.parentLocale]._config}
|
||||
// backwards compat for now: also set the locale
|
||||
// make sure we set the locale AFTER all child locales have been
|
||||
// created, so we won't end up with the child locale set.
|
||||
return Be[a]=new C(B(c,b)),Ce[a]&&Ce[a].forEach(function(a){_a(a.name,a.config)}),$a(a),Be[a]}
|
||||
// useful for testing
|
||||
return delete Be[a],null}function ab(a,b){if(null!=b){var c,d=Ae;
|
||||
// MERGE
|
||||
null!=Be[a]&&(d=Be[a]._config),b=B(d,b),c=new C(b),c.parentLocale=Be[a],Be[a]=c,
|
||||
// backwards compat for now: also set the locale
|
||||
$a(a)}else
|
||||
// pass null for config to unupdate, useful for tests
|
||||
null!=Be[a]&&(null!=Be[a].parentLocale?Be[a]=Be[a].parentLocale:null!=Be[a]&&delete Be[a]);return Be[a]}
|
||||
// returns locale data
|
||||
function bb(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return xe;if(!c(a)){if(
|
||||
//short-circuit everything else
|
||||
b=Za(a))return b;a=[a]}return Ya(a)}function cb(){return wd(Be)}function db(a){var b,c=a._a;return c&&m(a).overflow===-2&&(b=c[be]<0||c[be]>11?be:c[ce]<1||c[ce]>ea(c[ae],c[be])?ce:c[de]<0||c[de]>24||24===c[de]&&(0!==c[ee]||0!==c[fe]||0!==c[ge])?de:c[ee]<0||c[ee]>59?ee:c[fe]<0||c[fe]>59?fe:c[ge]<0||c[ge]>999?ge:-1,m(a)._overflowDayOfYear&&(b<ae||b>ce)&&(b=ce),m(a)._overflowWeeks&&b===-1&&(b=he),m(a)._overflowWeekday&&b===-1&&(b=ie),m(a).overflow=b),a}
|
||||
// date from iso format
|
||||
function eb(a){var b,c,d,e,f,g,h=a._i,i=De.exec(h)||Ee.exec(h);if(i){for(m(a).iso=!0,b=0,c=Ge.length;b<c;b++)if(Ge[b][1].exec(i[1])){e=Ge[b][0],d=Ge[b][2]!==!1;break}if(null==e)return void(a._isValid=!1);if(i[3]){for(b=0,c=He.length;b<c;b++)if(He[b][1].exec(i[3])){
|
||||
// match[2] should be 'T' or space
|
||||
f=(i[2]||" ")+He[b][0];break}if(null==f)return void(a._isValid=!1)}if(!d&&null!=f)return void(a._isValid=!1);if(i[4]){if(!Fe.exec(i[4]))return void(a._isValid=!1);g="Z"}a._f=e+(f||"")+(g||""),kb(a)}else a._isValid=!1}
|
||||
// date from iso format or fallback
|
||||
function fb(b){var c=Ie.exec(b._i);return null!==c?void(b._d=new Date(+c[1])):(eb(b),void(b._isValid===!1&&(delete b._isValid,a.createFromInputFallback(b))))}
|
||||
// Pick the first defined of two or three arguments.
|
||||
function gb(a,b,c){return null!=a?a:null!=b?b:c}function hb(b){
|
||||
// hooks is actually the exported moment object
|
||||
var c=new Date(a.now());return b._useUTC?[c.getUTCFullYear(),c.getUTCMonth(),c.getUTCDate()]:[c.getFullYear(),c.getMonth(),c.getDate()]}
|
||||
// convert an array to a date.
|
||||
// the array should mirror the parameters below
|
||||
// note: all values past the year are optional and will default to the lowest possible value.
|
||||
// [year, month, day , hour, minute, second, millisecond]
|
||||
function ib(a){var b,c,d,e,f=[];if(!a._d){
|
||||
// Default to current date.
|
||||
// * if no year, month, day of month are given, default to today
|
||||
// * if day of month is given, default month and year
|
||||
// * if month is given, default only year
|
||||
// * if year is given, don't default anything
|
||||
for(d=hb(a),
|
||||
//compute day of the year from weeks and weekdays
|
||||
a._w&&null==a._a[ce]&&null==a._a[be]&&jb(a),
|
||||
//if the day of the year is set, figure out what it is
|
||||
a._dayOfYear&&(e=gb(a._a[ae],d[ae]),a._dayOfYear>pa(e)&&(m(a)._overflowDayOfYear=!0),c=ta(e,0,a._dayOfYear),a._a[be]=c.getUTCMonth(),a._a[ce]=c.getUTCDate()),b=0;b<3&&null==a._a[b];++b)a._a[b]=f[b]=d[b];
|
||||
// Zero out whatever was not defaulted, including time
|
||||
for(;b<7;b++)a._a[b]=f[b]=null==a._a[b]?2===b?1:0:a._a[b];
|
||||
// Check for 24:00:00.000
|
||||
24===a._a[de]&&0===a._a[ee]&&0===a._a[fe]&&0===a._a[ge]&&(a._nextDay=!0,a._a[de]=0),a._d=(a._useUTC?ta:sa).apply(null,f),
|
||||
// Apply timezone offset from input. The actual utcOffset can be changed
|
||||
// with parseZone.
|
||||
null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[de]=24)}}function jb(a){var b,c,d,e,f,g,h,i;if(b=a._w,null!=b.GG||null!=b.W||null!=b.E)f=1,g=4,
|
||||
// TODO: We need to take the current isoWeekYear, but that depends on
|
||||
// how we interpret now (local, utc, fixed offset). So create
|
||||
// a now version of current config (take local/utc/offset flags, and
|
||||
// create now).
|
||||
c=gb(b.GG,a._a[ae],wa(sb(),1,4).year),d=gb(b.W,1),e=gb(b.E,1),(e<1||e>7)&&(i=!0);else{f=a._locale._week.dow,g=a._locale._week.doy;var j=wa(sb(),f,g);c=gb(b.gg,a._a[ae],j.year),
|
||||
// Default to current week.
|
||||
d=gb(b.w,j.week),null!=b.d?(
|
||||
// weekday -- low day numbers are considered next week
|
||||
e=b.d,(e<0||e>6)&&(i=!0)):null!=b.e?(
|
||||
// local weekday -- counting starts from begining of week
|
||||
e=b.e+f,(b.e<0||b.e>6)&&(i=!0)):
|
||||
// default to begining of week
|
||||
e=f}d<1||d>xa(c,f,g)?m(a)._overflowWeeks=!0:null!=i?m(a)._overflowWeekday=!0:(h=va(c,d,e,f,g),a._a[ae]=h.year,a._dayOfYear=h.dayOfYear)}
|
||||
// date from string and format string
|
||||
function kb(b){
|
||||
// TODO: Move this to another part of the creation flow to prevent circular deps
|
||||
if(b._f===a.ISO_8601)return void eb(b);b._a=[],m(b).empty=!0;
|
||||
// This array is used to make a Date, either with `new Date` or `Date.UTC`
|
||||
var c,d,e,f,g,h=""+b._i,i=h.length,j=0;for(e=Y(b._f,b._locale).match(Fd)||[],c=0;c<e.length;c++)f=e[c],d=(h.match($(f,b))||[])[0],
|
||||
// console.log('token', token, 'parsedInput', parsedInput,
|
||||
// 'regex', getParseRegexForToken(token, config));
|
||||
d&&(g=h.substr(0,h.indexOf(d)),g.length>0&&m(b).unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),j+=d.length),
|
||||
// don't parse if it's not a known token
|
||||
Id[f]?(d?m(b).empty=!1:m(b).unusedTokens.push(f),da(f,d,b)):b._strict&&!d&&m(b).unusedTokens.push(f);
|
||||
// add remaining unparsed input length to the string
|
||||
m(b).charsLeftOver=i-j,h.length>0&&m(b).unusedInput.push(h),
|
||||
// clear _12h flag if hour is <= 12
|
||||
b._a[de]<=12&&m(b).bigHour===!0&&b._a[de]>0&&(m(b).bigHour=void 0),m(b).parsedDateParts=b._a.slice(0),m(b).meridiem=b._meridiem,
|
||||
// handle meridiem
|
||||
b._a[de]=lb(b._locale,b._a[de],b._meridiem),ib(b),db(b)}function lb(a,b,c){var d;
|
||||
// Fallback
|
||||
return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&b<12&&(b+=12),d||12!==b||(b=0),b):b}
|
||||
// date from string and array of format strings
|
||||
function mb(a){var b,c,d,e,f;if(0===a._f.length)return m(a).invalidFormat=!0,void(a._d=new Date(NaN));for(e=0;e<a._f.length;e++)f=0,b=q({},a),null!=a._useUTC&&(b._useUTC=a._useUTC),b._f=a._f[e],kb(b),n(b)&&(
|
||||
// if there is any input that was not parsed add a penalty for that format
|
||||
f+=m(b).charsLeftOver,
|
||||
//or tokens
|
||||
f+=10*m(b).unusedTokens.length,m(b).score=f,(null==d||f<d)&&(d=f,c=b));j(a,c||b)}function nb(a){if(!a._d){var b=L(a._i);a._a=h([b.year,b.month,b.day||b.date,b.hour,b.minute,b.second,b.millisecond],function(a){return a&&parseInt(a,10)}),ib(a)}}function ob(a){var b=new r(db(pb(a)));
|
||||
// Adding is smart enough around DST
|
||||
return b._nextDay&&(b.add(1,"d"),b._nextDay=void 0),b}function pb(a){var b=a._i,d=a._f;return a._locale=a._locale||bb(a._l),null===b||void 0===d&&""===b?o({nullInput:!0}):("string"==typeof b&&(a._i=b=a._locale.preparse(b)),s(b)?new r(db(b)):(g(b)?a._d=b:c(d)?mb(a):d?kb(a):qb(a),n(a)||(a._d=null),a))}function qb(b){var d=b._i;void 0===d?b._d=new Date(a.now()):g(d)?b._d=new Date(d.valueOf()):"string"==typeof d?fb(b):c(d)?(b._a=h(d.slice(0),function(a){return parseInt(a,10)}),ib(b)):"object"==typeof d?nb(b):f(d)?
|
||||
// from milliseconds
|
||||
b._d=new Date(d):a.createFromInputFallback(b)}function rb(a,b,f,g,h){var i={};
|
||||
// object construction must be done this way.
|
||||
// https://github.com/moment/moment/issues/1423
|
||||
return f!==!0&&f!==!1||(g=f,f=void 0),(d(a)&&e(a)||c(a)&&0===a.length)&&(a=void 0),i._isAMomentObject=!0,i._useUTC=i._isUTC=h,i._l=f,i._i=a,i._f=b,i._strict=g,ob(i)}function sb(a,b,c,d){return rb(a,b,c,d,!1)}
|
||||
// Pick a moment m from moments so that m[fn](other) is true for all
|
||||
// other. This relies on the function fn to be transitive.
|
||||
//
|
||||
// moments should either be an array of moment objects or an array, whose
|
||||
// first element is an array of moment objects.
|
||||
function tb(a,b){var d,e;if(1===b.length&&c(b[0])&&(b=b[0]),!b.length)return sb();for(d=b[0],e=1;e<b.length;++e)b[e].isValid()&&!b[e][a](d)||(d=b[e]);return d}
|
||||
// TODO: Use [].sort instead?
|
||||
function ub(){var a=[].slice.call(arguments,0);return tb("isBefore",a)}function vb(){var a=[].slice.call(arguments,0);return tb("isAfter",a)}function wb(a){var b=L(a),c=b.year||0,d=b.quarter||0,e=b.month||0,f=b.week||0,g=b.day||0,h=b.hour||0,i=b.minute||0,j=b.second||0,k=b.millisecond||0;
|
||||
// representation for dateAddRemove
|
||||
this._milliseconds=+k+1e3*j+// 1000
|
||||
6e4*i+// 1000 * 60
|
||||
1e3*h*60*60,//using 1000 * 60 * 60 instead of 36e5 to avoid floating point rounding errors https://github.com/moment/moment/issues/2978
|
||||
// Because of dateAddRemove treats 24 hours as different from a
|
||||
// day when working around DST, we need to store them separately
|
||||
this._days=+g+7*f,
|
||||
// It is impossible translate months into days without knowing
|
||||
// which months you are are talking about, so we have to store
|
||||
// it separately.
|
||||
this._months=+e+3*d+12*c,this._data={},this._locale=bb(),this._bubble()}function xb(a){return a instanceof wb}function yb(a){return a<0?Math.round(-1*a)*-1:Math.round(a)}
|
||||
// FORMATTING
|
||||
function zb(a,b){U(a,0,0,function(){var a=this.utcOffset(),c="+";return a<0&&(a=-a,c="-"),c+T(~~(a/60),2)+b+T(~~a%60,2)})}function Ab(a,b){var c=(b||"").match(a);if(null===c)return null;var d=c[c.length-1]||[],e=(d+"").match(Me)||["-",0,0],f=+(60*e[1])+u(e[2]);return 0===f?0:"+"===e[0]?f:-f}
|
||||
// Return a moment from input, that is local/utc/zone equivalent to model.
|
||||
function Bb(b,c){var d,e;
|
||||
// Use low-level api, because this fn is low-level api.
|
||||
return c._isUTC?(d=c.clone(),e=(s(b)||g(b)?b.valueOf():sb(b).valueOf())-d.valueOf(),d._d.setTime(d._d.valueOf()+e),a.updateOffset(d,!1),d):sb(b).local()}function Cb(a){
|
||||
// On Firefox.24 Date#getTimezoneOffset returns a floating point.
|
||||
// https://github.com/moment/moment/pull/1871
|
||||
return 15*-Math.round(a._d.getTimezoneOffset()/15)}
|
||||
// MOMENTS
|
||||
// keepLocalTime = true means only change the timezone, without
|
||||
// affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]-->
|
||||
// 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset
|
||||
// +0200, so we adjust the time as needed, to be valid.
|
||||
//
|
||||
// Keeping the time actually adds/subtracts (one hour)
|
||||
// from the actual represented time. That is why we call updateOffset
|
||||
// a second time. In case it wants us to change the offset again
|
||||
// _changeInProgress == true case, then we have to adjust, because
|
||||
// there is no such time in the given timezone.
|
||||
function Db(b,c){var d,e=this._offset||0;if(!this.isValid())return null!=b?this:NaN;if(null!=b){if("string"==typeof b){if(b=Ab(Xd,b),null===b)return this}else Math.abs(b)<16&&(b=60*b);return!this._isUTC&&c&&(d=Cb(this)),this._offset=b,this._isUTC=!0,null!=d&&this.add(d,"m"),e!==b&&(!c||this._changeInProgress?Tb(this,Ob(b-e,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?e:Cb(this)}function Eb(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}function Fb(a){return this.utcOffset(0,a)}function Gb(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(Cb(this),"m")),this}function Hb(){if(null!=this._tzm)this.utcOffset(this._tzm);else if("string"==typeof this._i){var a=Ab(Wd,this._i);null!=a?this.utcOffset(a):this.utcOffset(0,!0)}return this}function Ib(a){return!!this.isValid()&&(a=a?sb(a).utcOffset():0,(this.utcOffset()-a)%60===0)}function Jb(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Kb(){if(!p(this._isDSTShifted))return this._isDSTShifted;var a={};if(q(a,this),a=pb(a),a._a){var b=a._isUTC?k(a._a):sb(a._a);this._isDSTShifted=this.isValid()&&v(a._a,b.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Lb(){return!!this.isValid()&&!this._isUTC}function Mb(){return!!this.isValid()&&this._isUTC}function Nb(){return!!this.isValid()&&(this._isUTC&&0===this._offset)}function Ob(a,b){var c,d,e,g=a,
|
||||
// matching against regexp is expensive, do it on demand
|
||||
h=null;// checks for null or undefined
|
||||
return xb(a)?g={ms:a._milliseconds,d:a._days,M:a._months}:f(a)?(g={},b?g[b]=a:g.milliseconds=a):(h=Ne.exec(a))?(c="-"===h[1]?-1:1,g={y:0,d:u(h[ce])*c,h:u(h[de])*c,m:u(h[ee])*c,s:u(h[fe])*c,ms:u(yb(1e3*h[ge]))*c}):(h=Oe.exec(a))?(c="-"===h[1]?-1:1,g={y:Pb(h[2],c),M:Pb(h[3],c),w:Pb(h[4],c),d:Pb(h[5],c),h:Pb(h[6],c),m:Pb(h[7],c),s:Pb(h[8],c)}):null==g?g={}:"object"==typeof g&&("from"in g||"to"in g)&&(e=Rb(sb(g.from),sb(g.to)),g={},g.ms=e.milliseconds,g.M=e.months),d=new wb(g),xb(a)&&i(a,"_locale")&&(d._locale=a._locale),d}function Pb(a,b){
|
||||
// We'd normally use ~~inp for this, but unfortunately it also
|
||||
// converts floats to ints.
|
||||
// inp may be undefined, so careful calling replace on it.
|
||||
var c=a&&parseFloat(a.replace(",","."));
|
||||
// apply sign while we're at it
|
||||
return(isNaN(c)?0:c)*b}function Qb(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function Rb(a,b){var c;return a.isValid()&&b.isValid()?(b=Bb(b,a),a.isBefore(b)?c=Qb(a,b):(c=Qb(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c):{milliseconds:0,months:0}}
|
||||
// TODO: remove 'name' arg after deprecation is removed
|
||||
function Sb(a,b){return function(c,d){var e,f;
|
||||
//invert the arguments, but complain about it
|
||||
return null===d||isNaN(+d)||(y(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),f=c,c=d,d=f),c="string"==typeof c?+c:c,e=Ob(c,d),Tb(this,e,a),this}}function Tb(b,c,d,e){var f=c._milliseconds,g=yb(c._days),h=yb(c._months);b.isValid()&&(e=null==e||e,f&&b._d.setTime(b._d.valueOf()+f*d),g&&Q(b,"Date",P(b,"Date")+g*d),h&&ja(b,P(b,"Month")+h*d),e&&a.updateOffset(b,g||h))}function Ub(a,b){var c=a.diff(b,"days",!0);return c<-6?"sameElse":c<-1?"lastWeek":c<0?"lastDay":c<1?"sameDay":c<2?"nextDay":c<7?"nextWeek":"sameElse"}function Vb(b,c){
|
||||
// We want to compare the start of today, vs this.
|
||||
// Getting start-of-today depends on whether we're local/utc/offset or not.
|
||||
var d=b||sb(),e=Bb(d,this).startOf("day"),f=a.calendarFormat(this,e)||"sameElse",g=c&&(z(c[f])?c[f].call(this,d):c[f]);return this.format(g||this.localeData().calendar(f,this,sb(d)))}function Wb(){return new r(this)}function Xb(a,b){var c=s(a)?a:sb(a);return!(!this.isValid()||!c.isValid())&&(b=K(p(b)?"millisecond":b),"millisecond"===b?this.valueOf()>c.valueOf():c.valueOf()<this.clone().startOf(b).valueOf())}function Yb(a,b){var c=s(a)?a:sb(a);return!(!this.isValid()||!c.isValid())&&(b=K(p(b)?"millisecond":b),"millisecond"===b?this.valueOf()<c.valueOf():this.clone().endOf(b).valueOf()<c.valueOf())}function Zb(a,b,c,d){return d=d||"()",("("===d[0]?this.isAfter(a,c):!this.isBefore(a,c))&&(")"===d[1]?this.isBefore(b,c):!this.isAfter(b,c))}function $b(a,b){var c,d=s(a)?a:sb(a);return!(!this.isValid()||!d.isValid())&&(b=K(b||"millisecond"),"millisecond"===b?this.valueOf()===d.valueOf():(c=d.valueOf(),this.clone().startOf(b).valueOf()<=c&&c<=this.clone().endOf(b).valueOf()))}function _b(a,b){return this.isSame(a,b)||this.isAfter(a,b)}function ac(a,b){return this.isSame(a,b)||this.isBefore(a,b)}function bc(a,b,c){var d,e,f,g;// 1000
|
||||
// 1000 * 60
|
||||
// 1000 * 60 * 60
|
||||
// 1000 * 60 * 60 * 24, negate dst
|
||||
// 1000 * 60 * 60 * 24 * 7, negate dst
|
||||
return this.isValid()?(d=Bb(a,this),d.isValid()?(e=6e4*(d.utcOffset()-this.utcOffset()),b=K(b),"year"===b||"month"===b||"quarter"===b?(g=cc(this,d),"quarter"===b?g/=3:"year"===b&&(g/=12)):(f=this-d,g="second"===b?f/1e3:"minute"===b?f/6e4:"hour"===b?f/36e5:"day"===b?(f-e)/864e5:"week"===b?(f-e)/6048e5:f),c?g:t(g)):NaN):NaN}function cc(a,b){
|
||||
// difference in months
|
||||
var c,d,e=12*(b.year()-a.year())+(b.month()-a.month()),
|
||||
// b is in (anchor - 1 month, anchor + 1 month)
|
||||
f=a.clone().add(e,"months");
|
||||
//check for negative zero, return zero if negative zero
|
||||
// linear across the month
|
||||
// linear across the month
|
||||
return b-f<0?(c=a.clone().add(e-1,"months"),d=(b-f)/(f-c)):(c=a.clone().add(e+1,"months"),d=(b-f)/(c-f)),-(e+d)||0}function dc(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function ec(){var a=this.clone().utc();return 0<a.year()&&a.year()<=9999?z(Date.prototype.toISOString)?this.toDate().toISOString():X(a,"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]"):X(a,"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]")}/**
|
||||
* Return a human readable representation of a moment that can
|
||||
* also be evaluated to get a new moment which is the same
|
||||
*
|
||||
* @link https://nodejs.org/dist/latest/docs/api/util.html#util_custom_inspect_function_on_objects
|
||||
*/
|
||||
function fc(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var a="moment",b="";this.isLocal()||(a=0===this.utcOffset()?"moment.utc":"moment.parseZone",b="Z");var c="["+a+'("]',d=0<this.year()&&this.year()<=9999?"YYYY":"YYYYYY",e="-MM-DD[T]HH:mm:ss.SSS",f=b+'[")]';return this.format(c+d+e+f)}function gc(b){b||(b=this.isUtc()?a.defaultFormatUtc:a.defaultFormat);var c=X(this,b);return this.localeData().postformat(c)}function hc(a,b){return this.isValid()&&(s(a)&&a.isValid()||sb(a).isValid())?Ob({to:this,from:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function ic(a){return this.from(sb(),a)}function jc(a,b){return this.isValid()&&(s(a)&&a.isValid()||sb(a).isValid())?Ob({from:this,to:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function kc(a){return this.to(sb(),a)}
|
||||
// If passed a locale key, it will set the locale for this
|
||||
// instance. Otherwise, it will return the locale configuration
|
||||
// variables for this instance.
|
||||
function lc(a){var b;return void 0===a?this._locale._abbr:(b=bb(a),null!=b&&(this._locale=b),this)}function mc(){return this._locale}function nc(a){
|
||||
// the following switch intentionally omits break keywords
|
||||
// to utilize falling through the cases.
|
||||
switch(a=K(a)){case"year":this.month(0);/* falls through */
|
||||
case"quarter":case"month":this.date(1);/* falls through */
|
||||
case"week":case"isoWeek":case"day":case"date":this.hours(0);/* falls through */
|
||||
case"hour":this.minutes(0);/* falls through */
|
||||
case"minute":this.seconds(0);/* falls through */
|
||||
case"second":this.milliseconds(0)}
|
||||
// weeks are a special case
|
||||
// quarters are also special
|
||||
return"week"===a&&this.weekday(0),"isoWeek"===a&&this.isoWeekday(1),"quarter"===a&&this.month(3*Math.floor(this.month()/3)),this}function oc(a){
|
||||
// 'date' is an alias for 'day', so it should be considered as such.
|
||||
return a=K(a),void 0===a||"millisecond"===a?this:("date"===a&&(a="day"),this.startOf(a).add(1,"isoWeek"===a?"week":a).subtract(1,"ms"))}function pc(){return this._d.valueOf()-6e4*(this._offset||0)}function qc(){return Math.floor(this.valueOf()/1e3)}function rc(){return new Date(this.valueOf())}function sc(){var a=this;return[a.year(),a.month(),a.date(),a.hour(),a.minute(),a.second(),a.millisecond()]}function tc(){var a=this;return{years:a.year(),months:a.month(),date:a.date(),hours:a.hours(),minutes:a.minutes(),seconds:a.seconds(),milliseconds:a.milliseconds()}}function uc(){
|
||||
// new Date(NaN).toJSON() === null
|
||||
return this.isValid()?this.toISOString():null}function vc(){return n(this)}function wc(){return j({},m(this))}function xc(){return m(this).overflow}function yc(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}}function zc(a,b){U(0,[a,a.length],0,b)}
|
||||
// MOMENTS
|
||||
function Ac(a){return Ec.call(this,a,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)}function Bc(a){return Ec.call(this,a,this.isoWeek(),this.isoWeekday(),1,4)}function Cc(){return xa(this.year(),1,4)}function Dc(){var a=this.localeData()._week;return xa(this.year(),a.dow,a.doy)}function Ec(a,b,c,d,e){var f;return null==a?wa(this,d,e).year:(f=xa(a,d,e),b>f&&(b=f),Fc.call(this,a,b,c,d,e))}function Fc(a,b,c,d,e){var f=va(a,b,c,d,e),g=ta(f.year,0,f.dayOfYear);return this.year(g.getUTCFullYear()),this.month(g.getUTCMonth()),this.date(g.getUTCDate()),this}
|
||||
// MOMENTS
|
||||
function Gc(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)}
|
||||
// HELPERS
|
||||
// MOMENTS
|
||||
function Hc(a){var b=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")}function Ic(a,b){b[ge]=u(1e3*("0."+a))}
|
||||
// MOMENTS
|
||||
function Jc(){return this._isUTC?"UTC":""}function Kc(){return this._isUTC?"Coordinated Universal Time":""}function Lc(a){return sb(1e3*a)}function Mc(){return sb.apply(null,arguments).parseZone()}function Nc(a){return a}function Oc(a,b,c,d){var e=bb(),f=k().set(d,b);return e[c](f,a)}function Pc(a,b,c){if(f(a)&&(b=a,a=void 0),a=a||"",null!=b)return Oc(a,b,c,"month");var d,e=[];for(d=0;d<12;d++)e[d]=Oc(a,d,c,"month");return e}
|
||||
// ()
|
||||
// (5)
|
||||
// (fmt, 5)
|
||||
// (fmt)
|
||||
// (true)
|
||||
// (true, 5)
|
||||
// (true, fmt, 5)
|
||||
// (true, fmt)
|
||||
function Qc(a,b,c,d){"boolean"==typeof a?(f(b)&&(c=b,b=void 0),b=b||""):(b=a,c=b,a=!1,f(b)&&(c=b,b=void 0),b=b||"");var e=bb(),g=a?e._week.dow:0;if(null!=c)return Oc(b,(c+g)%7,d,"day");var h,i=[];for(h=0;h<7;h++)i[h]=Oc(b,(h+g)%7,d,"day");return i}function Rc(a,b){return Pc(a,b,"months")}function Sc(a,b){return Pc(a,b,"monthsShort")}function Tc(a,b,c){return Qc(a,b,c,"weekdays")}function Uc(a,b,c){return Qc(a,b,c,"weekdaysShort")}function Vc(a,b,c){return Qc(a,b,c,"weekdaysMin")}function Wc(){var a=this._data;return this._milliseconds=Ze(this._milliseconds),this._days=Ze(this._days),this._months=Ze(this._months),a.milliseconds=Ze(a.milliseconds),a.seconds=Ze(a.seconds),a.minutes=Ze(a.minutes),a.hours=Ze(a.hours),a.months=Ze(a.months),a.years=Ze(a.years),this}function Xc(a,b,c,d){var e=Ob(b,c);return a._milliseconds+=d*e._milliseconds,a._days+=d*e._days,a._months+=d*e._months,a._bubble()}
|
||||
// supports only 2.0-style add(1, 's') or add(duration)
|
||||
function Yc(a,b){return Xc(this,a,b,1)}
|
||||
// supports only 2.0-style subtract(1, 's') or subtract(duration)
|
||||
function Zc(a,b){return Xc(this,a,b,-1)}function $c(a){return a<0?Math.floor(a):Math.ceil(a)}function _c(){var a,b,c,d,e,f=this._milliseconds,g=this._days,h=this._months,i=this._data;
|
||||
// if we have a mix of positive and negative values, bubble down first
|
||||
// check: https://github.com/moment/moment/issues/2166
|
||||
// The following code bubbles up values, see the tests for
|
||||
// examples of what that means.
|
||||
// convert days to months
|
||||
// 12 months -> 1 year
|
||||
return f>=0&&g>=0&&h>=0||f<=0&&g<=0&&h<=0||(f+=864e5*$c(bd(h)+g),g=0,h=0),i.milliseconds=f%1e3,a=t(f/1e3),i.seconds=a%60,b=t(a/60),i.minutes=b%60,c=t(b/60),i.hours=c%24,g+=t(c/24),e=t(ad(g)),h+=e,g-=$c(bd(e)),d=t(h/12),h%=12,i.days=g,i.months=h,i.years=d,this}function ad(a){
|
||||
// 400 years have 146097 days (taking into account leap year rules)
|
||||
// 400 years have 12 months === 4800
|
||||
return 4800*a/146097}function bd(a){
|
||||
// the reverse of daysToMonths
|
||||
return 146097*a/4800}function cd(a){var b,c,d=this._milliseconds;if(a=K(a),"month"===a||"year"===a)return b=this._days+d/864e5,c=this._months+ad(b),"month"===a?c:c/12;switch(
|
||||
// handle milliseconds separately because of floating point math errors (issue #1867)
|
||||
b=this._days+Math.round(bd(this._months)),a){case"week":return b/7+d/6048e5;case"day":return b+d/864e5;case"hour":return 24*b+d/36e5;case"minute":return 1440*b+d/6e4;case"second":return 86400*b+d/1e3;
|
||||
// Math.floor prevents floating point math errors here
|
||||
case"millisecond":return Math.floor(864e5*b)+d;default:throw new Error("Unknown unit "+a)}}
|
||||
// TODO: Use this.as('ms')?
|
||||
function dd(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*u(this._months/12)}function ed(a){return function(){return this.as(a)}}function fd(a){return a=K(a),this[a+"s"]()}function gd(a){return function(){return this._data[a]}}function hd(){return t(this.days()/7)}
|
||||
// helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
|
||||
function id(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function jd(a,b,c){var d=Ob(a).abs(),e=of(d.as("s")),f=of(d.as("m")),g=of(d.as("h")),h=of(d.as("d")),i=of(d.as("M")),j=of(d.as("y")),k=e<pf.s&&["s",e]||f<=1&&["m"]||f<pf.m&&["mm",f]||g<=1&&["h"]||g<pf.h&&["hh",g]||h<=1&&["d"]||h<pf.d&&["dd",h]||i<=1&&["M"]||i<pf.M&&["MM",i]||j<=1&&["y"]||["yy",j];return k[2]=b,k[3]=+a>0,k[4]=c,id.apply(null,k)}
|
||||
// This function allows you to set the rounding function for relative time strings
|
||||
function kd(a){return void 0===a?of:"function"==typeof a&&(of=a,!0)}
|
||||
// This function allows you to set a threshold for relative time strings
|
||||
function ld(a,b){return void 0!==pf[a]&&(void 0===b?pf[a]:(pf[a]=b,!0))}function md(a){var b=this.localeData(),c=jd(this,!a,b);return a&&(c=b.pastFuture(+this,c)),b.postformat(c)}function nd(){
|
||||
// for ISO strings we do not use the normal bubbling rules:
|
||||
// * milliseconds bubble up until they become hours
|
||||
// * days do not bubble at all
|
||||
// * months bubble up until they become years
|
||||
// This is because there is no context-free conversion between hours and days
|
||||
// (think of clock changes)
|
||||
// and also not between days and months (28-31 days per month)
|
||||
var a,b,c,d=qf(this._milliseconds)/1e3,e=qf(this._days),f=qf(this._months);
|
||||
// 3600 seconds -> 60 minutes -> 1 hour
|
||||
a=t(d/60),b=t(a/60),d%=60,a%=60,
|
||||
// 12 months -> 1 year
|
||||
c=t(f/12),f%=12;
|
||||
// inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
|
||||
var g=c,h=f,i=e,j=b,k=a,l=d,m=this.asSeconds();return m?(m<0?"-":"")+"P"+(g?g+"Y":"")+(h?h+"M":"")+(i?i+"D":"")+(j||k||l?"T":"")+(j?j+"H":"")+(k?k+"M":"")+(l?l+"S":""):"P0D"}var od,pd;pd=Array.prototype.some?Array.prototype.some:function(a){for(var b=Object(this),c=b.length>>>0,d=0;d<c;d++)if(d in b&&a.call(this,b[d],d,b))return!0;return!1};var qd=pd,rd=a.momentProperties=[],sd=!1,td={};a.suppressDeprecationWarnings=!1,a.deprecationHandler=null;var ud;ud=Object.keys?Object.keys:function(a){var b,c=[];for(b in a)i(a,b)&&c.push(b);return c};var vd,wd=ud,xd={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},yd={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},zd="Invalid date",Ad="%d",Bd=/\d{1,2}/,Cd={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Dd={},Ed={},Fd=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,Gd=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,Hd={},Id={},Jd=/\d/,Kd=/\d\d/,Ld=/\d{3}/,Md=/\d{4}/,Nd=/[+-]?\d{6}/,Od=/\d\d?/,Pd=/\d\d\d\d?/,Qd=/\d\d\d\d\d\d?/,Rd=/\d{1,3}/,Sd=/\d{1,4}/,Td=/[+-]?\d{1,6}/,Ud=/\d+/,Vd=/[+-]?\d+/,Wd=/Z|[+-]\d\d:?\d\d/gi,Xd=/Z|[+-]\d\d(?::?\d\d)?/gi,Yd=/[+-]?\d+(\.\d{1,3})?/,Zd=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,$d={},_d={},ae=0,be=1,ce=2,de=3,ee=4,fe=5,ge=6,he=7,ie=8;vd=Array.prototype.indexOf?Array.prototype.indexOf:function(a){
|
||||
// I know
|
||||
var b;for(b=0;b<this.length;++b)if(this[b]===a)return b;return-1};var je=vd;
|
||||
// FORMATTING
|
||||
U("M",["MM",2],"Mo",function(){return this.month()+1}),U("MMM",0,0,function(a){return this.localeData().monthsShort(this,a)}),U("MMMM",0,0,function(a){return this.localeData().months(this,a)}),
|
||||
// ALIASES
|
||||
J("month","M"),
|
||||
// PRIORITY
|
||||
M("month",8),
|
||||
// PARSING
|
||||
Z("M",Od),Z("MM",Od,Kd),Z("MMM",function(a,b){return b.monthsShortRegex(a)}),Z("MMMM",function(a,b){return b.monthsRegex(a)}),ba(["M","MM"],function(a,b){b[be]=u(a)-1}),ba(["MMM","MMMM"],function(a,b,c,d){var e=c._locale.monthsParse(a,d,c._strict);
|
||||
// if we didn't find a month name, mark the date as invalid.
|
||||
null!=e?b[be]=e:m(c).invalidMonth=a});
|
||||
// LOCALES
|
||||
var ke=/D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/,le="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),me="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),ne=Zd,oe=Zd;
|
||||
// FORMATTING
|
||||
U("Y",0,0,function(){var a=this.year();return a<=9999?""+a:"+"+a}),U(0,["YY",2],0,function(){return this.year()%100}),U(0,["YYYY",4],0,"year"),U(0,["YYYYY",5],0,"year"),U(0,["YYYYYY",6,!0],0,"year"),
|
||||
// ALIASES
|
||||
J("year","y"),
|
||||
// PRIORITIES
|
||||
M("year",1),
|
||||
// PARSING
|
||||
Z("Y",Vd),Z("YY",Od,Kd),Z("YYYY",Sd,Md),Z("YYYYY",Td,Nd),Z("YYYYYY",Td,Nd),ba(["YYYYY","YYYYYY"],ae),ba("YYYY",function(b,c){c[ae]=2===b.length?a.parseTwoDigitYear(b):u(b)}),ba("YY",function(b,c){c[ae]=a.parseTwoDigitYear(b)}),ba("Y",function(a,b){b[ae]=parseInt(a,10)}),
|
||||
// HOOKS
|
||||
a.parseTwoDigitYear=function(a){return u(a)+(u(a)>68?1900:2e3)};
|
||||
// MOMENTS
|
||||
var pe=O("FullYear",!0);
|
||||
// FORMATTING
|
||||
U("w",["ww",2],"wo","week"),U("W",["WW",2],"Wo","isoWeek"),
|
||||
// ALIASES
|
||||
J("week","w"),J("isoWeek","W"),
|
||||
// PRIORITIES
|
||||
M("week",5),M("isoWeek",5),
|
||||
// PARSING
|
||||
Z("w",Od),Z("ww",Od,Kd),Z("W",Od),Z("WW",Od,Kd),ca(["w","ww","W","WW"],function(a,b,c,d){b[d.substr(0,1)]=u(a)});var qe={dow:0,// Sunday is the first day of the week.
|
||||
doy:6};
|
||||
// FORMATTING
|
||||
U("d",0,"do","day"),U("dd",0,0,function(a){return this.localeData().weekdaysMin(this,a)}),U("ddd",0,0,function(a){return this.localeData().weekdaysShort(this,a)}),U("dddd",0,0,function(a){return this.localeData().weekdays(this,a)}),U("e",0,0,"weekday"),U("E",0,0,"isoWeekday"),
|
||||
// ALIASES
|
||||
J("day","d"),J("weekday","e"),J("isoWeekday","E"),
|
||||
// PRIORITY
|
||||
M("day",11),M("weekday",11),M("isoWeekday",11),
|
||||
// PARSING
|
||||
Z("d",Od),Z("e",Od),Z("E",Od),Z("dd",function(a,b){return b.weekdaysMinRegex(a)}),Z("ddd",function(a,b){return b.weekdaysShortRegex(a)}),Z("dddd",function(a,b){return b.weekdaysRegex(a)}),ca(["dd","ddd","dddd"],function(a,b,c,d){var e=c._locale.weekdaysParse(a,d,c._strict);
|
||||
// if we didn't get a weekday name, mark the date as invalid
|
||||
null!=e?b.d=e:m(c).invalidWeekday=a}),ca(["d","e","E"],function(a,b,c,d){b[d]=u(a)});
|
||||
// LOCALES
|
||||
var re="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),se="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),te="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),ue=Zd,ve=Zd,we=Zd;U("H",["HH",2],0,"hour"),U("h",["hh",2],0,Ra),U("k",["kk",2],0,Sa),U("hmm",0,0,function(){return""+Ra.apply(this)+T(this.minutes(),2)}),U("hmmss",0,0,function(){return""+Ra.apply(this)+T(this.minutes(),2)+T(this.seconds(),2)}),U("Hmm",0,0,function(){return""+this.hours()+T(this.minutes(),2)}),U("Hmmss",0,0,function(){return""+this.hours()+T(this.minutes(),2)+T(this.seconds(),2)}),Ta("a",!0),Ta("A",!1),
|
||||
// ALIASES
|
||||
J("hour","h"),
|
||||
// PRIORITY
|
||||
M("hour",13),Z("a",Ua),Z("A",Ua),Z("H",Od),Z("h",Od),Z("HH",Od,Kd),Z("hh",Od,Kd),Z("hmm",Pd),Z("hmmss",Qd),Z("Hmm",Pd),Z("Hmmss",Qd),ba(["H","HH"],de),ba(["a","A"],function(a,b,c){c._isPm=c._locale.isPM(a),c._meridiem=a}),ba(["h","hh"],function(a,b,c){b[de]=u(a),m(c).bigHour=!0}),ba("hmm",function(a,b,c){var d=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d)),m(c).bigHour=!0}),ba("hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d,2)),b[fe]=u(a.substr(e)),m(c).bigHour=!0}),ba("Hmm",function(a,b,c){var d=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d))}),ba("Hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d,2)),b[fe]=u(a.substr(e))});var xe,ye=/[ap]\.?m?\.?/i,ze=O("Hours",!0),Ae={calendar:xd,longDateFormat:yd,invalidDate:zd,ordinal:Ad,ordinalParse:Bd,relativeTime:Cd,months:le,monthsShort:me,week:qe,weekdays:re,weekdaysMin:te,weekdaysShort:se,meridiemParse:ye},Be={},Ce={},De=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Ee=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Fe=/Z|[+-]\d\d(?::?\d\d)?/,Ge=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],
|
||||
// YYYYMM is NOT allowed by the standard
|
||||
["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],He=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Ie=/^\/?Date\((\-?\d+)/i;a.createFromInputFallback=x("value provided is not in a recognized ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non ISO date formats are discouraged and will be removed in an upcoming major release. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",function(a){a._d=new Date(a._i+(a._useUTC?" UTC":""))}),
|
||||
// constant that refers to the ISO standard
|
||||
a.ISO_8601=function(){};var Je=x("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var a=sb.apply(null,arguments);return this.isValid()&&a.isValid()?a<this?this:a:o()}),Ke=x("moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var a=sb.apply(null,arguments);return this.isValid()&&a.isValid()?a>this?this:a:o()}),Le=function(){return Date.now?Date.now():+new Date};zb("Z",":"),zb("ZZ",""),
|
||||
// PARSING
|
||||
Z("Z",Xd),Z("ZZ",Xd),ba(["Z","ZZ"],function(a,b,c){c._useUTC=!0,c._tzm=Ab(Xd,a)});
|
||||
// HELPERS
|
||||
// timezone chunker
|
||||
// '+10:00' > ['10', '00']
|
||||
// '-1530' > ['-15', '30']
|
||||
var Me=/([\+\-]|\d\d)/gi;
|
||||
// HOOKS
|
||||
// This function will be called whenever a moment is mutated.
|
||||
// It is intended to keep the offset in sync with the timezone.
|
||||
a.updateOffset=function(){};
|
||||
// ASP.NET json date format regex
|
||||
var Ne=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/,Oe=/^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;Ob.fn=wb.prototype;var Pe=Sb(1,"add"),Qe=Sb(-1,"subtract");a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",a.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var Re=x("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(a){return void 0===a?this.localeData():this.locale(a)});
|
||||
// FORMATTING
|
||||
U(0,["gg",2],0,function(){return this.weekYear()%100}),U(0,["GG",2],0,function(){return this.isoWeekYear()%100}),zc("gggg","weekYear"),zc("ggggg","weekYear"),zc("GGGG","isoWeekYear"),zc("GGGGG","isoWeekYear"),
|
||||
// ALIASES
|
||||
J("weekYear","gg"),J("isoWeekYear","GG"),
|
||||
// PRIORITY
|
||||
M("weekYear",1),M("isoWeekYear",1),
|
||||
// PARSING
|
||||
Z("G",Vd),Z("g",Vd),Z("GG",Od,Kd),Z("gg",Od,Kd),Z("GGGG",Sd,Md),Z("gggg",Sd,Md),Z("GGGGG",Td,Nd),Z("ggggg",Td,Nd),ca(["gggg","ggggg","GGGG","GGGGG"],function(a,b,c,d){b[d.substr(0,2)]=u(a)}),ca(["gg","GG"],function(b,c,d,e){c[e]=a.parseTwoDigitYear(b)}),
|
||||
// FORMATTING
|
||||
U("Q",0,"Qo","quarter"),
|
||||
// ALIASES
|
||||
J("quarter","Q"),
|
||||
// PRIORITY
|
||||
M("quarter",7),
|
||||
// PARSING
|
||||
Z("Q",Jd),ba("Q",function(a,b){b[be]=3*(u(a)-1)}),
|
||||
// FORMATTING
|
||||
U("D",["DD",2],"Do","date"),
|
||||
// ALIASES
|
||||
J("date","D"),
|
||||
// PRIOROITY
|
||||
M("date",9),
|
||||
// PARSING
|
||||
Z("D",Od),Z("DD",Od,Kd),Z("Do",function(a,b){return a?b._ordinalParse:b._ordinalParseLenient}),ba(["D","DD"],ce),ba("Do",function(a,b){b[ce]=u(a.match(Od)[0],10)});
|
||||
// MOMENTS
|
||||
var Se=O("Date",!0);
|
||||
// FORMATTING
|
||||
U("DDD",["DDDD",3],"DDDo","dayOfYear"),
|
||||
// ALIASES
|
||||
J("dayOfYear","DDD"),
|
||||
// PRIORITY
|
||||
M("dayOfYear",4),
|
||||
// PARSING
|
||||
Z("DDD",Rd),Z("DDDD",Ld),ba(["DDD","DDDD"],function(a,b,c){c._dayOfYear=u(a)}),
|
||||
// FORMATTING
|
||||
U("m",["mm",2],0,"minute"),
|
||||
// ALIASES
|
||||
J("minute","m"),
|
||||
// PRIORITY
|
||||
M("minute",14),
|
||||
// PARSING
|
||||
Z("m",Od),Z("mm",Od,Kd),ba(["m","mm"],ee);
|
||||
// MOMENTS
|
||||
var Te=O("Minutes",!1);
|
||||
// FORMATTING
|
||||
U("s",["ss",2],0,"second"),
|
||||
// ALIASES
|
||||
J("second","s"),
|
||||
// PRIORITY
|
||||
M("second",15),
|
||||
// PARSING
|
||||
Z("s",Od),Z("ss",Od,Kd),ba(["s","ss"],fe);
|
||||
// MOMENTS
|
||||
var Ue=O("Seconds",!1);
|
||||
// FORMATTING
|
||||
U("S",0,0,function(){return~~(this.millisecond()/100)}),U(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),U(0,["SSS",3],0,"millisecond"),U(0,["SSSS",4],0,function(){return 10*this.millisecond()}),U(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),U(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),U(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),U(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),U(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),
|
||||
// ALIASES
|
||||
J("millisecond","ms"),
|
||||
// PRIORITY
|
||||
M("millisecond",16),
|
||||
// PARSING
|
||||
Z("S",Rd,Jd),Z("SS",Rd,Kd),Z("SSS",Rd,Ld);var Ve;for(Ve="SSSS";Ve.length<=9;Ve+="S")Z(Ve,Ud);for(Ve="S";Ve.length<=9;Ve+="S")ba(Ve,Ic);
|
||||
// MOMENTS
|
||||
var We=O("Milliseconds",!1);
|
||||
// FORMATTING
|
||||
U("z",0,0,"zoneAbbr"),U("zz",0,0,"zoneName");var Xe=r.prototype;Xe.add=Pe,Xe.calendar=Vb,Xe.clone=Wb,Xe.diff=bc,Xe.endOf=oc,Xe.format=gc,Xe.from=hc,Xe.fromNow=ic,Xe.to=jc,Xe.toNow=kc,Xe.get=R,Xe.invalidAt=xc,Xe.isAfter=Xb,Xe.isBefore=Yb,Xe.isBetween=Zb,Xe.isSame=$b,Xe.isSameOrAfter=_b,Xe.isSameOrBefore=ac,Xe.isValid=vc,Xe.lang=Re,Xe.locale=lc,Xe.localeData=mc,Xe.max=Ke,Xe.min=Je,Xe.parsingFlags=wc,Xe.set=S,Xe.startOf=nc,Xe.subtract=Qe,Xe.toArray=sc,Xe.toObject=tc,Xe.toDate=rc,Xe.toISOString=ec,Xe.inspect=fc,Xe.toJSON=uc,Xe.toString=dc,Xe.unix=qc,Xe.valueOf=pc,Xe.creationData=yc,
|
||||
// Year
|
||||
Xe.year=pe,Xe.isLeapYear=ra,
|
||||
// Week Year
|
||||
Xe.weekYear=Ac,Xe.isoWeekYear=Bc,
|
||||
// Quarter
|
||||
Xe.quarter=Xe.quarters=Gc,
|
||||
// Month
|
||||
Xe.month=ka,Xe.daysInMonth=la,
|
||||
// Week
|
||||
Xe.week=Xe.weeks=Ba,Xe.isoWeek=Xe.isoWeeks=Ca,Xe.weeksInYear=Dc,Xe.isoWeeksInYear=Cc,
|
||||
// Day
|
||||
Xe.date=Se,Xe.day=Xe.days=Ka,Xe.weekday=La,Xe.isoWeekday=Ma,Xe.dayOfYear=Hc,
|
||||
// Hour
|
||||
Xe.hour=Xe.hours=ze,
|
||||
// Minute
|
||||
Xe.minute=Xe.minutes=Te,
|
||||
// Second
|
||||
Xe.second=Xe.seconds=Ue,
|
||||
// Millisecond
|
||||
Xe.millisecond=Xe.milliseconds=We,
|
||||
// Offset
|
||||
Xe.utcOffset=Db,Xe.utc=Fb,Xe.local=Gb,Xe.parseZone=Hb,Xe.hasAlignedHourOffset=Ib,Xe.isDST=Jb,Xe.isLocal=Lb,Xe.isUtcOffset=Mb,Xe.isUtc=Nb,Xe.isUTC=Nb,
|
||||
// Timezone
|
||||
Xe.zoneAbbr=Jc,Xe.zoneName=Kc,
|
||||
// Deprecations
|
||||
Xe.dates=x("dates accessor is deprecated. Use date instead.",Se),Xe.months=x("months accessor is deprecated. Use month instead",ka),Xe.years=x("years accessor is deprecated. Use year instead",pe),Xe.zone=x("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",Eb),Xe.isDSTShifted=x("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",Kb);var Ye=C.prototype;Ye.calendar=D,Ye.longDateFormat=E,Ye.invalidDate=F,Ye.ordinal=G,Ye.preparse=Nc,Ye.postformat=Nc,Ye.relativeTime=H,Ye.pastFuture=I,Ye.set=A,
|
||||
// Month
|
||||
Ye.months=fa,Ye.monthsShort=ga,Ye.monthsParse=ia,Ye.monthsRegex=na,Ye.monthsShortRegex=ma,
|
||||
// Week
|
||||
Ye.week=ya,Ye.firstDayOfYear=Aa,Ye.firstDayOfWeek=za,
|
||||
// Day of Week
|
||||
Ye.weekdays=Fa,Ye.weekdaysMin=Ha,Ye.weekdaysShort=Ga,Ye.weekdaysParse=Ja,Ye.weekdaysRegex=Na,Ye.weekdaysShortRegex=Oa,Ye.weekdaysMinRegex=Pa,
|
||||
// Hours
|
||||
Ye.isPM=Va,Ye.meridiem=Wa,$a("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===u(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),
|
||||
// Side effect imports
|
||||
a.lang=x("moment.lang is deprecated. Use moment.locale instead.",$a),a.langData=x("moment.langData is deprecated. Use moment.localeData instead.",bb);var Ze=Math.abs,$e=ed("ms"),_e=ed("s"),af=ed("m"),bf=ed("h"),cf=ed("d"),df=ed("w"),ef=ed("M"),ff=ed("y"),gf=gd("milliseconds"),hf=gd("seconds"),jf=gd("minutes"),kf=gd("hours"),lf=gd("days"),mf=gd("months"),nf=gd("years"),of=Math.round,pf={s:45,// seconds to minute
|
||||
m:45,// minutes to hour
|
||||
h:22,// hours to day
|
||||
d:26,// days to month
|
||||
M:11},qf=Math.abs,rf=wb.prototype;
|
||||
// Deprecations
|
||||
// Side effect imports
|
||||
// FORMATTING
|
||||
// PARSING
|
||||
// Side effect imports
|
||||
return rf.abs=Wc,rf.add=Yc,rf.subtract=Zc,rf.as=cd,rf.asMilliseconds=$e,rf.asSeconds=_e,rf.asMinutes=af,rf.asHours=bf,rf.asDays=cf,rf.asWeeks=df,rf.asMonths=ef,rf.asYears=ff,rf.valueOf=dd,rf._bubble=_c,rf.get=fd,rf.milliseconds=gf,rf.seconds=hf,rf.minutes=jf,rf.hours=kf,rf.days=lf,rf.weeks=hd,rf.months=mf,rf.years=nf,rf.humanize=md,rf.toISOString=nd,rf.toString=nd,rf.toJSON=nd,rf.locale=lc,rf.localeData=mc,rf.toIsoString=x("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",nd),rf.lang=Re,U("X",0,0,"unix"),U("x",0,0,"valueOf"),Z("x",Vd),Z("X",Yd),ba("X",function(a,b,c){c._d=new Date(1e3*parseFloat(a,10))}),ba("x",function(a,b,c){c._d=new Date(u(a))}),a.version="2.17.1",b(sb),a.fn=Xe,a.min=ub,a.max=vb,a.now=Le,a.utc=k,a.unix=Lc,a.months=Rc,a.isDate=g,a.locale=$a,a.invalid=o,a.duration=Ob,a.isMoment=s,a.weekdays=Tc,a.parseZone=Mc,a.localeData=bb,a.isDuration=xb,a.monthsShort=Sc,a.weekdaysMin=Vc,a.defineLocale=_a,a.updateLocale=ab,a.locales=cb,a.weekdaysShort=Uc,a.normalizeUnits=K,a.relativeTimeRounding=kd,a.relativeTimeThreshold=ld,a.calendarFormat=Ub,a.prototype=Xe,a});
|
||||
File diff suppressed because one or more lines are too long
3195
src/main/webapp/assets/vendors/datepicker/js/moment.js
vendored
3195
src/main/webapp/assets/vendors/datepicker/js/moment.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,8 @@ class GitBucketCoreModuleSpec extends FunSuite {
|
||||
.withPort(3306)
|
||||
.withUser("sa", "sa")
|
||||
.withCharset(Charset.UTF8)
|
||||
.withServerVariable("log_syslog", 0)
|
||||
.withServerVariable("bind-address", "127.0.0.1")
|
||||
.build()
|
||||
|
||||
val mysqld = anEmbeddedMysql(config)
|
||||
@@ -41,7 +43,7 @@ class GitBucketCoreModuleSpec extends FunSuite {
|
||||
|
||||
try {
|
||||
new Solidbase().migrate(
|
||||
DriverManager.getConnection("jdbc:mysql://localhost:3306/gitbucket", "sa", "sa"),
|
||||
DriverManager.getConnection("jdbc:mysql://localhost:3306/gitbucket?useSSL=false", "sa", "sa"),
|
||||
Thread.currentThread().getContextClassLoader(),
|
||||
new MySQLDatabase(),
|
||||
new Module(GitBucketCoreModule.getModuleId, GitBucketCoreModule.getVersions)
|
||||
|
||||
@@ -10,7 +10,7 @@ class AccountServiceSpec extends FunSuite with ServiceSpecBase {
|
||||
|
||||
test("getAllUsers") { withTestDB { implicit session =>
|
||||
assert(AccountService.getAllUsers() match {
|
||||
case List(Account("root", "root", RootMailAddress, _, true, _, _, _, None, None, false, false)) => true
|
||||
case List(Account("root", "root", RootMailAddress, _, true, _, _, _, None, None, false, false, None)) => true
|
||||
case _ => false
|
||||
})
|
||||
}}
|
||||
@@ -47,12 +47,20 @@ class AccountServiceSpec extends FunSuite with ServiceSpecBase {
|
||||
val newAddress = "new mail address"
|
||||
AccountService.updateAccount(user().copy(mailAddress = newAddress))
|
||||
assert(user().mailAddress == newAddress)
|
||||
|
||||
val newUrl = Some("http://new.url.example/path")
|
||||
AccountService.updateAccount(user().copy(url = newUrl))
|
||||
assert(user().url == newUrl)
|
||||
|
||||
val newDescription = Some("http://new.url.example/path")
|
||||
AccountService.updateAccount(user().copy(description = newDescription))
|
||||
assert(user().description == newDescription)
|
||||
}}
|
||||
|
||||
test("group") { withTestDB { implicit session =>
|
||||
val group1 = "group1"
|
||||
val user1 = "root"
|
||||
AccountService.createGroup(group1, None)
|
||||
AccountService.createGroup(group1, None, None)
|
||||
|
||||
assert(AccountService.getGroupMembers(group1) == Nil)
|
||||
assert(AccountService.getGroupsByUserName(user1) == Nil)
|
||||
@@ -67,5 +75,20 @@ class AccountServiceSpec extends FunSuite with ServiceSpecBase {
|
||||
assert(AccountService.getGroupMembers(group1) == Nil)
|
||||
assert(AccountService.getGroupsByUserName(user1) == Nil)
|
||||
}}
|
||||
}
|
||||
|
||||
test("createGroup save description") { withTestDB { implicit session =>
|
||||
AccountService.createGroup("some-group", Some("some clever description"), None)
|
||||
val maybeGroup = AccountService.getAccountByUserName("some-group")
|
||||
|
||||
assert(maybeGroup.flatMap(_.description) == Some("some clever description"))
|
||||
}}
|
||||
|
||||
test("updateGroup save description") { withTestDB { implicit session =>
|
||||
AccountService.createGroup("a-group", None, None)
|
||||
|
||||
AccountService.updateGroup("a-group", Some("new description"), None, false)
|
||||
|
||||
val group = AccountService.getAccountByUserName("a-group")
|
||||
assert(group.flatMap(_.description) == Some("new description"))
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ class ProtectedBranchServiceSpec extends FunSpec with ServiceSpecBase with Prote
|
||||
it("administrator is manager") {
|
||||
withTestDB { implicit session =>
|
||||
val x = ProtectedBranchInfo("grp1", "repo1", true, Nil, false)
|
||||
x.createGroup("grp1", None)
|
||||
x.createGroup("grp1", None, None)
|
||||
generateNewAccount("user1")
|
||||
generateNewAccount("user2")
|
||||
generateNewAccount("user3")
|
||||
|
||||
@@ -38,7 +38,7 @@ trait ServiceSpecBase {
|
||||
}
|
||||
|
||||
def generateNewAccount(name:String)(implicit s:Session):Account = {
|
||||
AccountService.createAccount(name, name, name, s"${name}@example.com", false, None)
|
||||
AccountService.createAccount(name, name, name, s"${name}@example.com", false, None, None)
|
||||
user(name)
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ trait ServiceSpecBase {
|
||||
}
|
||||
|
||||
def generateNewIssue(userName:String, repositoryName:String, loginUser:String="root")(implicit s:Session): Int = {
|
||||
dummyService.createIssue(
|
||||
dummyService.insertIssue(
|
||||
owner = userName,
|
||||
repository = repositoryName,
|
||||
loginUser = loginUser,
|
||||
|
||||
@@ -94,7 +94,8 @@ class AvatarImageProviderSpec extends FunSpec with MockitoSugar {
|
||||
lastLoginDate = None,
|
||||
image = image,
|
||||
isGroupAccount = false,
|
||||
isRemoved = false)
|
||||
isRemoved = false,
|
||||
description = None)
|
||||
|
||||
private def createSystemSettings(useGravatar: Boolean) =
|
||||
SystemSettings(
|
||||
|
||||
Reference in New Issue
Block a user