mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-08 00:27:36 +02:00
Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54d1bff213 | ||
|
|
0075664b9a | ||
|
|
45f41a13e4 | ||
|
|
faae237ac5 | ||
|
|
e01758e74c | ||
|
|
db7dd31c79 | ||
|
|
7d7ac5e2be | ||
|
|
577016a33f | ||
|
|
fdf2102923 | ||
|
|
8b47e57be0 | ||
|
|
8dad6b64b0 | ||
|
|
05d36abdab | ||
|
|
04e31c5b4f | ||
|
|
6470428a85 | ||
|
|
a7efb3989a | ||
|
|
32072d0bbf | ||
|
|
78a3e4454d | ||
|
|
e576178e1e | ||
|
|
0f8bc2b03d | ||
|
|
43565458d4 | ||
|
|
71cc3be6d5 | ||
|
|
9a8eef7b19 | ||
|
|
a08c4368b7 | ||
|
|
3b456b2aab | ||
|
|
31559418ba | ||
|
|
692a6e43bc | ||
|
|
af4cce654c | ||
|
|
c63b02fd4a | ||
|
|
e6974b6e51 | ||
|
|
da3b7dbeff | ||
|
|
9737bd7012 | ||
|
|
55fd8e5e2d | ||
|
|
9c4f181d93 | ||
|
|
374342cfc1 | ||
|
|
6fbdd237d1 | ||
|
|
4e78f01a09 | ||
|
|
8853264808 | ||
|
|
6db9b8038f | ||
|
|
c73e89ccd4 | ||
|
|
f58a506780 | ||
|
|
411d19e74e | ||
|
|
bd51ffd9d2 | ||
|
|
83980fdccd | ||
|
|
baab243bc8 | ||
|
|
4dd8e1dc63 | ||
|
|
0b11b8b084 | ||
|
|
704775dc60 | ||
|
|
c7a7be1de0 | ||
|
|
e21a970977 | ||
|
|
a526dcf2dd | ||
|
|
a7b48d63e4 | ||
|
|
3677906e95 | ||
|
|
e86710fbbd | ||
|
|
73e850493a | ||
|
|
7d735f6f8a | ||
|
|
043e99a9eb | ||
|
|
65e079b1d3 | ||
|
|
32e6f584d8 | ||
|
|
ebc5219ce6 | ||
|
|
ad8e620bdf | ||
|
|
8590c693b9 | ||
|
|
5568acc5f3 | ||
|
|
c467594199 | ||
|
|
8b29bf7d93 | ||
|
|
43b7f83082 | ||
|
|
9ef5c981ef | ||
|
|
f027ac34d4 | ||
|
|
2ea31d6869 | ||
|
|
a1af7d0f9c | ||
|
|
797bf37bfa | ||
|
|
7f78815c11 | ||
|
|
d8a3f308ed | ||
|
|
74e18a982d | ||
|
|
e86ad423c5 | ||
|
|
2f00060c57 | ||
|
|
3b7e6aa68e | ||
|
|
5f3d6242fd | ||
|
|
6793a86bae | ||
|
|
d99ce20529 | ||
|
|
93e7b604cd | ||
|
|
59c18056fc | ||
|
|
5c81ce9b68 | ||
|
|
c9cf62701f | ||
|
|
f65babca4b | ||
|
|
23c146fc5d | ||
|
|
e14f336142 | ||
|
|
54647be5bd | ||
|
|
9517d65646 | ||
|
|
0156e401fa | ||
|
|
d6e2bc464d | ||
|
|
5124ff593d | ||
|
|
727c90afdc | ||
|
|
9ebd5e3265 | ||
|
|
d120142127 | ||
|
|
f9e4cddcaf | ||
|
|
8f64f174d9 | ||
|
|
d6817796b3 | ||
|
|
ab19d473c4 | ||
|
|
48f0116358 | ||
|
|
d8e6e97845 | ||
|
|
9a8920788c | ||
|
|
27864a3a3c | ||
|
|
b39e863591 | ||
|
|
d8d18ed25c | ||
|
|
7661e8cadd | ||
|
|
83fd2648f5 | ||
|
|
8e81758941 | ||
|
|
41a6a29771 | ||
|
|
3e0a50926f | ||
|
|
e408eb43bb | ||
|
|
dc0aa0851e | ||
|
|
51d7c43489 |
16
README.md
16
README.md
@@ -79,6 +79,22 @@ Run the following commands in `Terminal` to
|
||||
|
||||
Release Notes
|
||||
--------
|
||||
### 3.3 - 31 May 2015
|
||||
- Rich graphical diff for images
|
||||
- File finder is available in the repository viewer
|
||||
- Blame is displayed at the source viewer
|
||||
- Remain user data and repositories even if user is disabled
|
||||
- Mobile view improvement
|
||||
|
||||
### 3.2 - 3 May 2015
|
||||
- Directory history button
|
||||
- Compare / pull request button
|
||||
- Limit of activity log
|
||||
|
||||
### 3.1.1 - 4 Apr 2015
|
||||
- Rolled back H2 version to avoid version compatibility issue
|
||||
- Plug-ins became possible to access ServletContext
|
||||
|
||||
### 3.1 - 28 Mar 2015
|
||||
- Web APIs for Jenkins github pull-request builder
|
||||
- Improved diff view
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<property name="embed.classes.dir" value="${target.dir}/embed-classes"/>
|
||||
<property name="jetty.dir" value="embed-jetty"/>
|
||||
<property name="scala.version" value="2.11"/>
|
||||
<property name="gitbucket.version" value="3.0.0"/>
|
||||
<property name="gitbucket.version" value="3.2.0"/>
|
||||
<property name="jetty.version" value="8.1.16.v20140903"/>
|
||||
<property name="servlet.version" value="3.0.0.v201112011016"/>
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#!/bin/sh
|
||||
./sbt.sh clean assembly
|
||||
|
||||
mvn deploy:deploy-file \
|
||||
-DgroupId=gitbucket\
|
||||
-DartifactId=gitbucket-assembly\
|
||||
-Dversion=3.0.0\
|
||||
-Dversion=3.2.0\
|
||||
-Dpackaging=jar\
|
||||
-Dfile=../target/scala-2.11/gitbucket-assembly-3.0.0.jar\
|
||||
-Dfile=../target/scala-2.11/gitbucket-assembly-3.3.0.jar\
|
||||
-DrepositoryId=sourceforge.jp\
|
||||
-Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/mvn/
|
||||
@@ -6,7 +6,7 @@ This directory has following structure:
|
||||
|
||||
```
|
||||
* /HOME/gitbucket
|
||||
* /repositoties
|
||||
* /repositories
|
||||
* /USER_NAME
|
||||
* / REPO_NAME.git (substance of repository. GitServlet sees this directory)
|
||||
* / REPO_NAME
|
||||
|
||||
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
@@ -8,3 +8,4 @@ Developer's Guide
|
||||
* [Activity Types](activity.md)
|
||||
* [Notification Email](notification.md)
|
||||
* [Automatic Schema Updating](auto_update.md)
|
||||
* [Release Operation](release.md)
|
||||
|
||||
68
doc/release.md
Normal file
68
doc/release.md
Normal file
@@ -0,0 +1,68 @@
|
||||
Release Operation
|
||||
========
|
||||
|
||||
Update version number
|
||||
--------
|
||||
|
||||
Note to update version number in files below:
|
||||
|
||||
### project/build.scala
|
||||
|
||||
```scala
|
||||
object MyBuild extends Build {
|
||||
val Organization = "gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val Version = "3.2.0" // <---- update here!!
|
||||
val ScalaVersion = "2.11.6"
|
||||
val ScalatraVersion = "2.3.1"
|
||||
```
|
||||
|
||||
### src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
|
||||
|
||||
```scala
|
||||
object AutoUpdate {
|
||||
|
||||
/**
|
||||
* The history of versions. A head of this sequence is the current BitBucket version.
|
||||
*/
|
||||
val versions = Seq(
|
||||
new Version(3, 2), // <---- add this!!
|
||||
new Version(3, 1),
|
||||
...
|
||||
```
|
||||
|
||||
### deploy-assembly/deploy-assembly-jar.sh
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
./sbt.sh assembly
|
||||
|
||||
mvn deploy:deploy-file \
|
||||
-DgroupId=gitbucket\
|
||||
-DartifactId=gitbucket-assembly\
|
||||
-Dversion=3.2.0\ # <---- update here!!
|
||||
-Dpackaging=jar\
|
||||
-Dfile=../target/scala-2.11/gitbucket-assembly-x.x.x.jar\ # <---- update here!!
|
||||
-DrepositoryId=sourceforge.jp\
|
||||
-Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/mvn/
|
||||
```
|
||||
|
||||
Generate release files
|
||||
--------
|
||||
|
||||
Note: Release operation requires [Ant](http://ant.apache.org/) and [Maven](https://maven.apache.org/).
|
||||
|
||||
### Make release war file
|
||||
|
||||
Run ant with `build.xml` in the root directory. The release war file is generated into `target/scala-2.11/gitbucket.war`.
|
||||
|
||||
### Deploy assemnbly jar file
|
||||
|
||||
For plug-in development, we have to publish the assembly jar file to the public Maven repository.
|
||||
|
||||
```
|
||||
cd deploy-assembly/
|
||||
./deploy-assembly-jar.sh
|
||||
```
|
||||
|
||||
This script runs `sbt assembly` and `mvn deploy`.
|
||||
14
gitbucket-assembly.iml
Normal file
14
gitbucket-assembly.iml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="false">
|
||||
<output url="file://$MODULE_DIR$/target/classes" />
|
||||
<output-test url="file://$MODULE_DIR$/target/test-classes" />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -10,7 +10,7 @@ import sbtassembly.AssemblyKeys._
|
||||
object MyBuild extends Build {
|
||||
val Organization = "gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val Version = "3.0.0"
|
||||
val Version = "3.3.0"
|
||||
val ScalaVersion = "2.11.6"
|
||||
val ScalatraVersion = "2.3.1"
|
||||
|
||||
@@ -57,14 +57,16 @@ object MyBuild extends Build {
|
||||
"org.apache.sshd" % "apache-sshd" % "0.11.0",
|
||||
"com.typesafe.slick" %% "slick" % "2.1.0",
|
||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||
"com.h2database" % "h2" % "1.4.186",
|
||||
"com.h2database" % "h2" % "1.4.180",
|
||||
// "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % "8.1.16.v20140903" % "container;provided",
|
||||
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
|
||||
"junit" % "junit" % "4.12" % "test",
|
||||
"com.mchange" % "c3p0" % "0.9.5",
|
||||
"com.typesafe" % "config" % "1.2.1",
|
||||
"com.typesafe.play" %% "twirl-compiler" % "1.0.4"
|
||||
"com.typesafe.play" %% "twirl-compiler" % "1.0.4",
|
||||
"com.typesafe.akka" %% "akka-actor" % "2.3.10",
|
||||
"com.enragedginger" %% "akka-quartz-scheduler" % "1.3.0-akka-2.3.x"
|
||||
),
|
||||
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._",
|
||||
EclipseKeys.withSource := true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import gitbucket.core.controller._
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.servlet.{AccessTokenAuthenticationFilter, BasicAuthenticationFilter, TransactionFilter}
|
||||
import gitbucket.core.servlet.{AccessTokenAuthenticationFilter, BasicAuthenticationFilter, Database, TransactionFilter}
|
||||
import gitbucket.core.util.Directory
|
||||
|
||||
import java.util.EnumSet
|
||||
@@ -47,4 +47,8 @@ class ScalatraBootstrap extends LifeCycle {
|
||||
dir.mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
override def destroy(context: ServletContext): Unit = {
|
||||
Database.closeDataSource()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.model.IssueComment
|
||||
import gitbucket.core.util.RepositoryName
|
||||
|
||||
import java.util.Date
|
||||
|
||||
@@ -13,14 +14,16 @@ case class ApiComment(
|
||||
user: ApiUser,
|
||||
body: String,
|
||||
created_at: Date,
|
||||
updated_at: Date)
|
||||
updated_at: Date)(repositoryName: RepositoryName, issueId: Int){
|
||||
val html_url = ApiPath(s"/${repositoryName.fullName}/issues/${issueId}#comment-${id}")
|
||||
}
|
||||
|
||||
object ApiComment{
|
||||
def apply(comment: IssueComment, user: ApiUser): ApiComment =
|
||||
def apply(comment: IssueComment, repositoryName: RepositoryName, issueId: Int, user: ApiUser): ApiComment =
|
||||
ApiComment(
|
||||
id = comment.commentId,
|
||||
user = user,
|
||||
body = comment.content,
|
||||
created_at = comment.registeredDate,
|
||||
updated_at = comment.updatedDate)
|
||||
updated_at = comment.updatedDate)(repositoryName, issueId)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.model.Issue
|
||||
import gitbucket.core.util.RepositoryName
|
||||
|
||||
import java.util.Date
|
||||
|
||||
@@ -16,10 +17,13 @@ case class ApiIssue(
|
||||
state: String,
|
||||
created_at: Date,
|
||||
updated_at: Date,
|
||||
body: String)
|
||||
body: String)(repositoryName: RepositoryName){
|
||||
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
|
||||
val html_url = ApiPath(s"/${repositoryName.fullName}/issues/${number}")
|
||||
}
|
||||
|
||||
object ApiIssue{
|
||||
def apply(issue: Issue, user: ApiUser): ApiIssue =
|
||||
def apply(issue: Issue, repositoryName: RepositoryName, user: ApiUser): ApiIssue =
|
||||
ApiIssue(
|
||||
number = issue.issueId,
|
||||
title = issue.title,
|
||||
@@ -27,5 +31,5 @@ object ApiIssue{
|
||||
state = if(issue.closed){ "closed" }else{ "open" },
|
||||
body = issue.content.getOrElse(""),
|
||||
created_at = issue.registeredDate,
|
||||
updated_at = issue.updatedDate)
|
||||
updated_at = issue.updatedDate)(repositoryName)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ case class ApiUser(
|
||||
|
||||
object ApiUser{
|
||||
def apply(user: Account): ApiUser = ApiUser(
|
||||
login = user.fullName,
|
||||
login = user.userName,
|
||||
email = user.mailAddress,
|
||||
`type` = if(user.isGroupAccount){ "Organization" }else{ "User" },
|
||||
site_admin = user.isAdmin,
|
||||
|
||||
@@ -23,7 +23,7 @@ object JsonFormat {
|
||||
) + FieldSerializer[ApiUser]() + FieldSerializer[ApiPullRequest]() + FieldSerializer[ApiRepository]() +
|
||||
FieldSerializer[ApiCommitListItem.Parent]() + FieldSerializer[ApiCommitListItem]() + FieldSerializer[ApiCommitListItem.Commit]() +
|
||||
FieldSerializer[ApiCommitStatus]() + FieldSerializer[ApiCommit]() + FieldSerializer[ApiCombinedCommitStatus]() +
|
||||
FieldSerializer[ApiPullRequest.Commit]()
|
||||
FieldSerializer[ApiPullRequest.Commit]() + FieldSerializer[ApiIssue]() + FieldSerializer[ApiComment]()
|
||||
|
||||
|
||||
def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](format =>
|
||||
|
||||
@@ -202,15 +202,15 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
val userName = params("userName")
|
||||
|
||||
getAccountByUserName(userName, true).foreach { account =>
|
||||
// Remove repositories
|
||||
getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
||||
deleteRepository(userName, repositoryName)
|
||||
FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
||||
FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
||||
FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
||||
}
|
||||
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
||||
removeUserRelatedData(userName)
|
||||
// // Remove repositories
|
||||
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
||||
// deleteRepository(userName, repositoryName)
|
||||
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
||||
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
||||
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
||||
// }
|
||||
// // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
||||
// removeUserRelatedData(userName)
|
||||
|
||||
updateAccount(account.copy(isRemoved = true))
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ trait IndexControllerBase extends ControllerBase {
|
||||
get("/_user/proposals")(usersOnly {
|
||||
contentType = formats("json")
|
||||
org.json4s.jackson.Serialization.write(
|
||||
Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray)
|
||||
Map("options" -> getAllUsers(false).filter(!_.isGroupAccount).map(_.userName).toArray)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
issueId <- params("id").toIntOpt
|
||||
comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
|
||||
} yield {
|
||||
JsonFormat(comments.map{ case (issueComment, user) => ApiComment(issueComment, ApiUser(user)) })
|
||||
JsonFormat(comments.map{ case (issueComment, user) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user)) })
|
||||
}).getOrElse(NotFound)
|
||||
})
|
||||
|
||||
@@ -132,11 +132,11 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
|
||||
// call web hooks
|
||||
callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)
|
||||
}
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
|
||||
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
|
||||
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||
}
|
||||
}
|
||||
|
||||
redirect(s"/${owner}/${name}/issues/${issueId}")
|
||||
@@ -190,7 +190,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
(issue, id) <- handleComment(issueId, Some(body), repository)()
|
||||
issueComment <- getComment(repository.owner, repository.name, id.toString())
|
||||
} yield {
|
||||
JsonFormat(ApiComment(issueComment, ApiUser(context.loginAccount.get)))
|
||||
JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get)))
|
||||
}) getOrElse NotFound
|
||||
})
|
||||
|
||||
@@ -418,13 +418,13 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
Notifier() match {
|
||||
case f =>
|
||||
content foreach {
|
||||
f.toNotify(repository, issueId, _){
|
||||
f.toNotify(repository, issue, _){
|
||||
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
|
||||
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId.get}")
|
||||
}
|
||||
}
|
||||
action foreach {
|
||||
f.toNotify(repository, issueId, _){
|
||||
f.toNotify(repository, issue, _){
|
||||
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,10 +125,10 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
(for{
|
||||
issueId <- params("id").toIntOpt
|
||||
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
|
||||
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.userName), Set())
|
||||
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set())
|
||||
baseOwner <- users.get(repository.owner)
|
||||
headOwner <- users.get(pullRequest.requestUserName)
|
||||
issueUser <- users.get(issue.userName)
|
||||
issueUser <- users.get(issue.openedUserName)
|
||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
|
||||
} yield {
|
||||
JsonFormat(ApiPullRequest(
|
||||
@@ -236,7 +236,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issueId, "merge"){
|
||||
Notifier().toNotify(repository, issue, "merge"){
|
||||
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
|
||||
}
|
||||
|
||||
@@ -248,6 +248,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
|
||||
val headBranch:Option[String] = params.get("head")
|
||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||
case (Some(originUserName), Some(originRepositoryName)) => {
|
||||
getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository =>
|
||||
@@ -255,8 +256,8 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
|
||||
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
|
||||
){ (oldGit, newGit) =>
|
||||
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
|
||||
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
|
||||
val newBranch = headBranch.getOrElse(JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2)
|
||||
val oldBranch = originRepository.branchList.find( _ == newBranch).getOrElse(JGitUtil.getDefaultBranch(oldGit, originRepository).get._2)
|
||||
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
|
||||
}
|
||||
@@ -265,7 +266,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
case _ => {
|
||||
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
|
||||
JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${headBranch.getOrElse(defaultBranch)}")
|
||||
} getOrElse {
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
|
||||
}
|
||||
@@ -394,8 +395,10 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
|
||||
|
||||
// notifications
|
||||
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
|
||||
Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
getIssue(repository.owner, repository.name, issueId.toString) foreach { issue =>
|
||||
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
|
||||
Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
}
|
||||
}
|
||||
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
|
||||
@@ -163,7 +163,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
post("/:owner/:repository/settings/hooks/test", webHookForm)(ownerOnly { (form, repository) =>
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
import scala.collection.JavaConverters._
|
||||
val commits = git.log
|
||||
val commits = if(repository.commitCount == 0) List.empty else git.log
|
||||
.add(git.getRepository.resolve(repository.repository.defaultBranch))
|
||||
.setMaxCount(3)
|
||||
.call.iterator.asScala.map(new CommitInfo(_))
|
||||
|
||||
@@ -32,7 +32,6 @@ class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
|
||||
with WebHookPullRequestService
|
||||
|
||||
|
||||
/**
|
||||
* The repository viewer.
|
||||
*/
|
||||
@@ -284,27 +283,60 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
/**
|
||||
* Displays the file content of the specified branch or commit.
|
||||
*/
|
||||
get("/:owner/:repository/blob/*")(referrersOnly { repository =>
|
||||
val blobRoute = get("/:owner/:repository/blob/*")(referrersOnly { repository =>
|
||||
val (id, path) = splitPath(repository, multiParams("splat").head)
|
||||
val raw = params.get("raw").getOrElse("false").toBoolean
|
||||
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
|
||||
val lastModifiedCommit = JGitUtil.getLastModifiedCommit(git, revCommit, path)
|
||||
getPathObjectId(git, path, revCommit).map { objectId =>
|
||||
if(raw){
|
||||
// Download
|
||||
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
|
||||
RawData(FileUtil.getContentType(path, bytes), bytes)
|
||||
}
|
||||
JGitUtil.getContentFromId(git, objectId, true).map { bytes =>
|
||||
RawData("application/octet-stream", bytes)
|
||||
} getOrElse NotFound
|
||||
} else {
|
||||
html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
|
||||
new JGitUtil.CommitInfo(lastModifiedCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
html.blob(id, repository, path.split("/").toList,
|
||||
JGitUtil.getContentInfo(git, path, objectId),
|
||||
new JGitUtil.CommitInfo(JGitUtil.getLastModifiedCommit(git, revCommit, path)),
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
||||
request.paths(2) == "blame")
|
||||
}
|
||||
} getOrElse NotFound
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/blame/*"){
|
||||
blobRoute.action()
|
||||
}
|
||||
|
||||
/**
|
||||
* Blame data.
|
||||
*/
|
||||
ajaxGet("/:owner/:repository/get-blame/*")(referrersOnly { repository =>
|
||||
val (id, path) = splitPath(repository, multiParams("splat").head)
|
||||
contentType = formats("json")
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
val last = git.log.add(git.getRepository.resolve(id)).addPath(path).setMaxCount(1).call.iterator.next.name
|
||||
Map(
|
||||
"root" -> s"${context.baseUrl}/${repository.owner}/${repository.name}",
|
||||
"id" -> id,
|
||||
"path" -> path,
|
||||
"last" -> last,
|
||||
"blame" -> JGitUtil.getBlame(git, id, path).map{ blame =>
|
||||
Map(
|
||||
"id" -> blame.id,
|
||||
"author" -> view.helpers.user(blame.authorName, blame.authorEmailAddress).toString,
|
||||
"avatar" -> view.helpers.avatarLink(blame.authorName, 32, blame.authorEmailAddress).toString,
|
||||
"authed" -> helper.html.datetimeago(blame.authorTime).toString,
|
||||
"prev" -> blame.prev,
|
||||
"prevPath" -> blame.prevPath,
|
||||
"commited" -> blame.commitTime.getTime,
|
||||
"message" -> blame.message,
|
||||
"lines" -> blame.lines)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Displays details of the specified commit.
|
||||
*/
|
||||
@@ -475,6 +507,34 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
repository)
|
||||
})
|
||||
|
||||
/**
|
||||
* Displays the file find of branch.
|
||||
*/
|
||||
get("/:owner/:repository/find/:ref")(referrersOnly { repository =>
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
JGitUtil.getTreeId(git, params("ref")).map{ treeId =>
|
||||
html.find(params("ref"),
|
||||
treeId,
|
||||
repository,
|
||||
context.loginAccount match {
|
||||
case None => List()
|
||||
case account: Option[Account] => getGroupsByUserName(account.get.userName)
|
||||
})
|
||||
} getOrElse NotFound
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Get all file list of branch.
|
||||
*/
|
||||
ajaxGet("/:owner/:repository/tree-list/:tree")(referrersOnly { repository =>
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
val treeId = params("tree")
|
||||
contentType = formats("json")
|
||||
Map("paths" -> JGitUtil.getAllFileListByTreeId(git, treeId))
|
||||
}
|
||||
})
|
||||
|
||||
private def splitPath(repository: RepositoryService.RepositoryInfo, path: String): (String, String) = {
|
||||
val id = repository.branchList.collectFirst {
|
||||
case branch if(path == branch || path.startsWith(branch + "/")) => branch
|
||||
@@ -525,6 +585,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
}, // groups of current user
|
||||
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
|
||||
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
||||
getPullRequestFromBranch(repository.owner, repository.name, revstr, repository.repository.defaultBranch),
|
||||
flash.get("info"), flash.get("error"))
|
||||
}
|
||||
} getOrElse NotFound
|
||||
|
||||
@@ -21,6 +21,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
||||
"isCreateRepoOptionPublic" -> trim(label("Default option to create a new repository", boolean())),
|
||||
"gravatar" -> trim(label("Gravatar", boolean())),
|
||||
"notification" -> trim(label("Notification", boolean())),
|
||||
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
|
||||
"ssh" -> trim(label("SSH access", boolean())),
|
||||
"sshPort" -> trim(label("SSH port", optional(number()))),
|
||||
"smtp" -> optionalIfNotChecked("notification", mapping(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package gitbucket.core.plugin
|
||||
|
||||
import javax.servlet.ServletContext
|
||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||
import gitbucket.core.util.Version
|
||||
|
||||
/**
|
||||
@@ -17,12 +19,12 @@ trait Plugin {
|
||||
* This method is invoked in initialization of plugin system.
|
||||
* Register plugin functionality to PluginRegistry.
|
||||
*/
|
||||
def initialize(registry: PluginRegistry): Unit
|
||||
def initialize(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit
|
||||
|
||||
/**
|
||||
* This method is invoked in shutdown of plugin system.
|
||||
* If the plugin has any resources, release them in this method.
|
||||
*/
|
||||
def shutdown(registry: PluginRegistry): Unit
|
||||
def shutdown(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
|
||||
|
||||
import gitbucket.core.controller.{Context, ControllerBase}
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.JDBCUtil._
|
||||
@@ -89,7 +90,7 @@ object PluginRegistry {
|
||||
/**
|
||||
* Initializes all installed plugins.
|
||||
*/
|
||||
def initialize(context: ServletContext, conn: java.sql.Connection): Unit = {
|
||||
def initialize(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = {
|
||||
val pluginDir = new File(PluginHome)
|
||||
if(pluginDir.exists && pluginDir.isDirectory){
|
||||
pluginDir.listFiles(new FilenameFilter {
|
||||
@@ -119,7 +120,7 @@ object PluginRegistry {
|
||||
}
|
||||
|
||||
// Initialize
|
||||
plugin.initialize(instance)
|
||||
plugin.initialize(instance, context, settings)
|
||||
instance.addPlugin(PluginInfo(
|
||||
pluginId = plugin.pluginId,
|
||||
pluginName = plugin.pluginName,
|
||||
@@ -137,10 +138,10 @@ object PluginRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
def shutdown(context: ServletContext): Unit = {
|
||||
def shutdown(context: ServletContext, settings: SystemSettings): Unit = {
|
||||
instance.getPlugins().foreach { pluginInfo =>
|
||||
try {
|
||||
pluginInfo.pluginClass.shutdown(instance)
|
||||
pluginInfo.pluginClass.shutdown(instance, context, settings)
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
logger.error(s"Error during plugin shutdown", e)
|
||||
|
||||
@@ -7,6 +7,12 @@ import profile.simple._
|
||||
|
||||
trait ActivityService {
|
||||
|
||||
def deleteOldActivities(limit: Int)(implicit s: Session): Int = {
|
||||
Activities.map(_.activityId).sortBy(_ desc).drop(limit).firstOption.map { id =>
|
||||
Activities.filter(_.activityId <= id.bind).delete
|
||||
} getOrElse 0
|
||||
}
|
||||
|
||||
def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit s: Session): List[Activity] =
|
||||
Activities
|
||||
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
|
||||
|
||||
@@ -22,10 +22,11 @@ trait IssuesService {
|
||||
def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) =
|
||||
IssueComments filter (_.byIssue(owner, repository, issueId)) list
|
||||
|
||||
def getCommentsForApi(owner: String, repository: String, issueId: Int)(implicit s: Session) =
|
||||
/** @return IssueComment and commentedUser */
|
||||
def getCommentsForApi(owner: String, repository: String, issueId: Int)(implicit s: Session): List[(IssueComment, Account)] =
|
||||
IssueComments.filter(_.byIssue(owner, repository, issueId))
|
||||
.filter(_.action inSetBind Set("comment" , "close_comment", "reopen_comment"))
|
||||
.innerJoin(Accounts).on( (t1, t2) => t1.userName === t2.userName )
|
||||
.innerJoin(Accounts).on( (t1, t2) => t1.commentedUserName === t2.userName )
|
||||
.list
|
||||
|
||||
def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
|
||||
@@ -168,7 +169,7 @@ trait IssuesService {
|
||||
}
|
||||
|
||||
/** for api
|
||||
* @return (issue, commentCount, pullRequest, headRepository, headOwner)
|
||||
* @return (issue, issueUser, commentCount, pullRequest, headRepo, headOwner)
|
||||
*/
|
||||
def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)
|
||||
(implicit s: Session): List[(Issue, Account, Int, PullRequest, Repository, Account)] = {
|
||||
@@ -176,7 +177,7 @@ trait IssuesService {
|
||||
searchIssueQueryBase(condition, true, offset, limit, repos)
|
||||
.innerJoin(PullRequests).on { case ((t1, t2), t3) => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) }
|
||||
.innerJoin(Repositories).on { case (((t1, t2), t3), t4) => t4.byRepository(t1.userName, t1.repositoryName) }
|
||||
.innerJoin(Accounts).on { case ((((t1, t2), t3), t4), t5) => t5.userName === t1.userName }
|
||||
.innerJoin(Accounts).on { case ((((t1, t2), t3), t4), t5) => t5.userName === t1.openedUserName }
|
||||
.innerJoin(Accounts).on { case (((((t1, t2), t3), t4), t5), t6) => t6.userName === t4.userName }
|
||||
.map { case (((((t1, t2), t3), t4), t5), t6) =>
|
||||
(t1, t5, t2.commentCount, t3, t4, t6)
|
||||
|
||||
@@ -83,6 +83,28 @@ trait PullRequestService { self: IssuesService =>
|
||||
.map { case (t1, t2) => t1 }
|
||||
.list
|
||||
|
||||
/**
|
||||
* for repository viewer.
|
||||
* 1. find pull request from from `branch` to othre branch on same repository
|
||||
* 1. return if exists pull request to `defaultBranch`
|
||||
* 2. return if exists pull request to othre branch
|
||||
* 2. return None
|
||||
*/
|
||||
def getPullRequestFromBranch(userName: String, repositoryName: String, branch: String, defaultBranch: String)
|
||||
(implicit s: Session): Option[(PullRequest, Issue)] =
|
||||
PullRequests
|
||||
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
|
||||
.filter { case (t1, t2) =>
|
||||
(t1.requestUserName === userName.bind) &&
|
||||
(t1.requestRepositoryName === repositoryName.bind) &&
|
||||
(t1.requestBranch === branch.bind) &&
|
||||
(t1.userName === userName.bind) &&
|
||||
(t1.repositoryName === repositoryName.bind) &&
|
||||
(t2.closed === false.bind)
|
||||
}
|
||||
.sortBy{ case (t1, t2) => t1.branch =!= defaultBranch.bind }
|
||||
.firstOption
|
||||
|
||||
/**
|
||||
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,7 @@ trait SystemSettingsService {
|
||||
props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString)
|
||||
props.setProperty(Gravatar, settings.gravatar.toString)
|
||||
props.setProperty(Notification, settings.notification.toString)
|
||||
settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString))
|
||||
props.setProperty(Ssh, settings.ssh.toString)
|
||||
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
|
||||
if(settings.notification) {
|
||||
@@ -65,12 +66,13 @@ trait SystemSettingsService {
|
||||
}
|
||||
SystemSettings(
|
||||
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
|
||||
getOptionValue[String](props, Information, None),
|
||||
getOptionValue(props, Information, None),
|
||||
getValue(props, AllowAccountRegistration, false),
|
||||
getValue(props, AllowAnonymousAccess, true),
|
||||
getValue(props, IsCreateRepoOptionPublic, true),
|
||||
getValue(props, Gravatar, true),
|
||||
getValue(props, Notification, false),
|
||||
getOptionValue[Int](props, ActivityLogLimit, None),
|
||||
getValue(props, Ssh, false),
|
||||
getOptionValue(props, SshPort, Some(DefaultSshPort)),
|
||||
if(getValue(props, Notification, false)){
|
||||
@@ -120,6 +122,7 @@ object SystemSettingsService {
|
||||
isCreateRepoOptionPublic: Boolean,
|
||||
gravatar: Boolean,
|
||||
notification: Boolean,
|
||||
activityLogLimit: Option[Int],
|
||||
ssh: Boolean,
|
||||
sshPort: Option[Int],
|
||||
smtp: Option[Smtp],
|
||||
@@ -166,6 +169,7 @@ object SystemSettingsService {
|
||||
private val IsCreateRepoOptionPublic = "is_create_repository_option_public"
|
||||
private val Gravatar = "gravatar"
|
||||
private val Notification = "notification"
|
||||
private val ActivityLogLimit = "activity_log_limit"
|
||||
private val Ssh = "ssh"
|
||||
private val SshPort = "ssh.port"
|
||||
private val SmtpHost = "smtp.host"
|
||||
|
||||
@@ -50,6 +50,7 @@ trait WebHookService {
|
||||
val f = Future {
|
||||
logger.debug(s"start web hook invocation for ${webHookUrl}")
|
||||
val httpPost = new HttpPost(webHookUrl.url)
|
||||
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
httpPost.addHeader("X-Github-Event", eventName)
|
||||
|
||||
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
|
||||
@@ -80,16 +81,16 @@ trait WebHookPullRequestService extends WebHookService {
|
||||
// https://developer.github.com/v3/activity/events/types/#issuesevent
|
||||
def callIssuesWebHook(action: String, repository: RepositoryService.RepositoryInfo, issue: Issue, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
|
||||
callWebHookOf(repository.owner, repository.name, "issues"){
|
||||
val users = getAccountsByUserNames(Set(repository.owner, issue.userName), Set(sender))
|
||||
val users = getAccountsByUserNames(Set(repository.owner, issue.openedUserName), Set(sender))
|
||||
for{
|
||||
repoOwner <- users.get(repository.owner)
|
||||
issueUser <- users.get(issue.userName)
|
||||
issueUser <- users.get(issue.openedUserName)
|
||||
} yield {
|
||||
WebHookIssuesPayload(
|
||||
action = action,
|
||||
number = issue.issueId,
|
||||
repository = ApiRepository(repository, ApiUser(repoOwner)),
|
||||
issue = ApiIssue(issue, ApiUser(issueUser)),
|
||||
issue = ApiIssue(issue, RepositoryName(repository), ApiUser(issueUser)),
|
||||
sender = ApiUser(sender))
|
||||
}
|
||||
}
|
||||
@@ -100,14 +101,16 @@ trait WebHookPullRequestService extends WebHookService {
|
||||
callWebHookOf(repository.owner, repository.name, "pull_request"){
|
||||
for{
|
||||
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
|
||||
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName), Set(sender))
|
||||
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
|
||||
baseOwner <- users.get(repository.owner)
|
||||
headOwner <- users.get(pullRequest.requestUserName)
|
||||
issueUser <- users.get(issue.openedUserName)
|
||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
|
||||
} yield {
|
||||
WebHookPullRequestPayload(
|
||||
action = action,
|
||||
issue = issue,
|
||||
issueUser = issueUser,
|
||||
pullRequest = pullRequest,
|
||||
headRepository = headRepo,
|
||||
headOwner = headOwner,
|
||||
@@ -118,8 +121,9 @@ trait WebHookPullRequestService extends WebHookService {
|
||||
}
|
||||
}
|
||||
|
||||
/** @return Map[(issue, issueUser, pullRequest, baseOwner, headOwner), webHooks] */
|
||||
def getPullRequestsByRequestForWebhook(userName:String, repositoryName:String, branch:String)
|
||||
(implicit s: Session): Map[(Issue, PullRequest, Account, Account), List[WebHook]] =
|
||||
(implicit s: Session): Map[(Issue, Account, PullRequest, Account, Account), List[WebHook]] =
|
||||
(for{
|
||||
is <- Issues if is.closed === false.bind
|
||||
pr <- PullRequests if pr.byPrimaryKey(is.userName, is.repositoryName, is.issueId)
|
||||
@@ -128,20 +132,22 @@ trait WebHookPullRequestService extends WebHookService {
|
||||
if pr.requestBranch === branch.bind
|
||||
bu <- Accounts if bu.userName === pr.userName
|
||||
ru <- Accounts if ru.userName === pr.requestUserName
|
||||
iu <- Accounts if iu.userName === is.openedUserName
|
||||
wh <- WebHooks if wh.byRepository(is.userName , is.repositoryName)
|
||||
} yield {
|
||||
((is, pr, bu, ru), wh)
|
||||
((is, iu, pr, bu, ru), wh)
|
||||
}).list.groupBy(_._1).mapValues(_.map(_._2))
|
||||
|
||||
def callPullRequestWebHookByRequestBranch(action: String, requestRepository: RepositoryService.RepositoryInfo, requestBranch: String, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
|
||||
import WebHookService._
|
||||
for{
|
||||
((issue, pullRequest, baseOwner, headOwner), webHooks) <- getPullRequestsByRequestForWebhook(requestRepository.owner, requestRepository.name, requestBranch)
|
||||
((issue, issueUser, pullRequest, baseOwner, headOwner), webHooks) <- getPullRequestsByRequestForWebhook(requestRepository.owner, requestRepository.name, requestBranch)
|
||||
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName, baseUrl)
|
||||
} yield {
|
||||
val payload = WebHookPullRequestPayload(
|
||||
action = action,
|
||||
issue = issue,
|
||||
issueUser = issueUser,
|
||||
pullRequest = pullRequest,
|
||||
headRepository = requestRepository,
|
||||
headOwner = headOwner,
|
||||
@@ -161,10 +167,10 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
|
||||
callWebHookOf(repository.owner, repository.name, "issue_comment"){
|
||||
for{
|
||||
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
|
||||
users = getAccountsByUserNames(Set(issue.userName, repository.owner, issueComment.userName), Set(sender))
|
||||
issueUser <- users.get(issue.userName)
|
||||
users = getAccountsByUserNames(Set(issue.openedUserName, repository.owner, issueComment.commentedUserName), Set(sender))
|
||||
issueUser <- users.get(issue.openedUserName)
|
||||
repoOwner <- users.get(repository.owner)
|
||||
commenter <- users.get(issueComment.userName)
|
||||
commenter <- users.get(issueComment.commentedUserName)
|
||||
} yield {
|
||||
WebHookIssueCommentPayload(
|
||||
issue = issue,
|
||||
@@ -224,6 +230,7 @@ object WebHookService {
|
||||
object WebHookPullRequestPayload{
|
||||
def apply(action: String,
|
||||
issue: Issue,
|
||||
issueUser: Account,
|
||||
pullRequest: PullRequest,
|
||||
headRepository: RepositoryInfo,
|
||||
headOwner: Account,
|
||||
@@ -233,7 +240,7 @@ object WebHookService {
|
||||
val headRepoPayload = ApiRepository(headRepository, headOwner)
|
||||
val baseRepoPayload = ApiRepository(baseRepository, baseOwner)
|
||||
val senderPayload = ApiUser(sender)
|
||||
val pr = ApiPullRequest(issue, pullRequest, headRepoPayload, baseRepoPayload, senderPayload)
|
||||
val pr = ApiPullRequest(issue, pullRequest, headRepoPayload, baseRepoPayload, ApiUser(issueUser))
|
||||
WebHookPullRequestPayload(
|
||||
action = action,
|
||||
number = issue.issueId,
|
||||
@@ -265,8 +272,8 @@ object WebHookService {
|
||||
WebHookIssueCommentPayload(
|
||||
action = "created",
|
||||
repository = ApiRepository(repository, repositoryUser),
|
||||
issue = ApiIssue(issue, ApiUser(issueUser)),
|
||||
comment = ApiComment(comment, ApiUser(commentUser)),
|
||||
issue = ApiIssue(issue, RepositoryName(repository), ApiUser(issueUser)),
|
||||
comment = ApiComment(comment, RepositoryName(repository), issue.issueId, ApiUser(commentUser)),
|
||||
sender = ApiUser(sender))
|
||||
}
|
||||
}
|
||||
|
||||
168
src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
Normal file
168
src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
Normal file
@@ -0,0 +1,168 @@
|
||||
package gitbucket.core.servlet
|
||||
|
||||
import java.io.File
|
||||
import java.sql.{DriverManager, Connection}
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.service.SystemSettingsService
|
||||
import gitbucket.core.util._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import javax.servlet.{ServletContextListener, ServletContextEvent}
|
||||
import org.slf4j.LoggerFactory
|
||||
import Directory._
|
||||
import ControlUtil._
|
||||
import JDBCUtil._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import gitbucket.core.util.Versions
|
||||
import gitbucket.core.util.Directory
|
||||
|
||||
object AutoUpdate {
|
||||
|
||||
/**
|
||||
* The history of versions. A head of this sequence is the current BitBucket version.
|
||||
*/
|
||||
val versions = Seq(
|
||||
new Version(3, 3),
|
||||
new Version(3, 2),
|
||||
new Version(3, 1),
|
||||
new Version(3, 0),
|
||||
new Version(2, 8),
|
||||
new Version(2, 7) {
|
||||
override def update(conn: Connection, cl: ClassLoader): Unit = {
|
||||
super.update(conn, cl)
|
||||
conn.select("SELECT * FROM REPOSITORY"){ rs =>
|
||||
// Rename attached files directory from /issues to /comments
|
||||
val userName = rs.getString("USER_NAME")
|
||||
val repoName = rs.getString("REPOSITORY_NAME")
|
||||
defining(Directory.getAttachedDir(userName, repoName)){ newDir =>
|
||||
val oldDir = new File(newDir.getParentFile, "issues")
|
||||
if(oldDir.exists && oldDir.isDirectory){
|
||||
oldDir.renameTo(newDir)
|
||||
}
|
||||
}
|
||||
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist
|
||||
val originalUserName = rs.getString("ORIGIN_USER_NAME")
|
||||
val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME")
|
||||
if(originalUserName != null && originalRepoName != null){
|
||||
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
|
||||
originalUserName, originalRepoName) == 0){
|
||||
conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " +
|
||||
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
|
||||
}
|
||||
}
|
||||
// Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist
|
||||
val parentUserName = rs.getString("PARENT_USER_NAME")
|
||||
val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME")
|
||||
if(parentUserName != null && parentRepoName != null){
|
||||
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
|
||||
parentUserName, parentRepoName) == 0){
|
||||
conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " +
|
||||
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
new Version(2, 6),
|
||||
new Version(2, 5),
|
||||
new Version(2, 4),
|
||||
new Version(2, 3) {
|
||||
override def update(conn: Connection, cl: ClassLoader): Unit = {
|
||||
super.update(conn, cl)
|
||||
conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs =>
|
||||
val curInfo = rs.getString("ADDITIONAL_INFO")
|
||||
val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
|
||||
if (curInfo != newInfo) {
|
||||
conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID"))
|
||||
}
|
||||
}
|
||||
ignore {
|
||||
FileUtils.deleteDirectory(Directory.getPluginCacheDir())
|
||||
//FileUtils.deleteDirectory(new File(Directory.PluginHome))
|
||||
}
|
||||
}
|
||||
},
|
||||
new Version(2, 2),
|
||||
new Version(2, 1),
|
||||
new Version(2, 0){
|
||||
override def update(conn: Connection, cl: ClassLoader): Unit = {
|
||||
import eu.medsea.mimeutil.{MimeUtil2, MimeType}
|
||||
|
||||
val mimeUtil = new MimeUtil2()
|
||||
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
|
||||
|
||||
super.update(conn, cl)
|
||||
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
|
||||
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
|
||||
if(dir.exists && dir.isDirectory){
|
||||
dir.listFiles.foreach { file =>
|
||||
if(file.getName.indexOf('.') < 0){
|
||||
val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
|
||||
if(mimeType.startsWith("image/")){
|
||||
file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Version(1, 13),
|
||||
Version(1, 12),
|
||||
Version(1, 11),
|
||||
Version(1, 10),
|
||||
Version(1, 9),
|
||||
Version(1, 8),
|
||||
Version(1, 7),
|
||||
Version(1, 6),
|
||||
Version(1, 5),
|
||||
Version(1, 4),
|
||||
new Version(1, 3){
|
||||
override def update(conn: Connection, cl: ClassLoader): Unit = {
|
||||
super.update(conn, cl)
|
||||
// Fix wiki repository configuration
|
||||
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
|
||||
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
|
||||
defining(git.getRepository.getConfig){ config =>
|
||||
if(!config.getBoolean("http", "receivepack", false)){
|
||||
config.setBoolean("http", null, "receivepack", true)
|
||||
config.save
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Version(1, 2),
|
||||
Version(1, 1),
|
||||
Version(1, 0),
|
||||
Version(0, 0)
|
||||
)
|
||||
|
||||
/**
|
||||
* The head version of BitBucket.
|
||||
*/
|
||||
val headVersion = versions.head
|
||||
|
||||
/**
|
||||
* The version file (GITBUCKET_HOME/version).
|
||||
*/
|
||||
lazy val versionFile = new File(GitBucketHome, "version")
|
||||
|
||||
/**
|
||||
* Returns the current version from the version file.
|
||||
*/
|
||||
def getCurrentVersion(): Version = {
|
||||
if(versionFile.exists){
|
||||
FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match {
|
||||
case Array(majorVersion, minorVersion) => {
|
||||
versions.find { v =>
|
||||
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
|
||||
}.getOrElse(Version(0, 0))
|
||||
}
|
||||
case _ => Version(0, 0)
|
||||
}
|
||||
} else Version(0, 0)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -40,15 +40,19 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
||||
} else {
|
||||
request.getHeader("Authorization") match {
|
||||
case null => requireAuth(response)
|
||||
case auth => decodeAuthHeader(auth).split(":") match {
|
||||
case auth => decodeAuthHeader(auth).split(":", 2) match {
|
||||
case Array(username, password) => {
|
||||
authenticate(settings, username, password) match {
|
||||
case Some(account) if (isUpdating || repository.repository.isPrivate) => {
|
||||
if(hasWritePermission(repository.owner, repository.name, Some(account))){
|
||||
request.setAttribute(Keys.Request.UserName, account.userName)
|
||||
chain.doFilter(req, wrappedResponse)
|
||||
case Some(account) => {
|
||||
if (isUpdating || repository.repository.isPrivate) {
|
||||
if(hasWritePermission(repository.owner, repository.name, Some(account))){
|
||||
request.setAttribute(Keys.Request.UserName, account.userName)
|
||||
chain.doFilter(req, wrappedResponse)
|
||||
} else {
|
||||
requireAuth(response)
|
||||
}
|
||||
} else {
|
||||
requireAuth(response)
|
||||
chain.doFilter(req, wrappedResponse)
|
||||
}
|
||||
}
|
||||
case _ => requireAuth(response)
|
||||
|
||||
@@ -1,176 +1,22 @@
|
||||
package gitbucket.core.servlet
|
||||
|
||||
import java.io.File
|
||||
import java.sql.{DriverManager, Connection}
|
||||
import akka.event.Logging
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.service.{ActivityService, SystemSettingsService}
|
||||
import org.apache.commons.io.FileUtils
|
||||
import javax.servlet.{ServletContextListener, ServletContextEvent}
|
||||
import org.slf4j.LoggerFactory
|
||||
import Directory._
|
||||
import ControlUtil._
|
||||
import JDBCUtil._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import gitbucket.core.util.Versions
|
||||
import gitbucket.core.util.Directory
|
||||
import gitbucket.core.plugin._
|
||||
|
||||
object AutoUpdate {
|
||||
|
||||
/**
|
||||
* The history of versions. A head of this sequence is the current BitBucket version.
|
||||
*/
|
||||
val versions = Seq(
|
||||
new Version(3, 1),
|
||||
new Version(3, 0),
|
||||
new Version(2, 8),
|
||||
new Version(2, 7) {
|
||||
override def update(conn: Connection, cl: ClassLoader): Unit = {
|
||||
super.update(conn, cl)
|
||||
conn.select("SELECT * FROM REPOSITORY"){ rs =>
|
||||
// Rename attached files directory from /issues to /comments
|
||||
val userName = rs.getString("USER_NAME")
|
||||
val repoName = rs.getString("REPOSITORY_NAME")
|
||||
defining(Directory.getAttachedDir(userName, repoName)){ newDir =>
|
||||
val oldDir = new File(newDir.getParentFile, "issues")
|
||||
if(oldDir.exists && oldDir.isDirectory){
|
||||
oldDir.renameTo(newDir)
|
||||
}
|
||||
}
|
||||
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist
|
||||
val originalUserName = rs.getString("ORIGIN_USER_NAME")
|
||||
val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME")
|
||||
if(originalUserName != null && originalRepoName != null){
|
||||
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
|
||||
originalUserName, originalRepoName) == 0){
|
||||
conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " +
|
||||
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
|
||||
}
|
||||
}
|
||||
// Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist
|
||||
val parentUserName = rs.getString("PARENT_USER_NAME")
|
||||
val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME")
|
||||
if(parentUserName != null && parentRepoName != null){
|
||||
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
|
||||
parentUserName, parentRepoName) == 0){
|
||||
conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " +
|
||||
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
new Version(2, 6),
|
||||
new Version(2, 5),
|
||||
new Version(2, 4),
|
||||
new Version(2, 3) {
|
||||
override def update(conn: Connection, cl: ClassLoader): Unit = {
|
||||
super.update(conn, cl)
|
||||
conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs =>
|
||||
val curInfo = rs.getString("ADDITIONAL_INFO")
|
||||
val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
|
||||
if (curInfo != newInfo) {
|
||||
conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID"))
|
||||
}
|
||||
}
|
||||
ignore {
|
||||
FileUtils.deleteDirectory(Directory.getPluginCacheDir())
|
||||
//FileUtils.deleteDirectory(new File(Directory.PluginHome))
|
||||
}
|
||||
}
|
||||
},
|
||||
new Version(2, 2),
|
||||
new Version(2, 1),
|
||||
new Version(2, 0){
|
||||
override def update(conn: Connection, cl: ClassLoader): Unit = {
|
||||
import eu.medsea.mimeutil.{MimeUtil2, MimeType}
|
||||
|
||||
val mimeUtil = new MimeUtil2()
|
||||
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
|
||||
|
||||
super.update(conn, cl)
|
||||
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
|
||||
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
|
||||
if(dir.exists && dir.isDirectory){
|
||||
dir.listFiles.foreach { file =>
|
||||
if(file.getName.indexOf('.') < 0){
|
||||
val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
|
||||
if(mimeType.startsWith("image/")){
|
||||
file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Version(1, 13),
|
||||
Version(1, 12),
|
||||
Version(1, 11),
|
||||
Version(1, 10),
|
||||
Version(1, 9),
|
||||
Version(1, 8),
|
||||
Version(1, 7),
|
||||
Version(1, 6),
|
||||
Version(1, 5),
|
||||
Version(1, 4),
|
||||
new Version(1, 3){
|
||||
override def update(conn: Connection, cl: ClassLoader): Unit = {
|
||||
super.update(conn, cl)
|
||||
// Fix wiki repository configuration
|
||||
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
|
||||
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
|
||||
defining(git.getRepository.getConfig){ config =>
|
||||
if(!config.getBoolean("http", "receivepack", false)){
|
||||
config.setBoolean("http", null, "receivepack", true)
|
||||
config.save
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Version(1, 2),
|
||||
Version(1, 1),
|
||||
Version(1, 0),
|
||||
Version(0, 0)
|
||||
)
|
||||
|
||||
/**
|
||||
* The head version of BitBucket.
|
||||
*/
|
||||
val headVersion = versions.head
|
||||
|
||||
/**
|
||||
* The version file (GITBUCKET_HOME/version).
|
||||
*/
|
||||
lazy val versionFile = new File(GitBucketHome, "version")
|
||||
|
||||
/**
|
||||
* Returns the current version from the version file.
|
||||
*/
|
||||
def getCurrentVersion(): Version = {
|
||||
if(versionFile.exists){
|
||||
FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match {
|
||||
case Array(majorVersion, minorVersion) => {
|
||||
versions.find { v =>
|
||||
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
|
||||
}.getOrElse(Version(0, 0))
|
||||
}
|
||||
case _ => Version(0, 0)
|
||||
}
|
||||
} else Version(0, 0)
|
||||
}
|
||||
|
||||
}
|
||||
import akka.actor.{Actor, Props, ActorSystem}
|
||||
import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension
|
||||
import AutoUpdate._
|
||||
|
||||
/**
|
||||
* Initialize GitBucket system.
|
||||
* Update database schema and load plug-ins automatically in the context initializing.
|
||||
*/
|
||||
class InitializeListener extends ServletContextListener {
|
||||
import AutoUpdate._
|
||||
class InitializeListener extends ServletContextListener with SystemSettingsService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[InitializeListener])
|
||||
|
||||
@@ -181,28 +27,62 @@ class InitializeListener extends ServletContextListener {
|
||||
}
|
||||
org.h2.Driver.load()
|
||||
|
||||
defining(getConnection()){ conn =>
|
||||
Database() withTransaction { session =>
|
||||
val conn = session.conn
|
||||
|
||||
// Migration
|
||||
logger.debug("Start schema update")
|
||||
Versions.update(conn, headVersion, getCurrentVersion(), versions, Thread.currentThread.getContextClassLoader){ conn =>
|
||||
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
|
||||
}
|
||||
|
||||
// Load plugins
|
||||
logger.debug("Initialize plugins")
|
||||
PluginRegistry.initialize(event.getServletContext, conn)
|
||||
PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn)
|
||||
}
|
||||
|
||||
// Start Quartz scheduler
|
||||
val system = ActorSystem("job", ConfigFactory.parseString(
|
||||
"""
|
||||
|akka {
|
||||
| quartz {
|
||||
| schedules {
|
||||
| Daily {
|
||||
| expression = "0 0 0 * * ?"
|
||||
| }
|
||||
| }
|
||||
| }
|
||||
|}
|
||||
""".stripMargin))
|
||||
|
||||
val scheduler = QuartzSchedulerExtension(system)
|
||||
|
||||
scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity")
|
||||
}
|
||||
|
||||
def contextDestroyed(event: ServletContextEvent): Unit = {
|
||||
override def contextDestroyed(event: ServletContextEvent): Unit = {
|
||||
// Shutdown plugins
|
||||
PluginRegistry.shutdown(event.getServletContext)
|
||||
PluginRegistry.shutdown(event.getServletContext, loadSystemSettings())
|
||||
// Close datasource
|
||||
Database.closeDataSource()
|
||||
}
|
||||
|
||||
private def getConnection(): Connection =
|
||||
DriverManager.getConnection(
|
||||
DatabaseConfig.url,
|
||||
DatabaseConfig.user,
|
||||
DatabaseConfig.password)
|
||||
|
||||
}
|
||||
|
||||
class DeleteOldActivityActor extends Actor with SystemSettingsService with ActivityService {
|
||||
|
||||
private val logger = Logging(context.system, this)
|
||||
|
||||
def receive = {
|
||||
case s: String => {
|
||||
loadSystemSettings().activityLogLimit.foreach { limit =>
|
||||
if(limit > 0){
|
||||
Database() withTransaction { implicit session =>
|
||||
val rows = deleteOldActivities(limit)
|
||||
logger.info(s"Deleted ${rows} activity logs")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import javax.servlet._
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import com.mchange.v2.c3p0.ComboPooledDataSource
|
||||
import gitbucket.core.util.DatabaseConfig
|
||||
import org.scalatra.ScalatraBase
|
||||
import org.slf4j.LoggerFactory
|
||||
import slick.jdbc.JdbcBackend.{Database => SlickDatabase, Session}
|
||||
import gitbucket.core.util.Keys
|
||||
@@ -25,6 +26,12 @@ class TransactionFilter extends Filter {
|
||||
chain.doFilter(req, res)
|
||||
} else {
|
||||
Database() withTransaction { session =>
|
||||
// Register Scalatra error callback to rollback transaction
|
||||
ScalatraBase.onFailure { _ =>
|
||||
logger.debug("Rolled back transaction")
|
||||
session.rollback()
|
||||
}(req.asInstanceOf[HttpServletRequest])
|
||||
|
||||
logger.debug("begin transaction")
|
||||
req.setAttribute(Keys.Request.DBSession, session)
|
||||
chain.doFilter(req, res)
|
||||
@@ -39,17 +46,18 @@ object Database {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(Database.getClass)
|
||||
|
||||
private val db: SlickDatabase = {
|
||||
val datasource = new ComboPooledDataSource
|
||||
|
||||
datasource.setDriverClass(DatabaseConfig.driver)
|
||||
datasource.setJdbcUrl(DatabaseConfig.url)
|
||||
datasource.setUser(DatabaseConfig.user)
|
||||
datasource.setPassword(DatabaseConfig.password)
|
||||
|
||||
private val dataSource: ComboPooledDataSource = {
|
||||
val ds = new ComboPooledDataSource
|
||||
ds.setDriverClass(DatabaseConfig.driver)
|
||||
ds.setJdbcUrl(DatabaseConfig.url)
|
||||
ds.setUser(DatabaseConfig.user)
|
||||
ds.setPassword(DatabaseConfig.password)
|
||||
logger.debug("load database connection pool")
|
||||
ds
|
||||
}
|
||||
|
||||
SlickDatabase.forDataSource(datasource)
|
||||
private val db: SlickDatabase = {
|
||||
SlickDatabase.forDataSource(dataSource)
|
||||
}
|
||||
|
||||
def apply(): SlickDatabase = db
|
||||
@@ -57,4 +65,6 @@ object Database {
|
||||
def getSession(req: ServletRequest): Session =
|
||||
req.getAttribute(Keys.Request.DBSession).asInstanceOf[Session]
|
||||
|
||||
def closeDataSource(): Unit = dataSource.close
|
||||
|
||||
}
|
||||
|
||||
@@ -63,10 +63,10 @@ object Implicits {
|
||||
|
||||
implicit class RichRequest(request: HttpServletRequest){
|
||||
|
||||
def paths: Array[String] = (request.getRequestURI match{
|
||||
case path if path.startsWith("/api/v3/repos/") => path.substring(13/* "/api/v3/repos".length */)
|
||||
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 */)
|
||||
case path => path
|
||||
}).substring(request.getContextPath.length + 1).split("/")
|
||||
}).split("/")
|
||||
|
||||
def hasQueryString: Boolean = request.getQueryString != null
|
||||
|
||||
|
||||
@@ -100,7 +100,8 @@ object JGitUtil {
|
||||
def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress
|
||||
}
|
||||
|
||||
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String])
|
||||
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String],
|
||||
oldIsImage: Boolean, newIsImage: Boolean, oldObjectId: Option[String], newObjectId: Option[String])
|
||||
|
||||
/**
|
||||
* The file content data for the file content view of the repository viewer.
|
||||
@@ -138,6 +139,9 @@ object JGitUtil {
|
||||
|
||||
case class BranchInfo(name: String, committerName: String, commitTime: Date, committerEmailAddress:String, mergeInfo: Option[BranchMergeInfo], commitId: String)
|
||||
|
||||
case class BlameInfo(id: String, authorName: String, authorEmailAddress: String, authorTime:java.util.Date,
|
||||
prev: Option[String], prevPath: Option[String], commitTime:java.util.Date, message:String, lines:Set[Int])
|
||||
|
||||
/**
|
||||
* Returns RevCommit from the commit or tag id.
|
||||
*
|
||||
@@ -196,80 +200,121 @@ object JGitUtil {
|
||||
* @return HTML of the file list
|
||||
*/
|
||||
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
|
||||
var list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
|
||||
|
||||
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||
val objectId = git.getRepository.resolve(revision)
|
||||
if(objectId==null) return Nil
|
||||
val revCommit = revWalk.parseCommit(objectId)
|
||||
|
||||
val treeWalk = if (path == ".") {
|
||||
def useTreeWalk(rev:RevCommit)(f:TreeWalk => Any): Unit = if (path == ".") {
|
||||
val treeWalk = new TreeWalk(git.getRepository)
|
||||
treeWalk.addTree(revCommit.getTree)
|
||||
treeWalk
|
||||
treeWalk.addTree(rev.getTree)
|
||||
using(treeWalk)(f)
|
||||
} else {
|
||||
val treeWalk = TreeWalk.forPath(git.getRepository, path, revCommit.getTree)
|
||||
treeWalk.enterSubtree()
|
||||
treeWalk
|
||||
val treeWalk = TreeWalk.forPath(git.getRepository, path, rev.getTree)
|
||||
if(treeWalk != null){
|
||||
treeWalk.enterSubtree
|
||||
using(treeWalk)(f)
|
||||
}
|
||||
}
|
||||
@tailrec
|
||||
def simplifyPath(tuple: (ObjectId, FileMode, String, Option[String], RevCommit)): (ObjectId, FileMode, String, Option[String], RevCommit) = tuple match {
|
||||
case (oid, FileMode.TREE, name, _, commit ) =>
|
||||
(using(new TreeWalk(git.getRepository)) { walk =>
|
||||
walk.addTree(oid)
|
||||
// single tree child, or None
|
||||
if(walk.next() && walk.getFileMode(0) == FileMode.TREE){
|
||||
Some((walk.getObjectId(0), walk.getFileMode(0), name + "/" + walk.getNameString, None, commit)).filterNot(_ => walk.next())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) match {
|
||||
case Some(child) => simplifyPath(child)
|
||||
case _ => tuple
|
||||
}
|
||||
case _ => tuple
|
||||
}
|
||||
|
||||
using(treeWalk) { treeWalk =>
|
||||
def tupleAdd(tuple:(ObjectId, FileMode, String, Option[String]), rev:RevCommit) = tuple match {
|
||||
case (oid, fmode, name, opt) => (oid, fmode, name, opt, rev)
|
||||
}
|
||||
|
||||
@tailrec
|
||||
def findLastCommits(result:List[(ObjectId, FileMode, String, Option[String], RevCommit)],
|
||||
restList:List[((ObjectId, FileMode, String, Option[String]), Map[RevCommit, RevCommit])],
|
||||
revIterator:java.util.Iterator[RevCommit]): List[(ObjectId, FileMode, String, Option[String], RevCommit)] ={
|
||||
if(restList.isEmpty){
|
||||
result
|
||||
}else if(!revIterator.hasNext){ // maybe, revCommit has only 1 log. other case, restList be empty
|
||||
result ++ restList.map{ case (tuple, map) => tupleAdd(tuple, map.values.headOption.getOrElse(revCommit)) }
|
||||
}else{
|
||||
val newCommit = revIterator.next
|
||||
val (thisTimeChecks,skips) = restList.partition{ case (tuple, parentsMap) => parentsMap.contains(newCommit) }
|
||||
if(thisTimeChecks.isEmpty){
|
||||
findLastCommits(result, restList, revIterator)
|
||||
}else{
|
||||
var nextRest = skips
|
||||
var nextResult = result
|
||||
// Map[(name, oid), (tuple, parentsMap)]
|
||||
val rest = scala.collection.mutable.Map(thisTimeChecks.map{ t => (t._1._3 -> t._1._1) -> t }:_*)
|
||||
lazy val newParentsMap = newCommit.getParents.map(_ -> newCommit).toMap
|
||||
useTreeWalk(newCommit){ walk =>
|
||||
while(walk.next){
|
||||
rest.remove(walk.getNameString -> walk.getObjectId(0)).map{ case (tuple, _) =>
|
||||
if(newParentsMap.isEmpty){
|
||||
nextResult +:= tupleAdd(tuple, newCommit)
|
||||
}else{
|
||||
nextRest +:= tuple -> newParentsMap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rest.values.map{ case (tuple, parentsMap) =>
|
||||
val restParentsMap = parentsMap - newCommit
|
||||
if(restParentsMap.isEmpty){
|
||||
nextResult +:= tupleAdd(tuple, parentsMap(newCommit))
|
||||
}else{
|
||||
nextRest +:= tuple -> restParentsMap
|
||||
}
|
||||
}
|
||||
findLastCommits(nextResult, nextRest, revIterator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fileList: List[(ObjectId, FileMode, String, Option[String])] = Nil
|
||||
useTreeWalk(revCommit){ treeWalk =>
|
||||
while (treeWalk.next()) {
|
||||
// submodule
|
||||
val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){
|
||||
val linkUrl =if (treeWalk.getFileMode(0) == FileMode.GITLINK) {
|
||||
getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url)
|
||||
} else None
|
||||
|
||||
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl))
|
||||
fileList +:= (treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getNameString, linkUrl)
|
||||
}
|
||||
|
||||
list.transform(tuple =>
|
||||
if (tuple._2 != FileMode.TREE)
|
||||
tuple
|
||||
else
|
||||
simplifyPath(tuple)
|
||||
)
|
||||
|
||||
@tailrec
|
||||
def simplifyPath(tuple: (ObjectId, FileMode, String, String, Option[String])): (ObjectId, FileMode, String, String, Option[String]) = {
|
||||
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
|
||||
using(new TreeWalk(git.getRepository)) { walk =>
|
||||
walk.addTree(tuple._1)
|
||||
while (walk.next() && list.size < 2) {
|
||||
val linkUrl = if (walk.getFileMode(0) == FileMode.GITLINK) {
|
||||
getSubmodules(git, revCommit.getTree).find(_.path == walk.getPathString).map(_.url)
|
||||
} else None
|
||||
list.append((walk.getObjectId(0), walk.getFileMode(0), tuple._3 + "/" + walk.getPathString, tuple._4 + "/" + walk.getNameString, linkUrl))
|
||||
}
|
||||
}
|
||||
revWalk.markStart(revCommit)
|
||||
val it = revWalk.iterator
|
||||
val lastCommit = it.next
|
||||
val nextParentsMap = Option(lastCommit).map(_.getParents.map(_ -> lastCommit).toMap).getOrElse(Map())
|
||||
findLastCommits(List.empty, fileList.map(a => a -> nextParentsMap), it)
|
||||
.map(simplifyPath)
|
||||
.map { case (objectId, fileMode, name, linkUrl, commit) =>
|
||||
FileInfo(
|
||||
objectId,
|
||||
fileMode == FileMode.TREE || fileMode == FileMode.GITLINK,
|
||||
name,
|
||||
getSummaryMessage(commit.getFullMessage, commit.getShortMessage),
|
||||
commit.getName,
|
||||
commit.getAuthorIdent.getWhen,
|
||||
commit.getAuthorIdent.getName,
|
||||
commit.getAuthorIdent.getEmailAddress,
|
||||
linkUrl)
|
||||
}.sortWith { (file1, file2) =>
|
||||
(file1.isDirectory, file2.isDirectory) match {
|
||||
case (true , false) => true
|
||||
case (false, true ) => false
|
||||
case _ => file1.name.compareTo(file2.name) < 0
|
||||
}
|
||||
if (list.size != 1 || list.exists(_._2 != FileMode.TREE))
|
||||
tuple
|
||||
else
|
||||
simplifyPath(list(0))
|
||||
}
|
||||
}
|
||||
}.toList
|
||||
}
|
||||
|
||||
val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
|
||||
list.map { case (objectId, fileMode, path, name, linkUrl) =>
|
||||
defining(commits(path)){ commit =>
|
||||
FileInfo(
|
||||
objectId,
|
||||
fileMode == FileMode.TREE || fileMode == FileMode.GITLINK,
|
||||
name,
|
||||
getSummaryMessage(commit.getFullMessage, commit.getShortMessage),
|
||||
commit.getName,
|
||||
commit.getAuthorIdent.getWhen,
|
||||
commit.getAuthorIdent.getName,
|
||||
commit.getAuthorIdent.getEmailAddress,
|
||||
linkUrl)
|
||||
}
|
||||
}.sortWith { (file1, file2) =>
|
||||
(file1.isDirectory, file2.isDirectory) match {
|
||||
case (true , false) => true
|
||||
case (false, true ) => false
|
||||
case _ => file1.name.compareTo(file2.name) < 0
|
||||
}
|
||||
}.toList
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,6 +328,39 @@ object JGitUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get all file list by revision. only file.
|
||||
*/
|
||||
def getTreeId(git: Git, revision: String): Option[String] = {
|
||||
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||
val objectId = git.getRepository.resolve(revision)
|
||||
if(objectId==null) return None
|
||||
val revCommit = revWalk.parseCommit(objectId)
|
||||
Some(revCommit.getTree.name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get all file list by tree object id.
|
||||
*/
|
||||
def getAllFileListByTreeId(git: Git, treeId: String): List[String] = {
|
||||
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||
val objectId = git.getRepository.resolve(treeId+"^{tree}")
|
||||
if(objectId==null) return Nil
|
||||
using(new TreeWalk(git.getRepository)){ treeWalk =>
|
||||
treeWalk.addTree(objectId)
|
||||
treeWalk.setRecursive(true)
|
||||
var ret: List[String] = Nil
|
||||
if(treeWalk != null){
|
||||
while (treeWalk.next()) {
|
||||
ret +:= treeWalk.getPathString
|
||||
}
|
||||
}
|
||||
ret.reverse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the commit list of the specified branch.
|
||||
*
|
||||
@@ -313,12 +391,7 @@ object JGitUtil {
|
||||
} else {
|
||||
revWalk.markStart(revWalk.parseCommit(objectId))
|
||||
if(path.nonEmpty){
|
||||
revWalk.setRevFilter(new RevFilter(){
|
||||
def include(walk: RevWalk, commit: RevCommit): Boolean = {
|
||||
getDiffs(git, commit.getName, false)._1.find(_.newPath == path).nonEmpty
|
||||
}
|
||||
override def clone(): RevFilter = this
|
||||
})
|
||||
revWalk.setTreeFilter(AndTreeFilter.create(PathFilter.create(path), TreeFilter.ANY_DIFF))
|
||||
}
|
||||
Right(getCommitLog(revWalk.iterator, 0, Nil))
|
||||
}
|
||||
@@ -420,11 +493,13 @@ object JGitUtil {
|
||||
treeWalk.addTree(revCommit.getTree)
|
||||
val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
|
||||
while(treeWalk.next){
|
||||
val newIsImage = FileUtil.isImage(treeWalk.getPathString)
|
||||
buffer.append((if(!fetchContent){
|
||||
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None)
|
||||
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None, false, newIsImage, None, Option(treeWalk.getObjectId(0)).map(_.name))
|
||||
} else {
|
||||
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
|
||||
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
|
||||
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray),
|
||||
false, newIsImage, None, Option(treeWalk.getObjectId(0)).map(_.name))
|
||||
}))
|
||||
}
|
||||
(buffer.toList, None)
|
||||
@@ -444,12 +519,15 @@ object JGitUtil {
|
||||
import scala.collection.JavaConverters._
|
||||
git.getRepository.getConfig.setString("diff", null, "renames", "copies")
|
||||
git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
|
||||
if(!fetchContent || FileUtil.isImage(diff.getOldPath) || FileUtil.isImage(diff.getNewPath)){
|
||||
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
|
||||
val oldIsImage = FileUtil.isImage(diff.getOldPath)
|
||||
val newIsImage = FileUtil.isImage(diff.getNewPath)
|
||||
if(!fetchContent || oldIsImage || newIsImage){
|
||||
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None, oldIsImage, newIsImage, Option(diff.getOldId).map(_.name), Option(diff.getNewId).map(_.name))
|
||||
} else {
|
||||
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
|
||||
JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
|
||||
JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
|
||||
JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
|
||||
oldIsImage, newIsImage, Option(diff.getOldId).map(_.name), Option(diff.getNewId).map(_.name))
|
||||
}
|
||||
}.toList
|
||||
}
|
||||
@@ -603,21 +681,24 @@ object JGitUtil {
|
||||
|
||||
def getContentInfo(git: Git, path: String, objectId: ObjectId): ContentInfo = {
|
||||
// Viewer
|
||||
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
|
||||
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
|
||||
val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None
|
||||
using(git.getRepository.getObjectDatabase){ db =>
|
||||
val loader = db.open(objectId)
|
||||
val large = FileUtil.isLarge(loader.getSize)
|
||||
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
|
||||
val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None
|
||||
|
||||
if(viewer == "other"){
|
||||
if(bytes.isDefined && FileUtil.isText(bytes.get)){
|
||||
// text
|
||||
ContentInfo("text", Some(StringUtil.convertFromByteArray(bytes.get)), Some(StringUtil.detectEncoding(bytes.get)))
|
||||
if(viewer == "other"){
|
||||
if(bytes.isDefined && FileUtil.isText(bytes.get)){
|
||||
// text
|
||||
ContentInfo("text", Some(StringUtil.convertFromByteArray(bytes.get)), Some(StringUtil.detectEncoding(bytes.get)))
|
||||
} else {
|
||||
// binary
|
||||
ContentInfo("binary", None, None)
|
||||
}
|
||||
} else {
|
||||
// binary
|
||||
ContentInfo("binary", None, None)
|
||||
// image or large
|
||||
ContentInfo(viewer, None, None)
|
||||
}
|
||||
} else {
|
||||
// image or large
|
||||
ContentInfo(viewer, None, None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -630,12 +711,12 @@ object JGitUtil {
|
||||
* @return the byte array of content or None if object does not exist
|
||||
*/
|
||||
def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try {
|
||||
val loader = git.getRepository.getObjectDatabase.open(id)
|
||||
if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){
|
||||
None
|
||||
} else {
|
||||
using(git.getRepository.getObjectDatabase){ db =>
|
||||
Some(db.open(id).getBytes)
|
||||
using(git.getRepository.getObjectDatabase){ db =>
|
||||
val loader = db.open(id)
|
||||
if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){
|
||||
None
|
||||
} else {
|
||||
Some(loader.getBytes)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -750,6 +831,36 @@ object JGitUtil {
|
||||
}
|
||||
}
|
||||
|
||||
def getBlame(git: Git, id: String, path: String): Iterable[BlameInfo] = {
|
||||
Option(git.getRepository.resolve(id)).map{ commitId =>
|
||||
val blamer = new org.eclipse.jgit.api.BlameCommand(git.getRepository);
|
||||
blamer.setStartCommit(commitId)
|
||||
blamer.setFilePath(path)
|
||||
val blame = blamer.call()
|
||||
var blameMap = Map[String, JGitUtil.BlameInfo]()
|
||||
var idLine = List[(String, Int)]()
|
||||
val commits = 0.to(blame.getResultContents().size()-1).map{ i =>
|
||||
val c = blame.getSourceCommit(i)
|
||||
if(!blameMap.contains(c.name)){
|
||||
blameMap += c.name -> JGitUtil.BlameInfo(
|
||||
c.name,
|
||||
c.getAuthorIdent.getName,
|
||||
c.getAuthorIdent.getEmailAddress,
|
||||
c.getAuthorIdent.getWhen,
|
||||
Option(git.log.add(c).addPath(blame.getSourcePath(i)).setSkip(1).setMaxCount(2).call.iterator.next)
|
||||
.map(_.name),
|
||||
if(blame.getSourcePath(i)==path){ None }else{ Some(blame.getSourcePath(i)) },
|
||||
c.getCommitterIdent.getWhen,
|
||||
c.getShortMessage,
|
||||
Set.empty)
|
||||
}
|
||||
idLine :+= (c.name, i)
|
||||
}
|
||||
val limeMap = idLine.groupBy(_._1).mapValues(_.map(_._2).toSet)
|
||||
blameMap.values.map{b => b.copy(lines=limeMap(b.id))}
|
||||
}.getOrElse(Seq.empty)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns sha1
|
||||
* @param owner repository owner
|
||||
|
||||
@@ -15,7 +15,7 @@ import SystemSettingsService.Smtp
|
||||
import ControlUtil.defining
|
||||
|
||||
trait Notifier extends RepositoryService with AccountService with IssuesService {
|
||||
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
|
||||
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) =
|
||||
@@ -67,16 +67,15 @@ object Notifier {
|
||||
class Mailer(private val smtp: Smtp) extends Notifier {
|
||||
private val logger = LoggerFactory.getLogger(classOf[Mailer])
|
||||
|
||||
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
|
||||
def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)
|
||||
(msg: String => String)(implicit context: Context) = {
|
||||
val database = Database()
|
||||
|
||||
val f = Future {
|
||||
database withSession { implicit session =>
|
||||
getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
|
||||
defining(
|
||||
s"[${r.name}] ${issue.title} (#${issueId})" ->
|
||||
msg(Markdown.toHtml(content, r, false, true))) { case (subject, msg) =>
|
||||
defining(
|
||||
s"[${r.name}] ${issue.title} (#${issue.issueId})" ->
|
||||
msg(Markdown.toHtml(content, r, false, true))) { case (subject, msg) =>
|
||||
recipients(issue) { to =>
|
||||
val email = new HtmlEmail
|
||||
email.setHostName(smtp.host)
|
||||
@@ -92,14 +91,13 @@ class Mailer(private val smtp: Smtp) extends Notifier {
|
||||
.orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName))
|
||||
.foreach { case (address, name) =>
|
||||
email.setFrom(address, name)
|
||||
}
|
||||
}
|
||||
email.setCharset("UTF-8")
|
||||
email.setSubject(subject)
|
||||
email.setHtmlMsg(msg)
|
||||
|
||||
email.addTo(to).send
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"Notifications Successful."
|
||||
@@ -113,6 +111,6 @@ class Mailer(private val smtp: Smtp) extends Notifier {
|
||||
}
|
||||
}
|
||||
class MockMailer extends Notifier {
|
||||
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
|
||||
def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)
|
||||
(msg: String => String)(implicit context: Context): Unit = {}
|
||||
}
|
||||
|
||||
@@ -123,8 +123,10 @@ class GitBucketHtmlSerializer(
|
||||
}
|
||||
|
||||
private def fixUrl(url: String, isImage: Boolean = false): String = {
|
||||
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#") || url.startsWith("/")){
|
||||
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/")){
|
||||
url
|
||||
} else if(url.startsWith("#")){
|
||||
("#" + GitBucketHtmlSerializer.generateAnchorName(url.substring(1)))
|
||||
} else if(!enableWikiLink){
|
||||
if(context.currentPath.contains("/blob/")){
|
||||
url + (if(isImage) "?raw=true" else "")
|
||||
|
||||
@@ -122,8 +122,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
* Returns <img> which displays the avatar icon for the given user name.
|
||||
* This method looks up Gravatar if avatar icon has not been configured in user settings.
|
||||
*/
|
||||
def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: Context): Html =
|
||||
getAvatarImageHtml(userName, size, "", tooltip)
|
||||
def avatar(userName: String, size: Int, tooltip: Boolean = false, mailAddress: String = "")(implicit context: Context): Html =
|
||||
getAvatarImageHtml(userName, size, mailAddress, tooltip)
|
||||
|
||||
/**
|
||||
* Returns <img> which displays the avatar icon for the given mail address.
|
||||
@@ -148,7 +148,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
import scala.util.matching.Regex._
|
||||
implicit class RegexReplaceString(s: String) {
|
||||
def replaceAll(pattern: String, replacer: (Match) => String): String = {
|
||||
pattern.r.replaceAllIn(s, replacer)
|
||||
pattern.r.replaceAllIn(s, (m: Match) => replacer(m).replace("$", "\\$"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
* If user does not exist or disabled, this method returns avatar image without link.
|
||||
*/
|
||||
def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(implicit context: Context): Html =
|
||||
userWithContent(userName, mailAddress)(avatar(userName, size, tooltip))
|
||||
userWithContent(userName, mailAddress)(avatar(userName, size, tooltip, mailAddress))
|
||||
|
||||
private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)(implicit context: Context): Html =
|
||||
(if(mailAddress.isEmpty){
|
||||
|
||||
@@ -21,7 +21,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
||||
<input type="hidden" name="owner" id="owner" value="@loginAccount.get.userName"/>
|
||||
</div>
|
||||
<span class="slash">/</span>
|
||||
<input type="text" name="name" id="name" />
|
||||
<input type="text" name="name" id="name" autofocus />
|
||||
<span id="error-name" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="span6">
|
||||
<fieldset>
|
||||
<label for="userName" class="strong">Username:</label>
|
||||
<input type="text" name="userName" id="userName" value=""/>
|
||||
<input type="text" name="userName" id="userName" value="" autofocus/>
|
||||
<span id="error-userName" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
|
||||
@@ -81,6 +81,15 @@
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Activity -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><span class="strong">Limit of activity logs</span> (Unlimited if it's not specified or zero)</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="activityLogLimit" name="activityLogLimit" class="input-mini" value="@settings.activityLogLimit"/>
|
||||
<span id="error-activityLogLimit" class="error"></span>
|
||||
</div>
|
||||
<!--====================================================================-->
|
||||
<!-- Services -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
|
||||
@@ -90,13 +90,32 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 0;">
|
||||
@if(diff.newContent != None || diff.oldContent != None){
|
||||
@if(diff.oldObjectId == diff.newObjectId){
|
||||
<div class="diff-same">File renamed without changes</div>
|
||||
} else { @if(diff.newContent != None || diff.oldContent != None){
|
||||
<div id="diffText-@i" class="diffText"></div>
|
||||
<textarea id="newText-@i" style="display: none;" data-file-name="@diff.oldPath">@diff.newContent.getOrElse("")</textarea>
|
||||
<textarea id="oldText-@i" style="display: none;" data-file-name="@diff.newPath">@diff.oldContent.getOrElse("")</textarea>
|
||||
} else { @if(diff.newIsImage || diff.oldIsImage){
|
||||
<div class="diff-image-render diff2up">
|
||||
@if(oldCommitId.isDefined && diff.oldIsImage){
|
||||
<div class="diff-image-frame diff-old"><img src="@url(repository)/blob/@oldCommitId.get/@diff.oldPath?raw=true" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
|
||||
} else {
|
||||
@if(diff.changeType != ChangeType.ADD){
|
||||
Not supported
|
||||
}
|
||||
}
|
||||
@if(newCommitId.isDefined && diff.newIsImage){
|
||||
<div class="diff-image-frame diff-new"><img src="@url(repository)/blob/@newCommitId.get/@diff.newPath?raw=true" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
|
||||
} else {
|
||||
@if(diff.changeType != ChangeType.DELETE){
|
||||
Not supported
|
||||
}
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
Not supported
|
||||
}
|
||||
} } }
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -160,7 +179,7 @@ $(function(){
|
||||
return $('<tr class="not-diff"><td colspan="3" class="comment-box-container"></td></tr>');
|
||||
}
|
||||
if (typeof $('#show-notes')[0] !== 'undefined' && !$('#show-notes')[0].checked) {
|
||||
$('.inline-comment').hide();
|
||||
$('#comment-list').children('.inline-comment').hide();
|
||||
}
|
||||
$('.diff-outside').on('click','table.diff .add-comment',function() {
|
||||
var $this = $(this),
|
||||
|
||||
@@ -42,6 +42,13 @@ div#clickable {
|
||||
line-height: 120px;
|
||||
}
|
||||
|
||||
div.dz-message, div.dz-fallback {
|
||||
width: 240px;
|
||||
color: #000000;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
div#avatar {
|
||||
background-color: #f8f8f8;
|
||||
border: 1px dashed silver;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
@user(issue.get.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.get.registeredDate)</span>
|
||||
<span class="pull-right">
|
||||
@if(hasWritePermission || loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){
|
||||
<a href="#" data-issue-id="@issue.get.issueId"><i class="icon-pencil"></i></a>
|
||||
<a href="#" data-issue-id="@issue.get.issueId"><i class="icon-pencil" aria-label="Edit"></i></a>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
@@ -41,8 +41,8 @@
|
||||
<span class="pull-right">
|
||||
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer"
|
||||
&& (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
|
||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
|
||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
|
||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil" aria-label="Edit"></i></a>
|
||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle" aria-label="Remove"></i></a>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="box issue-box">
|
||||
<div class="box-content">
|
||||
<span id="error-title" class="error"></span>
|
||||
<input type="text" name="title" value="" placeholder="Title" style="width: 565px;"/>
|
||||
<input type="text" name="title" value="" placeholder="Title" style="width: 565px;" autofocus/>
|
||||
<div>
|
||||
<span id="label-assigned">No one is assigned</span>
|
||||
@if(hasWritePermission){
|
||||
|
||||
@@ -8,21 +8,17 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>@title</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<link rel="icon" href="@assets/common/images/gitbucket.png" type="image/vnd.microsoft.icon" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- Le styles -->
|
||||
<link href="@assets/vendors/bootstrap/css/bootstrap.css" rel="stylesheet">
|
||||
<link href="@assets/vendors/bootstrap/css/bootstrap-responsive.css" rel="stylesheet">
|
||||
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="@assets/vendors/bootstrap/js/html5shiv.js"></script>
|
||||
<![endif]-->
|
||||
<link href="@assets/vendors/datepicker/css/datepicker.css" rel="stylesheet">
|
||||
<link href="@assets/vendors/colorpicker/css/bootstrap-colorpicker.css" rel="stylesheet">
|
||||
<link href="@assets/vendors/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
|
||||
<link href="@assets/vendors/facebox/facebox.css" rel="stylesheet"/>
|
||||
<link href="@assets/common/css/gitbucket.css" rel="stylesheet">
|
||||
<script src="@assets/vendors/jquery/jquery-1.9.1.js"></script>
|
||||
<script src="@assets/vendors/jquery/jquery-1.11.1.js"></script>
|
||||
<script src="@assets/vendors/dropzone/dropzone.js"></script>
|
||||
<script src="@assets/common/js/validation.js"></script>
|
||||
<script src="@assets/common/js/gitbucket.js"></script>
|
||||
@@ -33,6 +29,7 @@
|
||||
<script src="@assets/vendors/zclip/ZeroClipboard.min.js"></script>
|
||||
<script src="@assets/vendors/elastic/jquery.elastic.source.js"></script>
|
||||
<script src="@assets/vendors/facebox/facebox.js"></script>
|
||||
<script src="@assets/vendors/jquery-hotkeys/jquery.hotkeys.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<form id="search" action="@path/search" method="POST">
|
||||
@@ -63,11 +60,11 @@
|
||||
<li><a href="@path/new">New repository</a></li>
|
||||
<li><a href="@path/groups/new">New group</a></li>
|
||||
</ul>
|
||||
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a>
|
||||
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user" aria-label="Account settings"></i></a>
|
||||
@if(loginAccount.get.isAdmin){
|
||||
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>
|
||||
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench" aria-label="Administration"></i></a>
|
||||
}
|
||||
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
|
||||
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt" aria-label="Sign out"></i></a>
|
||||
} else {
|
||||
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn btn-last" id="signin">Sign in</a>
|
||||
}
|
||||
|
||||
@@ -3,12 +3,32 @@
|
||||
pathList: List[String],
|
||||
content: gitbucket.core.util.JGitUtil.ContentInfo,
|
||||
latestCommit: gitbucket.core.util.JGitUtil.CommitInfo,
|
||||
hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||
hasWritePermission: Boolean,
|
||||
isBlame: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
||||
@html.menu("code", repository){
|
||||
<div class="head">
|
||||
<div class="pull-right hide-if-blame"><div class="btn-group">
|
||||
<a href="@url(repository)/find/@encodeRefName(branch)" class="btn btn-mini" data-toggle="tooltip" data-placement="bottom" data-hotkey="t" title="Quickly jump between files"><i class="icon icon-th-list"></i></a>
|
||||
</div></div>
|
||||
<div class="line-age-legend">
|
||||
<span>Newer</span>
|
||||
<ol>
|
||||
<li class="heat1"></li>
|
||||
<li class="heat2"></li>
|
||||
<li class="heat3"></li>
|
||||
<li class="heat4"></li>
|
||||
<li class="heat5"></li>
|
||||
<li class="heat6"></li>
|
||||
<li class="heat7"></li>
|
||||
<li class="heat8"></li>
|
||||
<li class="heat9"></li>
|
||||
<li class="heat10"></li>
|
||||
</ol>
|
||||
<span>Older</span>
|
||||
</div>
|
||||
@helper.html.branchcontrol(
|
||||
branch,
|
||||
repository,
|
||||
@@ -42,6 +62,9 @@
|
||||
<a class="btn btn-mini" href="@url(repository)/edit/@encodeRefName(branch)/@pathList.mkString("/")">Edit</a>
|
||||
}
|
||||
<a class="btn btn-mini" href="?raw=true">Raw</a>
|
||||
@if(content.viewType == "text"){
|
||||
<a class="btn btn-mini blame-action" href="@url(repository)/blame/@latestCommit.id/@pathList.mkString("/")" data-url="@url(repository)/get-blame/@latestCommit.id/@pathList.mkString("/")" data-repository="@url(repository)">Blame</a>
|
||||
}
|
||||
<a class="btn btn-mini" href="@url(repository)/commits/@encodeRefName(branch)/@pathList.mkString("/")">History</a>
|
||||
@if(hasWritePermission){
|
||||
<a class="btn btn-mini btn-danger" href="@url(repository)/remove/@encodeRefName(branch)/@pathList.mkString("/")">Delete</a>
|
||||
@@ -52,13 +75,13 @@
|
||||
<tr>
|
||||
<td>
|
||||
@if(content.viewType == "text"){
|
||||
@defining(pathList.reverse.head) { file =>
|
||||
@if(renderableSuffixes.find(suffix => file.toLowerCase.endsWith(suffix))) {
|
||||
@defining(renderableSuffixes.find(suffix => pathList.reverse.head.toLowerCase.endsWith(suffix))) { isRrenderable =>
|
||||
@if(!isBlame && isRrenderable) {
|
||||
<div class="box-content markdown-body" style="border: none; padding-left: 16px; padding-right: 16px;">
|
||||
@renderMarkup(pathList, content.content.get, branch, repository, false, false)
|
||||
</div>
|
||||
} else {
|
||||
<pre class="prettyprint linenums blob">@content.content.get</pre>
|
||||
<pre class="prettyprint linenums blob @if(!isRrenderable){ no-renderable } ">@content.content.get</pre>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,30 +107,111 @@ $(window).load(function(){
|
||||
updateHighlighting();
|
||||
}).hashchange();
|
||||
|
||||
$('pre.prettyprint ol.linenums li').each(function(i, e){
|
||||
var pre = $('pre.prettyprint');
|
||||
pre.append($('<div class="source-line-num">')
|
||||
.data('line', (i + 1))
|
||||
.css({
|
||||
cursor : 'pointer',
|
||||
var pre = $('pre.prettyprint');
|
||||
function updateSourceLineNum(){
|
||||
$('.source-line-num').remove();
|
||||
var pos = pre.find('ol.linenums').position();
|
||||
$('<div class="source-line-num">').css({
|
||||
height:pre.height(),
|
||||
width:'48px',
|
||||
cursor:'pointer',
|
||||
position: 'absolute',
|
||||
top : $(e).position().top + 'px',
|
||||
left : pre.position().left + 'px',
|
||||
width : ($(e).position().left - pre.position().left) + 'px',
|
||||
height : '16px'
|
||||
}));
|
||||
});
|
||||
|
||||
$('div.source-line-num').click(function(e){
|
||||
var line = $(e.target).data('line');
|
||||
var hash = location.hash;
|
||||
if(e.shiftKey == true && hash.match(/#L\d+(-L\d+)?/)){
|
||||
var lines = hash.split('-');
|
||||
location.hash = lines[0] + '-L' + line;
|
||||
} else {
|
||||
location.hash = '#L' + line;
|
||||
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();
|
||||
$(this).data("pos",pos);
|
||||
}
|
||||
for(var i=0;i<pos.length-1;i++){
|
||||
if(pos[i+1].top>e.pageY){
|
||||
break;
|
||||
}
|
||||
}
|
||||
var line = pos[i].id.replace(/^L/,'');
|
||||
var hash = location.hash;
|
||||
if(e.shiftKey == true && hash.match(/#L\d+(-L\d+)?/)){
|
||||
var lines = hash.split('-');
|
||||
location.hash = lines[0] + '-L' + line;
|
||||
} else {
|
||||
var p = $("#L"+line).attr('id',"");
|
||||
location.hash = '#L' + line;
|
||||
p.attr('id','L'+line);
|
||||
}
|
||||
}).appendTo(pre);
|
||||
}
|
||||
var repository = $('.blame-action').data('repository');
|
||||
$('.blame-action').click(function(e){
|
||||
if(history.pushState && $('pre.prettyprint.no-renderable').length){
|
||||
e.preventDefault();
|
||||
history.pushState(null, null, this.href);
|
||||
updateBlame();
|
||||
}
|
||||
});
|
||||
|
||||
function updateBlame(){
|
||||
var m = /^\/(blame|blob)(\/.*)$/.exec(location.pathname.substring(repository.length));
|
||||
var mode = m[1];
|
||||
$('.blame-action').toggleClass("active", mode=='blame').attr('href', repository + (m[1]=='blame'?'/blob':'/blame')+m[2]);
|
||||
if(pre.parents("td").find(".blame").length){
|
||||
pre.parents("div.container").toggleClass("blame-container", mode=='blame');
|
||||
updateSourceLineNum();
|
||||
return;
|
||||
}
|
||||
if(mode=='blob'){
|
||||
updateSourceLineNum();
|
||||
return;
|
||||
}
|
||||
$(document.body).toggleClass('no-box-shadow',document.body.style.boxShadow===undefined);
|
||||
$('.blame-action').addClass("active");
|
||||
var base = $('<div class="blame">').css({height:pre.height()}).prependTo(pre.parents("td")[0]);
|
||||
base.parents("div.container").addClass("blame-container");
|
||||
updateSourceLineNum();
|
||||
$.get($('.blame-action').data('url')).done(function(data){
|
||||
var blame = data.blame;
|
||||
var index = [];
|
||||
for(var i=0;i<blame.length;i++){
|
||||
for(var j=0;j<blame[i].lines.length;j++){
|
||||
index[blame[i].lines[j]]=blame[i];
|
||||
}
|
||||
}
|
||||
var blame, lastDiv, now=new Date().getTime();
|
||||
|
||||
$('pre.prettyprint ol.linenums li').each(function(i, e){
|
||||
var p=$(e).position();
|
||||
var h=$(e).height();
|
||||
if(blame == index[i]){
|
||||
lastDiv.css("min-height",(p.top+h+1) - lastDiv.position().top);
|
||||
}else{
|
||||
$(e).addClass('blame-sep')
|
||||
blame = index[i];
|
||||
var sha = $('<div class="blame-sha">')
|
||||
.append($('<a>').attr("href",data.root+'/commit/'+blame.id).text(blame.id.substr(0,7)));
|
||||
if(blame.prev){
|
||||
sha.append($('<br />'))
|
||||
.append($('<a class="muted-link">').text('prev').attr("href",data.root+'/blame/'+blame.prev+'/'+(blame.prevPath||data.path)));
|
||||
}
|
||||
lastDiv = $('<div class="blame-info">')
|
||||
.addClass('heat'+Math.min(10,Math.max(1,Math.ceil((now-blame.commited)/(24*3600*1000*70)))))
|
||||
.toggleClass('blame-last',blame.id==data.last)
|
||||
.data('line', (i + 1))
|
||||
.css({
|
||||
"top" : p.top + 'px',
|
||||
"min-height" : h+'px'
|
||||
})
|
||||
.append(sha)
|
||||
.append($(blame.avatar).addClass('avatar').css({"float":"left"}))
|
||||
.append($('<div class="blame-commit-title">').text(blame.message))
|
||||
.append($('<div class="muted">').html(blame.author+ " authed "+blame.authed))
|
||||
.appendTo(base);
|
||||
}
|
||||
});
|
||||
});
|
||||
return false;
|
||||
};
|
||||
updateBlame();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -120,15 +224,20 @@ function updateHighlighting(){
|
||||
var lines = hash.substr(1).split('-');
|
||||
if(lines.length == 1){
|
||||
$('#' + lines[0]).addClass('highlight');
|
||||
$(window).scrollTop($('#' + lines[0]).offset().top - 40);
|
||||
if(!updateHighlighting.scrolling){
|
||||
$(window).scrollTop($('#' + lines[0]).offset().top - 40);
|
||||
}
|
||||
} else if(lines.length > 1){
|
||||
var start = parseInt(lines[0].substr(1));
|
||||
var end = parseInt(lines[1].substr(1));
|
||||
for(var i = start; i <= end; i++){
|
||||
$('#L' + i).addClass('highlight');
|
||||
}
|
||||
$(window).scrollTop($('#L' + start).offset().top - 40);
|
||||
if(!updateHighlighting.scrolling){
|
||||
$(window).scrollTop($('#L' + start).offset().top - 40);
|
||||
}
|
||||
}
|
||||
updateHighlighting.scrolling = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -6,6 +6,7 @@
|
||||
files: List[gitbucket.core.util.JGitUtil.FileInfo],
|
||||
readme: Option[(List[String], String)],
|
||||
hasWritePermission: Boolean,
|
||||
branchPullRequest: Option[(gitbucket.core.model.PullRequest, gitbucket.core.model.Issue)],
|
||||
info: Option[Any] = None,
|
||||
error: Option[Any] = None)(implicit context: gitbucket.core.controller.Context)
|
||||
@import context._
|
||||
@@ -13,6 +14,17 @@
|
||||
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
||||
@html.menu("code", repository, Some(branch), pathList.isEmpty, groupNames.isEmpty, info, error){
|
||||
<div class="head">
|
||||
<div class="pull-right"><div class="btn-group">
|
||||
<a href="@url(repository)/find/@encodeRefName(branch)" class="btn btn-mini" data-toggle="tooltip" data-placement="bottom" data-hotkey="t" title="Quickly jump between files"><i class="icon icon-th-list"></i></a>
|
||||
@if(pathList.nonEmpty){
|
||||
<a href="@url(repository)/commits/@encodeRefName(branch)/@pathList.mkString("/")" class="btn btn-mini" data-toggle="tooltip" data-placement="bottom" title="Browse commits for this branch"><i class="icon icon-time"></i></a>
|
||||
}
|
||||
</div></div>
|
||||
@branchPullRequest.map{ case (pullRequest, issue) =>
|
||||
<a href="@url(repository)/pull/@pullRequest.issueId" class="btn btn-pullrequest-branch btn-mini" title="@issue.title" data-toggle="tooltip">#@pullRequest.issueId</a>
|
||||
}.getOrElse{
|
||||
<a href="@url(repository)/compare?head=@urlEncode(encodeRefName(branch))" class="btn btn-success btn-mini"><i class="icon-white icon-retweet" data-toggle="tooltip" title="Compare, review, create a pull request"></i></a>
|
||||
}
|
||||
@helper.html.branchcontrol(
|
||||
branch,
|
||||
repository,
|
||||
|
||||
121
src/main/twirl/gitbucket/core/repo/find.scala.html
Normal file
121
src/main/twirl/gitbucket/core/repo/find.scala.html
Normal file
@@ -0,0 +1,121 @@
|
||||
@(branch: String,
|
||||
treeId: String,
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
groupNames: List[String]
|
||||
)(implicit context: gitbucket.core.controller.Context)
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
||||
@html.menu("code", repository, Some(branch), false, groupNames.isEmpty){
|
||||
|
||||
<div>
|
||||
<div class="find-input">
|
||||
<span class="bold"><a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a></span>
|
||||
/
|
||||
<input type="text" name="query" autocomplete="off" spellcheck="false" autofocus id="tree-finder-field" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
You've activated the <em>file finder</em>
|
||||
by pressing <code>t</code>.
|
||||
Start typing to filter the file list. Use <code>↑</code> and
|
||||
<code>↓</code> to navigate,
|
||||
<code>enter</code> to view files.
|
||||
</div>
|
||||
<table id="tree-finder-results" class="table table-file-list" data-url="@url(repository)/tree-list/@treeId">
|
||||
<tbody class="tree-browser-result-template">
|
||||
<tr class="tree-browser-result">
|
||||
<td class="icon"><span class="icon icon-chevron-right"></span></td>
|
||||
<td class="icon"><img src="@assets/common/images/file.png"/></td>
|
||||
<td>
|
||||
<a href="@url(repository)/blob/@encodeRefName(branch)"></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody class="no-results" style="display:none">
|
||||
<tr><th colspan="3">No matching files</th><tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<script>
|
||||
$(function(){
|
||||
var paths = [];
|
||||
var template = $('.tree-browser-result-template tr').clone();
|
||||
var res = $('.tree-browser-result-template');
|
||||
var cursor = 0;
|
||||
var pathBase = template.find("a").attr("href");
|
||||
var preKeyword;
|
||||
$.ajax({
|
||||
url:$('#tree-finder-results').data('url'),
|
||||
cache: true,
|
||||
dataType: 'json',
|
||||
success:function(data){
|
||||
paths = data.paths;
|
||||
filter();
|
||||
}
|
||||
});
|
||||
var timer;
|
||||
$("#tree-finder-field").keydown(function(e){
|
||||
var target = $(this);
|
||||
if(e.keyCode == 40){ // DOWN
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
changeCursor(cursor+1);
|
||||
}else if(e.keyCode==38){ // UP
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
changeCursor(cursor-1);
|
||||
}else if(e.keyCode==13){ // ENTER
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
target = $(".tree-browser-result.navigation-focus a");
|
||||
if(target[0]){
|
||||
target[0].click();
|
||||
}
|
||||
}else if(e.keyCode==27){ // ESC
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
history.back();
|
||||
}else{
|
||||
clearTimeout(timer);
|
||||
timer=setTimeout(filter,300);
|
||||
}
|
||||
});
|
||||
function changeCursor(newPos){
|
||||
if(!$(".tree-browser-result")[newPos]){
|
||||
return $(".tree-browser-result.navigation-focus");
|
||||
}
|
||||
$(".tree-browser-result.navigation-focus").removeClass("navigation-focus");
|
||||
cursor=newPos;
|
||||
scrollIntoView($($(".tree-browser-result")[cursor]).addClass("navigation-focus"));
|
||||
}
|
||||
function filter(){
|
||||
var v = $('#tree-finder-field').val();
|
||||
if(v==preKeyword || paths.length==0){
|
||||
return;
|
||||
}
|
||||
scrollIntoView('#tree-finder-field');
|
||||
preKeyword=v;
|
||||
cursor=0;
|
||||
var p = string_score_sort(v, paths, 50);
|
||||
res.html("");
|
||||
if(p.length==0){
|
||||
$(".no-results").show();
|
||||
return;
|
||||
}else{
|
||||
$(".no-results").hide();
|
||||
for(var i=0;i < p.length;i++){
|
||||
var row = template.clone();
|
||||
row.find("a").attr("href",pathBase+"/"+p[i].string).html(string_score_highlight(p[i], '<b>'));
|
||||
if(cursor==i){
|
||||
row.addClass("navigation-focus");
|
||||
}
|
||||
row.appendTo(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
<form action="@path/signin" method="POST" validate="true">
|
||||
<label for="userName">Username:</label>
|
||||
<span id="error-userName" class="error"></span>
|
||||
<input type="text" name="userName" id="userName" style="width: 95%"/>
|
||||
<input type="text" name="userName" id="userName" style="width: 95%" autofocus/>
|
||||
<label for="password">Password:</label>
|
||||
<span id="error-password" class="error"></span>
|
||||
<input type="password" name="password" id="password" style="width: 95%"/>
|
||||
|
||||
@@ -701,6 +701,12 @@ span.simplified-path {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.btn-pullrequest-branch{
|
||||
background: none;
|
||||
border: 1px solid #0088cc;
|
||||
color: #0088cc;
|
||||
}
|
||||
|
||||
/****************************************************************************/
|
||||
/* nav pulls group */
|
||||
/****************************************************************************/
|
||||
@@ -1145,6 +1151,149 @@ table.diff tbody tr.not-diff:hover td{
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0px;
|
||||
}
|
||||
.diff-same{
|
||||
background: #DDD;
|
||||
color: #BBB;
|
||||
font-size: 16px;
|
||||
padding: 20px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
/* ------- for imageDiff */
|
||||
.diff-image-frame{
|
||||
display: table-cell;
|
||||
*float: left; /* for ie7 */
|
||||
vertical-align: middle;
|
||||
padding: 20px;
|
||||
background-color: #eee;
|
||||
}
|
||||
.diff-image-frame.diff-old{
|
||||
padding-right: 2px;
|
||||
}
|
||||
.diff-image-frame.diff-new{
|
||||
padding-left: 2px;
|
||||
}
|
||||
.diff-image-frame .diff-meta{
|
||||
margin-top: 12px;
|
||||
color: #999;
|
||||
font-family: Helvetica,arial,freesans,clean,sans-serif;
|
||||
font-size: 12px;
|
||||
}
|
||||
.diff-image-frame.diff-old .diff-meta .diff{
|
||||
color: #bd2c00;
|
||||
}
|
||||
.diff-image-frame.diff-new .diff-meta .diff{
|
||||
color: #55a532;
|
||||
}
|
||||
.diff-image-frame img{
|
||||
max-height: 410px;
|
||||
max-width: 410px;
|
||||
background: url(../images/checker.png);
|
||||
}
|
||||
.diff-image-render.diff2up{
|
||||
width:100%;
|
||||
text-align: center;
|
||||
display: table;
|
||||
}
|
||||
.diff-image-frame.diff-new img{
|
||||
border: 1px solid #55a532;
|
||||
}
|
||||
.diff-image-frame.diff-old img{
|
||||
border: 1px solid #bd2c00;
|
||||
}
|
||||
.diff-image-stack{
|
||||
position: relative;
|
||||
background: #EEE;
|
||||
padding-top: 20px;
|
||||
}
|
||||
.diff-image-stack .diff-old,
|
||||
.diff-image-stack .diff-new{
|
||||
position:absolute;
|
||||
overflow: hidden;
|
||||
margin:0 20px;
|
||||
}
|
||||
.diff-image-stack img {
|
||||
max-width: none;
|
||||
background: url(../images/checker.png);
|
||||
}
|
||||
.diff-image-stack .diff-new{
|
||||
border: 1px solid #55a532;
|
||||
background: #EEE;
|
||||
}
|
||||
.diff-image-stack .diff-old{
|
||||
border: 1px solid #bd2c00;
|
||||
}
|
||||
.diff-swipe-handle{
|
||||
position:absolute;
|
||||
margin-left: 325px;
|
||||
left: 100px;
|
||||
}
|
||||
.diff-silde-bar{
|
||||
width: 200px;
|
||||
position: absolute;
|
||||
left: 325px;
|
||||
margin: 6px 0 0 7px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid gray;
|
||||
height: 8px;
|
||||
}
|
||||
.image-diff-tools{
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
.image-diff-tools{
|
||||
font-family: 'Helvetica Neue', Helvetica, arial, freesans, clean, sans-serif;
|
||||
font-size: 12px;
|
||||
background: #f7f7f7;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.image-diff-tools li{
|
||||
background: none;
|
||||
display: inline;
|
||||
cursor: pointer;
|
||||
border-right: 1px solid #ccc;
|
||||
padding: 0 5px;
|
||||
position: relative;
|
||||
color: #666;
|
||||
}
|
||||
.image-diff-tools li:last-child{
|
||||
border-right: none;
|
||||
}
|
||||
.image-diff-tools li.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
.no-canvas .need-canvas{
|
||||
display: none;
|
||||
}
|
||||
.diff-image-stack.swipe .diff-new{
|
||||
border-right: 1px solid #888;
|
||||
}
|
||||
.diff-image-stack.swipe .diff-swipe-handle{
|
||||
margin-left: 15px;
|
||||
left: 410px;
|
||||
}
|
||||
.diff-image-stack.swipe .diff-silde-bar{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.diff-image-stack.onion .diff-silde-bar{
|
||||
background: -ms-linear-gradient(left, #bd2c00 0%,#55a532 100%); /* IE10+ */
|
||||
background: linear-gradient(to right, #bd2c00 0%,#55a532 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#bd2c00', endColorstr='#55a532',GradientType=1 ); /* IE6-9 */
|
||||
}
|
||||
|
||||
.diff-image-stack.blink .diff-silde-bar{
|
||||
border-style: dotted;
|
||||
background-image: linear-gradient(to right, #bd2c00, #bd2c00 50%, #55a532 50%, #55a532 100%);
|
||||
background-size: 2px 2px;
|
||||
}
|
||||
|
||||
.diff-image-stack.difference {
|
||||
padding-bottom: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
/****************************************************************************/
|
||||
/* Repository Settings */
|
||||
/****************************************************************************/
|
||||
@@ -1152,7 +1301,7 @@ ul.collaborator {
|
||||
list-style-type: none;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
|
||||
ul.collaborator li {
|
||||
background-color: #eee;
|
||||
border: 1px solid #ccc;
|
||||
@@ -1160,7 +1309,7 @@ ul.collaborator li {
|
||||
padding: 6px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
|
||||
ul.collaborator li:hover {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
@@ -1219,6 +1368,8 @@ div.markdown-body pre {
|
||||
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
font-size: 12px;
|
||||
white-space: pre;
|
||||
word-wrap: normal;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
div.markdown-body code {
|
||||
@@ -1399,3 +1550,227 @@ h5 a.markdown-anchor-link {
|
||||
h6 a.markdown-anchor-link {
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
/****************************************************************************/
|
||||
/* File finder */
|
||||
/****************************************************************************/
|
||||
#tree-finder-field{
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
vertical-align: baseline;
|
||||
font-size: 100%;
|
||||
height: inherit;
|
||||
width: 780px;
|
||||
}
|
||||
.find-input{
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
#tree-finder-results td{
|
||||
padding:7px 6px;
|
||||
}
|
||||
#tree-finder-results td.icon{
|
||||
width:16px; padding: 7px 2px 7px 6px;
|
||||
}
|
||||
#tree-finder-results .tree-browser-result .icon-chevron-right{
|
||||
visibility: hidden;
|
||||
}
|
||||
#tree-finder-results .tree-browser-result.navigation-focus .icon-chevron-right{
|
||||
visibility: visible;
|
||||
}
|
||||
#tree-finder-results .navigation-focus td{
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/****************************************************************************/
|
||||
/* blame */
|
||||
/****************************************************************************/
|
||||
.blobview pre.blob{
|
||||
padding-left: 0;
|
||||
}
|
||||
.blobview ol.linenums{
|
||||
margin-left: 0;
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
div.container.blame-container{
|
||||
width:1270px;
|
||||
}
|
||||
.line-age-legend {
|
||||
display: none;
|
||||
}
|
||||
.blame-container .line-age-legend {
|
||||
display: block;
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
color: #777;
|
||||
}
|
||||
.blame-container .line-age-legend ol {
|
||||
display: inline-block;
|
||||
*display: inline;
|
||||
*zoom: 1;
|
||||
list-style: none;
|
||||
margin: 0 5px;
|
||||
}
|
||||
.blame-container .line-age-legend ol li {
|
||||
display: inline-block;
|
||||
*display: inline;
|
||||
*zoom: 1;
|
||||
width: 8px;
|
||||
height: 10px;
|
||||
}
|
||||
.blame-container pre.blob{
|
||||
margin-left: 350px;
|
||||
}
|
||||
.blame-container pre.prettyprint ol.linenums li.blame-sep{
|
||||
border-top: 1px solid rgb(219, 219, 219);
|
||||
margin-top: -1px;
|
||||
}
|
||||
.blame-container .hide-if-blame {
|
||||
display: none;
|
||||
}
|
||||
.blame{
|
||||
font-size: 12px;
|
||||
white-space: normal;
|
||||
width: 340px;
|
||||
float: left;
|
||||
min-height: 100px;
|
||||
display: none;
|
||||
}
|
||||
.blame-container .blame{
|
||||
display: block;
|
||||
}
|
||||
.blame .blame-commit-title{
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.blame .avatar{
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.blame .blame-info{
|
||||
background: white;
|
||||
box-shadow:rgba(113, 135, 164, 0.65098) 0px 0px 4px 0px;
|
||||
position: absolute;
|
||||
width: 340px;
|
||||
padding: 2px;
|
||||
border-right: 2px solid;
|
||||
}
|
||||
.no-box-shadow .blame .blame-info{
|
||||
border-top: 1px solid #888;
|
||||
border-bottom: 1px solid #888;
|
||||
border-left: 1px solid #888;
|
||||
}
|
||||
.blame-sha{
|
||||
font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
.blame-sha .muted-link{
|
||||
color: #777;
|
||||
}
|
||||
.blame-sha .muted-link:hover{
|
||||
color: #4183c4;
|
||||
}
|
||||
|
||||
.blame .blame-info:hover{
|
||||
z-index: 100;
|
||||
box-shadow:rgba(113, 135, 164, 0.65098) 0px 0px 4px 3px;
|
||||
}
|
||||
.blame .blame-info.blame-last{
|
||||
background: #FDFCED;
|
||||
}
|
||||
.blame-info.heat1{ border-right-color:#ffeca7}
|
||||
.blame-info.heat2{ border-right-color:#ffdd8c}
|
||||
.blame-info.heat3{ border-right-color:#ffdd7c}
|
||||
.blame-info.heat4{ border-right-color:#fba447}
|
||||
.blame-info.heat5{ border-right-color:#f68736}
|
||||
.blame-info.heat6{ border-right-color:#f37636}
|
||||
.blame-info.heat7{ border-right-color:#ca6632}
|
||||
.blame-info.heat8{ border-right-color:#c0513f}
|
||||
.blame-info.heat9{ border-right-color:#a2503a}
|
||||
.blame-info.heat10{border-right-color:#793738}
|
||||
|
||||
.heat1{background-color:#ffeca7}
|
||||
.heat2{background-color:#ffdd8c}
|
||||
.heat3{background-color:#ffdd7c}
|
||||
.heat4{background-color:#fba447}
|
||||
.heat5{background-color:#f68736}
|
||||
.heat6{background-color:#f37636}
|
||||
.heat7{background-color:#ca6632}
|
||||
.heat8{background-color:#c0513f}
|
||||
.heat9{background-color:#a2503a}
|
||||
.heat10{background-color:#793738}
|
||||
|
||||
/****************************************************************************/
|
||||
/* Mobile */
|
||||
/****************************************************************************/
|
||||
@media (max-width: 767px) {
|
||||
body>form#search {
|
||||
margin: 0 -20px 20px -20px;
|
||||
}
|
||||
body>div.dashboard-nav {
|
||||
margin: 0 -20px 20px -20px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.container {
|
||||
width: auto !important;
|
||||
}
|
||||
.nav-pills-group .pull-right #search-filter-box {
|
||||
width: 90% !important;
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
margin-top: 42px;
|
||||
}
|
||||
.nav-pills-group .pull-right form#search-filter-form {
|
||||
margin-bottom: 60px !important;
|
||||
}
|
||||
.table-issues a.button-link {
|
||||
width: 42px;
|
||||
height: 16px;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
}
|
||||
.nav-tabs a.btn[href$="/_edit"] {
|
||||
width: 24px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
padding: 4px 6px;
|
||||
margin: 3px 4px 0 0;
|
||||
}
|
||||
body>div.container.body {
|
||||
margin: 0 -12px 40px -12px;
|
||||
}
|
||||
.container.body>div[style="width: 170px;"]{
|
||||
width: 32px !important;
|
||||
margin-right: -5px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.container.body>div[style="margin-right: 180px;"]{
|
||||
margin-right: 32px !important;
|
||||
}
|
||||
.container.body>div[style="width: 170px;"] .sidemenu i, .container.body>div[style="width: 170px;"] .sidemenu img {
|
||||
padding-right: 5px;
|
||||
}
|
||||
/* .container.body>div[style="width: 170px;"] .small,.container.body>div[style="width: 170px;"] .input-append, .container.body>div[style="width: 170px;"] div[style="margin-top: 10px;"] { */
|
||||
.container.body>div[style="width: 170px;"] .small,.container.body>div[style="width: 170px;"] .input-append {
|
||||
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container.body>div[style="width: 170px;"] div[style="margin-top: 10px;"] a.btn{
|
||||
width: 26px !important;
|
||||
padding: 2px;
|
||||
}
|
||||
.container.body>div[style="width: 170px;"] div[style="margin-top: 10px;"] a.btn i {
|
||||
margin: 5px 10px 5px 6px;
|
||||
}
|
||||
body>.container>#fork-form{
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
BIN
src/main/webapp/assets/common/images/checker.png
Normal file
BIN
src/main/webapp/assets/common/images/checker.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 949 B |
@@ -12,6 +12,12 @@ $(function(){
|
||||
$('a[data-toggle=tooltip]').tooltip();
|
||||
$('li[data-toggle=tooltip]').tooltip();
|
||||
|
||||
// activate hotkey
|
||||
$('a[data-hotkey]').each(function(){
|
||||
var target = this;
|
||||
$(document).bind('keydown', $(target).data('hotkey'), function(){ target.click(); });
|
||||
});
|
||||
|
||||
// anchor icon for markdown
|
||||
$('.markdown-head').mouseenter(function(e){
|
||||
$(e.target).children('a.markdown-anchor-link').show();
|
||||
@@ -334,3 +340,366 @@ $.extend(JsDiffRender.prototype,{
|
||||
return ret;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* scroll target into view ( on bottom edge, or on top edge)
|
||||
*/
|
||||
function scrollIntoView(target){
|
||||
target = $(target);
|
||||
var $window = $(window);
|
||||
var docViewTop = $window.scrollTop();
|
||||
var docViewBottom = docViewTop + $window.height();
|
||||
|
||||
var elemTop = target.offset().top;
|
||||
var elemBottom = elemTop + target.height();
|
||||
|
||||
if(elemBottom > docViewBottom){
|
||||
$('html, body').scrollTop(elemBottom - $window.height());
|
||||
}else if(elemTop < docViewTop){
|
||||
$('html, body').scrollTop(elemTop);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* escape html
|
||||
*/
|
||||
function escapeHtml(text){
|
||||
return text.replace(/&/g,'&').replace(/</g,'<').replace(/"/g,'"').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
/**
|
||||
* calculate string ranking for path.
|
||||
* Original ported from:
|
||||
* http://joshaven.com/string_score
|
||||
* https://github.com/joshaven/string_score
|
||||
*
|
||||
* Copyright (C) 2009-2011 Joshaven Potter <yourtech@@gmail.com>
|
||||
* Special thanks to all of the contributors listed here https://github.com/joshaven/string_score
|
||||
* MIT license: http://www.opensource.org/licenses/mit-license.php
|
||||
*/
|
||||
function string_score(string, word) {
|
||||
'use strict';
|
||||
var zero = {score:0,matchingPositions:[]};
|
||||
|
||||
// If the string is equal to the word, perfect match.
|
||||
if (string === word || word === "") { return {score:1, matchingPositions:[]}; }
|
||||
|
||||
var lString = string.toUpperCase(),
|
||||
strLength = string.length,
|
||||
lWord = word.toUpperCase(),
|
||||
wordLength = word.length;
|
||||
|
||||
return calc(zero, 0, 0, 0, 0, []);
|
||||
function calc(score, startAt, skip, runningScore, i, matchingPositions){
|
||||
if( i < wordLength) {
|
||||
var charScore = 0;
|
||||
|
||||
// Find next first case-insensitive match of a character.
|
||||
var idxOf = lString.indexOf(lWord[i], skip);
|
||||
|
||||
if (-1 === idxOf) { return score; }
|
||||
score = calc(score, startAt, idxOf+1, runningScore, i, matchingPositions);
|
||||
if (startAt === idxOf) {
|
||||
// Consecutive letter & start-of-string Bonus
|
||||
charScore = 0.8;
|
||||
} else {
|
||||
charScore = 0.1;
|
||||
|
||||
// Acronym Bonus
|
||||
// Weighing Logic: Typing the first character of an acronym is as if you
|
||||
// preceded it with two perfect character matches.
|
||||
if (/^[^A-Za-z0-9]/.test(string[idxOf - 1])){
|
||||
charScore += 0.7;
|
||||
}else if(string[idxOf]==lWord[i]) {
|
||||
// Upper case bonus
|
||||
charScore += 0.2;
|
||||
// Camel case bonus
|
||||
if(/^[a-z]/.test(string[idxOf - 1])){
|
||||
charScore += 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Same case bonus.
|
||||
if (string[idxOf] === word[i]) { charScore += 0.1; }
|
||||
|
||||
// next round
|
||||
return calc(score, idxOf + 1, idxOf + 1, runningScore + charScore, i+1, matchingPositions.concat(idxOf));
|
||||
}else{
|
||||
// skip non match folder
|
||||
var effectiveLength = strLength;
|
||||
if(matchingPositions.length){
|
||||
var lastSlash = string.lastIndexOf('/',matchingPositions[0]);
|
||||
if(lastSlash!==-1){
|
||||
effectiveLength = strLength-lastSlash;
|
||||
}
|
||||
}
|
||||
// Reduce penalty for longer strings.
|
||||
var finalScore = 0.5 * (runningScore / effectiveLength + runningScore / wordLength);
|
||||
|
||||
if ((lWord[0] === lString[0]) && (finalScore < 0.85)) {
|
||||
finalScore += 0.15;
|
||||
}
|
||||
if(score.score >= finalScore){
|
||||
return score;
|
||||
}
|
||||
return {score:finalScore, matchingPositions:matchingPositions};
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* sort by string_score.
|
||||
* @param word {String} search word
|
||||
* @param strings {Array[String]} search targets
|
||||
* @param limit {Integer} result limit
|
||||
* @return {Array[{score:"float matching score", string:"string target string", matchingPositions:"Array[Interger] matchng positions"}]}
|
||||
*/
|
||||
function string_score_sort(word, strings, limit){
|
||||
var ret = [], i=0, l = (word==="")?Math.min(strings.length, limit):strings.length;
|
||||
for(; i < l; i++){
|
||||
var score = string_score(strings[i],word);
|
||||
if(score.score){
|
||||
score.string = strings[i];
|
||||
ret.push(score);
|
||||
}
|
||||
}
|
||||
ret.sort(function(a,b){
|
||||
var s = b.score - a.score;
|
||||
if(s === 0){
|
||||
return a.string > b.string ? 1 : -1;
|
||||
}
|
||||
return s;
|
||||
});
|
||||
ret = ret.slice(0,limit);
|
||||
return ret;
|
||||
}
|
||||
/**
|
||||
* highlight by result.
|
||||
* @param score {string:"string target string", matchingPositions:"Array[Interger] matchng positions"}
|
||||
* @param highlight tag ex: '<b>'
|
||||
* @return array of highlighted html elements.
|
||||
*/
|
||||
function string_score_highlight(result, tag){
|
||||
var str = result.string, msp=0;
|
||||
return hilight([], 0, result.matchingPositions[msp]);
|
||||
function hilight(html, c, mpos){
|
||||
if(mpos === undefined){
|
||||
return html.concat(document.createTextNode(str.substr(c)));
|
||||
}else{
|
||||
return hilight(html.concat([
|
||||
document.createTextNode(str.substring(c,mpos)),
|
||||
$(tag).text(str[mpos])]),
|
||||
mpos+1, result.matchingPositions[++msp]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/****************************************************************************/
|
||||
/* Diff */
|
||||
/****************************************************************************/
|
||||
// add naturalWidth and naturalHeight for ie 8
|
||||
function setNatural(img) {
|
||||
if(typeof img.naturalWidth == 'undefined'){
|
||||
var tmp = new Image();
|
||||
tmp.src = img.src;
|
||||
img.naturalWidth = tmp.width;
|
||||
img.naturalHeight = tmp.height;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* onload handler
|
||||
* @param img <img>
|
||||
*/
|
||||
function onLoadedDiffImages(img){
|
||||
setNatural(img);
|
||||
img = $(img);
|
||||
img.show();
|
||||
var tb = img.parents(".diff-image-render");
|
||||
// Find images. If the image has not loaded yet, value is undefined.
|
||||
var old = tb.find(".diff-old img.diff-image:visible")[0];
|
||||
var neo = tb.find(".diff-new img.diff-image:visible")[0];
|
||||
imageDiff.appendImageMeta(tb, old, neo);
|
||||
if(old && neo){
|
||||
imageDiff.createToolSelector(old, neo).appendTo(tb.parent());
|
||||
}
|
||||
}
|
||||
var imageDiff ={
|
||||
/** append image meta div after image nodes.
|
||||
* @param tb <div class="diff-image-2up">
|
||||
* @param old <img>||undefined
|
||||
* @param neo <img>||undefined
|
||||
*/
|
||||
appendImageMeta:function(tb, old, neo){
|
||||
old = old || {};
|
||||
neo = neo || {};
|
||||
tb.find(".diff-meta").remove();
|
||||
// before loaded, image is not visible.
|
||||
tb.find("img.diff-image:visible").each(function(){
|
||||
var div = $('<p class="diff-meta"><b>W:</b><span class="w"></span> | <b>W:</b><span class="h"></span></p>');
|
||||
div.find('.w').text(this.naturalWidth+"px").toggleClass("diff", old.naturalWidth != neo.naturalWidth);
|
||||
div.find('.h').text(this.naturalHeight+"px").toggleClass("diff", old.naturalHeight != neo.naturalHeight);
|
||||
div.appendTo(this.parentNode);
|
||||
});
|
||||
},
|
||||
/** check this browser can use canvas tag.
|
||||
*/
|
||||
hasCanvasSupport:function(){
|
||||
if(!this.hasCanvasSupport.hasOwnProperty('resultCache')){
|
||||
this.hasCanvasSupport.resultCache = (typeof $('<canvas>')[0].getContext)=='function';
|
||||
}
|
||||
return this.hasCanvasSupport.resultCache;
|
||||
},
|
||||
/** create toolbar
|
||||
* @param old <img>
|
||||
* @param neo <img>
|
||||
* @return jQuery(<ul class="image-diff-tools">)
|
||||
*/
|
||||
createToolSelector:function(old, neo){
|
||||
var self = this;
|
||||
return $('<ul class="image-diff-tools">'+
|
||||
'<li data-mode="diff2up" class="active">2-up</li>'+
|
||||
'<li data-mode="swipe">Swipe</li>'+
|
||||
'<li data-mode="onion">Onion Skin</li>'+
|
||||
'<li data-mode="difference" class="need-canvas">Difference</li>'+
|
||||
'<li data-mode="blink">Blink</li>'+
|
||||
'</ul>')
|
||||
.toggleClass('no-canvas', !this.hasCanvasSupport())
|
||||
.on('click', 'li', function(e){
|
||||
var td = $(this).parents("td");
|
||||
$(e.delegateTarget).find('li').each(function(){ $(this).toggleClass('active',this == e.target); });
|
||||
var mode = $(e.target).data('mode');
|
||||
td.find(".diff-image-render").hide();
|
||||
// create div if not created yet
|
||||
if(td.find(".diff-image-render."+mode).show().length===0){
|
||||
self[mode](old, neo).insertBefore(e.delegateTarget).addClass("diff-image-render");
|
||||
}
|
||||
return false;
|
||||
});
|
||||
},
|
||||
/** (private) calc size from images and css (const)
|
||||
* @param old <img>
|
||||
* @param neo <img>
|
||||
*/
|
||||
calcSizes:function(old, neo){
|
||||
var maxWidth = 869 - 20 - 20 - 4; // set by css
|
||||
var h = Math.min(Math.max(old.naturalHeight, neo.naturalHeight),maxWidth);
|
||||
var w = Math.min(Math.max(old.naturalWidth, neo.naturalWidth),maxWidth);
|
||||
var oldRate = Math.min(h/old.naturalHeight, w/old.naturalWidth);
|
||||
var neoRate = Math.min(h/neo.naturalHeight, w/neo.naturalWidth);
|
||||
var neoW = neo.naturalWidth*neoRate;
|
||||
var neoH = neo.naturalHeight*neoRate;
|
||||
var oldW = old.naturalWidth*oldRate;
|
||||
var oldH = old.naturalHeight*oldRate;
|
||||
var paddingLeft = (maxWidth/2)-Math.max(neoW,oldW)/2;
|
||||
return {
|
||||
height:Math.max(oldH, neoH),
|
||||
width:w,
|
||||
padding:paddingLeft,
|
||||
paddingTop:20, // set by css
|
||||
barWidth:200, // set by css
|
||||
old:{ rate:oldRate, width:oldW, height:oldH },
|
||||
neo:{ rate:neoRate, width:neoW, height:neoH }
|
||||
};
|
||||
},
|
||||
/** (private) create div
|
||||
* @param old <img>
|
||||
* @param neo <img>
|
||||
*/
|
||||
stack:function(old, neo){
|
||||
var size = this.calcSizes(old, neo);
|
||||
var diffNew = $('<div class="diff-new">')
|
||||
.append($("<img>").attr('src',neo.src).css({width:size.neo.width, height:size.neo.height}));
|
||||
var diffOld = $('<div class="diff-old">')
|
||||
.append($("<img>").attr('src',old.src).css({width:size.old.width, height:size.old.height}));
|
||||
var handle = $('<span class="diff-swipe-handle icon icon-resize-horizontal"></span>')
|
||||
.css({marginTop:size.height-5});
|
||||
var bar = $('<hr class="diff-silde-bar">').css({top:size.height+size.paddingTop});
|
||||
var div = $('<div class="diff-image-stack">')
|
||||
.css({height:size.height+size.paddingTop, paddingLeft:size.padding})
|
||||
.append(diffOld, diffNew, bar, handle);
|
||||
return {
|
||||
neo:diffNew,
|
||||
old:diffOld,
|
||||
size:size,
|
||||
handle:handle,
|
||||
bar:bar,
|
||||
div:div,
|
||||
/* add event listener 'on mousemove' */
|
||||
onMoveHandleOnBar:function(callback){
|
||||
div.on('mousemove',function(e){
|
||||
var x = Math.max(Math.min((e.pageX - bar.offset().left), size.barWidth), 0);
|
||||
handle.css({left:x});
|
||||
callback(x, e);
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
/** create swipe box
|
||||
* @param old <img>
|
||||
* @param neo <img>
|
||||
* @return jQuery(<div class="diff-image-stack swipe">)
|
||||
*/
|
||||
swipe:function(old, neo){
|
||||
var stack = this.stack(old, neo);
|
||||
function setX(x){
|
||||
stack.neo.css({width:x});
|
||||
stack.handle.css({left:x+stack.size.padding});
|
||||
}
|
||||
setX(stack.size.neo.width/2);
|
||||
stack.div.on('mousemove',function(e){
|
||||
setX(Math.max(Math.min(e.pageX - stack.neo.offset().left, stack.size.neo.width),0));
|
||||
});
|
||||
return stack.div.addClass('swipe');
|
||||
},
|
||||
/** create blink box
|
||||
* @param old <img>
|
||||
* @param neo <img>
|
||||
* @return jQuery(<div class="diff-image-stack blink">)
|
||||
*/
|
||||
blink:function(old, neo){
|
||||
var stack = this.stack(old, neo);
|
||||
stack.onMoveHandleOnBar(function(x){
|
||||
stack.neo.toggle(Math.floor(x) % 2 === 0);
|
||||
});
|
||||
return stack.div.addClass('blink');
|
||||
},
|
||||
/** create onion skin box
|
||||
* @param old <img>
|
||||
* @param neo <img>
|
||||
* @return jQuery(<div class="diff-image-stack onion">)
|
||||
*/
|
||||
onion:function(old, neo){
|
||||
var stack = this.stack(old, neo);
|
||||
stack.neo.css({opacity:0.5});
|
||||
stack.onMoveHandleOnBar(function(x){
|
||||
stack.neo.css({opacity:x/stack.size.barWidth});
|
||||
});
|
||||
return stack.div.addClass('onion');
|
||||
},
|
||||
/** create difference box
|
||||
* @param old <img>
|
||||
* @param neo <img>
|
||||
* @return jQuery(<div class="diff-image-stack difference">)
|
||||
*/
|
||||
difference:function(old, neo){
|
||||
var size = this.calcSizes(old,neo);
|
||||
var canvas = $('<canvas>').attr({width:size.width, height:size.height})[0];
|
||||
var context = canvas.getContext('2d');
|
||||
|
||||
context.clearRect(0, 0, size.width, size.height);
|
||||
context.drawImage(neo, 0, 0, size.neo.width, size.neo.height);
|
||||
var neoData = context.getImageData(0, 0, size.neo.width, size.neo.height).data;
|
||||
context.clearRect(0, 0, size.width, size.height);
|
||||
|
||||
context.drawImage(old, 0, 0, size.old.width, size.old.height);
|
||||
var c = context.getImageData(0, 0, size.neo.width, size.neo.height);
|
||||
var cData = c.data;
|
||||
for (var i = cData.length -1; i>0; i --){
|
||||
cData[i] = (i%4===3) ? Math.max(cData[i], neoData[i]) : Math.abs(cData[i] - neoData[i]);
|
||||
}
|
||||
context.putImageData(c, 0, 0);
|
||||
|
||||
return $('<div class="diff-image-stack difference">').append(canvas);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
204
src/main/webapp/assets/vendors/jquery-hotkeys/jquery.hotkeys.js
vendored
Normal file
204
src/main/webapp/assets/vendors/jquery-hotkeys/jquery.hotkeys.js
vendored
Normal file
@@ -0,0 +1,204 @@
|
||||
/*jslint browser: true*/
|
||||
/*jslint jquery: true*/
|
||||
|
||||
/*
|
||||
* jQuery Hotkeys Plugin
|
||||
* Copyright 2010, John Resig
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
*
|
||||
* Based upon the plugin by Tzury Bar Yochay:
|
||||
* https://github.com/tzuryby/jquery.hotkeys
|
||||
*
|
||||
* Original idea by:
|
||||
* Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/
|
||||
*/
|
||||
|
||||
/*
|
||||
* One small change is: now keys are passed by object { keys: '...' }
|
||||
* Might be useful, when you want to pass some other data to your handler
|
||||
*/
|
||||
|
||||
(function(jQuery) {
|
||||
|
||||
jQuery.hotkeys = {
|
||||
version: "0.8",
|
||||
|
||||
specialKeys: {
|
||||
8: "backspace",
|
||||
9: "tab",
|
||||
10: "return",
|
||||
13: "return",
|
||||
16: "shift",
|
||||
17: "ctrl",
|
||||
18: "alt",
|
||||
19: "pause",
|
||||
20: "capslock",
|
||||
27: "esc",
|
||||
32: "space",
|
||||
33: "pageup",
|
||||
34: "pagedown",
|
||||
35: "end",
|
||||
36: "home",
|
||||
37: "left",
|
||||
38: "up",
|
||||
39: "right",
|
||||
40: "down",
|
||||
45: "insert",
|
||||
46: "del",
|
||||
59: ";",
|
||||
61: "=",
|
||||
96: "0",
|
||||
97: "1",
|
||||
98: "2",
|
||||
99: "3",
|
||||
100: "4",
|
||||
101: "5",
|
||||
102: "6",
|
||||
103: "7",
|
||||
104: "8",
|
||||
105: "9",
|
||||
106: "*",
|
||||
107: "+",
|
||||
109: "-",
|
||||
110: ".",
|
||||
111: "/",
|
||||
112: "f1",
|
||||
113: "f2",
|
||||
114: "f3",
|
||||
115: "f4",
|
||||
116: "f5",
|
||||
117: "f6",
|
||||
118: "f7",
|
||||
119: "f8",
|
||||
120: "f9",
|
||||
121: "f10",
|
||||
122: "f11",
|
||||
123: "f12",
|
||||
144: "numlock",
|
||||
145: "scroll",
|
||||
173: "-",
|
||||
186: ";",
|
||||
187: "=",
|
||||
188: ",",
|
||||
189: "-",
|
||||
190: ".",
|
||||
191: "/",
|
||||
192: "`",
|
||||
219: "[",
|
||||
220: "\\",
|
||||
221: "]",
|
||||
222: "'"
|
||||
},
|
||||
|
||||
shiftNums: {
|
||||
"`": "~",
|
||||
"1": "!",
|
||||
"2": "@",
|
||||
"3": "#",
|
||||
"4": "$",
|
||||
"5": "%",
|
||||
"6": "^",
|
||||
"7": "&",
|
||||
"8": "*",
|
||||
"9": "(",
|
||||
"0": ")",
|
||||
"-": "_",
|
||||
"=": "+",
|
||||
";": ": ",
|
||||
"'": "\"",
|
||||
",": "<",
|
||||
".": ">",
|
||||
"/": "?",
|
||||
"\\": "|"
|
||||
},
|
||||
|
||||
// excludes: button, checkbox, file, hidden, image, password, radio, reset, search, submit, url
|
||||
textAcceptingInputTypes: [
|
||||
"text", "password", "number", "email", "url", "range", "date", "month", "week", "time", "datetime",
|
||||
"datetime-local", "search", "color", "tel"],
|
||||
|
||||
// default input types not to bind to unless bound directly
|
||||
textInputTypes: /textarea|input|select/i,
|
||||
|
||||
options: {
|
||||
filterInputAcceptingElements: true,
|
||||
filterTextInputs: true,
|
||||
filterContentEditable: true
|
||||
}
|
||||
};
|
||||
|
||||
function keyHandler(handleObj) {
|
||||
if (typeof handleObj.data === "string") {
|
||||
handleObj.data = {
|
||||
keys: handleObj.data
|
||||
};
|
||||
}
|
||||
|
||||
// Only care when a possible input has been specified
|
||||
if (!handleObj.data || !handleObj.data.keys || typeof handleObj.data.keys !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
var origHandler = handleObj.handler,
|
||||
keys = handleObj.data.keys.toLowerCase().split(" ");
|
||||
|
||||
handleObj.handler = function(event) {
|
||||
// Don't fire in text-accepting inputs that we didn't directly bind to
|
||||
if (this !== event.target &&
|
||||
(jQuery.hotkeys.options.filterInputAcceptingElements &&
|
||||
jQuery.hotkeys.textInputTypes.test(event.target.nodeName) ||
|
||||
(jQuery.hotkeys.options.filterContentEditable && jQuery(event.target).attr('contenteditable')) ||
|
||||
(jQuery.hotkeys.options.filterTextInputs &&
|
||||
jQuery.inArray(event.target.type, jQuery.hotkeys.textAcceptingInputTypes) > -1))) {
|
||||
return;
|
||||
}
|
||||
|
||||
var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[event.which],
|
||||
character = String.fromCharCode(event.which).toLowerCase(),
|
||||
modif = "",
|
||||
possible = {};
|
||||
|
||||
jQuery.each(["alt", "ctrl", "shift"], function(index, specialKey) {
|
||||
|
||||
if (event[specialKey + 'Key'] && special !== specialKey) {
|
||||
modif += specialKey + '+';
|
||||
}
|
||||
});
|
||||
|
||||
// metaKey is triggered off ctrlKey erronously
|
||||
if (event.metaKey && !event.ctrlKey && special !== "meta") {
|
||||
modif += "meta+";
|
||||
}
|
||||
|
||||
if (event.metaKey && special !== "meta" && modif.indexOf("alt+ctrl+shift+") > -1) {
|
||||
modif = modif.replace("alt+ctrl+shift+", "hyper+");
|
||||
}
|
||||
|
||||
if (special) {
|
||||
possible[modif + special] = true;
|
||||
}
|
||||
else {
|
||||
possible[modif + character] = true;
|
||||
possible[modif + jQuery.hotkeys.shiftNums[character]] = true;
|
||||
|
||||
// "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
|
||||
if (modif === "shift+") {
|
||||
possible[jQuery.hotkeys.shiftNums[character]] = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0, l = keys.length; i < l; i++) {
|
||||
if (possible[keys[i]]) {
|
||||
return origHandler.apply(this, arguments);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
jQuery.each(["keydown", "keyup", "keypress"], function() {
|
||||
jQuery.event.special[this] = {
|
||||
add: keyHandler
|
||||
};
|
||||
});
|
||||
|
||||
})(jQuery || this.jQuery || window.jQuery);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,9 @@ DAMAGE.
|
||||
***/
|
||||
/* Author: Chas Emerick <cemerick@snowtide.com> */
|
||||
__whitespace = {" ":true, "\t":true, "\n":true, "\f":true, "\r":true};
|
||||
function __hasOwnProperty(obj, target){
|
||||
return Object.prototype.hasOwnProperty.call(obj, target);
|
||||
}
|
||||
|
||||
difflib = {
|
||||
defaultJunkFunction: function (c) {
|
||||
@@ -93,7 +96,7 @@ difflib = {
|
||||
|
||||
// replacement for python's dict.get function -- need easy default values
|
||||
__dictget: function (dict, key, defaultValue) {
|
||||
return dict.hasOwnProperty(key) ? dict[key] : defaultValue;
|
||||
return __hasOwnProperty(dict, key) ? dict[key] : defaultValue;
|
||||
},
|
||||
|
||||
SequenceMatcher: function (a, b, isjunk) {
|
||||
@@ -122,7 +125,7 @@ difflib = {
|
||||
var populardict = {};
|
||||
for (var i = 0; i < b.length; i++) {
|
||||
var elt = b[i];
|
||||
if (b2j.hasOwnProperty(elt)) {
|
||||
if (__hasOwnProperty(b2j, elt)) {
|
||||
var indices = b2j[elt];
|
||||
if (n >= 200 && indices.length * 100 > n) {
|
||||
populardict[elt] = 1;
|
||||
@@ -151,7 +154,7 @@ difflib = {
|
||||
}
|
||||
}
|
||||
for (var elt in b2j) {
|
||||
if (b2j.hasOwnProperty(elt) && isjunk(elt)) {
|
||||
if (__hasOwnProperty(b2j, elt) && isjunk(elt)) {
|
||||
junkdict[elt] = 1;
|
||||
delete b2j[elt];
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ table.diff thead th.texttitle {
|
||||
}
|
||||
table.diff tbody td {
|
||||
padding:0px .4em;
|
||||
padding-top:.4em;
|
||||
padding-top:.2em;
|
||||
vertical-align:top;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
@@ -90,11 +90,12 @@ class JsonFormatSpec extends Specification {
|
||||
user = apiUser,
|
||||
body= "Me too",
|
||||
created_at= date1,
|
||||
updated_at= date1)
|
||||
updated_at= date1)(RepositoryName("octocat","Hello-World"), 100)
|
||||
val apiCommentJson = s"""{
|
||||
"id": 1,
|
||||
"body": "Me too",
|
||||
"user": $apiUserJson,
|
||||
"html_url" : "${context.baseUrl}/octocat/Hello-World/issues/100#comment-1",
|
||||
"created_at": "2011-04-14T16:00:49Z",
|
||||
"updated_at": "2011-04-14T16:00:49Z"
|
||||
}"""
|
||||
@@ -157,13 +158,15 @@ class JsonFormatSpec extends Specification {
|
||||
state = "open",
|
||||
body = "I'm having a problem with this.",
|
||||
created_at = date1,
|
||||
updated_at = date1)
|
||||
updated_at = date1)(RepositoryName("octocat","Hello-World"))
|
||||
val apiIssueJson = s"""{
|
||||
"number": 1347,
|
||||
"state": "open",
|
||||
"title": "Found a bug",
|
||||
"body": "I'm having a problem with this.",
|
||||
"user": $apiUserJson,
|
||||
"comments_url": "${context.baseUrl}/api/v3/repos/octocat/Hello-World/issues/1347/comments",
|
||||
"html_url": "${context.baseUrl}/octocat/Hello-World/issues/1347",
|
||||
"created_at": "2011-04-14T16:00:49Z",
|
||||
"updated_at": "2011-04-14T16:00:49Z"
|
||||
}"""
|
||||
|
||||
@@ -5,6 +5,7 @@ import gitbucket.core.util.JGitUtil
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.GitSpecUtil._
|
||||
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.eclipse.jgit.api.Git
|
||||
@@ -14,51 +15,22 @@ import org.eclipse.jgit.revwalk._
|
||||
import org.eclipse.jgit.treewalk._
|
||||
import org.specs2.mutable.Specification
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file._
|
||||
import java.util.Date
|
||||
|
||||
|
||||
class MergeServiceSpec extends Specification {
|
||||
sequential
|
||||
val service = new MergeService{}
|
||||
val branch = "master"
|
||||
val issueId = 10
|
||||
def initRepository(owner:String, name:String) = {
|
||||
val repo1Dir = getRepositoryDir(owner, name)
|
||||
RepositoryCache.clear()
|
||||
FileUtils.deleteQuietly(repo1Dir)
|
||||
Files.createDirectories(repo1Dir.toPath())
|
||||
JGitUtil.initRepository(repo1Dir)
|
||||
using(Git.open(repo1Dir)){ git =>
|
||||
createFile(git, s"refs/heads/master", "test.txt", "hoge" )
|
||||
git.branchCreate().setStartPoint(s"refs/heads/master").setName(s"refs/pull/${issueId}/head").call()
|
||||
}
|
||||
repo1Dir
|
||||
}
|
||||
def createFile(git:Git, branch:String, name:String, content:String){
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(branch + "^{commit}")
|
||||
builder.add(JGitUtil.createDirCacheEntry(name, FileMode.REGULAR_FILE,
|
||||
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||
builder.finish()
|
||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
|
||||
branch, "dummy", "dummy@example.com", "Initial commit")
|
||||
}
|
||||
def getFile(git:Git, branch:String, path:String) = {
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
|
||||
val objectId = using(new TreeWalk(git.getRepository)){ walk =>
|
||||
walk.addTree(revCommit.getTree)
|
||||
walk.setRecursive(true)
|
||||
@scala.annotation.tailrec
|
||||
def _getPathObjectId: ObjectId = walk.next match {
|
||||
case true if(walk.getPathString == path) => walk.getObjectId(0)
|
||||
case true => _getPathObjectId
|
||||
case false => throw new Exception(s"not found ${branch} / ${path}")
|
||||
}
|
||||
_getPathObjectId
|
||||
}
|
||||
JGitUtil.getContentInfo(git, path, objectId)
|
||||
def initRepository(owner:String, name:String): File = {
|
||||
val dir = createTestRepository(getRepositoryDir(owner, name))
|
||||
using(Git.open(dir)){ git =>
|
||||
createFile(git, s"refs/heads/master", "test.txt", "hoge" )
|
||||
git.branchCreate().setStartPoint(s"refs/heads/master").setName(s"refs/pull/${issueId}/head").call()
|
||||
}
|
||||
dir
|
||||
}
|
||||
def createConfrict(git:Git) = {
|
||||
createFile(git, s"refs/heads/${branch}", "test.txt", "hoge2" )
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.model._
|
||||
import gitbucket.core.model.Profile._
|
||||
|
||||
import org.specs2.mutable.Specification
|
||||
|
||||
class PullRequestServiceSpec extends Specification with ServiceSpecBase with PullRequestService with IssuesService {
|
||||
def swap(r: (Issue, PullRequest)) = (r._2 -> r._1)
|
||||
"PullRequestService.getPullRequestFromBranch" should {
|
||||
"""
|
||||
|return pull request if exists pull request from `branch` to `defaultBranch` and not closed.
|
||||
|return pull request if exists pull request from `branch` to othre branch and not closed.
|
||||
|return None if all pull request is closed""".stripMargin.trim in { withTestDB { implicit se =>
|
||||
generateNewUserWithDBRepository("user1", "repo1")
|
||||
generateNewUserWithDBRepository("user1", "repo2")
|
||||
generateNewUserWithDBRepository("user2", "repo1")
|
||||
generateNewPullRequest("user1/repo1/master", "user1/repo1/head2") // not target branch
|
||||
generateNewPullRequest("user1/repo1/head1", "user1/repo1/master") // not target branch ( swap from, to )
|
||||
generateNewPullRequest("user1/repo1/master", "user2/repo1/head1") // othre user
|
||||
generateNewPullRequest("user1/repo1/master", "user1/repo2/head1") // othre repository
|
||||
val r1 = swap(generateNewPullRequest("user1/repo1/master2", "user1/repo1/head1"))
|
||||
val r2 = swap(generateNewPullRequest("user1/repo1/master", "user1/repo1/head1"))
|
||||
val r3 = swap(generateNewPullRequest("user1/repo1/master4", "user1/repo1/head1"))
|
||||
getPullRequestFromBranch("user1", "repo1", "head1", "master") must_== Some(r2)
|
||||
updateClosed("user1", "repo1", r2._1.issueId, true)
|
||||
getPullRequestFromBranch("user1", "repo1", "head1", "master").get must beOneOf(r1, r2)
|
||||
updateClosed("user1", "repo1", r1._1.issueId, true)
|
||||
updateClosed("user1", "repo1", r3._1.issueId, true)
|
||||
getPullRequestFromBranch("user1", "repo1", "head1", "master") must beNone
|
||||
} }
|
||||
}
|
||||
}
|
||||
@@ -32,23 +32,25 @@ trait ServiceSpecBase {
|
||||
|
||||
def generateNewAccount(name:String)(implicit s:Session):Account = {
|
||||
AccountService.createAccount(name, name, name, s"${name}@example.com", false, None)
|
||||
AccountService.getAccountByUserName(name).get
|
||||
user(name)
|
||||
}
|
||||
|
||||
def user(name:String)(implicit s:Session):Account = AccountService.getAccountByUserName(name).get
|
||||
|
||||
lazy val dummyService = new RepositoryService with AccountService with IssuesService with PullRequestService
|
||||
with CommitStatusService (){}
|
||||
|
||||
def generateNewUserWithDBRepository(userName:String, repositoryName:String)(implicit s:Session):Account = {
|
||||
val ac = generateNewAccount(userName)
|
||||
val ac = AccountService.getAccountByUserName(userName).getOrElse(generateNewAccount(userName))
|
||||
dummyService.createRepository(repositoryName, userName, None, false)
|
||||
ac
|
||||
}
|
||||
|
||||
def generateNewIssue(userName:String, repositoryName:String, requestUserName:String="root")(implicit s:Session): Int = {
|
||||
def generateNewIssue(userName:String, repositoryName:String, loginUser:String="root")(implicit s:Session): Int = {
|
||||
dummyService.createIssue(
|
||||
owner = userName,
|
||||
repository = repositoryName,
|
||||
loginUser = requestUserName,
|
||||
loginUser = loginUser,
|
||||
title = "issue title",
|
||||
content = None,
|
||||
assignedUserName = None,
|
||||
@@ -56,10 +58,10 @@ trait ServiceSpecBase {
|
||||
isPullRequest = true)
|
||||
}
|
||||
|
||||
def generateNewPullRequest(base:String, request:String)(implicit s:Session):(Issue, PullRequest) = {
|
||||
def generateNewPullRequest(base:String, request:String, loginUser:String=null)(implicit s:Session):(Issue, PullRequest) = {
|
||||
val Array(baseUserName, baseRepositoryName, baesBranch)=base.split("/")
|
||||
val Array(requestUserName, requestRepositoryName, requestBranch)=request.split("/")
|
||||
val issueId = generateNewIssue(baseUserName, baseRepositoryName, requestUserName)
|
||||
val issueId = generateNewIssue(baseUserName, baseRepositoryName, Option(loginUser).getOrElse(requestUserName))
|
||||
dummyService.createPullRequest(
|
||||
originUserName = baseUserName,
|
||||
originRepositoryName = baseRepositoryName,
|
||||
|
||||
@@ -11,9 +11,10 @@ class WebHookServiceSpec extends Specification with ServiceSpecBase {
|
||||
val user1 = generateNewUserWithDBRepository("user1","repo1")
|
||||
val user2 = generateNewUserWithDBRepository("user2","repo2")
|
||||
val user3 = generateNewUserWithDBRepository("user3","repo3")
|
||||
val (issue1, pullreq1) = generateNewPullRequest("user1/repo1/master1", "user2/repo2/master2")
|
||||
val (issue3, pullreq3) = generateNewPullRequest("user3/repo3/master3", "user2/repo2/master2")
|
||||
val (issue32, pullreq32) = generateNewPullRequest("user3/repo3/master32", "user2/repo2/master2")
|
||||
val issueUser = user("root")
|
||||
val (issue1, pullreq1) = generateNewPullRequest("user1/repo1/master1", "user2/repo2/master2", loginUser="root")
|
||||
val (issue3, pullreq3) = generateNewPullRequest("user3/repo3/master3", "user2/repo2/master2", loginUser="root")
|
||||
val (issue32, pullreq32) = generateNewPullRequest("user3/repo3/master32", "user2/repo2/master2", loginUser="root")
|
||||
generateNewPullRequest("user2/repo2/master2", "user1/repo1/master2")
|
||||
service.addWebHookURL("user1", "repo1", "webhook1-1")
|
||||
service.addWebHookURL("user1", "repo1", "webhook1-2")
|
||||
@@ -25,18 +26,19 @@ class WebHookServiceSpec extends Specification with ServiceSpecBase {
|
||||
service.getPullRequestsByRequestForWebhook("user1","repo1","master1") must_== Map.empty
|
||||
|
||||
var r = service.getPullRequestsByRequestForWebhook("user2","repo2","master2").mapValues(_.map(_.url).toSet)
|
||||
|
||||
r.size must_== 3
|
||||
r((issue1, pullreq1, user1, user2)) must_== Set("webhook1-1","webhook1-2")
|
||||
r((issue3, pullreq3, user3, user2)) must_== Set("webhook3-1","webhook3-2")
|
||||
r((issue32, pullreq32, user3, user2)) must_== Set("webhook3-1","webhook3-2")
|
||||
r((issue1, issueUser, pullreq1, user1, user2)) must_== Set("webhook1-1","webhook1-2")
|
||||
r((issue3, issueUser, pullreq3, user3, user2)) must_== Set("webhook3-1","webhook3-2")
|
||||
r((issue32, issueUser, pullreq32, user3, user2)) must_== Set("webhook3-1","webhook3-2")
|
||||
|
||||
// when closed, it not founds.
|
||||
service.updateClosed("user1","repo1",issue1.issueId, true)
|
||||
|
||||
var r2 = service.getPullRequestsByRequestForWebhook("user2","repo2","master2").mapValues(_.map(_.url).toSet)
|
||||
r2.size must_== 2
|
||||
r2((issue3, pullreq3, user3, user2)) must_== Set("webhook3-1","webhook3-2")
|
||||
r2((issue32, pullreq32, user3, user2)) must_== Set("webhook3-1","webhook3-2")
|
||||
r2((issue3, issueUser, pullreq3, user3, user2)) must_== Set("webhook3-1","webhook3-2")
|
||||
r2((issue32, issueUser, pullreq32, user3, user2)) must_== Set("webhook3-1","webhook3-2")
|
||||
} }
|
||||
}
|
||||
}
|
||||
110
src/test/scala/gitbucket/core/util/GitSpecUtil.scala
Normal file
110
src/test/scala/gitbucket/core/util/GitSpecUtil.scala
Normal file
@@ -0,0 +1,110 @@
|
||||
package gitbucket.core.util
|
||||
|
||||
import gitbucket.core.model._
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.dircache.DirCache
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.eclipse.jgit.revwalk._
|
||||
import org.eclipse.jgit.treewalk._
|
||||
import org.eclipse.jgit.merge._
|
||||
import org.eclipse.jgit.errors._
|
||||
|
||||
import java.nio.file._
|
||||
import java.util.Date
|
||||
import java.io.File
|
||||
|
||||
object GitSpecUtil {
|
||||
def withTestFolder[U](f: File => U) {
|
||||
val folder = new File(System.getProperty("java.io.tmpdir"), "test-" + System.nanoTime)
|
||||
if(!folder.mkdirs()){
|
||||
throw new java.io.IOException("can't create folder "+folder.getAbsolutePath)
|
||||
}
|
||||
try {
|
||||
f(folder)
|
||||
} finally {
|
||||
FileUtils.deleteQuietly(folder)
|
||||
}
|
||||
}
|
||||
def withTestRepository[U](f: Git => U) = withTestFolder(folder => using(Git.open(createTestRepository(folder)))(f))
|
||||
def createTestRepository(dir: File): File = {
|
||||
RepositoryCache.clear()
|
||||
FileUtils.deleteQuietly(dir)
|
||||
Files.createDirectories(dir.toPath())
|
||||
JGitUtil.initRepository(dir)
|
||||
dir
|
||||
}
|
||||
def createFile(git: Git, branch: String, name: String, content: String,
|
||||
autorName: String = "dummy", autorEmail: String = "dummy@example.com",
|
||||
message: String = "test commit") {
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(branch + "^{commit}")
|
||||
if(headId!=null){
|
||||
JGitUtil.processTree(git, headId){ (path, tree) =>
|
||||
if(name != path){
|
||||
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.add(JGitUtil.createDirCacheEntry(name, FileMode.REGULAR_FILE,
|
||||
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||
builder.finish()
|
||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
|
||||
branch, autorName, autorEmail, message)
|
||||
inserter.flush()
|
||||
inserter.release()
|
||||
}
|
||||
def getFile(git: Git, branch: String, path: String) = {
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
|
||||
val objectId = using(new TreeWalk(git.getRepository)) { walk =>
|
||||
walk.addTree(revCommit.getTree)
|
||||
walk.setRecursive(true)
|
||||
@scala.annotation.tailrec
|
||||
def _getPathObjectId: ObjectId = walk.next match {
|
||||
case true if (walk.getPathString == path) => walk.getObjectId(0)
|
||||
case true => _getPathObjectId
|
||||
case false => throw new Exception(s"not found ${branch} / ${path}")
|
||||
}
|
||||
_getPathObjectId
|
||||
}
|
||||
JGitUtil.getContentInfo(git, path, objectId)
|
||||
}
|
||||
def mergeAndCommit(git: Git, into:String, branch:String, message:String = null):Unit = {
|
||||
val repository = git.getRepository
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(repository, true)
|
||||
val mergeBaseTip = repository.resolve(into)
|
||||
val mergeTip = repository.resolve(branch)
|
||||
val conflicted = try {
|
||||
!merger.merge(mergeBaseTip, mergeTip)
|
||||
} catch {
|
||||
case e: NoMergeBaseException => true
|
||||
}
|
||||
if(conflicted){
|
||||
throw new RuntimeException("conflict!")
|
||||
}
|
||||
val mergeTipCommit = using(new RevWalk( repository ))(_.parseCommit( mergeTip ))
|
||||
val committer = mergeTipCommit.getCommitterIdent;
|
||||
// creates merge commit
|
||||
val mergeCommit = new CommitBuilder()
|
||||
mergeCommit.setTreeId(merger.getResultTreeId)
|
||||
mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*)
|
||||
mergeCommit.setAuthor(committer)
|
||||
mergeCommit.setCommitter(committer)
|
||||
mergeCommit.setMessage(message)
|
||||
// insertObject and got mergeCommit Object Id
|
||||
val inserter = repository.newObjectInserter
|
||||
val mergeCommitId = inserter.insert(mergeCommit)
|
||||
inserter.flush()
|
||||
inserter.release()
|
||||
// update refs
|
||||
val refUpdate = repository.updateRef(into)
|
||||
refUpdate.setNewObjectId(mergeCommitId)
|
||||
refUpdate.setForceUpdate(true)
|
||||
refUpdate.setRefLogIdent(committer)
|
||||
refUpdate.update()
|
||||
}
|
||||
}
|
||||
103
src/test/scala/gitbucket/core/util/JGitUtilSpec.scala
Normal file
103
src/test/scala/gitbucket/core/util/JGitUtilSpec.scala
Normal file
@@ -0,0 +1,103 @@
|
||||
package gitbucket.core.util
|
||||
|
||||
import org.specs2.mutable._
|
||||
import GitSpecUtil._
|
||||
|
||||
class JGitUtilSpec extends Specification {
|
||||
|
||||
"getFileList(git: Git, revision: String, path)" should {
|
||||
withTestRepository { git =>
|
||||
def list(branch: String, path: String) =
|
||||
JGitUtil.getFileList(git, branch, path).map(finfo => (finfo.name, finfo.message, finfo.isDirectory))
|
||||
list("master", ".") mustEqual Nil
|
||||
list("master", "dir/subdir") mustEqual Nil
|
||||
list("branch", ".") mustEqual Nil
|
||||
list("branch", "dir/subdir") mustEqual Nil
|
||||
|
||||
createFile(git, "master", "README.md", "body1", message = "commit1")
|
||||
|
||||
list("master", ".") mustEqual List(("README.md", "commit1", false))
|
||||
list("master", "dir/subdir") mustEqual Nil
|
||||
list("branch", ".") mustEqual Nil
|
||||
list("branch", "dir/subdir") mustEqual Nil
|
||||
|
||||
createFile(git, "master", "README.md", "body2", message = "commit2")
|
||||
|
||||
list("master", ".") mustEqual List(("README.md", "commit2", false))
|
||||
list("master", "dir/subdir") mustEqual Nil
|
||||
list("branch", ".") mustEqual Nil
|
||||
list("branch", "dir/subdir") mustEqual Nil
|
||||
|
||||
createFile(git, "master", "dir/subdir/File3.md", "body3", message = "commit3")
|
||||
|
||||
list("master", ".") mustEqual List(("dir/subdir", "commit3", true), ("README.md", "commit2", false))
|
||||
list("master", "dir/subdir") mustEqual List(("File3.md", "commit3", false))
|
||||
list("branch", ".") mustEqual Nil
|
||||
list("branch", "dir/subdir") mustEqual Nil
|
||||
|
||||
createFile(git, "master", "dir/subdir/File4.md", "body4", message = "commit4")
|
||||
|
||||
list("master", ".") mustEqual List(("dir/subdir", "commit4", true), ("README.md", "commit2", false))
|
||||
list("master", "dir/subdir") mustEqual List(("File3.md", "commit3", false), ("File4.md", "commit4", false))
|
||||
list("branch", ".") mustEqual Nil
|
||||
list("branch", "dir/subdir") mustEqual Nil
|
||||
|
||||
createFile(git, "master", "README5.md", "body5", message = "commit5")
|
||||
|
||||
list("master", ".") mustEqual List(("dir/subdir", "commit4", true), ("README.md", "commit2", false), ("README5.md", "commit5", false))
|
||||
list("master", "dir/subdir") mustEqual List(("File3.md", "commit3", false), ("File4.md", "commit4", false))
|
||||
list("branch", ".") mustEqual Nil
|
||||
list("branch", "dir/subdir") mustEqual Nil
|
||||
|
||||
createFile(git, "master", "README.md", "body6", message = "commit6")
|
||||
|
||||
list("master", ".") mustEqual List(("dir/subdir", "commit4", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
|
||||
list("master", "dir/subdir") mustEqual List(("File3.md", "commit3", false), ("File4.md", "commit4", false))
|
||||
list("branch", ".") mustEqual Nil
|
||||
list("branch", "dir/subdir") mustEqual Nil
|
||||
|
||||
git.branchCreate().setName("branch").setStartPoint("master").call()
|
||||
|
||||
list("master", ".") mustEqual List(("dir/subdir", "commit4", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
|
||||
list("master", "dir/subdir") mustEqual List(("File3.md", "commit3", false), ("File4.md", "commit4", false))
|
||||
list("branch", ".") mustEqual List(("dir/subdir", "commit4", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
|
||||
list("branch", "dir/subdir") mustEqual List(("File3.md", "commit3", false), ("File4.md", "commit4", false))
|
||||
|
||||
createFile(git, "branch", "dir/subdir/File3.md", "body7", message = "commit7")
|
||||
|
||||
list("master", ".") mustEqual List(("dir/subdir", "commit4", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
|
||||
list("master", "dir/subdir") mustEqual List(("File3.md", "commit3", false), ("File4.md", "commit4", false))
|
||||
list("branch", ".") mustEqual List(("dir/subdir", "commit7", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
|
||||
list("branch", "dir/subdir") mustEqual List(("File3.md", "commit7", false), ("File4.md", "commit4", false))
|
||||
|
||||
createFile(git, "master", "dir8/File8.md", "body8", message = "commit8")
|
||||
|
||||
list("master", ".") mustEqual List(("dir/subdir", "commit4", true), ("dir8", "commit8", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
|
||||
list("master", "dir/subdir") mustEqual List(("File3.md", "commit3", false), ("File4.md", "commit4", false))
|
||||
list("branch", ".") mustEqual List(("dir/subdir", "commit7", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
|
||||
list("branch", "dir/subdir") mustEqual List(("File3.md", "commit7", false), ("File4.md", "commit4", false))
|
||||
|
||||
createFile(git, "branch", "dir/subdir9/File9.md", "body9", message = "commit9")
|
||||
|
||||
list("master", ".") mustEqual List(("dir/subdir", "commit4", true), ("dir8", "commit8", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
|
||||
list("master", "dir/subdir") mustEqual List(("File3.md", "commit3", false), ("File4.md", "commit4", false))
|
||||
list("branch", ".") mustEqual List(("dir", "commit9", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
|
||||
list("branch", "dir/subdir") mustEqual List(("File3.md", "commit7", false), ("File4.md", "commit4", false))
|
||||
|
||||
mergeAndCommit(git, "master", "branch", message = "merge10")
|
||||
|
||||
list("master", ".") mustEqual List(("dir", "commit9", true), ("dir8", "commit8", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
|
||||
list("master", "dir/subdir") mustEqual List(("File3.md", "commit7", false), ("File4.md", "commit4", false))
|
||||
}
|
||||
}
|
||||
"getFileList subfolder multi-origin (issue #721)" should {
|
||||
withTestRepository { git =>
|
||||
def list(branch: String, path: String) =
|
||||
JGitUtil.getFileList(git, branch, path).map(finfo => (finfo.name, finfo.message, finfo.isDirectory))
|
||||
createFile(git, "master", "README.md", "body1", message = "commit1")
|
||||
createFile(git, "branch", "test/text2.txt", "body2", message = "commit2")
|
||||
mergeAndCommit(git, "master", "branch", message = "merge3")
|
||||
list("master", "test") mustEqual List(("text2.txt", "commit2", false))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,7 @@ class AvatarImageProviderSpec extends Specification with Mockito {
|
||||
isCreateRepoOptionPublic = true,
|
||||
gravatar = useGravatar,
|
||||
notification = false,
|
||||
activityLogLimit = None,
|
||||
ssh = false,
|
||||
sshPort = None,
|
||||
smtp = None,
|
||||
|
||||
Reference in New Issue
Block a user