mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-01 19:15:59 +01:00
413 lines
17 KiB
Scala
413 lines
17 KiB
Scala
package app
|
|
|
|
import jp.sf.amateras.scalatra.forms._
|
|
|
|
import service._
|
|
import IssuesService._
|
|
import util._
|
|
import util.Implicits._
|
|
import util.ControlUtil._
|
|
import org.scalatra.Ok
|
|
import model.Issue
|
|
import plugin.PluginSystem
|
|
|
|
class IssuesController extends IssuesControllerBase
|
|
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
|
|
|
|
trait IssuesControllerBase extends ControllerBase {
|
|
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
|
|
|
case class IssueCreateForm(title: String, content: Option[String],
|
|
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
|
|
case class CommentForm(issueId: Int, content: String)
|
|
case class IssueStateForm(issueId: Int, content: Option[String])
|
|
|
|
val issueCreateForm = mapping(
|
|
"title" -> trim(label("Title", text(required))),
|
|
"content" -> trim(optional(text())),
|
|
"assignedUserName" -> trim(optional(text())),
|
|
"milestoneId" -> trim(optional(number())),
|
|
"labelNames" -> trim(optional(text()))
|
|
)(IssueCreateForm.apply)
|
|
|
|
val issueTitleEditForm = mapping(
|
|
"title" -> trim(label("Title", text(required)))
|
|
)(x => x)
|
|
val issueEditForm = mapping(
|
|
"content" -> trim(optional(text()))
|
|
)(x => x)
|
|
|
|
val commentForm = mapping(
|
|
"issueId" -> label("Issue Id", number()),
|
|
"content" -> trim(label("Comment", text(required)))
|
|
)(CommentForm.apply)
|
|
|
|
val issueStateForm = mapping(
|
|
"issueId" -> label("Issue Id", number()),
|
|
"content" -> trim(optional(text()))
|
|
)(IssueStateForm.apply)
|
|
|
|
get("/:owner/:repository/issues")(referrersOnly { repository =>
|
|
searchIssues(repository)
|
|
})
|
|
|
|
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
|
|
defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
|
|
getIssue(owner, name, issueId) map {
|
|
issues.html.issue(
|
|
_,
|
|
getComments(owner, name, issueId.toInt),
|
|
getIssueLabels(owner, name, issueId.toInt),
|
|
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
|
getMilestonesWithIssueCount(owner, name),
|
|
getLabels(owner, name),
|
|
hasWritePermission(owner, name, context.loginAccount),
|
|
repository)
|
|
} getOrElse NotFound
|
|
}
|
|
})
|
|
|
|
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
|
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
|
issues.html.create(
|
|
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
|
getMilestones(owner, name),
|
|
getLabels(owner, name),
|
|
hasWritePermission(owner, name, context.loginAccount),
|
|
repository)
|
|
}
|
|
})
|
|
|
|
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
|
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
|
val writable = hasWritePermission(owner, name, context.loginAccount)
|
|
val userName = context.loginAccount.get.userName
|
|
|
|
// insert issue
|
|
val issueId = createIssue(owner, name, userName, form.title, form.content,
|
|
if(writable) form.assignedUserName else None,
|
|
if(writable) form.milestoneId else None)
|
|
|
|
// insert labels
|
|
if(writable){
|
|
form.labelNames.map { value =>
|
|
val labels = getLabels(owner, name)
|
|
value.split(",").foreach { labelName =>
|
|
labels.find(_.labelName == labelName).map { label =>
|
|
registerIssueLabel(owner, name, issueId, label.labelId)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// record activity
|
|
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
|
|
|
|
// extract references and create refer comment
|
|
getIssue(owner, name, issueId.toString).foreach { issue =>
|
|
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
|
|
}
|
|
|
|
// notifications
|
|
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
|
|
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
|
|
}
|
|
|
|
redirect(s"/${owner}/${name}/issues/${issueId}")
|
|
}
|
|
})
|
|
|
|
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(isEditable(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)
|
|
|
|
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
|
} else Unauthorized
|
|
} getOrElse NotFound
|
|
}
|
|
})
|
|
|
|
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) =>
|
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
|
getIssue(owner, name, params("id")).map { issue =>
|
|
if(isEditable(owner, name, issue.openedUserName)){
|
|
// update issue
|
|
updateIssue(owner, name, issue.issueId, issue.title, content)
|
|
// extract references and create refer comment
|
|
createReferComment(owner, name, issue, content.getOrElse(""))
|
|
|
|
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)() map { case (issue, id) =>
|
|
redirect(s"/${repository.owner}/${repository.name}/${
|
|
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
|
} getOrElse NotFound
|
|
})
|
|
|
|
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
|
|
handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
|
|
redirect(s"/${repository.owner}/${repository.name}/${
|
|
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
|
} getOrElse NotFound
|
|
})
|
|
|
|
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
|
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
|
getComment(owner, name, params("id")).map { comment =>
|
|
if(isEditable(owner, name, comment.commentedUserName)){
|
|
updateComment(comment.commentId, form.content)
|
|
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
|
|
} else Unauthorized
|
|
} getOrElse NotFound
|
|
}
|
|
})
|
|
|
|
ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository =>
|
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
|
getComment(owner, name, params("id")).map { comment =>
|
|
if(isEditable(owner, name, comment.commentedUserName)){
|
|
Ok(deleteComment(comment.commentId))
|
|
} else Unauthorized
|
|
} getOrElse NotFound
|
|
}
|
|
})
|
|
|
|
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
|
|
getIssue(repository.owner, repository.name, params("id")) map { x =>
|
|
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
|
|
params.get("dataType") collect {
|
|
case t if t == "html" => issues.html.editissue(
|
|
x.content, x.issueId, x.userName, x.repositoryName)
|
|
} getOrElse {
|
|
contentType = formats("json")
|
|
org.json4s.jackson.Serialization.write(
|
|
Map("title" -> x.title,
|
|
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
|
|
repository, false, true)
|
|
))
|
|
}
|
|
} else Unauthorized
|
|
} getOrElse NotFound
|
|
})
|
|
|
|
ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
|
|
getComment(repository.owner, repository.name, params("id")) map { x =>
|
|
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
|
|
params.get("dataType") collect {
|
|
case t if t == "html" => issues.html.editcomment(
|
|
x.content, x.commentId, x.userName, x.repositoryName)
|
|
} getOrElse {
|
|
contentType = formats("json")
|
|
org.json4s.jackson.Serialization.write(
|
|
Map("content" -> view.Markdown.toHtml(x.content,
|
|
repository, false, true)
|
|
))
|
|
}
|
|
} else Unauthorized
|
|
} getOrElse NotFound
|
|
})
|
|
|
|
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
|
|
defining(params("id").toInt){ issueId =>
|
|
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
|
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
|
}
|
|
})
|
|
|
|
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
|
|
defining(params("id").toInt){ issueId =>
|
|
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
|
|
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
|
|
}
|
|
})
|
|
|
|
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
|
|
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, 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)
|
|
} getOrElse NotFound
|
|
} getOrElse Ok()
|
|
})
|
|
|
|
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
|
|
defining(params.get("value")){ action =>
|
|
action match {
|
|
case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) }
|
|
case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) }
|
|
case _ => // TODO BadRequest
|
|
}
|
|
}
|
|
})
|
|
|
|
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
|
|
params("value").toIntOpt.map{ labelId =>
|
|
executeBatch(repository) { issueId =>
|
|
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
|
|
registerIssueLabel(repository.owner, repository.name, issueId, labelId)
|
|
}
|
|
}
|
|
} getOrElse NotFound
|
|
})
|
|
|
|
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
|
|
defining(assignedUserName("value")){ value =>
|
|
executeBatch(repository) {
|
|
updateAssignedUserName(repository.owner, repository.name, _, value)
|
|
}
|
|
}
|
|
})
|
|
|
|
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
|
|
defining(milestoneId("value")){ value =>
|
|
executeBatch(repository) {
|
|
updateMilestoneId(repository.owner, repository.name, _, value)
|
|
}
|
|
}
|
|
})
|
|
|
|
get("/:owner/:repository/_attached/:file")(referrersOnly { repository =>
|
|
(Directory.getAttachedDir(repository.owner, repository.name) match {
|
|
case dir if(dir.exists && dir.isDirectory) =>
|
|
dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
|
|
contentType = FileUtil.getMimeType(file.getName)
|
|
file
|
|
}
|
|
case _ => None
|
|
}) getOrElse NotFound
|
|
})
|
|
|
|
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
|
|
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
|
|
|
|
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 executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
|
|
params("checked").split(',') map(_.toInt) foreach execute
|
|
params("from") match {
|
|
case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues")
|
|
case "pulls" => redirect(s"/${repository.owner}/${repository.name}/pulls")
|
|
}
|
|
}
|
|
|
|
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
|
|
StringUtil.extractIssueId(message).foreach { issueId =>
|
|
if(getIssue(owner, repository, issueId).isDefined){
|
|
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt,
|
|
fromIssue.issueId + ":" + fromIssue.title, "refer")
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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))) = {
|
|
|
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
|
val userName = context.loginAccount.get.userName
|
|
|
|
getIssue(owner, name, issueId.toString) map { issue =>
|
|
val (action, recordActivity) =
|
|
getAction(issue)
|
|
.collect {
|
|
case "close" if(!issue.closed) => true ->
|
|
(Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
|
|
case "reopen" if(issue.closed) => false ->
|
|
(Some("reopen") -> Some(recordReopenIssueActivity _))
|
|
}
|
|
.map { case (closed, t) =>
|
|
updateClosed(owner, name, issueId, closed)
|
|
t
|
|
}
|
|
.getOrElse(None -> None)
|
|
|
|
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 comment activity if comment is entered
|
|
content foreach {
|
|
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
|
|
(owner, name, userName, issueId, _)
|
|
}
|
|
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
|
|
|
|
// extract references and create refer comment
|
|
content.map { content =>
|
|
createReferComment(owner, name, issue, content)
|
|
}
|
|
|
|
// notifications
|
|
Notifier() match {
|
|
case f =>
|
|
content foreach {
|
|
f.toNotify(repository, issueId, _){
|
|
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
|
|
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
|
|
}
|
|
}
|
|
action foreach {
|
|
f.toNotify(repository, issueId, _){
|
|
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
|
|
}
|
|
}
|
|
}
|
|
|
|
issue -> commentId
|
|
}
|
|
}
|
|
}
|
|
|
|
private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
|
|
defining(repository.owner, repository.name){ case (owner, repoName) =>
|
|
val page = IssueSearchCondition.page(request)
|
|
val sessionKey = Keys.Session.Issues(owner, repoName)
|
|
|
|
// retrieve search condition
|
|
val condition = session.putAndGet(sessionKey,
|
|
if(request.hasQueryString) IssueSearchCondition(request)
|
|
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
|
|
)
|
|
|
|
issues.html.list(
|
|
"issues",
|
|
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
|
|
page,
|
|
(getCollaborators(owner, repoName) :+ owner).sorted,
|
|
getMilestones(owner, repoName),
|
|
getLabels(owner, repoName),
|
|
countIssue(condition.copy(state = "open" ), false, owner -> repoName),
|
|
countIssue(condition.copy(state = "closed"), false, owner -> repoName),
|
|
condition,
|
|
repository,
|
|
hasWritePermission(owner, repoName, context.loginAccount))
|
|
}
|
|
}
|
|
|
|
}
|