mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-10-31 18:46:28 +01:00
371 lines
15 KiB
Scala
371 lines
15 KiB
Scala
package service
|
|
|
|
import scala.slick.driver.H2Driver.simple._
|
|
import Database.threadLocalSession
|
|
import scala.slick.jdbc.{StaticQuery => Q}
|
|
import Q.interpolation
|
|
|
|
import model._
|
|
import util.Implicits._
|
|
import util.StringUtil._
|
|
|
|
trait IssuesService {
|
|
import IssuesService._
|
|
|
|
def getIssue(owner: String, repository: String, issueId: String) =
|
|
if (issueId forall (_.isDigit))
|
|
Query(Issues) filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption
|
|
else None
|
|
|
|
def getComments(owner: String, repository: String, issueId: Int) =
|
|
Query(IssueComments) filter (_.byIssue(owner, repository, issueId)) list
|
|
|
|
def getComment(owner: String, repository: String, commentId: String) =
|
|
if (commentId forall (_.isDigit))
|
|
Query(IssueComments) filter { t =>
|
|
t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository)
|
|
} firstOption
|
|
else None
|
|
|
|
def getIssueLabels(owner: String, repository: String, issueId: Int) =
|
|
IssueLabels
|
|
.innerJoin(Labels).on { (t1, t2) =>
|
|
t1.byLabel(t2.userName, t2.repositoryName, t2.labelId)
|
|
}
|
|
.filter ( _._1.byIssue(owner, repository, issueId) )
|
|
.map ( _._2 )
|
|
.list
|
|
|
|
def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) =
|
|
Query(IssueLabels) filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption
|
|
|
|
/**
|
|
* Returns the count of the search result against issues.
|
|
*
|
|
* @param condition the search condition
|
|
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
|
|
* @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request.
|
|
* @param repos Tuple of the repository owner and the repository name
|
|
* @return the count of the search result
|
|
*/
|
|
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
|
|
repos: (String, String)*): Int =
|
|
Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
|
|
/**
|
|
* Returns the Map which contains issue count for each labels.
|
|
*
|
|
* @param owner the repository owner
|
|
* @param repository the repository name
|
|
* @param condition the search condition
|
|
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
|
|
* @return the Map which contains issue count for each labels (key is label name, value is issue count)
|
|
*/
|
|
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
|
|
filterUser: Map[String, String]): Map[String, Int] = {
|
|
|
|
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false)
|
|
.innerJoin(IssueLabels).on { (t1, t2) =>
|
|
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
|
}
|
|
.innerJoin(Labels).on { case ((t1, t2), t3) =>
|
|
t2.byLabel(t3.userName, t3.repositoryName, t3.labelId)
|
|
}
|
|
.groupBy { case ((t1, t2), t3) =>
|
|
t3.labelName
|
|
}
|
|
.map { case (labelName, t) =>
|
|
labelName ~ t.length
|
|
}
|
|
.toMap
|
|
}
|
|
/**
|
|
* Returns list which contains issue count for each repository.
|
|
* If the issue does not exist, its repository is not included in the result.
|
|
*
|
|
* @param condition the search condition
|
|
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
|
|
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
|
|
* @param repos Tuple of the repository owner and the repository name
|
|
* @return list which contains issue count for each repository
|
|
*/
|
|
def countIssueGroupByRepository(
|
|
condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
|
|
repos: (String, String)*): List[(String, String, Int)] = {
|
|
searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
|
|
.groupBy { t =>
|
|
t.userName ~ t.repositoryName
|
|
}
|
|
.map { case (repo, t) =>
|
|
repo ~ t.length
|
|
}
|
|
.sortBy(_._3 desc)
|
|
.list
|
|
}
|
|
|
|
/**
|
|
* Returns the search result against issues.
|
|
*
|
|
* @param condition the search condition
|
|
* @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name)
|
|
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
|
|
* @param offset the offset for pagination
|
|
* @param limit the limit for pagination
|
|
* @param repos Tuple of the repository owner and the repository name
|
|
* @return the search result (list of tuples which contain issue, labels and comment count)
|
|
*/
|
|
def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
|
|
offset: Int, limit: Int, repos: (String, String)*): List[(Issue, List[Label], Int)] = {
|
|
|
|
// get issues and comment count and labels
|
|
searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
|
|
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
|
|
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
|
|
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
|
|
.map { case (((t1, t2), t3), t4) =>
|
|
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
|
|
}
|
|
.sortBy(_._4) // labelName
|
|
.sortBy { case (t1, commentCount, _,_,_) =>
|
|
(condition.sort match {
|
|
case "created" => t1.registeredDate
|
|
case "comments" => commentCount
|
|
case "updated" => t1.updatedDate
|
|
}) match {
|
|
case sort => condition.direction match {
|
|
case "asc" => sort asc
|
|
case "desc" => sort desc
|
|
}
|
|
}
|
|
}
|
|
.drop(offset).take(limit)
|
|
.list
|
|
.splitWith { (c1, c2) =>
|
|
c1._1.userName == c2._1.userName &&
|
|
c1._1.repositoryName == c2._1.repositoryName &&
|
|
c1._1.issueId == c2._1.issueId
|
|
}
|
|
.map { issues => issues.head match {
|
|
case (issue, commentCount, _,_,_) =>
|
|
(issue,
|
|
issues.flatMap { t => t._3.map (
|
|
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
|
|
)} toList,
|
|
commentCount)
|
|
}} toList
|
|
}
|
|
|
|
/**
|
|
* Assembles query for conditional issue searching.
|
|
*/
|
|
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
|
|
filterUser: Map[String, String], onlyPullRequest: Boolean) =
|
|
Query(Issues) filter { t1 =>
|
|
condition.repo
|
|
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
|
|
.getOrElse (repos)
|
|
.map { case (owner, repository) => t1.byRepository(owner, repository) }
|
|
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
|
|
(t1.closed is (condition.state == "closed").bind) &&
|
|
(t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
|
|
(t1.milestoneId isNull, condition.milestoneId == Some(None)) &&
|
|
(t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
|
|
(t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
|
|
(t1.openedUserName isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) &&
|
|
(t1.pullRequest is true.bind, onlyPullRequest) &&
|
|
(IssueLabels filter { t2 =>
|
|
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
|
|
(t2.labelId in
|
|
(Labels filter { t3 =>
|
|
(t3.byRepository(t1.userName, t1.repositoryName)) &&
|
|
(t3.labelName inSetBind condition.labels)
|
|
} map(_.labelId)))
|
|
} exists, condition.labels.nonEmpty)
|
|
}
|
|
|
|
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
|
|
assignedUserName: Option[String], milestoneId: Option[Int], isPullRequest: Boolean = false) =
|
|
// next id number
|
|
sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int]
|
|
.firstOption.filter { id =>
|
|
Issues insert Issue(
|
|
owner,
|
|
repository,
|
|
id,
|
|
loginUser,
|
|
milestoneId,
|
|
assignedUserName,
|
|
title,
|
|
content,
|
|
false,
|
|
currentDate,
|
|
currentDate,
|
|
isPullRequest)
|
|
|
|
// increment issue id
|
|
IssueId
|
|
.filter (_.byPrimaryKey(owner, repository))
|
|
.map (_.issueId)
|
|
.update (id) > 0
|
|
} get
|
|
|
|
def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) =
|
|
IssueLabels insert (IssueLabel(owner, repository, issueId, labelId))
|
|
|
|
def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) =
|
|
IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
|
|
|
|
def createComment(owner: String, repository: String, loginUser: String,
|
|
issueId: Int, content: String, action: String) =
|
|
IssueComments.autoInc insert (
|
|
owner,
|
|
repository,
|
|
issueId,
|
|
action,
|
|
loginUser,
|
|
content,
|
|
currentDate,
|
|
currentDate)
|
|
|
|
def updateIssue(owner: String, repository: String, issueId: Int,
|
|
title: String, content: Option[String]) =
|
|
Issues
|
|
.filter (_.byPrimaryKey(owner, repository, issueId))
|
|
.map { t =>
|
|
t.title ~ t.content.? ~ t.updatedDate
|
|
}
|
|
.update (title, content, currentDate)
|
|
|
|
def updateAssignedUserName(owner: String, repository: String, issueId: Int, assignedUserName: Option[String]) =
|
|
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName)
|
|
|
|
def updateMilestoneId(owner: String, repository: String, issueId: Int, milestoneId: Option[Int]) =
|
|
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId)
|
|
|
|
def updateComment(commentId: Int, content: String) =
|
|
IssueComments
|
|
.filter (_.byPrimaryKey(commentId))
|
|
.map { t =>
|
|
t.content ~ t.updatedDate
|
|
}
|
|
.update (content, currentDate)
|
|
|
|
def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean) =
|
|
Issues
|
|
.filter (_.byPrimaryKey(owner, repository, issueId))
|
|
.map { t =>
|
|
t.closed ~ t.updatedDate
|
|
}
|
|
.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 = splitWords(query.toLowerCase)
|
|
|
|
// Search Issue
|
|
val issues = Issues
|
|
.innerJoin(IssueOutline).on { case (t1, t2) =>
|
|
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
|
}
|
|
.filter { case (t1, t2) =>
|
|
keywords.map { keyword =>
|
|
(t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) ||
|
|
(t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^'))
|
|
} .reduceLeft(_ && _)
|
|
}
|
|
.map { case (t1, t2) =>
|
|
(t1, 0, t1.content.?, t2.commentCount)
|
|
}
|
|
|
|
// Search IssueComment
|
|
val comments = IssueComments
|
|
.innerJoin(Issues).on { case (t1, t2) =>
|
|
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
|
}
|
|
.innerJoin(IssueOutline).on { case ((t1, t2), t3) =>
|
|
t2.byIssue(t3.userName, t3.repositoryName, t3.issueId)
|
|
}
|
|
.filter { case ((t1, t2), t3) =>
|
|
keywords.map { query =>
|
|
t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^')
|
|
}.reduceLeft(_ && _)
|
|
}
|
|
.map { case ((t1, t2), t3) =>
|
|
(t2, t1.commentId, t1.content.?, t3.commentCount)
|
|
}
|
|
|
|
issues.union(comments).sortBy { case (issue, commentId, _, _) =>
|
|
issue.issueId ~ commentId
|
|
}.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) =>
|
|
issue1.issueId == issue2.issueId
|
|
}.map { _.head match {
|
|
case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse(""))
|
|
}
|
|
}.toList
|
|
}
|
|
|
|
}
|
|
|
|
object IssuesService {
|
|
import javax.servlet.http.HttpServletRequest
|
|
|
|
val IssueLimit = 30
|
|
|
|
case class IssueSearchCondition(
|
|
labels: Set[String] = Set.empty,
|
|
milestoneId: Option[Option[Int]] = None,
|
|
repo: Option[String] = None,
|
|
state: String = "open",
|
|
sort: String = "created",
|
|
direction: String = "desc"){
|
|
|
|
def toURL: String =
|
|
"?" + List(
|
|
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(" "))),
|
|
milestoneId.map { id => "milestone=" + (id match {
|
|
case Some(x) => x.toString
|
|
case None => "none"
|
|
})},
|
|
repo.map("for=" + urlEncode(_)),
|
|
Some("state=" + urlEncode(state)),
|
|
Some("sort=" + urlEncode(sort)),
|
|
Some("direction=" + urlEncode(direction))).flatten.mkString("&")
|
|
|
|
}
|
|
|
|
object IssueSearchCondition {
|
|
|
|
private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = {
|
|
val value = request.getParameter(name)
|
|
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
|
|
}
|
|
|
|
def apply(request: HttpServletRequest): IssueSearchCondition =
|
|
IssueSearchCondition(
|
|
param(request, "labels").map(_.split(" ").toSet).getOrElse(Set.empty),
|
|
param(request, "milestone").map{
|
|
case "none" => None
|
|
case x => x.toIntOpt
|
|
},
|
|
param(request, "for"),
|
|
param(request, "state", Seq("open", "closed")).getOrElse("open"),
|
|
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
|
|
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
|
|
|
|
def page(request: HttpServletRequest) = try {
|
|
val i = param(request, "page").getOrElse("1").toInt
|
|
if(i <= 0) 1 else i
|
|
} catch {
|
|
case e: NumberFormatException => 1
|
|
}
|
|
}
|
|
|
|
}
|