mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-08 14:35:52 +01:00
Merge branch 'master' into fork-and-pullreq
Conflicts: src/main/resources/update/1_3.sql src/main/resources/update/1_4.sql src/main/scala/app/CreateRepositoryController.scala src/main/scala/service/WikiService.scala src/main/twirl/account/repositories.scala.html
This commit is contained in:
@@ -5,6 +5,7 @@ import javax.servlet._
|
||||
class ScalatraBootstrap extends LifeCycle {
|
||||
override def init(context: ServletContext) {
|
||||
context.mount(new IndexController, "/")
|
||||
context.mount(new SearchController, "/")
|
||||
context.mount(new FileUploadController, "/upload")
|
||||
context.mount(new SignInController, "/*")
|
||||
context.mount(new UserManagementController, "/*")
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package app
|
||||
|
||||
import service._
|
||||
import util.{FileUtil, FileUploadUtil, OneselfAuthenticator}
|
||||
import util.{FileUtil, OneselfAuthenticator}
|
||||
import util.StringUtil._
|
||||
import util.Directory._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.scalatra.FlashMapSupport
|
||||
|
||||
class AccountController extends AccountControllerBase
|
||||
@@ -43,12 +42,23 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
|
||||
*/
|
||||
get("/:userName") {
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName).map { x =>
|
||||
getAccountByUserName(userName).map { account =>
|
||||
params.getOrElse("tab", "repositories") match {
|
||||
// Public Activity
|
||||
case "activity" => account.html.activity(x, getActivitiesByUser(userName, true))
|
||||
case "activity" =>
|
||||
_root_.account.html.activity(account,
|
||||
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
|
||||
getActivitiesByUser(userName, true))
|
||||
|
||||
// Members
|
||||
case "members" if(account.isGroupAccount) =>
|
||||
_root_.account.html.members(account, getGroupMembers(account.userName))
|
||||
|
||||
// Repositories
|
||||
case _ => account.html.repositories(x, getVisibleRepositories(userName, baseUrl, context.loginAccount.map(_.userName)))
|
||||
case _ =>
|
||||
_root_.account.html.repositories(account,
|
||||
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
|
||||
getVisibleRepositories(userName, baseUrl, context.loginAccount.map(_.userName)))
|
||||
}
|
||||
} getOrElse NotFound
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package app
|
||||
|
||||
import _root_.util.Directory._
|
||||
import _root_.util.{FileUploadUtil, FileUtil, Validations}
|
||||
import _root_.util.{FileUtil, Validations}
|
||||
import org.scalatra._
|
||||
import org.scalatra.json._
|
||||
import org.json4s._
|
||||
@@ -10,7 +10,9 @@ import org.apache.commons.io.FileUtils
|
||||
import model.Account
|
||||
import scala.Some
|
||||
import service.AccountService
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.{HttpSession, HttpServletRequest}
|
||||
import java.text.SimpleDateFormat
|
||||
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
|
||||
|
||||
/**
|
||||
* Provides generic features for controller implementations.
|
||||
@@ -20,6 +22,21 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
|
||||
implicit val jsonFormats = DefaultFormats
|
||||
|
||||
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
|
||||
val httpRequest = request.asInstanceOf[HttpServletRequest]
|
||||
val path = httpRequest.getRequestURI.substring(request.getServletContext.getContextPath.length)
|
||||
|
||||
if(path.startsWith("/console/")){
|
||||
Option(httpRequest.getSession.getAttribute("LOGIN_ACCOUNT").asInstanceOf[Account]).collect {
|
||||
case account if(account.isAdmin) => chain.doFilter(request, response)
|
||||
}
|
||||
} else if(path.startsWith("/git/")){
|
||||
chain.doFilter(request, response)
|
||||
} else {
|
||||
super.doFilter(request, response, chain)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the context object for the request.
|
||||
*/
|
||||
@@ -116,7 +133,8 @@ case class Context(path: String, loginAccount: Option[Account], currentUrl: Stri
|
||||
/**
|
||||
* Base trait for controllers which manages account information.
|
||||
*/
|
||||
trait AccountManagementControllerBase extends ControllerBase { self: AccountService =>
|
||||
trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
|
||||
self: AccountService =>
|
||||
|
||||
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit = {
|
||||
if(clearImage){
|
||||
@@ -126,9 +144,9 @@ trait AccountManagementControllerBase extends ControllerBase { self: AccountServ
|
||||
}
|
||||
} else {
|
||||
fileId.map { fileId =>
|
||||
val filename = "avatar." + FileUtil.getExtension(FileUploadUtil.getUploadedFilename(fileId).get)
|
||||
val filename = "avatar." + FileUtil.getExtension(getUploadedFilename(fileId).get)
|
||||
FileUtils.moveFile(
|
||||
FileUploadUtil.getTemporaryFile(fileId),
|
||||
getTemporaryFile(fileId),
|
||||
new java.io.File(getUserUploadDir(userName), filename)
|
||||
)
|
||||
updateAvatarImage(userName, Some(filename))
|
||||
@@ -148,4 +166,34 @@ trait AccountManagementControllerBase extends ControllerBase { self: AccountServ
|
||||
.map { _ => "Mail address is already registered." }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Base trait for controllers which needs file uploading feature.
|
||||
*/
|
||||
trait FileUploadControllerBase {
|
||||
|
||||
def generateFileId: String =
|
||||
new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis))
|
||||
|
||||
def TemporaryDir(implicit session: HttpSession): java.io.File =
|
||||
new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}")
|
||||
|
||||
def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File =
|
||||
new java.io.File(TemporaryDir, fileId)
|
||||
|
||||
// def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit =
|
||||
// getTemporaryFile(fileId).delete()
|
||||
|
||||
def removeTemporaryFiles()(implicit session: HttpSession): Unit =
|
||||
FileUtils.deleteDirectory(TemporaryDir)
|
||||
|
||||
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] = {
|
||||
val filename = Option(session.getAttribute("upload_" + fileId).asInstanceOf[String])
|
||||
if(filename.isDefined){
|
||||
session.removeAttribute("upload_" + fileId)
|
||||
}
|
||||
filename
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import util.{JGitUtil, UsersAuthenticator, ReferrerAuthenticator}
|
||||
import service._
|
||||
import java.io.File
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.apache.commons.io._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.eclipse.jgit.lib.PersonIdent
|
||||
|
||||
class CreateRepositoryController extends CreateRepositoryControllerBase
|
||||
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
|
||||
@@ -17,14 +17,15 @@ class CreateRepositoryController extends CreateRepositoryControllerBase
|
||||
* Creates new repository.
|
||||
*/
|
||||
trait CreateRepositoryControllerBase extends ControllerBase {
|
||||
self: RepositoryService with WikiService with LabelsService with ActivityService
|
||||
self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService
|
||||
with UsersAuthenticator with ReferrerAuthenticator =>
|
||||
|
||||
case class RepositoryCreationForm(name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
|
||||
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
|
||||
|
||||
case class ForkRepositoryForm(owner: String, name: String)
|
||||
|
||||
val newForm = mapping(
|
||||
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
|
||||
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))),
|
||||
"description" -> trim(label("Description" , optional(text()))),
|
||||
"isPrivate" -> trim(label("Repository Type", boolean())),
|
||||
@@ -40,28 +41,36 @@ trait CreateRepositoryControllerBase extends ControllerBase {
|
||||
* Show the new repository form.
|
||||
*/
|
||||
get("/new")(usersOnly {
|
||||
html.newrepo()
|
||||
html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
|
||||
})
|
||||
|
||||
/**
|
||||
* Create new repository.
|
||||
*/
|
||||
post("/new", newForm)(usersOnly { form =>
|
||||
val ownerAccount = getAccountByUserName(form.owner).get
|
||||
val loginAccount = context.loginAccount.get
|
||||
val loginUserName = loginAccount.userName
|
||||
|
||||
// Insert to the database at first
|
||||
createRepository(form.name, loginUserName, form.description, form.isPrivate)
|
||||
createRepository(form.name, form.owner, form.description, form.isPrivate)
|
||||
|
||||
// Add collaborators for group repository
|
||||
if(ownerAccount.isGroupAccount){
|
||||
getGroupMembers(form.owner).foreach { userName =>
|
||||
addCollaborator(form.owner, form.name, userName)
|
||||
}
|
||||
}
|
||||
|
||||
// Insert default labels
|
||||
insertDefaultLabels(loginUserName, form.name)
|
||||
|
||||
// Create the actual repository
|
||||
val gitdir = getRepositoryDir(loginUserName, form.name)
|
||||
val gitdir = getRepositoryDir(form.owner, form.name)
|
||||
JGitUtil.initRepository(gitdir)
|
||||
|
||||
if(form.createReadme){
|
||||
val tmpdir = getInitRepositoryDir(loginUserName, form.name)
|
||||
val tmpdir = getInitRepositoryDir(form.owner, form.name)
|
||||
try {
|
||||
// Clone the repository
|
||||
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
|
||||
@@ -91,13 +100,13 @@ trait CreateRepositoryControllerBase extends ControllerBase {
|
||||
}
|
||||
|
||||
// Create Wiki repository
|
||||
createWikiRepository(loginAccount, form.name)
|
||||
createWikiRepository(loginAccount, form.owner, form.name)
|
||||
|
||||
// Record activity
|
||||
recordCreateRepositoryActivity(loginUserName, form.name, loginUserName)
|
||||
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
|
||||
|
||||
// redirect to the repository
|
||||
redirect(s"/${loginUserName}/${form.name}")
|
||||
redirect(s"/${form.owner}/${form.name}")
|
||||
})
|
||||
|
||||
post("/:owner/:repository/_fork")(referrersOnly { repository =>
|
||||
@@ -153,12 +162,19 @@ trait CreateRepositoryControllerBase extends ControllerBase {
|
||||
createLabel(userName, repositoryName, "wontfix", "ffffff")
|
||||
}
|
||||
|
||||
private def existsAccount: Constraint = new Constraint(){
|
||||
def validate(name: String, value: String): Option[String] =
|
||||
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate check for the repository name.
|
||||
*/
|
||||
private def unique: Constraint = new Constraint(){
|
||||
def validate(name: String, value: String): Option[String] =
|
||||
getRepositoryNamesOfUser(context.loginAccount.get.userName).find(_ == value).map(_ => "Repository already exists.")
|
||||
params.get("owner").flatMap { userName =>
|
||||
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package app
|
||||
|
||||
import util.{FileUtil, FileUploadUtil}
|
||||
import util.{FileUtil}
|
||||
import org.scalatra._
|
||||
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport}
|
||||
import org.apache.commons.io.FileUtils
|
||||
@@ -9,17 +9,18 @@ import org.apache.commons.io.FileUtils
|
||||
* Provides Ajax based file upload functionality.
|
||||
*
|
||||
* This servlet saves uploaded file as temporary file and returns the unique id.
|
||||
* You can get uploaded file using [[util.FileUploadUtil#getTemporaryFile()]] with this id.
|
||||
* You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
|
||||
*/
|
||||
// TODO Remove temporary files at session timeout by session listener.
|
||||
class FileUploadController extends ScalatraServlet with FileUploadSupport with FlashMapSupport {
|
||||
class FileUploadController extends ScalatraServlet
|
||||
with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
|
||||
|
||||
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
|
||||
|
||||
post("/image"){
|
||||
fileParams.get("file") match {
|
||||
case Some(file) if(FileUtil.isImage(file.name)) => {
|
||||
val fileId = FileUploadUtil.generateFileId
|
||||
FileUtils.writeByteArrayToFile(FileUploadUtil.getTemporaryFile(fileId), file.get)
|
||||
val fileId = generateFileId
|
||||
FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
|
||||
session += "upload_" + fileId -> file.name
|
||||
Ok(fileId)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package app
|
||||
|
||||
import util._
|
||||
import service._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
class IndexController extends IndexControllerBase
|
||||
with RepositoryService with AccountService with SystemSettingsService with ActivityService
|
||||
with RepositoryService with SystemSettingsService with ActivityService with AccountService
|
||||
with UsersAuthenticator
|
||||
|
||||
trait IndexControllerBase extends ControllerBase {
|
||||
self: RepositoryService with SystemSettingsService with ActivityService with AccountService
|
||||
with UsersAuthenticator =>
|
||||
|
||||
trait IndexControllerBase extends ControllerBase { self: RepositoryService
|
||||
with SystemSettingsService with ActivityService =>
|
||||
|
||||
get("/"){
|
||||
val loginAccount = context.loginAccount
|
||||
|
||||
@@ -18,4 +22,14 @@ trait IndexControllerBase extends ControllerBase { self: RepositoryService
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* JSON API for collaborator completion.
|
||||
*/
|
||||
// TODO Move to other controller?
|
||||
get("/_user/proposals")(usersOnly {
|
||||
contentType = formats("json")
|
||||
org.json4s.jackson.Serialization.write(Map("options" -> getAllUsers.filter(!_.isGroupAccount).map(_.userName).toArray))
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
getComments(owner, name, issueId.toInt),
|
||||
getIssueLabels(owner, name, issueId.toInt),
|
||||
(getCollaborators(owner, name) :+ owner).sorted,
|
||||
getMilestones(owner, name),
|
||||
getMilestonesWithIssueCount(owner, name),
|
||||
getLabels(owner, name),
|
||||
hasWritePermission(owner, name, context.loginAccount),
|
||||
repository)
|
||||
@@ -112,7 +112,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
// record activity
|
||||
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
|
||||
|
||||
redirect("/%s/%s/issues/%d".format(owner, name, issueId))
|
||||
redirect(s"/${owner}/${name}/issues/${issueId}")
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
|
||||
@@ -122,17 +122,21 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
getIssue(owner, name, params("id")).map { issue =>
|
||||
if(isEditable(owner, name, issue.openedUserName)){
|
||||
updateIssue(owner, name, issue.issueId, form.title, form.content)
|
||||
redirect("/%s/%s/issues/_data/%d".format(owner, name, issue.issueId))
|
||||
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
||||
} else Unauthorized
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||
handleComment(form.issueId, Some(form.content), repository)
|
||||
handleComment(form.issueId, Some(form.content), repository)() map { id =>
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
|
||||
handleComment(form.issueId, form.content, repository)
|
||||
handleComment(form.issueId, form.content, repository)() map { id =>
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
|
||||
@@ -142,7 +146,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
getComment(owner, name, params("id")).map { comment =>
|
||||
if(isEditable(owner, name, comment.commentedUserName)){
|
||||
updateComment(comment.commentId, form.content)
|
||||
redirect("/%s/%s/issue_comments/_data/%d".format(owner, name, comment.commentId))
|
||||
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
|
||||
} else Unauthorized
|
||||
} getOrElse NotFound
|
||||
})
|
||||
@@ -197,79 +201,81 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
|
||||
updateAssignedUserName(repository.owner, repository.name, params("id").toInt,
|
||||
params.get("assignedUserName") filter (_.trim != ""))
|
||||
updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
|
||||
Ok("updated")
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
|
||||
updateMilestoneId(repository.owner, repository.name, params("id").toInt,
|
||||
params.get("milestoneId") collect { case x if x.trim != "" => x.toInt })
|
||||
Ok("updated")
|
||||
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
|
||||
milestoneId("milestoneId").map { milestoneId =>
|
||||
getMilestonesWithIssueCount(repository.owner, repository.name)
|
||||
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
|
||||
issues.milestones.html.progress(openCount + closeCount, closeCount, false)
|
||||
} getOrElse NotFound
|
||||
} getOrElse Ok()
|
||||
})
|
||||
|
||||
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
val userName = context.loginAccount.get.userName
|
||||
val action = params.get("value")
|
||||
|
||||
params.get("value") collect {
|
||||
case s if s == "close" => (s.capitalize, Some(s), true)
|
||||
case s if s == "reopen" => (s.capitalize, Some(s), false)
|
||||
} map { case (content, action, closed) =>
|
||||
params("checked").split(',') foreach { issueId =>
|
||||
createComment(owner, name, userName, issueId.toInt, content, action)
|
||||
updateClosed(owner, name, issueId.toInt, closed)
|
||||
}
|
||||
redirect("/%s/%s/issues".format(owner, name))
|
||||
} getOrElse NotFound
|
||||
executeBatch(repository) {
|
||||
handleComment(_, None, repository)( _ => action)
|
||||
}
|
||||
})
|
||||
|
||||
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
val labelId = params("value").toInt
|
||||
|
||||
params.get("value").map(_.toInt) map { labelId =>
|
||||
params("checked").split(',') foreach { issueId =>
|
||||
getIssueLabel(owner, name, issueId.toInt, labelId) getOrElse {
|
||||
registerIssueLabel(owner, name, issueId.toInt, labelId)
|
||||
}
|
||||
executeBatch(repository) { issueId =>
|
||||
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
|
||||
registerIssueLabel(repository.owner, repository.name, issueId, labelId)
|
||||
}
|
||||
redirect("/%s/%s/issues".format(owner, name))
|
||||
} getOrElse NotFound
|
||||
}
|
||||
})
|
||||
|
||||
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
|
||||
params("checked").split(',') foreach { issueId =>
|
||||
updateAssignedUserName(repository.owner, repository.name, issueId.toInt,
|
||||
params.get("value") filter (_.trim != ""))
|
||||
val value = assignedUserName("value")
|
||||
|
||||
executeBatch(repository) {
|
||||
updateAssignedUserName(repository.owner, repository.name, _, value)
|
||||
}
|
||||
redirect("/%s/%s/issues".format(repository.owner, repository.name))
|
||||
})
|
||||
|
||||
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
|
||||
params("checked").split(',') foreach { issueId =>
|
||||
updateMilestoneId(repository.owner, repository.name, issueId.toInt,
|
||||
params.get("value") collect { case x if x.trim != "" => x.toInt })
|
||||
val value = milestoneId("value")
|
||||
|
||||
executeBatch(repository) {
|
||||
updateMilestoneId(repository.owner, repository.name, _, value)
|
||||
}
|
||||
redirect("/%s/%s/issues".format(repository.owner, repository.name))
|
||||
})
|
||||
|
||||
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
|
||||
val milestoneId = (key: String) => params.get(key) collect { case x if x.trim != "" => x.toInt }
|
||||
|
||||
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
|
||||
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
|
||||
|
||||
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo) = {
|
||||
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
|
||||
params("checked").split(',') map(_.toInt) foreach execute
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues")
|
||||
}
|
||||
|
||||
/**
|
||||
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
|
||||
*/
|
||||
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
|
||||
(getAction: model.Issue => Option[String] =
|
||||
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
val userName = context.loginAccount.get.userName
|
||||
|
||||
getIssue(owner, name, issueId.toString) map { issue =>
|
||||
val (action, recordActivity) =
|
||||
params.get("action")
|
||||
.filter(_ => isEditable(owner, name, issue.openedUserName))
|
||||
getAction(issue)
|
||||
.collect {
|
||||
case s if s == "close" => true -> (Some(s) -> Some(recordCloseIssueActivity _))
|
||||
case s if s == "reopen" => false -> (Some(s) -> Some(recordReopenIssueActivity _))
|
||||
case "close" => true -> (Some("close") -> Some(recordCloseIssueActivity _))
|
||||
case "reopen" => false -> (Some("reopen") -> Some(recordReopenIssueActivity _))
|
||||
}
|
||||
.map { case (closed, t) =>
|
||||
updateClosed(owner, name, issueId, closed)
|
||||
@@ -277,21 +283,26 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
}
|
||||
.getOrElse(None -> None)
|
||||
|
||||
val commentId = createComment(owner, name, userName, issueId, content.getOrElse(action.get.capitalize), action)
|
||||
val commentId = content
|
||||
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
|
||||
.getOrElse ( action.get.capitalize -> action.get )
|
||||
match {
|
||||
case (content, action) => createComment(owner, name, userName, issueId, content, action)
|
||||
}
|
||||
|
||||
// record activity
|
||||
content foreach ( recordCommentIssueActivity(owner, name, userName, issueId, _) )
|
||||
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
|
||||
|
||||
redirect("/%s/%s/issues/%d#comment-%d".format(owner, name, issueId, commentId))
|
||||
} getOrElse NotFound
|
||||
commentId
|
||||
}
|
||||
}
|
||||
|
||||
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
|
||||
val owner = repository.owner
|
||||
val repoName = repository.name
|
||||
val userName = if(filter != "all") Some(params("userName")) else None
|
||||
val sessionKey = "%s/%s/issues".format(owner, repoName)
|
||||
val sessionKey = s"${owner}/${repoName}/issues"
|
||||
|
||||
val page = try {
|
||||
val i = params.getOrElse("page", "1").toInt
|
||||
@@ -311,7 +322,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
searchIssue(owner, repoName, condition, filter, userName, (page - 1) * IssueLimit, IssueLimit),
|
||||
page,
|
||||
(getCollaborators(owner, repoName) :+ owner).sorted,
|
||||
getMilestones(owner, repoName).filter(_.closedDate.isEmpty),
|
||||
getMilestones(owner, repoName),
|
||||
getLabels(owner, repoName),
|
||||
countIssue(owner, repoName, condition.copy(state = "open"), filter, userName),
|
||||
countIssue(owner, repoName, condition.copy(state = "closed"), filter, userName),
|
||||
|
||||
@@ -3,7 +3,7 @@ package app
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
import service._
|
||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, UsersAuthenticator}
|
||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
|
||||
|
||||
class MilestonesController extends MilestonesControllerBase
|
||||
with MilestonesService with RepositoryService with AccountService
|
||||
|
||||
@@ -54,22 +54,19 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
|
||||
* Display the Collaborators page.
|
||||
*/
|
||||
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
|
||||
settings.html.collaborators(getCollaborators(repository.owner, repository.name), repository)
|
||||
})
|
||||
|
||||
/**
|
||||
* JSON API for collaborator completion.
|
||||
*/
|
||||
get("/:owner/:repository/settings/collaborators/proposals")(usersOnly {
|
||||
contentType = formats("json")
|
||||
org.json4s.jackson.Serialization.write(Map("options" -> getAllUsers.map(_.userName).toArray))
|
||||
settings.html.collaborators(
|
||||
getCollaborators(repository.owner, repository.name),
|
||||
getAccountByUserName(repository.owner).get.isGroupAccount,
|
||||
repository)
|
||||
})
|
||||
|
||||
/**
|
||||
* Add the collaborator.
|
||||
*/
|
||||
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
|
||||
addCollaborator(repository.owner, repository.name, form.userName)
|
||||
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
|
||||
addCollaborator(repository.owner, repository.name, form.userName)
|
||||
}
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
|
||||
})
|
||||
|
||||
@@ -77,7 +74,9 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
|
||||
* Add the collaborator.
|
||||
*/
|
||||
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
|
||||
removeCollaborator(repository.owner, repository.name, params("name"))
|
||||
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
|
||||
removeCollaborator(repository.owner, repository.name, params("name"))
|
||||
}
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
|
||||
})
|
||||
|
||||
|
||||
51
src/main/scala/app/SearchController.scala
Normal file
51
src/main/scala/app/SearchController.scala
Normal file
@@ -0,0 +1,51 @@
|
||||
package app
|
||||
|
||||
import util._
|
||||
import service._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
class SearchController extends SearchControllerBase
|
||||
with RepositoryService with AccountService with SystemSettingsService with ActivityService
|
||||
with RepositorySearchService with IssuesService
|
||||
with ReferrerAuthenticator
|
||||
|
||||
trait SearchControllerBase extends ControllerBase { self: RepositoryService
|
||||
with SystemSettingsService with ActivityService with RepositorySearchService
|
||||
with ReferrerAuthenticator =>
|
||||
|
||||
val searchForm = mapping(
|
||||
"query" -> trim(text(required)),
|
||||
"owner" -> trim(text(required)),
|
||||
"repository" -> trim(text(required))
|
||||
)(SearchForm.apply)
|
||||
|
||||
case class SearchForm(query: String, owner: String, repository: String)
|
||||
|
||||
post("/search", searchForm){ form =>
|
||||
redirect(s"${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
|
||||
}
|
||||
|
||||
get("/:owner/:repository/search")(referrersOnly { repository =>
|
||||
val query = params("q").trim
|
||||
val target = params.getOrElse("type", "code")
|
||||
val page = try {
|
||||
val i = params.getOrElse("page", "1").toInt
|
||||
if(i <= 0) 1 else i
|
||||
} catch {
|
||||
case e: NumberFormatException => 1
|
||||
}
|
||||
|
||||
target.toLowerCase match {
|
||||
case "issue" => search.html.issues(
|
||||
searchIssues(repository.owner, repository.name, query),
|
||||
countFiles(repository.owner, repository.name, query),
|
||||
query, page, repository)
|
||||
|
||||
case _ => search.html.code(
|
||||
searchFiles(repository.owner, repository.name, query),
|
||||
countIssues(repository.owner, repository.name, query),
|
||||
query, page, repository)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -24,20 +24,19 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
|
||||
}
|
||||
|
||||
post("/signin", form){ form =>
|
||||
val account = getAccountByUserName(form.userName)
|
||||
if(account.isEmpty || account.get.password != sha1(form.password)){
|
||||
redirect("/signin")
|
||||
} else {
|
||||
session.setAttribute("LOGIN_ACCOUNT", account.get)
|
||||
updateLastLoginDate(account.get.userName)
|
||||
getAccountByUserName(form.userName).collect {
|
||||
case account if(!account.isGroupAccount && account.password == sha1(form.password)) => {
|
||||
session.setAttribute("LOGIN_ACCOUNT", account)
|
||||
updateLastLoginDate(account.userName)
|
||||
|
||||
session.get("REDIRECT").map { redirectUrl =>
|
||||
session.removeAttribute("REDIRECT")
|
||||
redirect(redirectUrl.asInstanceOf[String])
|
||||
}.getOrElse {
|
||||
redirect("/")
|
||||
session.get("REDIRECT").map { redirectUrl =>
|
||||
session.removeAttribute("REDIRECT")
|
||||
redirect(redirectUrl.asInstanceOf[String])
|
||||
}.getOrElse {
|
||||
redirect("/")
|
||||
}
|
||||
}
|
||||
}
|
||||
} getOrElse redirect("/signin")
|
||||
}
|
||||
|
||||
get("/signout"){
|
||||
|
||||
@@ -1,34 +1,38 @@
|
||||
package app
|
||||
|
||||
import service._
|
||||
import util.{FileUploadUtil, FileUtil, AdminAuthenticator}
|
||||
import util.AdminAuthenticator
|
||||
import util.StringUtil._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import util.Directory._
|
||||
import scala.Some
|
||||
|
||||
class UserManagementController extends UserManagementControllerBase with AccountService with AdminAuthenticator
|
||||
class UserManagementController extends UserManagementControllerBase
|
||||
with AccountService with RepositoryService with AdminAuthenticator
|
||||
|
||||
trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
self: AccountService with AdminAuthenticator =>
|
||||
self: AccountService with RepositoryService with AdminAuthenticator =>
|
||||
|
||||
case class UserNewForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean,
|
||||
case class NewUserForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean,
|
||||
url: Option[String], fileId: Option[String])
|
||||
|
||||
case class UserEditForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean,
|
||||
case class EditUserForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean,
|
||||
url: Option[String], fileId: Option[String], clearImage: Boolean)
|
||||
|
||||
val newForm = mapping(
|
||||
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
memberNames: Option[String])
|
||||
|
||||
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
memberNames: Option[String], clearImage: Boolean)
|
||||
|
||||
val newUserForm = mapping(
|
||||
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
"password" -> trim(label("Password" , text(required, maxlength(20)))),
|
||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
|
||||
"isAdmin" -> trim(label("User Type" , boolean())),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text())))
|
||||
)(UserNewForm.apply)
|
||||
)(NewUserForm.apply)
|
||||
|
||||
val editForm = mapping(
|
||||
val editUserForm = mapping(
|
||||
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier))),
|
||||
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
|
||||
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
|
||||
@@ -36,28 +40,47 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" , boolean()))
|
||||
)(UserEditForm.apply)
|
||||
|
||||
)(EditUserForm.apply)
|
||||
|
||||
val newGroupForm = mapping(
|
||||
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier))),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text()))),
|
||||
"memberNames" -> trim(label("Member Names" , optional(text())))
|
||||
)(NewGroupForm.apply)
|
||||
|
||||
val editGroupForm = mapping(
|
||||
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier))),
|
||||
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" , optional(text()))),
|
||||
"memberNames" -> trim(label("Member Names" , optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" , boolean()))
|
||||
)(EditGroupForm.apply)
|
||||
|
||||
get("/admin/users")(adminOnly {
|
||||
admin.users.html.list(getAllUsers())
|
||||
val users = getAllUsers()
|
||||
val members = users.collect { case account if(account.isGroupAccount) =>
|
||||
account.userName -> getGroupMembers(account.userName)
|
||||
}.toMap
|
||||
admin.users.html.list(users, members)
|
||||
})
|
||||
|
||||
get("/admin/users/_new")(adminOnly {
|
||||
admin.users.html.edit(None)
|
||||
get("/admin/users/_newuser")(adminOnly {
|
||||
admin.users.html.user(None)
|
||||
})
|
||||
|
||||
post("/admin/users/_new", newForm)(adminOnly { form =>
|
||||
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
|
||||
createAccount(form.userName, sha1(form.password), form.mailAddress, form.isAdmin, form.url)
|
||||
updateImage(form.userName, form.fileId, false)
|
||||
redirect("/admin/users")
|
||||
})
|
||||
|
||||
get("/admin/users/:userName/_edit")(adminOnly {
|
||||
get("/admin/users/:userName/_edituser")(adminOnly {
|
||||
val userName = params("userName")
|
||||
admin.users.html.edit(getAccountByUserName(userName))
|
||||
admin.users.html.user(getAccountByUserName(userName))
|
||||
})
|
||||
|
||||
post("/admin/users/:name/_edit", editForm)(adminOnly { form =>
|
||||
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName).map { account =>
|
||||
updateAccount(getAccountByUserName(userName).get.copy(
|
||||
@@ -71,5 +94,46 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
|
||||
get("/admin/users/_newgroup")(adminOnly {
|
||||
admin.users.html.group(None, Nil)
|
||||
})
|
||||
|
||||
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
|
||||
createGroup(form.groupName, form.url)
|
||||
updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil))
|
||||
updateImage(form.groupName, form.fileId, false)
|
||||
redirect("/admin/users")
|
||||
})
|
||||
|
||||
get("/admin/users/:groupName/_editgroup")(adminOnly {
|
||||
val groupName = params("groupName")
|
||||
admin.users.html.group(getAccountByUserName(groupName), getGroupMembers(groupName))
|
||||
})
|
||||
|
||||
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
|
||||
val groupName = params("groupName")
|
||||
getAccountByUserName(groupName).map { account =>
|
||||
updateGroup(groupName, form.url)
|
||||
|
||||
val memberNames = form.memberNames.map(_.split(",").toList).getOrElse(Nil)
|
||||
updateGroupMembers(form.groupName, memberNames)
|
||||
|
||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||
removeCollaborators(form.groupName, repositoryName)
|
||||
memberNames.foreach { userName =>
|
||||
addCollaborator(form.groupName, repositoryName, userName)
|
||||
}
|
||||
}
|
||||
|
||||
updateImage(form.groupName, form.fileId, form.clearImage)
|
||||
redirect("/admin/users")
|
||||
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
post("/admin/users/_usercheck")(adminOnly {
|
||||
getAccountByUserName(params("userName")).isDefined
|
||||
})
|
||||
|
||||
}
|
||||
@@ -12,7 +12,8 @@ object Accounts extends Table[Account]("ACCOUNT") {
|
||||
def updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||
def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
|
||||
def image = column[String]("IMAGE")
|
||||
def * = userName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? <> (Account, Account.unapply _)
|
||||
def groupAccount = column[Boolean]("GROUP_ACCOUNT")
|
||||
def * = userName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount <> (Account, Account.unapply _)
|
||||
}
|
||||
|
||||
case class Account(
|
||||
@@ -24,5 +25,6 @@ case class Account(
|
||||
registeredDate: java.util.Date,
|
||||
updatedDate: java.util.Date,
|
||||
lastLoginDate: Option[java.util.Date],
|
||||
image: Option[String]
|
||||
image: Option[String],
|
||||
isGroupAccount: Boolean
|
||||
)
|
||||
|
||||
14
src/main/scala/model/GroupMembers.scala
Normal file
14
src/main/scala/model/GroupMembers.scala
Normal file
@@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
|
||||
object GroupMembers extends Table[GroupMember]("GROUP_MEMBER") {
|
||||
def groupName = column[String]("GROUP_NAME", O PrimaryKey)
|
||||
def userName = column[String]("USER_NAME", O PrimaryKey)
|
||||
def * = groupName ~ userName <> (GroupMember, GroupMember.unapply _)
|
||||
}
|
||||
|
||||
case class GroupMember(
|
||||
groupName: String,
|
||||
userName: String
|
||||
)
|
||||
@@ -9,9 +9,9 @@ object IssueComments extends Table[IssueComment]("ISSUE_COMMENT") with IssueTemp
|
||||
def content = column[String]("CONTENT")
|
||||
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||
def updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||
def * = userName ~ repositoryName ~ issueId ~ commentId ~ action.? ~ commentedUserName ~ content ~ registeredDate ~ updatedDate <> (IssueComment, IssueComment.unapply _)
|
||||
def * = userName ~ repositoryName ~ issueId ~ commentId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate <> (IssueComment, IssueComment.unapply _)
|
||||
|
||||
def autoInc = userName ~ repositoryName ~ issueId ~ action.? ~ commentedUserName ~ content ~ registeredDate ~ updatedDate returning commentId
|
||||
def autoInc = userName ~ repositoryName ~ issueId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate returning commentId
|
||||
def byPrimaryKey(commentId: Int) = this.commentId is commentId.bind
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ case class IssueComment(
|
||||
repositoryName: String,
|
||||
issueId: Int,
|
||||
commentId: Int,
|
||||
action: Option[String],
|
||||
action: String,
|
||||
commentedUserName: String,
|
||||
content: String,
|
||||
registeredDate: java.util.Date,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package object model {
|
||||
import scala.slick.lifted.MappedTypeMapper
|
||||
import scala.slick.driver.BasicDriver.Implicit._
|
||||
import scala.slick.lifted.{Column, MappedTypeMapper}
|
||||
|
||||
// java.util.Date TypeMapper
|
||||
implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp](
|
||||
@@ -7,6 +8,10 @@ package object model {
|
||||
t => new java.util.Date(t.getTime)
|
||||
)
|
||||
|
||||
implicit class RichColumn(c1: Column[Boolean]){
|
||||
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns system date.
|
||||
*/
|
||||
|
||||
@@ -24,7 +24,8 @@ trait AccountService {
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate,
|
||||
lastLoginDate = None,
|
||||
image = None)
|
||||
image = None,
|
||||
isGroupAccount = false)
|
||||
|
||||
def updateAccount(account: Account): Unit =
|
||||
Accounts
|
||||
@@ -44,5 +45,42 @@ trait AccountService {
|
||||
|
||||
def updateLastLoginDate(userName: String): Unit =
|
||||
Accounts.filter(_.userName is userName.bind).map(_.lastLoginDate).update(currentDate)
|
||||
|
||||
|
||||
def createGroup(groupName: String, url: Option[String]): Unit =
|
||||
Accounts insert Account(
|
||||
userName = groupName,
|
||||
password = "",
|
||||
mailAddress = groupName + "@devnull",
|
||||
isAdmin = false,
|
||||
url = url,
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate,
|
||||
lastLoginDate = None,
|
||||
image = None,
|
||||
isGroupAccount = true)
|
||||
|
||||
def updateGroup(groupName: String, url: Option[String]): Unit =
|
||||
Accounts.filter(_.userName is groupName.bind).map(_.url.?).update(url)
|
||||
|
||||
def updateGroupMembers(groupName: String, members: List[String]): Unit = {
|
||||
Query(GroupMembers).filter(_.groupName is groupName.bind).delete
|
||||
members.foreach { userName =>
|
||||
GroupMembers insert GroupMember (groupName, userName)
|
||||
}
|
||||
}
|
||||
|
||||
def getGroupMembers(groupName: String): List[String] =
|
||||
Query(GroupMembers)
|
||||
.filter(_.groupName is groupName.bind)
|
||||
.sortBy(_.userName)
|
||||
.map(_.userName)
|
||||
.list
|
||||
|
||||
def getGroupsByUserName(userName: String): List[String] =
|
||||
Query(GroupMembers)
|
||||
.filter(_.userName is userName.bind)
|
||||
.sortBy(_.groupName)
|
||||
.map(_.groupName)
|
||||
.list
|
||||
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import scala.slick.jdbc.{StaticQuery => Q}
|
||||
import Q.interpolation
|
||||
|
||||
import model._
|
||||
import util.StringUtil._
|
||||
import util.Implicits._
|
||||
import util.StringUtil
|
||||
|
||||
trait IssuesService {
|
||||
import IssuesService._
|
||||
@@ -102,7 +102,10 @@ trait IssuesService {
|
||||
// get issues and comment count
|
||||
val issues = searchIssueQuery(owner, repository, condition, filter, userName)
|
||||
.leftJoin(Query(IssueComments)
|
||||
.filter { _.byRepository(owner, repository) }
|
||||
.filter { t =>
|
||||
(t.byRepository(owner, repository)) &&
|
||||
(t.action inSetBind Seq("comment", "close_comment", "reopen_comment"))
|
||||
}
|
||||
.groupBy { _.issueId }
|
||||
.map { case (issueId, t) => issueId ~ t.length }).on((t1, t2) => t1.issueId is t2._1)
|
||||
.sortBy { case (t1, t2) =>
|
||||
@@ -192,7 +195,7 @@ trait IssuesService {
|
||||
IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
|
||||
|
||||
def createComment(owner: String, repository: String, loginUser: String,
|
||||
issueId: Int, content: String, action: Option[String]) =
|
||||
issueId: Int, content: String, action: String) =
|
||||
IssueComments.autoInc insert (
|
||||
owner,
|
||||
repository,
|
||||
@@ -234,10 +237,60 @@ trait IssuesService {
|
||||
}
|
||||
.update (closed, currentDate)
|
||||
|
||||
/**
|
||||
* Search issues by keyword.
|
||||
*
|
||||
* @param owner the repository owner
|
||||
* @param repository the repository name
|
||||
* @param query the keywords separated by whitespace.
|
||||
* @return issues with comment count and matched content of issue or comment
|
||||
*/
|
||||
def searchIssuesByKeyword(owner: String, repository: String, query: String): List[(Issue, Int, String)] = {
|
||||
import scala.slick.driver.H2Driver.likeEncode
|
||||
val keywords = StringUtil.splitWords(query.toLowerCase)
|
||||
|
||||
// Search Issue
|
||||
val issues = Query(Issues).filter { t =>
|
||||
keywords.map { keyword =>
|
||||
(t.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) ||
|
||||
(t.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^'))
|
||||
} .reduceLeft(_ && _)
|
||||
}.map { t => (t, 0, t.content.?) }
|
||||
|
||||
// Search IssueComment
|
||||
val comments = Query(IssueComments).innerJoin(Issues).on { case (t1, t2) =>
|
||||
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
||||
}.filter { case (t1, t2) =>
|
||||
keywords.map { query =>
|
||||
t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^')
|
||||
}.reduceLeft(_ && _)
|
||||
}.map { case (t1, t2) => (t2, t1.commentId, t1.content.?) }
|
||||
|
||||
def getCommentCount(issue: Issue): Int = {
|
||||
Query(IssueComments)
|
||||
.filter { t =>
|
||||
t.byIssue(issue.userName, issue.repositoryName, issue.issueId) &&
|
||||
(t.action inSetBind Seq("comment", "close_comment", "reopen_comment"))
|
||||
}
|
||||
.map(_.issueId)
|
||||
.list.length
|
||||
}
|
||||
|
||||
issues.union(comments).sortBy { case (issue, commentId, _) =>
|
||||
issue.issueId ~ commentId
|
||||
}.list.splitWith { case ((issue1, _, _), (issue2, _, _)) =>
|
||||
issue1.issueId == issue2.issueId
|
||||
}.map { result =>
|
||||
val (issue, _, content) = result.head
|
||||
(issue, getCommentCount(issue) , content.getOrElse(""))
|
||||
}.toList
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object IssuesService {
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import util.StringUtil._
|
||||
|
||||
val IssueLimit = 30
|
||||
|
||||
@@ -279,4 +332,5 @@ object IssuesService {
|
||||
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
|
||||
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
121
src/main/scala/service/RepositorySearchService.scala
Normal file
121
src/main/scala/service/RepositorySearchService.scala
Normal file
@@ -0,0 +1,121 @@
|
||||
package service
|
||||
|
||||
import model.Issue
|
||||
import util.{FileUtil, StringUtil, JGitUtil}
|
||||
import util.Directory._
|
||||
import model.Issue
|
||||
import org.eclipse.jgit.revwalk.RevWalk
|
||||
import org.eclipse.jgit.treewalk.TreeWalk
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import org.eclipse.jgit.lib.FileMode
|
||||
import org.eclipse.jgit.api.Git
|
||||
|
||||
trait RepositorySearchService { self: IssuesService =>
|
||||
import RepositorySearchService._
|
||||
|
||||
def countIssues(owner: String, repository: String, query: String): Int =
|
||||
searchIssuesByKeyword(owner, repository, query).length
|
||||
|
||||
def searchIssues(owner: String, repository: String, query: String): List[IssueSearchResult] =
|
||||
searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
|
||||
IssueSearchResult(
|
||||
issue.issueId,
|
||||
issue.title,
|
||||
issue.openedUserName,
|
||||
issue.registeredDate,
|
||||
commentCount,
|
||||
getHighlightText(content, query)._1)
|
||||
}
|
||||
|
||||
def countFiles(owner: String, repository: String, query: String): Int =
|
||||
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git =>
|
||||
searchRepositoryFiles(git, query).length
|
||||
}
|
||||
|
||||
def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] =
|
||||
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git =>
|
||||
val files = searchRepositoryFiles(git, query)
|
||||
val commits = JGitUtil.getLatestCommitFromPaths(git, files.toList.map(_._1), "HEAD")
|
||||
files.map { case (path, text) =>
|
||||
val (highlightText, lineNumber) = getHighlightText(text, query)
|
||||
FileSearchResult(
|
||||
path,
|
||||
commits(path).getCommitterIdent.getWhen,
|
||||
highlightText,
|
||||
lineNumber)
|
||||
}
|
||||
}
|
||||
|
||||
private def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = {
|
||||
val revWalk = new RevWalk(git.getRepository)
|
||||
val objectId = git.getRepository.resolve("HEAD")
|
||||
val revCommit = revWalk.parseCommit(objectId)
|
||||
val treeWalk = new TreeWalk(git.getRepository)
|
||||
treeWalk.setRecursive(true)
|
||||
treeWalk.addTree(revCommit.getTree)
|
||||
|
||||
val keywords = StringUtil.splitWords(query.toLowerCase)
|
||||
val list = new ListBuffer[(String, String)]
|
||||
|
||||
while (treeWalk.next()) {
|
||||
if(treeWalk.getFileMode(0) != FileMode.TREE){
|
||||
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes =>
|
||||
if(FileUtil.isText(bytes)){
|
||||
val text = new String(bytes, "UTF-8")
|
||||
val lowerText = text.toLowerCase
|
||||
val indices = keywords.map(lowerText.indexOf _)
|
||||
if(!indices.exists(_ < 0)){
|
||||
list.append((treeWalk.getPathString, text))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
treeWalk.release
|
||||
revWalk.release
|
||||
|
||||
list.toList
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object RepositorySearchService {
|
||||
|
||||
val CodeLimit = 10
|
||||
val IssueLimit = 10
|
||||
|
||||
def getHighlightText(content: String, query: String): (String, Int) = {
|
||||
val keywords = StringUtil.splitWords(query.toLowerCase)
|
||||
val lowerText = content.toLowerCase
|
||||
val indices = keywords.map(lowerText.indexOf _)
|
||||
|
||||
if(!indices.exists(_ < 0)){
|
||||
val lineNumber = content.substring(0, indices.min).split("\n").size - 1
|
||||
val highlightText = StringUtil.escapeHtml(content.split("\n").drop(lineNumber).take(5).mkString("\n"))
|
||||
.replaceAll("(?i)(" + keywords.map("\\Q" + _ + "\\E").mkString("|") + ")",
|
||||
"<span style=\"background-color: #ffff88;;\">$1</span>")
|
||||
(highlightText, lineNumber + 1)
|
||||
} else {
|
||||
(content.split("\n").take(5).mkString("\n"), 1)
|
||||
}
|
||||
}
|
||||
|
||||
case class SearchResult(
|
||||
files : List[(String, String)],
|
||||
issues: List[(Issue, Int, String)])
|
||||
|
||||
case class IssueSearchResult(
|
||||
issueId: Int,
|
||||
title: String,
|
||||
openedUserName: String,
|
||||
registeredDate: java.util.Date,
|
||||
commentCount: Int,
|
||||
highlightText: String)
|
||||
|
||||
case class FileSearchResult(
|
||||
path: String,
|
||||
lastModified: java.util.Date,
|
||||
highlightText: String,
|
||||
highlightLineNumber: Int)
|
||||
|
||||
}
|
||||
@@ -166,6 +166,15 @@ trait RepositoryService { self: AccountService =>
|
||||
def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit =
|
||||
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
|
||||
|
||||
/**
|
||||
* Remove all collaborators from the repository.
|
||||
*
|
||||
* @param userName the user name of the repository owner
|
||||
* @param repositoryName the repository name
|
||||
*/
|
||||
def removeCollaborators(userName: String, repositoryName: String): Unit =
|
||||
Collaborators.filter(_.byRepository(userName, repositoryName)).delete
|
||||
|
||||
/**
|
||||
* Returns the list of collaborators name which is sorted with ascending order.
|
||||
*
|
||||
|
||||
@@ -6,7 +6,6 @@ import org.eclipse.jgit.api.Git
|
||||
import org.apache.commons.io.FileUtils
|
||||
import util.JGitUtil.DiffInfo
|
||||
import util.{Directory, JGitUtil}
|
||||
import org.eclipse.jgit.lib.RepositoryBuilder
|
||||
import org.eclipse.jgit.treewalk.CanonicalTreeParser
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@@ -64,16 +63,16 @@ object WikiService {
|
||||
trait WikiService {
|
||||
import WikiService._
|
||||
|
||||
def createWikiRepository(owner: model.Account, repository: String): Unit = {
|
||||
lock(owner.userName, repository){
|
||||
val dir = Directory.getWikiRepositoryDir(owner.userName, repository)
|
||||
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit = {
|
||||
lock(owner, repository){
|
||||
val dir = Directory.getWikiRepositoryDir(owner, repository)
|
||||
if(!dir.exists){
|
||||
try {
|
||||
JGitUtil.initRepository(dir)
|
||||
saveWikiPage(owner.userName, repository, "Home", "Home", "Welcome to the %s wiki!!".format(repository), owner, "Initial Commit")
|
||||
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit")
|
||||
} finally {
|
||||
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
|
||||
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner.userName, repository))
|
||||
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,10 +118,12 @@ trait WikiService {
|
||||
* Returns the list of wiki page names.
|
||||
*/
|
||||
def getWikiPageList(owner: String, repository: String): List[String] = {
|
||||
JGitUtil.getFileList(Git.open(Directory.getWikiRepositoryDir(owner, repository)), "master", ".")
|
||||
.filter(_.name.endsWith(".md"))
|
||||
.map(_.name.replaceFirst("\\.md$", ""))
|
||||
.sortBy(x => x)
|
||||
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
|
||||
JGitUtil.getFileList(git, "master", ".")
|
||||
.filter(_.name.endsWith(".md"))
|
||||
.map(_.name.replaceFirst("\\.md$", ""))
|
||||
.sortBy(x => x)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,12 +190,16 @@ trait WikiService {
|
||||
|
||||
private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = {
|
||||
if(!workDir.exists){
|
||||
Git.cloneRepository
|
||||
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
|
||||
.setDirectory(workDir)
|
||||
.call
|
||||
val git =
|
||||
Git.cloneRepository
|
||||
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
|
||||
.setDirectory(workDir)
|
||||
.call
|
||||
git.getRepository.close // close .git resources.
|
||||
} else {
|
||||
Git.open(workDir).pull.call
|
||||
JGitUtil.withGit(workDir){ git =>
|
||||
git.pull.call
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,7 @@ class AutoUpdateListener extends org.h2.server.web.DbStarter {
|
||||
} catch {
|
||||
case ex: Throwable => {
|
||||
logger.error("Failed to schema update", ex)
|
||||
ex.printStackTrace()
|
||||
conn.rollback()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
||||
|
||||
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki", ""), "") match {
|
||||
case Some(repository) => {
|
||||
if(!request.getRequestURI.endsWith("/git-receive-pack") && !repository.repository.isPrivate){
|
||||
if(!request.getRequestURI.endsWith("/git-receive-pack") &&
|
||||
!"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){
|
||||
chain.doFilter(req, wrappedResponse)
|
||||
} else {
|
||||
request.getHeader("Authorization") match {
|
||||
|
||||
@@ -85,7 +85,7 @@ class CommitLogHook(owner: String, repository: String, userName: String) extends
|
||||
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
|
||||
val issueId = matchData.group(2)
|
||||
if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){
|
||||
createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage, Some("commit"))
|
||||
createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage, "commit")
|
||||
}
|
||||
}
|
||||
Some(commit)
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
package servlet
|
||||
|
||||
import util.FileUploadUtil
|
||||
import javax.servlet.http.{HttpSessionEvent, HttpSessionListener}
|
||||
import app.FileUploadControllerBase
|
||||
|
||||
/**
|
||||
* Removes session associated temporary files when session is destroyed.
|
||||
*/
|
||||
class SessionCleanupListener extends HttpSessionListener {
|
||||
class SessionCleanupListener extends HttpSessionListener with FileUploadControllerBase {
|
||||
|
||||
def sessionCreated(se: HttpSessionEvent): Unit = {}
|
||||
|
||||
def sessionDestroyed(se: HttpSessionEvent): Unit = {
|
||||
FileUploadUtil.removeTemporaryFiles()(se.getSession)
|
||||
}
|
||||
def sessionDestroyed(se: HttpSessionEvent): Unit = removeTemporaryFiles()(se.getSession)
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package util
|
||||
|
||||
import java.io.File
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.Ref
|
||||
|
||||
/**
|
||||
* Provides directories used by GitBucket.
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package util
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import javax.servlet.http.HttpSession
|
||||
import util.Directory._
|
||||
import org.apache.commons.io.FileUtils
|
||||
|
||||
object FileUploadUtil {
|
||||
|
||||
def generateFileId: String =
|
||||
new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis))
|
||||
|
||||
def TemporaryDir(implicit session: HttpSession): java.io.File =
|
||||
new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}")
|
||||
|
||||
def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File =
|
||||
new java.io.File(TemporaryDir, fileId)
|
||||
|
||||
// def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit =
|
||||
// getTemporaryFile(fileId).delete()
|
||||
|
||||
def removeTemporaryFiles()(implicit session: HttpSession): Unit =
|
||||
FileUtils.deleteDirectory(TemporaryDir)
|
||||
|
||||
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] = {
|
||||
val filename = Option(session.getAttribute("upload_" + fileId).asInstanceOf[String])
|
||||
if(filename.isDefined){
|
||||
session.removeAttribute("upload_" + fileId)
|
||||
}
|
||||
filename
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package util
|
||||
|
||||
import org.apache.commons.io.{IOUtils, FileUtils, FilenameUtils}
|
||||
import org.apache.commons.io.{IOUtils, FileUtils}
|
||||
import java.net.URLConnection
|
||||
import java.io.File
|
||||
import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package util
|
||||
|
||||
import scala.slick.driver.H2Driver.simple._
|
||||
import scala.util.matching.Regex
|
||||
|
||||
/**
|
||||
@@ -25,11 +24,6 @@ object Implicits {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Should this implicit conversion move to model.Functions?
|
||||
implicit class RichColumn(c1: Column[Boolean]){
|
||||
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
|
||||
}
|
||||
|
||||
implicit class RichString(value: String){
|
||||
def replaceBy(regex: Regex)(replace: Regex.MatchData => Option[String]): String = {
|
||||
val sb = new StringBuilder()
|
||||
|
||||
@@ -53,14 +53,21 @@ object JGitUtil {
|
||||
* @param id the commit id
|
||||
* @param time the commit time
|
||||
* @param committer the committer name
|
||||
* @param mailAddress the mail address of the committer
|
||||
* @param shortMessage the short message
|
||||
* @param fullMessage the full message
|
||||
* @param parents the list of parent commit id
|
||||
*/
|
||||
case class CommitInfo(id: String, time: Date, committer: String, shortMessage: String, fullMessage: String, parents: List[String]){
|
||||
case class CommitInfo(id: String, time: Date, committer: String, mailAddress: String,
|
||||
shortMessage: String, fullMessage: String, parents: List[String]){
|
||||
|
||||
def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this(
|
||||
rev.getName, rev.getCommitterIdent.getWhen, rev.getCommitterIdent.getName, rev.getShortMessage, rev.getFullMessage,
|
||||
rev.getName,
|
||||
rev.getCommitterIdent.getWhen,
|
||||
rev.getCommitterIdent.getName,
|
||||
rev.getCommitterIdent.getEmailAddress,
|
||||
rev.getShortMessage,
|
||||
rev.getFullMessage,
|
||||
rev.getParents().map(_.name).toList)
|
||||
|
||||
val summary = {
|
||||
|
||||
@@ -20,4 +20,9 @@ object StringUtil {
|
||||
|
||||
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")
|
||||
|
||||
def splitWords(value: String): Array[String] = value.split("[ \\t ]+")
|
||||
|
||||
def escapeHtml(value: String): String =
|
||||
value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package util
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import scala.Some
|
||||
|
||||
trait Validations {
|
||||
|
||||
|
||||
@@ -10,16 +10,21 @@ trait AvatarImageProvider { self: RequestCache =>
|
||||
* Returns <img> which displays the avatar icon.
|
||||
* Looks up Gravatar if avatar icon has not been configured in user settings.
|
||||
*/
|
||||
protected def getAvatarImageHtml(userName: String, size: Int, tooltip: Boolean = false)(implicit context: app.Context): Html = {
|
||||
protected def getAvatarImageHtml(userName: String, size: Int,
|
||||
mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = {
|
||||
val src = getAccountByUserName(userName).collect { case account if(account.image.isEmpty) =>
|
||||
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}"""
|
||||
} getOrElse {
|
||||
s"""${context.path}/${userName}/_avatar"""
|
||||
if(mailAddress.nonEmpty){
|
||||
s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress)}?s=${size}"""
|
||||
} else {
|
||||
s"""${context.path}/${userName}/_avatar"""
|
||||
}
|
||||
}
|
||||
if(tooltip){
|
||||
Html(s"""<img src=${src} class="avatar" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title=${userName}/>""")
|
||||
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}"/>""")
|
||||
} else {
|
||||
Html(s"""<img src=${src} class="avatar" style="width: ${size}px; height: ${size}px;" />""")
|
||||
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" />""")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,10 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
* Looks up Gravatar if avatar icon has not been configured in user settings.
|
||||
*/
|
||||
def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: app.Context): Html =
|
||||
getAvatarImageHtml(userName, size, tooltip)
|
||||
getAvatarImageHtml(userName, size, "", tooltip)
|
||||
|
||||
def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html =
|
||||
getAvatarImageHtml(commit.committer, size, commit.mailAddress)
|
||||
|
||||
/**
|
||||
* Converts commit id, issue id and username to the link.
|
||||
@@ -80,6 +83,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
def assets(implicit context: app.Context): String =
|
||||
s"${context.path}/assets"
|
||||
|
||||
def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime
|
||||
|
||||
/**
|
||||
* Implicit conversion to add mkHtml() to Seq[Html].
|
||||
|
||||
Reference in New Issue
Block a user