diff --git a/CHANGELOG.md b/CHANGELOG.md
index e83c5da..0811076 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,14 @@
# Changelog
All changes to the project will be documented in this file.
+### 4.25.0 - 29 May 2018
+- Security improvements
+- Show mail address at the profile page
+- Task list on commit comments
+- More detailed editing history of issues and pull requests
+- Expose user public keys
+- Download repository improvements
+
### 4.24.1 - 1 May 2018
- Fix bug in Web API authentication
diff --git a/README.md b/README.md
index 2a65ebc..36dd631 100644
--- a/README.md
+++ b/README.md
@@ -68,17 +68,14 @@
- If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles.
-What's New in 4.24.x
+What's New in 4.25.x
-------------
-### 4.24.1 - 1 May 2018
-- Fix bug in Web API authentication
-
-### 4.24.0 - 30 Apr 2018
-- Diff for each review comment on pull requests
-- Extra mail addresses support
-- Show tags at the commit list
-- Keep wrap mode of the online editor
-- Renew layout of gitbucket-gist-plugin
-- Web API of gitbucket-ci-plugin
+### 4.25.0 - 29 May 2018
+- Security improvements
+- Show mail address at the profile page
+- Task list on commit comments
+- More detailed editing history of issues and pull requests
+- Expose user public keys
+- Download repository improvements
See the [change log](CHANGELOG.md) for all of the updates.
diff --git a/build.sbt b/build.sbt
index 7517ca7..8f113a6 100644
--- a/build.sbt
+++ b/build.sbt
@@ -3,7 +3,7 @@
val Organization = "io.github.gitbucket"
val Name = "gitbucket"
-val GitBucketVersion = "4.24.1"
+val GitBucketVersion = "4.25.0"
val ScalatraVersion = "2.6.1"
val JettyVersion = "9.4.7.v20170914"
@@ -30,8 +30,8 @@
)
libraryDependencies ++= Seq(
- "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.11.0.201803080745-r",
- "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.11.0.201803080745-r",
+ "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "5.0.0.201805301535-rc2",
+ "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "5.0.0.201805301535-rc2",
"org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.scalatra" %% "scalatra-forms" % ScalatraVersion,
@@ -47,7 +47,7 @@
"com.github.takezoe" %% "blocking-slick-32" % "0.0.10",
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.196",
- "org.mariadb.jdbc" % "mariadb-java-client" % "2.2.3",
+ "org.mariadb.jdbc" % "mariadb-java-client" % "2.2.5",
"org.postgresql" % "postgresql" % "42.1.4",
"ch.qos.logback" % "logback-classic" % "1.2.3",
"com.zaxxer" % "HikariCP" % "2.7.4",
diff --git a/doc/comment_action.md b/doc/comment_action.md
index 55d154b..3adae83 100644
--- a/doc/comment_action.md
+++ b/doc/comment_action.md
@@ -6,22 +6,26 @@
To determine if it was any operation, you see the `ACTION` column.
And in the case of some actions, `CONTENT` column value contains additional information.
-|ACTION |CONTENT |
-|----------------|----------------------|
-|comment |comment |
-|close_comment |comment |
-|reopen_comment |comment |
-|close |"Close" |
-|reopen |"Reopen" |
-|commit |comment commitId |
-|merge |comment |
-|delete_branch |branchName |
-|refer |issueId:title |
-|add_label |labelName |
-|delete_label |labelName |
-|change_priority |oldPriority:priority |
-|change_milestone|oldMilestone:milestone|
-|assign |oldAssigned:assigned |
+|ACTION |CONTENT |
+|----------------|--------------------------|
+|comment |comment |
+|close_comment |comment |
+|reopen_comment |comment |
+|close |"Close" |
+|reopen |"Reopen" |
+|commit |comment commitId |
+|merge |comment |
+|delete_branch |branchName |
+|refer |issueId:title |
+|add_label |labelName |
+|delete_label |labelName |
+|change_priority |oldPriority:priority |
+|change_milestone|oldMilestone:milestone |
+|assign |oldAssigned:assigned |
+|change_title |oldTitle(CRLF)title \[1\] |
+
+\[1\]: (CRLF) is "\r\n"
+
### comment
@@ -79,3 +83,7 @@
### assign
This value is saved when users have assign issue/PR to user or remove the assign.
+
+### change_title
+
+This value is saved when users have changed the title.
diff --git a/plugins.json b/plugins.json
index c34cb56..3682660 100644
--- a/plugins.json
+++ b/plugins.json
@@ -31,9 +31,9 @@
"description": "Provides Gist feature on GitBucket.",
"versions": [
{
- "version": "4.14.0",
- "range": ">=4.23.0",
- "url": "https://github.com/gitbucket/gitbucket-gist-plugin/releases/download/4.14.0/gitbucket-gist-plugin-assembly-4.14.0.jar"
+ "version": "4.15.0",
+ "range": ">=4.25.0",
+ "url": "https://github.com/gitbucket/gitbucket-gist-plugin/releases/download/4.15.0/gitbucket-gist-plugin-gitbucket_4.25.0-4.15.0.jar"
}
],
"default": false
diff --git a/project/build.properties b/project/build.properties
index 7c81737..d6e3507 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version=1.1.5
+sbt.version=1.1.6
diff --git a/src/main/resources/update/gitbucket-core_4.25.xml b/src/main/resources/update/gitbucket-core_4.25.xml
new file mode 100644
index 0000000..8f78bd2
--- /dev/null
+++ b/src/main/resources/update/gitbucket-core_4.25.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+ EXTRA_MAIL_ADDRESS = ''
+
+
diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala
index 24885e4..33b19f4 100644
--- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala
+++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala
@@ -53,5 +53,6 @@
new Version("4.23.0", new LiquibaseMigration("update/gitbucket-core_4.23.xml")),
new Version("4.23.1"),
new Version("4.24.0", new LiquibaseMigration("update/gitbucket-core_4.24.xml")),
- new Version("4.24.1")
+ new Version("4.24.1"),
+ new Version("4.25.0", new LiquibaseMigration("update/gitbucket-core_4.25.xml"))
)
diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala
index e50b583..7663ba9 100644
--- a/src/main/scala/gitbucket/core/controller/AccountController.scala
+++ b/src/main/scala/gitbucket/core/controller/AccountController.scala
@@ -229,13 +229,15 @@
get("/:userName") {
val userName = params("userName")
getAccountByUserName(userName).map { account =>
+ val extraMailAddresses = getAccountExtraMailAddresses(userName)
params.getOrElse("tab", "repositories") match {
// Public Activity
case "activity" =>
gitbucket.core.account.html.activity(
account,
if (account.isGroupAccount) Nil else getGroupsByUserName(userName),
- getActivitiesByUser(userName, true)
+ getActivitiesByUser(userName, true),
+ extraMailAddresses
)
// Members
@@ -244,6 +246,7 @@
gitbucket.core.account.html.members(
account,
members,
+ extraMailAddresses,
context.loginAccount.exists(
x =>
members.exists { member =>
@@ -260,6 +263,7 @@
account,
if (account.isGroupAccount) Nil else getGroupsByUserName(userName),
getVisibleRepositories(context.loginAccount, Some(userName)),
+ extraMailAddresses,
context.loginAccount.exists(
x =>
members.exists { member =>
@@ -278,6 +282,12 @@
helper.xml.feed(getActivitiesByUser(userName, true))
}
+ get("/:userName.keys") {
+ val keys = getPublicKeys(params("userName"))
+ contentType = "text/plain; charset=utf-8"
+ keys.map(_.publicKey).mkString("", "\n", "\n")
+ }
+
get("/:userName/_avatar") {
val userName = params("userName")
contentType = "image/png"
@@ -318,7 +328,7 @@
account =>
updateAccount(
account.copy(
- password = form.password.map(sha1).getOrElse(account.password),
+ password = form.password.map(pbkdf2_sha256).getOrElse(account.password),
fullName = form.fullName,
mailAddress = form.mailAddress,
description = form.description,
@@ -559,7 +569,7 @@
if (context.settings.allowAccountRegistration) {
createAccount(
form.userName,
- sha1(form.password),
+ pbkdf2_sha256(form.password),
form.fullName,
form.mailAddress,
false,
@@ -567,7 +577,7 @@
form.url
)
updateImage(form.userName, form.fileId, false)
- updateAccountExtraMailAddresses(form.userName, form.extraMailAddresses)
+ updateAccountExtraMailAddresses(form.userName, form.extraMailAddresses.filter(_ != ""))
redirect("/signin")
} else NotFound()
}
@@ -703,7 +713,7 @@
}
helper.html.forkrepository(
repository,
- (groups zip managerPermissions).toMap
+ (groups zip managerPermissions).sortBy(_._1)
)
case _ => redirect(s"/${loginUserName}")
}
diff --git a/src/main/scala/gitbucket/core/controller/ApiController.scala b/src/main/scala/gitbucket/core/controller/ApiController.scala
index 6b8c4ad..fda5fdc 100644
--- a/src/main/scala/gitbucket/core/controller/ApiController.scala
+++ b/src/main/scala/gitbucket/core/controller/ApiController.scala
@@ -11,6 +11,8 @@
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util._
import gitbucket.core.plugin.PluginRegistry
+import gitbucket.core.servlet.Database
+import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.view.helpers.{isRenderable, renderMarkup}
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk
@@ -315,7 +317,10 @@
data.auto_init
)
Await.result(f, Duration.Inf)
- val repository = getRepository(owner, data.name).get
+
+ val repository = Database() withTransaction { session =>
+ getRepository(owner, data.name)(session).get
+ }
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
} else {
ApiError(
@@ -806,7 +811,7 @@
ApiCommits(
repositoryName = RepositoryName(repository),
commitInfo = commitInfo,
- diffs = JGitUtil.getDiffs(git, Some(commitInfo.parents.head), commitInfo.id, false, true),
+ diffs = JGitUtil.getDiffs(git, commitInfo.parents.headOption, commitInfo.id, false, true),
author = getAccount(commitInfo.authorName, commitInfo.authorEmailAddress),
committer = getAccount(commitInfo.committerName, commitInfo.committerEmailAddress),
commentCount = getCommitComment(repository.owner, repository.name, sha).size
diff --git a/src/main/scala/gitbucket/core/controller/ControllerBase.scala b/src/main/scala/gitbucket/core/controller/ControllerBase.scala
index d1b034e..107f4f1 100644
--- a/src/main/scala/gitbucket/core/controller/ControllerBase.scala
+++ b/src/main/scala/gitbucket/core/controller/ControllerBase.scala
@@ -254,7 +254,7 @@
repository: RepositoryService.RepositoryInfo
): Unit = {
JGitUtil.getObjectLoaderFromId(git, objectId) { loader =>
- contentType = FileUtil.getMimeType(path)
+ contentType = FileUtil.getSafeMimeType(path)
if (loader.isLarge) {
response.setContentLength(loader.getSize.toInt)
diff --git a/src/main/scala/gitbucket/core/controller/DashboardController.scala b/src/main/scala/gitbucket/core/controller/DashboardController.scala
index 2c8a982..ce0de3a 100644
--- a/src/main/scala/gitbucket/core/controller/DashboardController.scala
+++ b/src/main/scala/gitbucket/core/controller/DashboardController.scala
@@ -21,6 +21,16 @@
trait DashboardControllerBase extends ControllerBase {
self: IssuesService with PullRequestService with RepositoryService with AccountService with UsersAuthenticator =>
+ get("/dashboard/repos")(usersOnly {
+ val userName = context.loginAccount.get.userName
+
+ html.repos(
+ getGroupNames(userName),
+ getVisibleRepositories(None, withoutPhysicalInfo = true),
+ getUserRepositories(userName, withoutPhysicalInfo = true)
+ )
+ })
+
get("/dashboard/issues")(usersOnly {
searchIssues("created_by")
})
@@ -83,8 +93,7 @@
},
filter,
getGroupNames(userName),
- Nil,
- getUserRepositories(userName, withoutPhysicalInfo = true)
+ getVisibleRepositories(None, withoutPhysicalInfo = true)
)
}
@@ -109,8 +118,7 @@
},
filter,
getGroupNames(userName),
- Nil,
- getUserRepositories(userName, withoutPhysicalInfo = true)
+ getVisibleRepositories(None, withoutPhysicalInfo = true)
)
}
diff --git a/src/main/scala/gitbucket/core/controller/IndexController.scala b/src/main/scala/gitbucket/core/controller/IndexController.scala
index 528aebf..d7b3f5a 100644
--- a/src/main/scala/gitbucket/core/controller/IndexController.scala
+++ b/src/main/scala/gitbucket/core/controller/IndexController.scala
@@ -9,7 +9,7 @@
import gitbucket.core.service._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._
-import gitbucket.core.util.{Keys, LDAPUtil, ReferrerAuthenticator, UsersAuthenticator}
+import gitbucket.core.util._
import org.scalatra.Ok
import org.scalatra.forms._
@@ -64,8 +64,7 @@
val visibleOwnerSet: Set[String] = Set(account.userName) ++ getGroupsByUserName(account.userName)
gitbucket.core.html.index(
getRecentActivitiesByOwners(visibleOwnerSet),
- Nil,
- getUserRepositories(account.userName, withoutPhysicalInfo = true),
+ getVisibleRepositories(None, withoutPhysicalInfo = true),
showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken(
account.userName
)
@@ -75,7 +74,6 @@
gitbucket.core.html.index(
getRecentActivities(),
getVisibleRepositories(None, withoutPhysicalInfo = true),
- Nil,
showBannerToCreatePersonalAccessToken = false
)
}
@@ -208,7 +206,7 @@
}
.map { t =>
Map(
- "label" -> s"@${t.userName} ${t.fullName}",
+ "label" -> s"@${StringUtil.escapeHtml(t.userName)} ${StringUtil.escapeHtml(t.fullName)}",
"value" -> t.userName
)
}
@@ -273,18 +271,7 @@
val repositories = visibleRepositories.filter { repository =>
repository.name.toLowerCase.indexOf(query) >= 0 || repository.owner.toLowerCase.indexOf(query) >= 0
}
- context.loginAccount
- .map { account =>
- gitbucket.core.search.html.repositories(
- query,
- repositories,
- Nil,
- getUserRepositories(account.userName, withoutPhysicalInfo = true)
- )
- }
- .getOrElse {
- gitbucket.core.search.html.repositories(query, repositories, visibleRepositories, Nil)
- }
+ gitbucket.core.search.html.repositories(query, repositories, visibleRepositories)
}
}
diff --git a/src/main/scala/gitbucket/core/controller/IssuesController.scala b/src/main/scala/gitbucket/core/controller/IssuesController.scala
index 8fb441a..90676e6 100644
--- a/src/main/scala/gitbucket/core/controller/IssuesController.scala
+++ b/src/main/scala/gitbucket/core/controller/IssuesController.scala
@@ -154,15 +154,25 @@
ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) =>
defining(repository.owner, repository.name) {
case (owner, name) =>
- getIssue(owner, name, params("id")).map { issue =>
- if (isEditableContent(owner, name, issue.openedUserName)) {
- // update issue
- updateIssue(owner, name, issue.issueId, title, issue.content)
- // extract references and create refer comment
- createReferComment(owner, name, issue.copy(title = title), title, context.loginAccount.get)
-
- redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
- } else Unauthorized()
+ getIssue(owner, name, params("id")).map {
+ issue =>
+ if (isEditableContent(owner, name, issue.openedUserName)) {
+ if (issue.title != title) {
+ // update issue
+ updateIssue(owner, name, issue.issueId, title, issue.content)
+ // extract references and create refer comment
+ createReferComment(owner, name, issue.copy(title = title), title, context.loginAccount.get)
+ createComment(
+ owner,
+ name,
+ context.loginAccount.get.userName,
+ issue.issueId,
+ issue.title + "\r\n" + title,
+ "change_title"
+ )
+ }
+ redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
+ } else Unauthorized()
} getOrElse NotFound()
}
})
@@ -396,7 +406,7 @@
case dir if (dir.exists && dir.isDirectory) =>
dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
response.setHeader("Content-Disposition", f"""inline; filename=${file.getName}""")
- RawData(FileUtil.getMimeType(file.getName), file)
+ RawData(FileUtil.getSafeMimeType(file.getName), file)
}
case _ => None
}) getOrElse NotFound()
diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala
index 3cbd887..72bedbd 100644
--- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala
+++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala
@@ -113,7 +113,7 @@
val name = repository.name
getPullRequest(owner, name, issueId) map {
case (issue, pullreq) =>
- val (commits, _) =
+ val (commits, diffs) =
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
html.conversation(
@@ -121,6 +121,7 @@
pullreq,
commits.flatten,
getPullRequestComments(owner, name, issue.issueId, commits.flatten),
+ diffs.size,
getIssueLabels(owner, name, issueId),
getAssignableUserNames(owner, name),
getMilestonesWithIssueCount(owner, name),
@@ -157,23 +158,25 @@
})
get("/:owner/:repository/pull/:id/commits")(referrersOnly { repository =>
- params("id").toIntOpt.flatMap { issueId =>
- val owner = repository.owner
- val name = repository.name
- getPullRequest(owner, name, issueId) map {
- case (issue, pullreq) =>
- val (commits, _) =
- getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
+ params("id").toIntOpt.flatMap {
+ issueId =>
+ val owner = repository.owner
+ val name = repository.name
+ getPullRequest(owner, name, issueId) map {
+ case (issue, pullreq) =>
+ val (commits, diffs) =
+ getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
- html.commits(
- issue,
- pullreq,
- commits,
- getPullRequestComments(owner, name, issue.issueId, commits.flatten),
- isManageable(repository),
- repository
- )
- }
+ html.commits(
+ issue,
+ pullreq,
+ commits,
+ getPullRequestComments(owner, name, issue.issueId, commits.flatten),
+ diffs.size,
+ isManageable(repository),
+ repository
+ )
+ }
} getOrElse NotFound()
})
@@ -350,7 +353,16 @@
// close issue by commit message
if (pullreq.requestBranch == repository.repository.defaultBranch) {
commits.map { commit =>
- closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
+ closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name).foreach {
+ issueId =>
+ getIssue(repository.owner, repository.name, issueId.toString).map { issue =>
+ callIssuesWebHook("closed", repository, issue, baseUrl, loginAccount)
+ PluginRegistry().getIssueHooks
+ .foreach(
+ _.closedByCommitComment(issue, repository, commit.fullMessage, loginAccount)
+ )
+ }
+ }
}
}
@@ -455,15 +467,35 @@
val defaultBranch = getRepository(owner, name).get.repository.defaultBranch
if (pullreq.branch == defaultBranch) {
commits.flatten.foreach { commit =>
- closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
+ closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name).foreach {
+ issueId =>
+ getIssue(owner, name, issueId.toString).map { issue =>
+ callIssuesWebHook("closed", repository, issue, baseUrl, loginAccount)
+ PluginRegistry().getIssueHooks
+ .foreach(_.closedByCommitComment(issue, repository, commit.fullMessage, loginAccount))
+ }
+ }
}
+ val issueContent = issue.title + " " + issue.content.getOrElse("")
closeIssuesFromMessage(
- issue.title + " " + issue.content.getOrElse(""),
+ issueContent,
loginAccount.userName,
owner,
name
- )
- closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
+ ).foreach { issueId =>
+ getIssue(owner, name, issueId.toString).map { issue =>
+ callIssuesWebHook("closed", repository, issue, baseUrl, loginAccount)
+ PluginRegistry().getIssueHooks
+ .foreach(_.closedByCommitComment(issue, repository, issueContent, loginAccount))
+ }
+ }
+ closeIssuesFromMessage(form.message, loginAccount.userName, owner, name).foreach { issueId =>
+ getIssue(owner, name, issueId.toString).map { issue =>
+ callIssuesWebHook("closed", repository, issue, baseUrl, loginAccount)
+ PluginRegistry().getIssueHooks
+ .foreach(_.closedByCommitComment(issue, repository, issueContent, loginAccount))
+ }
+ }
}
updatePullRequests(owner, name, pullreq.branch)
@@ -749,6 +781,10 @@
})
ajaxGet("/:owner/:repository/pulls/proposals")(readableUsersOnly { repository =>
+ val thresholdTime = System.currentTimeMillis() - (1000 * 60 * 60)
+ val mailAddresses =
+ context.loginAccount.map(x => Seq(x.mailAddress) ++ getAccountExtraMailAddresses(x.userName)).getOrElse(Nil)
+
val branches = JGitUtil
.getBranches(
owner = repository.owner,
@@ -756,8 +792,14 @@
defaultBranch = repository.repository.defaultBranch,
origin = repository.repository.originUserName.isEmpty
)
- .filter(x => x.mergeInfo.map(_.ahead).getOrElse(0) > 0 && x.mergeInfo.map(_.behind).getOrElse(0) == 0)
- .sortBy(br => (br.mergeInfo.isEmpty, br.commitTime))
+ .filter { x =>
+ x.mergeInfo.map(_.ahead).getOrElse(0) > 0 && x.mergeInfo.map(_.behind).getOrElse(0) == 0 &&
+ x.commitTime.getTime > thresholdTime &&
+ mailAddresses.contains(x.committerEmailAddress)
+ }
+ .sortBy { br =>
+ (br.mergeInfo.isEmpty, br.commitTime)
+ }
.map(_.name)
.reverse
diff --git a/src/main/scala/gitbucket/core/controller/ReleasesController.scala b/src/main/scala/gitbucket/core/controller/ReleasesController.scala
index 2b37a49..ed44860 100644
--- a/src/main/scala/gitbucket/core/controller/ReleasesController.scala
+++ b/src/main/scala/gitbucket/core/controller/ReleasesController.scala
@@ -79,7 +79,7 @@
} yield {
response.setHeader("Content-Disposition", s"attachment; filename=${asset.label}")
RawData(
- FileUtil.getMimeType(asset.label),
+ FileUtil.getSafeMimeType(asset.label),
new File(getReleaseFilesDir(repository.owner, repository.name), FileUtil.checkFilename(tagName + "/" + fileId))
)
}).getOrElse(NotFound())
diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala
index f4660ae..ce03dc8 100644
--- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala
+++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala
@@ -1,8 +1,8 @@
package gitbucket.core.controller
import java.io.File
-import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
+import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.repo.html
import gitbucket.core.helper
@@ -17,6 +17,13 @@
import gitbucket.core.service.WebHookService._
import gitbucket.core.view
import gitbucket.core.view.helpers
+import org.apache.commons.compress.archivers.{ArchiveEntry, ArchiveOutputStream}
+import org.apache.commons.compress.archivers.tar.{TarArchiveEntry, TarArchiveOutputStream}
+import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream}
+import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream
+import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream
+import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream
+import org.apache.commons.compress.utils.IOUtils
import org.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.{ArchiveCommand, Git}
@@ -25,6 +32,8 @@
import org.eclipse.jgit.errors.MissingObjectException
import org.eclipse.jgit.lib._
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
+import org.eclipse.jgit.treewalk.TreeWalk
+import org.eclipse.jgit.treewalk.filter.PathFilter
import org.json4s.jackson.Serialization
import org.scalatra._
import org.scalatra.i18n.Messages
@@ -619,7 +628,8 @@
newLineNumber,
issueId,
hasWritePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
- repository = repository
+ repository = repository,
+ focus = true
)
})
@@ -705,6 +715,7 @@
enableRefsLink = true,
enableAnchor = true,
enableLineBreaks = true,
+ enableTaskList = true,
hasWritePermission = true
)
)
@@ -812,16 +823,54 @@
})
/**
- * Download repository contents as an archive.
+ * Download repository contents as a zip archive as compatible URL.
*/
- get("/:owner/:repository/archive/*")(referrersOnly { repository =>
- multiParams("splat").head match {
- case name if name.endsWith(".zip") =>
- archiveRepository(name, ".zip", repository)
- case name if name.endsWith(".tar.gz") =>
- archiveRepository(name, ".tar.gz", repository)
- case _ => BadRequest()
- }
+ get("/:owner/:repository/archive/:branch.zip")(referrersOnly { repository =>
+ val branch = params("branch")
+ archiveRepository(branch, branch + ".zip", repository, "")
+ })
+
+ /**
+ * Download repository contents as a tar.gz archive as compatible URL.
+ */
+ get("/:owner/:repository/archive/:branch.tar.gz")(referrersOnly { repository =>
+ val branch = params("branch")
+ archiveRepository(branch, branch + ".tar.gz", repository, "")
+ })
+
+ /**
+ * Download repository contents as a tar.bz2 archive as compatible URL.
+ */
+ get("/:owner/:repository/archive/:branch.tar.bz2")(referrersOnly { repository =>
+ val branch = params("branch")
+ archiveRepository(branch, branch + ".tar.bz2", repository, "")
+ })
+
+ /**
+ * Download repository contents as a tar.xz archive as compatible URL.
+ */
+ get("/:owner/:repository/archive/:branch.tar.xz")(referrersOnly { repository =>
+ val branch = params("branch")
+ archiveRepository(branch, branch + ".tar.xz", repository, "")
+ })
+
+ /**
+ * Download all repository contents as an archive.
+ */
+ get("/:owner/:repository/archive/:branch/:name")(referrersOnly { repository =>
+ val branch = params("branch")
+ val name = params("name")
+ archiveRepository(branch, name, repository, "")
+ })
+
+ /**
+ * Download repositories subtree contents as an archive.
+ */
+ get("/:owner/:repository/archive/:branch/*/:name")(referrersOnly { repository =>
+ val branch = params("branch")
+ val name = params("name")
+ val path = multiParams("splat").head
+ archiveRepository(branch, name, repository, path)
})
get("/:owner/:repository/network/members")(referrersOnly { repository =>
@@ -1015,7 +1064,14 @@
// close issue by commit message
if (branch == repository.repository.defaultBranch) {
- closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name)
+ closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name).foreach {
+ issueId =>
+ getIssue(repository.owner, repository.name, issueId.toString).map { issue =>
+ callIssuesWebHook("closed", repository, issue, baseUrl, loginAccount)
+ PluginRegistry().getIssueHooks
+ .foreach(_.closedByCommitComment(issue, repository, message, loginAccount))
+ }
+ }
}
// call post commit hook
@@ -1109,26 +1165,97 @@
}
}
- private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): Unit = {
- val revision = name.stripSuffix(suffix)
+ private def archiveRepository(
+ revision: String,
+ filename: String,
+ repository: RepositoryService.RepositoryInfo,
+ path: String
+ ) = {
+ def archive(archiveFormat: String, archive: ArchiveOutputStream)(
+ entryCreator: (String, Long, Int) => ArchiveEntry
+ ): Unit = {
+ using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
+ val oid = git.getRepository.resolve(revision)
+ val revCommit = JGitUtil.getRevCommitFromId(git, oid)
+ val sha1 = oid.getName()
+ val repositorySuffix = (if (sha1.startsWith(revision)) sha1 else revision).replace('/', '-')
+ val pathSuffix = if (path.isEmpty) "" else '-' + path.replace('/', '-')
+ val baseName = repository.name + "-" + repositorySuffix + pathSuffix
- using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
- val oid = git.getRepository.resolve(revision)
- val revCommit = JGitUtil.getRevCommitFromId(git, oid)
- val sha1 = oid.getName()
- val repositorySuffix = (if (sha1.startsWith(revision)) sha1 else revision).replace('/', '-')
- val filename = repository.name + "-" + repositorySuffix + suffix
+ using(new TreeWalk(git.getRepository)) { treeWalk =>
+ treeWalk.addTree(revCommit.getTree)
+ treeWalk.setRecursive(true)
+ if (!path.isEmpty) {
+ treeWalk.setFilter(PathFilter.create(path))
+ }
+ if (treeWalk != null) {
+ while (treeWalk.next()) {
+ val entryPath =
+ if (path.isEmpty) baseName + "/" + treeWalk.getPathString
+ else path.split("/").last + treeWalk.getPathString.substring(path.length)
+ val size = JGitUtil.getFileSize(git, repository, treeWalk)
+ val mode = treeWalk.getFileMode.getBits
+ val entry: ArchiveEntry = entryCreator(entryPath, size, mode)
+ JGitUtil.openFile(git, repository, revCommit.getTree, treeWalk.getPathString) { in =>
+ archive.putArchiveEntry(entry)
+ IOUtils.copy(in, archive)
+ archive.closeArchiveEntry()
+ }
+ }
+ }
+ }
+ }
+ }
- contentType = "application/octet-stream"
- response.setHeader("Content-Disposition", s"attachment; filename=${filename}")
- response.setBufferSize(1024 * 1024);
+ val suffix =
+ path.split("/").lastOption.collect { case x if x.length > 0 => "-" + x.replace('/', '_') }.getOrElse("")
+ val zipRe = """(.+)\.zip$""".r
+ val tarRe = """(.+)\.tar\.(gz|bz2|xz)$""".r
- git.archive
- .setFormat(suffix.tail)
- .setPrefix(repository.name + "-" + repositorySuffix + "/")
- .setTree(revCommit)
- .setOutputStream(response.getOutputStream)
- .call()
+ filename match {
+ case zipRe(branch) =>
+ response.setHeader(
+ "Content-Disposition",
+ s"attachment; filename=${repository.name}-${branch}${suffix}.zip"
+ )
+ contentType = "application/octet-stream"
+ response.setBufferSize(1024 * 1024);
+ using(new ZipArchiveOutputStream(response.getOutputStream)) { zip =>
+ archive(".zip", zip) { (path, size, mode) =>
+ val entry = new ZipArchiveEntry(path)
+ entry.setSize(size)
+ entry.setUnixMode(mode)
+ entry
+ }
+ }
+ ()
+ case tarRe(branch, compressor) =>
+ response.setHeader(
+ "Content-Disposition",
+ s"attachment; filename=${repository.name}-${branch}${suffix}.tar.${compressor}"
+ )
+ contentType = "application/octet-stream"
+ response.setBufferSize(1024 * 1024)
+ using(compressor match {
+ case "gz" => new GzipCompressorOutputStream(response.getOutputStream)
+ case "bz2" => new BZip2CompressorOutputStream(response.getOutputStream)
+ case "xz" => new XZCompressorOutputStream(response.getOutputStream)
+ }) { compressorOutputStream =>
+ using(new TarArchiveOutputStream(compressorOutputStream)) { tar =>
+ tar.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR)
+ tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU)
+ tar.setAddPaxHeadersForNonAsciiNames(true)
+ archive(".tar.gz", tar) { (path, size, mode) =>
+ val entry = new TarArchiveEntry(path)
+ entry.setSize(size)
+ entry.setMode(mode)
+ entry
+ }
+ }
+ }
+ ()
+ case _ =>
+ BadRequest()
}
}
diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala
index acd30d4..69f0223 100644
--- a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala
+++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala
@@ -90,7 +90,8 @@
"jwsAlgorithm" -> trim(label("Signature algorithm", optional(text())))
)(OIDC.apply)
),
- "skinName" -> trim(label("AdminLTE skin name", text(required)))
+ "skinName" -> trim(label("AdminLTE skin name", text(required))),
+ "showMailAddress" -> trim(label("Show mail address", boolean()))
)(SystemSettings.apply).verifying { settings =>
Vector(
if (settings.ssh && settings.baseUrl.isEmpty) {
@@ -416,7 +417,7 @@
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
createAccount(
form.userName,
- sha1(form.password),
+ pbkdf2_sha256(form.password),
form.fullName,
form.mailAddress,
form.isAdmin,
@@ -456,7 +457,7 @@
updateAccount(
account.copy(
- password = form.password.map(sha1).getOrElse(account.password),
+ password = form.password.map(pbkdf2_sha256).getOrElse(account.password),
fullName = form.fullName,
mailAddress = form.mailAddress,
isAdmin = form.isAdmin,
diff --git a/src/main/scala/gitbucket/core/plugin/IssueHook.scala b/src/main/scala/gitbucket/core/plugin/IssueHook.scala
index 12d050a..1b22971 100644
--- a/src/main/scala/gitbucket/core/plugin/IssueHook.scala
+++ b/src/main/scala/gitbucket/core/plugin/IssueHook.scala
@@ -1,7 +1,7 @@
package gitbucket.core.plugin
import gitbucket.core.controller.Context
-import gitbucket.core.model.Issue
+import gitbucket.core.model.{Account, Issue}
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.model.Profile._
import profile.api._
@@ -15,6 +15,19 @@
): Unit = ()
def closed(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
def reopened(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
+ def assigned(
+ issue: Issue,
+ repository: RepositoryInfo,
+ assigner: Option[String],
+ assigned: Option[String],
+ oldAssigned: Option[String]
+ )(
+ implicit session: Session,
+ context: Context
+ ): Unit = ()
+ def closedByCommitComment(issue: Issue, repository: RepositoryInfo, message: String, pusher: Account)(
+ implicit session: Session
+ ): Unit = ()
}
diff --git a/src/main/scala/gitbucket/core/service/AccessTokenService.scala b/src/main/scala/gitbucket/core/service/AccessTokenService.scala
index 102240d..8976ce2 100644
--- a/src/main/scala/gitbucket/core/service/AccessTokenService.scala
+++ b/src/main/scala/gitbucket/core/service/AccessTokenService.scala
@@ -5,13 +5,13 @@
import gitbucket.core.model.{AccessToken, Account}
import gitbucket.core.util.StringUtil
-import scala.util.Random
+import java.security.SecureRandom
trait AccessTokenService {
def makeAccessTokenString: String = {
val bytes = new Array[Byte](20)
- Random.nextBytes(bytes)
+ AccessTokenService.secureRandom.nextBytes(bytes)
bytes.map("%02x".format(_)).mkString
}
@@ -55,4 +55,6 @@
}
-object AccessTokenService extends AccessTokenService
+object AccessTokenService extends AccessTokenService {
+ private val secureRandom = new SecureRandom()
+}
diff --git a/src/main/scala/gitbucket/core/service/AccountService.scala b/src/main/scala/gitbucket/core/service/AccountService.scala
index 8f0ba63..ea851b1 100644
--- a/src/main/scala/gitbucket/core/service/AccountService.scala
+++ b/src/main/scala/gitbucket/core/service/AccountService.scala
@@ -33,7 +33,16 @@
* Authenticate by internal database.
*/
private def defaultAuthentication(userName: String, password: String)(implicit s: Session) = {
+ val pbkdf2re = """^\$pbkdf2-sha256\$(\d+)\$([0-9a-zA-Z+/=]+)\$([0-9a-zA-Z+/=]+)$""".r
getAccountByUserName(userName).collect {
+ case account if !account.isGroupAccount =>
+ account.password match {
+ case pbkdf2re(iter, salt, hash) if (pbkdf2_sha256(iter.toInt, salt, password) == hash) => Some(account)
+ case p if p == sha1(password) =>
+ updateAccount(account.copy(password = pbkdf2_sha256(password)))
+ Some(account)
+ case _ => None
+ }
case account if (!account.isGroupAccount && account.password == sha1(password)) => Some(account)
} getOrElse None
}
diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala
index c9854ea..05255c4 100644
--- a/src/main/scala/gitbucket/core/service/IssuesService.scala
+++ b/src/main/scala/gitbucket/core/service/IssuesService.scala
@@ -6,20 +6,21 @@
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.controller.Context
import gitbucket.core.model.{
+ Account,
+ CommitState,
Issue,
- PullRequest,
IssueComment,
IssueLabel,
Label,
- Account,
+ PullRequest,
Repository,
- CommitState,
Role
}
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile._
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.model.Profile.dateColumnType
+import gitbucket.core.plugin.PluginRegistry
trait IssuesService {
self: AccountService with RepositoryService with LabelsService with PrioritiesService with MilestonesService =>
@@ -511,20 +512,24 @@
assignedUserName: Option[String],
insertComment: Boolean = false
)(implicit context: Context, s: Session): Int = {
+ val oldAssigned = getIssue(owner, repository, s"${issueId}").get.assignedUserName
+ val assigned = assignedUserName
+ val assigner = context.loginAccount.map(_.userName)
if (insertComment) {
- val oldAssigned = getIssue(owner, repository, s"${issueId}").get.assignedUserName.getOrElse("Not assigned")
- val assigned = assignedUserName.getOrElse("Not assigned")
IssueComments insert IssueComment(
userName = owner,
repositoryName = repository,
issueId = issueId,
action = "assign",
- commentedUserName = context.loginAccount.map(_.userName).getOrElse("Unknown user"),
- content = s"${oldAssigned}:${assigned}",
+ commentedUserName = assigner.getOrElse("Unknown user"),
+ content = s"""${oldAssigned.getOrElse("Not assigned")}:${assigned.getOrElse("Not assigned")}""",
registeredDate = currentDate,
updatedDate = currentDate
)
}
+ for (issue <- getIssue(owner, repository, issueId.toString); repo <- getRepository(owner, repository)) {
+ PluginRegistry().getIssueHooks.foreach(_.assigned(issue, repo, assigner, assigned, oldAssigned))
+ }
Issues
.filter(_.byPrimaryKey(owner, repository, issueId))
.map(t => (t.assignedUserName ?, t.updatedDate))
diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala
index f4ab4be..70e2387 100644
--- a/src/main/scala/gitbucket/core/service/RepositoryService.scala
+++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala
@@ -338,7 +338,7 @@
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
),
- getRepositoryManagers(repository.userName)
+ getRepositoryManagers(repository.userName, repository.repositoryName)
)
}
}
@@ -407,7 +407,7 @@
if (withoutPhysicalInfo) {
Nil
} else {
- getRepositoryManagers(repository.userName)
+ getRepositoryManagers(repository.userName, repository.repositoryName)
}
)
}
@@ -485,18 +485,22 @@
if (withoutPhysicalInfo) {
Nil
} else {
- getRepositoryManagers(repository.userName)
+ getRepositoryManagers(repository.userName, repository.repositoryName)
}
)
}
}
- private def getRepositoryManagers(userName: String)(implicit s: Session): Seq[String] =
- if (getAccountByUserName(userName).exists(_.isGroupAccount)) {
- getGroupMembers(userName).collect { case x if (x.isManager) => x.userName }
- } else {
- Seq(userName)
- }
+ /**
+ * TODO It seems to be able to improve performance. For example, RequestCache can be used for getAccountByUserName call.
+ */
+ private def getRepositoryManagers(userName: String, repositoryName: String)(implicit s: Session): Seq[String] = {
+ (if (getAccountByUserName(userName).exists(_.isGroupAccount)) {
+ getGroupMembers(userName).collect { case x if (x.isManager) => x.userName }
+ } else {
+ Seq(userName)
+ }) ++ getCollaboratorUserNames(userName, repositoryName, Seq(Role.ADMIN))
+ }
/**
* Updates the last activity date of the repository.
diff --git a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala
index 4141822..7ac861e 100644
--- a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala
+++ b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala
@@ -68,6 +68,7 @@
}
}
props.setProperty(SkinName, settings.skinName.toString)
+ props.setProperty(ShowMailAddress, settings.showMailAddress.toString)
using(new java.io.FileOutputStream(GitBucketConf)) { out =>
props.store(out, null)
}
@@ -144,7 +145,8 @@
} else {
None
},
- getValue(props, SkinName, "skin-blue")
+ getValue(props, SkinName, "skin-blue"),
+ getValue(props, ShowMailAddress, false)
)
}
}
@@ -174,7 +176,8 @@
ldap: Option[Ldap],
oidcAuthentication: Boolean,
oidc: Option[OIDC],
- skinName: String
+ skinName: String,
+ showMailAddress: Boolean
) {
def baseUrl(request: HttpServletRequest): String =
@@ -283,6 +286,7 @@
private val OidcClientSecret = "oidc.client_secret"
private val OidcJwsAlgorithm = "oidc.jws_algorithm"
private val SkinName = "skinName"
+ private val ShowMailAddress = "showMailAddress"
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = {
getSystemProperty(key).getOrElse(getEnvironmentVariable(key).getOrElse {
diff --git a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala
index cfc5f3b..bbfc469 100644
--- a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala
+++ b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala
@@ -300,6 +300,8 @@
closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository).foreach { issueId =>
getIssue(owner, repository, issueId.toString).map { issue =>
callIssuesWebHook("closed", repositoryInfo, issue, baseUrl, pusherAccount)
+ PluginRegistry().getIssueHooks
+ .foreach(_.closedByCommitComment(issue, repositoryInfo, commit.fullMessage, pusherAccount))
}
}
}
diff --git a/src/main/scala/gitbucket/core/servlet/PluginAssetsServlet.scala b/src/main/scala/gitbucket/core/servlet/PluginAssetsServlet.scala
index 016d2ea..3e891af 100644
--- a/src/main/scala/gitbucket/core/servlet/PluginAssetsServlet.scala
+++ b/src/main/scala/gitbucket/core/servlet/PluginAssetsServlet.scala
@@ -26,7 +26,7 @@
try {
val bytes = IOUtils.toByteArray(in)
resp.setContentLength(bytes.length)
- resp.setContentType(FileUtil.getContentType(path, bytes))
+ resp.setContentType(FileUtil.getMimeType(path, bytes))
resp.setHeader("Cache-Control", "max-age=3600")
resp.getOutputStream.write(bytes)
} finally {
diff --git a/src/main/scala/gitbucket/core/util/FileUtil.scala b/src/main/scala/gitbucket/core/util/FileUtil.scala
index 0a668fe..ab42edd 100644
--- a/src/main/scala/gitbucket/core/util/FileUtil.scala
+++ b/src/main/scala/gitbucket/core/util/FileUtil.scala
@@ -16,7 +16,7 @@
}
}
- def getContentType(name: String, bytes: Array[Byte]): String = {
+ def getMimeType(name: String, bytes: Array[Byte]): String = {
defining(getMimeType(name)) { mimeType =>
if (mimeType == "application/octet-stream" && isText(bytes)) {
"text/plain"
@@ -26,6 +26,10 @@
}
}
+ def getSafeMimeType(name: String): String = {
+ getMimeType(name).replace("text/html", "text/plain")
+ }
+
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
def isLarge(size: Long): Boolean = (size > 1024 * 1000)
diff --git a/src/main/scala/gitbucket/core/util/JGitUtil.scala b/src/main/scala/gitbucket/core/util/JGitUtil.scala
index b6c8273..b9f0830 100644
--- a/src/main/scala/gitbucket/core/util/JGitUtil.scala
+++ b/src/main/scala/gitbucket/core/util/JGitUtil.scala
@@ -1,6 +1,6 @@
package gitbucket.core.util
-import java.io.ByteArrayOutputStream
+import java.io.{ByteArrayOutputStream, File, FileInputStream, InputStream}
import gitbucket.core.service.RepositoryService
import org.eclipse.jgit.api.Git
@@ -1220,4 +1220,61 @@
Option(git.getRepository.resolve(revstr)).map(ObjectId.toString(_))
}
}
+
+ def getFileSize(git: Git, repository: RepositoryService.RepositoryInfo, treeWalk: TreeWalk): Long = {
+ val attrs = treeWalk.getAttributes
+ val loader = git.getRepository.open(treeWalk.getObjectId(0))
+ if (attrs.containsKey("filter") && attrs.get("filter").getValue == "lfs") {
+ val lfsAttrs = getLfsAttributes(loader)
+ lfsAttrs.get("size").map(_.toLong).get
+ } else {
+ loader.getSize
+ }
+ }
+
+ def getFileSize(git: Git, repository: RepositoryService.RepositoryInfo, tree: RevTree, path: String): Long = {
+ using(TreeWalk.forPath(git.getRepository, path, tree)) { treeWalk =>
+ getFileSize(git, repository, treeWalk)
+ }
+ }
+
+ def openFile[T](git: Git, repository: RepositoryService.RepositoryInfo, treeWalk: TreeWalk)(
+ f: InputStream => T
+ ): T = {
+ val attrs = treeWalk.getAttributes
+ val loader = git.getRepository.open(treeWalk.getObjectId(0))
+ if (attrs.containsKey("filter") && attrs.get("filter").getValue == "lfs") {
+ val lfsAttrs = getLfsAttributes(loader)
+ if (lfsAttrs.nonEmpty) {
+ val oid = lfsAttrs("oid").split(":")(1)
+
+ val file = new File(FileUtil.getLfsFilePath(repository.owner, repository.name, oid))
+ using(new FileInputStream(FileUtil.getLfsFilePath(repository.owner, repository.name, oid))) { in =>
+ f(in)
+ }
+ } else {
+ throw new NoSuchElementException("LFS attribute is empty.")
+ }
+ } else {
+ using(loader.openStream()) { in =>
+ f(in)
+ }
+ }
+ }
+
+ def openFile[T](git: Git, repository: RepositoryService.RepositoryInfo, tree: RevTree, path: String)(
+ f: InputStream => T
+ ): T = {
+ using(TreeWalk.forPath(git.getRepository, path, tree)) { treeWalk =>
+ openFile(git, repository, treeWalk)(f)
+ }
+ }
+
+ private def getLfsAttributes(loader: ObjectLoader): Map[String, String] = {
+ val bytes = loader.getCachedBytes
+ val text = new String(bytes, "UTF-8")
+
+ JGitUtil.getLfsObjects(text)
+ }
+
}
diff --git a/src/main/scala/gitbucket/core/util/StringUtil.scala b/src/main/scala/gitbucket/core/util/StringUtil.scala
index bbbe5b4..b470eeb 100644
--- a/src/main/scala/gitbucket/core/util/StringUtil.scala
+++ b/src/main/scala/gitbucket/core/util/StringUtil.scala
@@ -1,10 +1,13 @@
package gitbucket.core.util
import java.net.{URLDecoder, URLEncoder}
+import java.security.SecureRandom
import java.util.{Base64, UUID}
import org.mozilla.universalchardet.UniversalDetector
import SyntaxSugars._
+import javax.crypto.SecretKeyFactory
+import javax.crypto.spec.PBEKeySpec
import org.apache.commons.io.input.BOMInputStream
import org.apache.commons.io.IOUtils
@@ -16,6 +19,32 @@
UUID.randomUUID().toString.substring(0, 16)
}
+ def base64Encode(value: Array[Byte]): String = {
+ Base64.getEncoder.encodeToString(value)
+ }
+
+ def base64Decode(value: String): Array[Byte] = {
+ Base64.getDecoder.decode(value)
+ }
+
+ def pbkdf2_sha256(iter: Int, salt: String, value: String): String = {
+ val keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
+ val ks = new PBEKeySpec(value.toCharArray, base64Decode(salt), iter, 256)
+ val s = keyFactory.generateSecret(ks)
+ base64Encode(s.getEncoded)
+ }
+
+ def pbkdf2_sha256(value: String) = {
+ val keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
+ val secureRandom = new SecureRandom
+ val salt: Array[Byte] = new Array(32)
+ secureRandom.nextBytes(salt)
+ val iter = 100000
+ val ks = new PBEKeySpec(value.toCharArray, salt, iter, 256)
+ val s = keyFactory.generateSecret(ks)
+ s"""$$pbkdf2-sha256$$${iter}$$${base64Encode(salt)}$$${base64Encode(s.getEncoded)}"""
+ }
+
def sha1(value: String): String =
defining(java.security.MessageDigest.getInstance("SHA-1")) { md =>
md.update(value.getBytes)
diff --git a/src/main/scala/gitbucket/core/view/helpers.scala b/src/main/scala/gitbucket/core/view/helpers.scala
index 848981a..4e757c7 100644
--- a/src/main/scala/gitbucket/core/view/helpers.scala
+++ b/src/main/scala/gitbucket/core/view/helpers.scala
@@ -250,12 +250,12 @@
* Generates the url to the repository.
*/
def url(repository: RepositoryService.RepositoryInfo)(implicit context: Context): String =
- s"${context.path}/${repository.owner}/${repository.name}"
+ s"${context.path}/${encodeRefName(repository.owner)}/${encodeRefName(repository.name)}"
/**
* Generates the url to the account page.
*/
- def url(userName: String)(implicit context: Context): String = s"${context.path}/${userName}"
+ def url(userName: String)(implicit context: Context): String = s"${context.path}/${encodeRefName(userName)}"
/**
* Returns the url to the root of assets.
@@ -273,7 +273,7 @@
* If user does not exist or disabled, this method returns user name as text without link.
*/
def user(userName: String, mailAddress: String = "", styleClass: String = "")(implicit context: Context): Html =
- userWithContent(userName, mailAddress, styleClass)(Html(userName))
+ userWithContent(userName, mailAddress, styleClass)(Html(StringUtil.escapeHtml(userName)))
/**
* Generates the avatar link to the account page.
@@ -316,44 +316,6 @@
*/
def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime
- /**
- * Returns file type for AceEditor.
- */
- def editorType(fileName: String): String = {
- fileName.toLowerCase match {
- case x if (x.endsWith(".bat")) => "batchfile"
- case x if (x.endsWith(".java")) => "java"
- case x if (x.endsWith(".scala")) => "scala"
- case x if (x.endsWith(".js")) => "javascript"
- case x if (x.endsWith(".css")) => "css"
- case x if (x.endsWith(".md")) => "markdown"
- case x if (x.endsWith(".html")) => "html"
- case x if (x.endsWith(".xml")) => "xml"
- case x if (x.endsWith(".c")) => "c_cpp"
- case x if (x.endsWith(".cpp")) => "c_cpp"
- case x if (x.endsWith(".coffee")) => "coffee"
- case x if (x.endsWith(".ejs")) => "ejs"
- case x if (x.endsWith(".hs")) => "haskell"
- case x if (x.endsWith(".json")) => "json"
- case x if (x.endsWith(".jsp")) => "jsp"
- case x if (x.endsWith(".jsx")) => "jsx"
- case x if (x.endsWith(".cl")) => "lisp"
- case x if (x.endsWith(".clojure")) => "lisp"
- case x if (x.endsWith(".lua")) => "lua"
- case x if (x.endsWith(".php")) => "php"
- case x if (x.endsWith(".py")) => "python"
- case x if (x.endsWith(".rdoc")) => "rdoc"
- case x if (x.endsWith(".rhtml")) => "rhtml"
- case x if (x.endsWith(".ruby")) => "ruby"
- case x if (x.endsWith(".sh")) => "sh"
- case x if (x.endsWith(".sql")) => "sql"
- case x if (x.endsWith(".tcl")) => "tcl"
- case x if (x.endsWith(".vbs")) => "vbscript"
- case x if (x.endsWith(".yml")) => "yaml"
- case _ => "plain_text"
- }
- }
-
def pre(value: Html): Html = Html(s"
${value.body.trim.split("\n").map(_.trim).mkString("\n")}
")
/**
diff --git a/src/main/twirl/gitbucket/core/account/activity.scala.html b/src/main/twirl/gitbucket/core/account/activity.scala.html
index e3b1307..f0d2cdc 100644
--- a/src/main/twirl/gitbucket/core/account/activity.scala.html
+++ b/src/main/twirl/gitbucket/core/account/activity.scala.html
@@ -1,8 +1,9 @@
@(account: gitbucket.core.model.Account,
groupNames: List[String],
- activities: List[gitbucket.core.model.Activity])(implicit context: gitbucket.core.controller.Context)
+ activities: List[gitbucket.core.model.Activity],
+ extraMailAddresses: List[String])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
-@gitbucket.core.account.html.main(account, groupNames, "activity"){
+@gitbucket.core.account.html.main(account, groupNames, "activity", extraMailAddresses){
diff --git a/src/main/twirl/gitbucket/core/account/main.scala.html b/src/main/twirl/gitbucket/core/account/main.scala.html
index 3a0c588..4c2ebc0 100644
--- a/src/main/twirl/gitbucket/core/account/main.scala.html
+++ b/src/main/twirl/gitbucket/core/account/main.scala.html
@@ -1,4 +1,4 @@
-@(account: gitbucket.core.model.Account, groupNames: List[String], active: String,
+@(account: gitbucket.core.model.Account, groupNames: List[String], active: String, extraMailAddresses: List[String],
isGroupManager: Boolean = false)(body: Html)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main(account.userName){
@@ -20,6 +20,16 @@
@account.url
}
+ @if(context.settings.showMailAddress){
+
+ @account.mailAddress
+
+ @extraMailAddresses.map{ mail =>
+
+ @mail
+
+ }
+ }
Joined on @helpers.date(account.registeredDate)
diff --git a/src/main/twirl/gitbucket/core/account/members.scala.html b/src/main/twirl/gitbucket/core/account/members.scala.html
index 996dc1d..ce8edc9 100644
--- a/src/main/twirl/gitbucket/core/account/members.scala.html
+++ b/src/main/twirl/gitbucket/core/account/members.scala.html
@@ -1,16 +1,16 @@
-@(account: gitbucket.core.model.Account, members: List[gitbucket.core.model.GroupMember], isGroupManager: Boolean)(implicit context: gitbucket.core.controller.Context)
+@(account: gitbucket.core.model.Account, members: List[gitbucket.core.model.GroupMember], extraMailAddresses: List[String], isGroupManager: Boolean)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
-@gitbucket.core.account.html.main(account, Nil, "members", isGroupManager){
+@gitbucket.core.account.html.main(account, Nil, "members", extraMailAddresses, isGroupManager){
@if(members.isEmpty){
No members
} else {
@members.map { member =>
}
}
-}
\ No newline at end of file
+}
diff --git a/src/main/twirl/gitbucket/core/account/newrepo.scala.html b/src/main/twirl/gitbucket/core/account/newrepo.scala.html
index fa0f307..0c853bd 100644
--- a/src/main/twirl/gitbucket/core/account/newrepo.scala.html
+++ b/src/main/twirl/gitbucket/core/account/newrepo.scala.html
@@ -32,7 +32,7 @@
- Repository name
-
-
+
diff --git a/src/main/twirl/gitbucket/core/account/repositories.scala.html b/src/main/twirl/gitbucket/core/account/repositories.scala.html
index b4c7acf..1d074f9 100644
--- a/src/main/twirl/gitbucket/core/account/repositories.scala.html
+++ b/src/main/twirl/gitbucket/core/account/repositories.scala.html
@@ -1,8 +1,9 @@
@(account: gitbucket.core.model.Account, groupNames: List[String],
repositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
+ extraMailAddresses: List[String],
isGroupManager: Boolean)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
-@gitbucket.core.account.html.main(account, groupNames, "repositories", isGroupManager){
+@gitbucket.core.account.html.main(account, groupNames, "repositories", extraMailAddresses, isGroupManager){
@if(repositories.isEmpty){
No repositories
} else {
diff --git a/src/main/twirl/gitbucket/core/admin/settings_system.scala.html b/src/main/twirl/gitbucket/core/admin/settings_system.scala.html
index 7d787fa..c768d6b 100644
--- a/src/main/twirl/gitbucket/core/admin/settings_system.scala.html
+++ b/src/main/twirl/gitbucket/core/admin/settings_system.scala.html
@@ -9,6 +9,10 @@
Value |
+ GITBUCKET_VERSION |
+ @gitbucket.core.GitBucketCoreModule.getVersions.last.getVersion |
+
+
GITBUCKET_HOME |
@gitbucket.core.util.Directory.GitBucketHome |
@@ -132,6 +136,21 @@
+
+
+
+
+
+
diff --git a/src/main/twirl/gitbucket/core/admin/userlist.scala.html b/src/main/twirl/gitbucket/core/admin/userlist.scala.html
index 131f357..85d694c 100644
--- a/src/main/twirl/gitbucket/core/admin/userlist.scala.html
+++ b/src/main/twirl/gitbucket/core/admin/userlist.scala.html
@@ -26,7 +26,7 @@
}
- @helpers.avatar(account.userName, 20)
+ @helpers.avatarLink(account.userName, 20)
@account.userName
@if(account.isGroupAccount){
(Group)
@@ -39,7 +39,7 @@
}
@if(account.isGroupAccount){
@members(account.userName).map { userName =>
- @helpers.avatar(userName, 20, tooltip = true)
+ @helpers.avatarLink(userName, 20, tooltip = true)
}
}
diff --git a/src/main/twirl/gitbucket/core/dashboard/issues.scala.html b/src/main/twirl/gitbucket/core/dashboard/issues.scala.html
index f23e576..c2e92ed 100644
--- a/src/main/twirl/gitbucket/core/dashboard/issues.scala.html
+++ b/src/main/twirl/gitbucket/core/dashboard/issues.scala.html
@@ -5,10 +5,9 @@
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
filter: String,
groups: List[String],
- recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
- userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context)
+ recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Issues"){
- @gitbucket.core.dashboard.html.sidebar(recentRepositories, userRepositories){
+ @gitbucket.core.dashboard.html.sidebar(recentRepositories){
@gitbucket.core.dashboard.html.tab("issues")
@gitbucket.core.dashboard.html.issuesnavi("issues", filter, openCount, closedCount, condition)
diff --git a/src/main/twirl/gitbucket/core/dashboard/pulls.scala.html b/src/main/twirl/gitbucket/core/dashboard/pulls.scala.html
index 3d05a84..b3cf909 100644
--- a/src/main/twirl/gitbucket/core/dashboard/pulls.scala.html
+++ b/src/main/twirl/gitbucket/core/dashboard/pulls.scala.html
@@ -5,10 +5,9 @@
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
filter: String,
groups: List[String],
- recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
- userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context)
+ recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Pull requests"){
- @gitbucket.core.dashboard.html.sidebar(recentRepositories, userRepositories){
+ @gitbucket.core.dashboard.html.sidebar(recentRepositories){
@gitbucket.core.dashboard.html.tab("pulls")
@gitbucket.core.dashboard.html.issuesnavi("pulls", filter, openCount, closedCount, condition)
diff --git a/src/main/twirl/gitbucket/core/dashboard/repos.scala.html b/src/main/twirl/gitbucket/core/dashboard/repos.scala.html
new file mode 100644
index 0000000..a3ac8c6
--- /dev/null
+++ b/src/main/twirl/gitbucket/core/dashboard/repos.scala.html
@@ -0,0 +1,71 @@
+@(groups: List[String],
+ recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
+ userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context)
+@import gitbucket.core.view.helpers
+@gitbucket.core.html.main("Repositories"){
+ @gitbucket.core.dashboard.html.sidebar(recentRepositories){
+ @gitbucket.core.dashboard.html.tab("repos")
+
+
+
+
+
+ @if(userRepositories.isEmpty){
+ No repositories
+ } else {
+ @userRepositories.map { repository =>
+
+
+ @gitbucket.core.helper.html.repositoryicon(repository, true)
+
+
+
+ @if(repository.repository.originUserName.isDefined){
+
+ }
+ @if(repository.repository.description.isDefined){
+
@repository.repository.description
+ }
+
Updated @gitbucket.core.helper.html.datetimeago(repository.repository.lastActivityDate)
+
+
+ }
+ }
+
+ }
+}
+
diff --git a/src/main/twirl/gitbucket/core/dashboard/sidebar.scala.html b/src/main/twirl/gitbucket/core/dashboard/sidebar.scala.html
index 9f59a2f..ee7feb0 100644
--- a/src/main/twirl/gitbucket/core/dashboard/sidebar.scala.html
+++ b/src/main/twirl/gitbucket/core/dashboard/sidebar.scala.html
@@ -1,29 +1,8 @@
-@(recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
- userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(body: Html)(implicit context: gitbucket.core.controller.Context)
+@(recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(body: Html)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
diff --git a/src/main/twirl/gitbucket/core/dashboard/tab.scala.html b/src/main/twirl/gitbucket/core/dashboard/tab.scala.html
index 597509f..ee2c17d 100644
--- a/src/main/twirl/gitbucket/core/dashboard/tab.scala.html
+++ b/src/main/twirl/gitbucket/core/dashboard/tab.scala.html
@@ -2,6 +2,7 @@
- News feed
@if(context.loginAccount.isDefined){
+ - Repositories
- Pull requests
- Issues
@gitbucket.core.plugin.PluginRegistry().getDashboardTabs.map { tab =>
diff --git a/src/main/twirl/gitbucket/core/helper/activities.scala.html b/src/main/twirl/gitbucket/core/helper/activities.scala.html
index 2b95fbc..306a539 100644
--- a/src/main/twirl/gitbucket/core/helper/activities.scala.html
+++ b/src/main/twirl/gitbucket/core/helper/activities.scala.html
@@ -67,7 +67,7 @@
@gitbucket.core.helper.html.datetimeago(activity.activityDate)
- @helpers.avatar(activity.activityUserName, 16)
+ @helpers.avatarLink(activity.activityUserName, 16)
@helpers.activityMessage(activity.message)
@activity.additionalInfo.map { additionalInfo =>
@@ -83,7 +83,7 @@
@gitbucket.core.helper.html.datetimeago(activity.activityDate)
- @helpers.avatar(activity.activityUserName, 16)
+ @helpers.avatarLink(activity.activityUserName, 16)
@helpers.activityMessage(activity.message)
@additionalInfo
@@ -97,7 +97,7 @@
@gitbucket.core.helper.html.datetimeago(activity.activityDate)
- @helpers.avatar(activity.activityUserName, 16)
+ @helpers.avatarLink(activity.activityUserName, 16)
@helpers.activityMessage(activity.message)
diff --git a/src/main/twirl/gitbucket/core/helper/commitcomment.scala.html b/src/main/twirl/gitbucket/core/helper/commitcomment.scala.html
index 614d7fa..63f099b 100644
--- a/src/main/twirl/gitbucket/core/helper/commitcomment.scala.html
+++ b/src/main/twirl/gitbucket/core/helper/commitcomment.scala.html
@@ -10,7 +10,7 @@
+
+@if(!latestCommitId.contains(comments.comments.head.commitId)) {
+
+}
diff --git a/src/main/twirl/gitbucket/core/helper/diff.scala.html b/src/main/twirl/gitbucket/core/helper/diff.scala.html
index ce58086..fb7d1dd 100644
--- a/src/main/twirl/gitbucket/core/helper/diff.scala.html
+++ b/src/main/twirl/gitbucket/core/helper/diff.scala.html
@@ -335,7 +335,11 @@
.append(renderStatBar(add, del).attr('title', (add + del) + " lines changed").tooltip());
@if(hasWritePermission) {
- diffText.find('.body').each(function(){ $('').prependTo(this); });
+ diffText.find('.body').filter(function(i, e) {
+ return $(e).has('span').length > 0;
+ }).each(function(){
+ $('').prependTo(this);
+ });
}
@if(showLineNotes){
var fileName = table.attr('filename');
diff --git a/src/main/twirl/gitbucket/core/helper/forkrepository.scala.html b/src/main/twirl/gitbucket/core/helper/forkrepository.scala.html
index 4dfed1d..e7de31a 100644
--- a/src/main/twirl/gitbucket/core/helper/forkrepository.scala.html
+++ b/src/main/twirl/gitbucket/core/helper/forkrepository.scala.html
@@ -1,18 +1,23 @@
@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
- groupAndPerm: Map[String, Boolean])(implicit context: gitbucket.core.controller.Context)
+ groupAndPerm: Seq[(String, Boolean)])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
\ No newline at end of file
+
diff --git a/src/main/twirl/gitbucket/core/helper/preview.scala.html b/src/main/twirl/gitbucket/core/helper/preview.scala.html
index 7bfe683..5b3122b 100644
--- a/src/main/twirl/gitbucket/core/helper/preview.scala.html
+++ b/src/main/twirl/gitbucket/core/helper/preview.scala.html
@@ -10,12 +10,13 @@
styleClass: String = "",
placeholder: String = "Leave a comment",
elastic: Boolean = false,
+ focus: Boolean = false,
tabIndex: Int = -2,
uid: Long = new java.util.Date().getTime())(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@@ -46,6 +47,13 @@
$('#content@uid').elastic();
$('#content@uid').trigger('blur');
}
+ @if(focus){
+ $('#content@uid').trigger('focus');
+ }
+
+ $('#write@uid').on('shown.bs.tab', function(){
+ $('#content@uid').trigger('focus');
+ });
$('#preview@uid').click(function(){
$('#preview-area@uid').html('

Previewing...');
@@ -57,6 +65,7 @@
enableTaskList : @enableTaskList
}, function(data){
$('#preview-area@uid').html(data);
+ $('#preview-area@uid input').prop('disabled', true);
prettyPrint();
});
});
diff --git a/src/main/twirl/gitbucket/core/index.scala.html b/src/main/twirl/gitbucket/core/index.scala.html
index 83ca41c..2552a01 100644
--- a/src/main/twirl/gitbucket/core/index.scala.html
+++ b/src/main/twirl/gitbucket/core/index.scala.html
@@ -1,10 +1,9 @@
@(activities: List[gitbucket.core.model.Activity],
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
- userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
showBannerToCreatePersonalAccessToken: Boolean)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main("GitBucket"){
- @gitbucket.core.dashboard.html.sidebar(recentRepositories, userRepositories){
+ @gitbucket.core.dashboard.html.sidebar(recentRepositories){
@context.settings.information.map { information =>
diff --git a/src/main/twirl/gitbucket/core/issues/commentlist.scala.html b/src/main/twirl/gitbucket/core/issues/commentlist.scala.html
index 063d671..6fdecdd 100644
--- a/src/main/twirl/gitbucket/core/issues/commentlist.scala.html
+++ b/src/main/twirl/gitbucket/core/issues/commentlist.scala.html
@@ -9,7 +9,7 @@
@showFormattedComment(comment: gitbucket.core.model.IssueComment)={