mirror of
				https://github.com/gitbucket/gitbucket.git
				synced 2025-10-31 10:36:05 +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
 | |
|     }
 | |
|   }
 | |
| 
 | |
| }
 |