mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-08 06:25:51 +01:00
(refs #310)Convert all CRLF to LF
This commit is contained in:
@@ -1,380 +1,380 @@
|
||||
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) }
|
||||
.sortBy { case (t1, t2) =>
|
||||
(condition.sort match {
|
||||
case "created" => t1.registeredDate
|
||||
case "comments" => t2.commentCount
|
||||
case "updated" => t1.updatedDate
|
||||
}) match {
|
||||
case sort => condition.direction match {
|
||||
case "asc" => sort asc
|
||||
case "desc" => sort desc
|
||||
}
|
||||
}
|
||||
}
|
||||
.drop(offset).take(limit)
|
||||
.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.?)
|
||||
}
|
||||
.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 deleteComment(commentId: Int) =
|
||||
IssueComments filter (_.byPrimaryKey(commentId)) delete
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String) = {
|
||||
extractCloseId(message).foreach { issueId =>
|
||||
for(issue <- getIssue(owner, repository, issueId) if !issue.closed){
|
||||
createComment(owner, repository, userName, issue.issueId, "Close", "close")
|
||||
updateClosed(owner, repository, issue.issueId, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
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) }
|
||||
.sortBy { case (t1, t2) =>
|
||||
(condition.sort match {
|
||||
case "created" => t1.registeredDate
|
||||
case "comments" => t2.commentCount
|
||||
case "updated" => t1.updatedDate
|
||||
}) match {
|
||||
case sort => condition.direction match {
|
||||
case "asc" => sort asc
|
||||
case "desc" => sort desc
|
||||
}
|
||||
}
|
||||
}
|
||||
.drop(offset).take(limit)
|
||||
.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.?)
|
||||
}
|
||||
.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 deleteComment(commentId: Int) =
|
||||
IssueComments filter (_.byPrimaryKey(commentId)) delete
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String) = {
|
||||
extractCloseId(message).foreach { issueId =>
|
||||
for(issue <- getIssue(owner, repository, issueId) if !issue.closed){
|
||||
createComment(owner, repository, userName, issue.issueId, "Close", "close")
|
||||
updateClosed(owner, repository, issue.issueId, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,190 +1,190 @@
|
||||
package service
|
||||
|
||||
import util.Directory._
|
||||
import util.ControlUtil._
|
||||
import SystemSettingsService._
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
trait SystemSettingsService {
|
||||
|
||||
def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl(request)
|
||||
|
||||
def saveSystemSettings(settings: SystemSettings): Unit = {
|
||||
defining(new java.util.Properties()){ props =>
|
||||
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
|
||||
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
|
||||
props.setProperty(Gravatar, settings.gravatar.toString)
|
||||
props.setProperty(Notification, settings.notification.toString)
|
||||
props.setProperty(Ssh, settings.ssh.toString)
|
||||
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
|
||||
if(settings.notification) {
|
||||
settings.smtp.foreach { smtp =>
|
||||
props.setProperty(SmtpHost, smtp.host)
|
||||
smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
|
||||
smtp.user.foreach(props.setProperty(SmtpUser, _))
|
||||
smtp.password.foreach(props.setProperty(SmtpPassword, _))
|
||||
smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
|
||||
smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _))
|
||||
smtp.fromName.foreach(props.setProperty(SmtpFromName, _))
|
||||
}
|
||||
}
|
||||
props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
|
||||
if(settings.ldapAuthentication){
|
||||
settings.ldap.map { ldap =>
|
||||
props.setProperty(LdapHost, ldap.host)
|
||||
ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
|
||||
ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x))
|
||||
ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
|
||||
props.setProperty(LdapBaseDN, ldap.baseDN)
|
||||
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
|
||||
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
|
||||
props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
|
||||
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
|
||||
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
|
||||
}
|
||||
}
|
||||
using(new java.io.FileOutputStream(GitBucketConf)){ out =>
|
||||
props.store(out, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def loadSystemSettings(): SystemSettings = {
|
||||
defining(new java.util.Properties()){ props =>
|
||||
if(GitBucketConf.exists){
|
||||
using(new java.io.FileInputStream(GitBucketConf)){ in =>
|
||||
props.load(in)
|
||||
}
|
||||
}
|
||||
SystemSettings(
|
||||
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
|
||||
getValue(props, AllowAccountRegistration, false),
|
||||
getValue(props, Gravatar, true),
|
||||
getValue(props, Notification, false),
|
||||
getValue(props, Ssh, false),
|
||||
getOptionValue(props, SshPort, Some(DefaultSshPort)),
|
||||
if(getValue(props, Notification, false)){
|
||||
Some(Smtp(
|
||||
getValue(props, SmtpHost, ""),
|
||||
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
|
||||
getOptionValue(props, SmtpUser, None),
|
||||
getOptionValue(props, SmtpPassword, None),
|
||||
getOptionValue[Boolean](props, SmtpSsl, None),
|
||||
getOptionValue(props, SmtpFromAddress, None),
|
||||
getOptionValue(props, SmtpFromName, None)))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
getValue(props, LdapAuthentication, false),
|
||||
if(getValue(props, LdapAuthentication, false)){
|
||||
Some(Ldap(
|
||||
getValue(props, LdapHost, ""),
|
||||
getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
|
||||
getOptionValue(props, LdapBindDN, None),
|
||||
getOptionValue(props, LdapBindPassword, None),
|
||||
getValue(props, LdapBaseDN, ""),
|
||||
getValue(props, LdapUserNameAttribute, ""),
|
||||
getOptionValue(props, LdapFullNameAttribute, None),
|
||||
getValue(props, LdapMailAddressAttribute, ""),
|
||||
getOptionValue[Boolean](props, LdapTls, None),
|
||||
getOptionValue(props, LdapKeystore, None)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object SystemSettingsService {
|
||||
import scala.reflect.ClassTag
|
||||
|
||||
case class SystemSettings(
|
||||
baseUrl: Option[String],
|
||||
allowAccountRegistration: Boolean,
|
||||
gravatar: Boolean,
|
||||
notification: Boolean,
|
||||
ssh: Boolean,
|
||||
sshPort: Option[Int],
|
||||
smtp: Option[Smtp],
|
||||
ldapAuthentication: Boolean,
|
||||
ldap: Option[Ldap]){
|
||||
def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse {
|
||||
defining(request.getRequestURL.toString){ url =>
|
||||
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
|
||||
}
|
||||
}.stripSuffix("/")
|
||||
}
|
||||
|
||||
case class Ldap(
|
||||
host: String,
|
||||
port: Option[Int],
|
||||
bindDN: Option[String],
|
||||
bindPassword: Option[String],
|
||||
baseDN: String,
|
||||
userNameAttribute: String,
|
||||
fullNameAttribute: Option[String],
|
||||
mailAttribute: String,
|
||||
tls: Option[Boolean],
|
||||
keystore: Option[String])
|
||||
|
||||
case class Smtp(
|
||||
host: String,
|
||||
port: Option[Int],
|
||||
user: Option[String],
|
||||
password: Option[String],
|
||||
ssl: Option[Boolean],
|
||||
fromAddress: Option[String],
|
||||
fromName: Option[String])
|
||||
|
||||
val DefaultSshPort = 29418
|
||||
val DefaultSmtpPort = 25
|
||||
val DefaultLdapPort = 389
|
||||
|
||||
private val BaseURL = "base_url"
|
||||
private val AllowAccountRegistration = "allow_account_registration"
|
||||
private val Gravatar = "gravatar"
|
||||
private val Notification = "notification"
|
||||
private val Ssh = "ssh"
|
||||
private val SshPort = "ssh.port"
|
||||
private val SmtpHost = "smtp.host"
|
||||
private val SmtpPort = "smtp.port"
|
||||
private val SmtpUser = "smtp.user"
|
||||
private val SmtpPassword = "smtp.password"
|
||||
private val SmtpSsl = "smtp.ssl"
|
||||
private val SmtpFromAddress = "smtp.from_address"
|
||||
private val SmtpFromName = "smtp.from_name"
|
||||
private val LdapAuthentication = "ldap_authentication"
|
||||
private val LdapHost = "ldap.host"
|
||||
private val LdapPort = "ldap.port"
|
||||
private val LdapBindDN = "ldap.bindDN"
|
||||
private val LdapBindPassword = "ldap.bind_password"
|
||||
private val LdapBaseDN = "ldap.baseDN"
|
||||
private val LdapUserNameAttribute = "ldap.username_attribute"
|
||||
private val LdapFullNameAttribute = "ldap.fullname_attribute"
|
||||
private val LdapMailAddressAttribute = "ldap.mail_attribute"
|
||||
private val LdapTls = "ldap.tls"
|
||||
private val LdapKeystore = "ldap.keystore"
|
||||
|
||||
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
|
||||
defining(props.getProperty(key)){ value =>
|
||||
if(value == null || value.isEmpty) default
|
||||
else convertType(value).asInstanceOf[A]
|
||||
}
|
||||
|
||||
private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] =
|
||||
defining(props.getProperty(key)){ value =>
|
||||
if(value == null || value.isEmpty) default
|
||||
else Some(convertType(value)).asInstanceOf[Option[A]]
|
||||
}
|
||||
|
||||
private def convertType[A: ClassTag](value: String) =
|
||||
defining(implicitly[ClassTag[A]].runtimeClass){ c =>
|
||||
if(c == classOf[Boolean]) value.toBoolean
|
||||
else if(c == classOf[Int]) value.toInt
|
||||
else value
|
||||
}
|
||||
|
||||
}
|
||||
package service
|
||||
|
||||
import util.Directory._
|
||||
import util.ControlUtil._
|
||||
import SystemSettingsService._
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
trait SystemSettingsService {
|
||||
|
||||
def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl(request)
|
||||
|
||||
def saveSystemSettings(settings: SystemSettings): Unit = {
|
||||
defining(new java.util.Properties()){ props =>
|
||||
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
|
||||
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
|
||||
props.setProperty(Gravatar, settings.gravatar.toString)
|
||||
props.setProperty(Notification, settings.notification.toString)
|
||||
props.setProperty(Ssh, settings.ssh.toString)
|
||||
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
|
||||
if(settings.notification) {
|
||||
settings.smtp.foreach { smtp =>
|
||||
props.setProperty(SmtpHost, smtp.host)
|
||||
smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
|
||||
smtp.user.foreach(props.setProperty(SmtpUser, _))
|
||||
smtp.password.foreach(props.setProperty(SmtpPassword, _))
|
||||
smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
|
||||
smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _))
|
||||
smtp.fromName.foreach(props.setProperty(SmtpFromName, _))
|
||||
}
|
||||
}
|
||||
props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
|
||||
if(settings.ldapAuthentication){
|
||||
settings.ldap.map { ldap =>
|
||||
props.setProperty(LdapHost, ldap.host)
|
||||
ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
|
||||
ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x))
|
||||
ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
|
||||
props.setProperty(LdapBaseDN, ldap.baseDN)
|
||||
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
|
||||
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
|
||||
props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
|
||||
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
|
||||
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
|
||||
}
|
||||
}
|
||||
using(new java.io.FileOutputStream(GitBucketConf)){ out =>
|
||||
props.store(out, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def loadSystemSettings(): SystemSettings = {
|
||||
defining(new java.util.Properties()){ props =>
|
||||
if(GitBucketConf.exists){
|
||||
using(new java.io.FileInputStream(GitBucketConf)){ in =>
|
||||
props.load(in)
|
||||
}
|
||||
}
|
||||
SystemSettings(
|
||||
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
|
||||
getValue(props, AllowAccountRegistration, false),
|
||||
getValue(props, Gravatar, true),
|
||||
getValue(props, Notification, false),
|
||||
getValue(props, Ssh, false),
|
||||
getOptionValue(props, SshPort, Some(DefaultSshPort)),
|
||||
if(getValue(props, Notification, false)){
|
||||
Some(Smtp(
|
||||
getValue(props, SmtpHost, ""),
|
||||
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
|
||||
getOptionValue(props, SmtpUser, None),
|
||||
getOptionValue(props, SmtpPassword, None),
|
||||
getOptionValue[Boolean](props, SmtpSsl, None),
|
||||
getOptionValue(props, SmtpFromAddress, None),
|
||||
getOptionValue(props, SmtpFromName, None)))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
getValue(props, LdapAuthentication, false),
|
||||
if(getValue(props, LdapAuthentication, false)){
|
||||
Some(Ldap(
|
||||
getValue(props, LdapHost, ""),
|
||||
getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
|
||||
getOptionValue(props, LdapBindDN, None),
|
||||
getOptionValue(props, LdapBindPassword, None),
|
||||
getValue(props, LdapBaseDN, ""),
|
||||
getValue(props, LdapUserNameAttribute, ""),
|
||||
getOptionValue(props, LdapFullNameAttribute, None),
|
||||
getValue(props, LdapMailAddressAttribute, ""),
|
||||
getOptionValue[Boolean](props, LdapTls, None),
|
||||
getOptionValue(props, LdapKeystore, None)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object SystemSettingsService {
|
||||
import scala.reflect.ClassTag
|
||||
|
||||
case class SystemSettings(
|
||||
baseUrl: Option[String],
|
||||
allowAccountRegistration: Boolean,
|
||||
gravatar: Boolean,
|
||||
notification: Boolean,
|
||||
ssh: Boolean,
|
||||
sshPort: Option[Int],
|
||||
smtp: Option[Smtp],
|
||||
ldapAuthentication: Boolean,
|
||||
ldap: Option[Ldap]){
|
||||
def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse {
|
||||
defining(request.getRequestURL.toString){ url =>
|
||||
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
|
||||
}
|
||||
}.stripSuffix("/")
|
||||
}
|
||||
|
||||
case class Ldap(
|
||||
host: String,
|
||||
port: Option[Int],
|
||||
bindDN: Option[String],
|
||||
bindPassword: Option[String],
|
||||
baseDN: String,
|
||||
userNameAttribute: String,
|
||||
fullNameAttribute: Option[String],
|
||||
mailAttribute: String,
|
||||
tls: Option[Boolean],
|
||||
keystore: Option[String])
|
||||
|
||||
case class Smtp(
|
||||
host: String,
|
||||
port: Option[Int],
|
||||
user: Option[String],
|
||||
password: Option[String],
|
||||
ssl: Option[Boolean],
|
||||
fromAddress: Option[String],
|
||||
fromName: Option[String])
|
||||
|
||||
val DefaultSshPort = 29418
|
||||
val DefaultSmtpPort = 25
|
||||
val DefaultLdapPort = 389
|
||||
|
||||
private val BaseURL = "base_url"
|
||||
private val AllowAccountRegistration = "allow_account_registration"
|
||||
private val Gravatar = "gravatar"
|
||||
private val Notification = "notification"
|
||||
private val Ssh = "ssh"
|
||||
private val SshPort = "ssh.port"
|
||||
private val SmtpHost = "smtp.host"
|
||||
private val SmtpPort = "smtp.port"
|
||||
private val SmtpUser = "smtp.user"
|
||||
private val SmtpPassword = "smtp.password"
|
||||
private val SmtpSsl = "smtp.ssl"
|
||||
private val SmtpFromAddress = "smtp.from_address"
|
||||
private val SmtpFromName = "smtp.from_name"
|
||||
private val LdapAuthentication = "ldap_authentication"
|
||||
private val LdapHost = "ldap.host"
|
||||
private val LdapPort = "ldap.port"
|
||||
private val LdapBindDN = "ldap.bindDN"
|
||||
private val LdapBindPassword = "ldap.bind_password"
|
||||
private val LdapBaseDN = "ldap.baseDN"
|
||||
private val LdapUserNameAttribute = "ldap.username_attribute"
|
||||
private val LdapFullNameAttribute = "ldap.fullname_attribute"
|
||||
private val LdapMailAddressAttribute = "ldap.mail_attribute"
|
||||
private val LdapTls = "ldap.tls"
|
||||
private val LdapKeystore = "ldap.keystore"
|
||||
|
||||
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
|
||||
defining(props.getProperty(key)){ value =>
|
||||
if(value == null || value.isEmpty) default
|
||||
else convertType(value).asInstanceOf[A]
|
||||
}
|
||||
|
||||
private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] =
|
||||
defining(props.getProperty(key)){ value =>
|
||||
if(value == null || value.isEmpty) default
|
||||
else Some(convertType(value)).asInstanceOf[Option[A]]
|
||||
}
|
||||
|
||||
private def convertType[A: ClassTag](value: String) =
|
||||
defining(implicitly[ClassTag[A]].runtimeClass){ c =>
|
||||
if(c == classOf[Boolean]) value.toBoolean
|
||||
else if(c == classOf[Int]) value.toInt
|
||||
else value
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,282 +1,282 @@
|
||||
package service
|
||||
|
||||
import java.util.Date
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.apache.commons.io.FileUtils
|
||||
import util._
|
||||
import _root_.util.ControlUtil._
|
||||
import org.eclipse.jgit.treewalk.{TreeWalk, CanonicalTreeParser}
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.eclipse.jgit.dircache.{DirCache, DirCacheEntry}
|
||||
import org.eclipse.jgit.revwalk.RevWalk
|
||||
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter}
|
||||
import java.io.ByteArrayInputStream
|
||||
import org.eclipse.jgit.patch._
|
||||
import org.eclipse.jgit.api.errors.PatchFormatException
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.Some
|
||||
import service.RepositoryService.RepositoryInfo
|
||||
|
||||
|
||||
object WikiService {
|
||||
|
||||
/**
|
||||
* The model for wiki page.
|
||||
*
|
||||
* @param name the page name
|
||||
* @param content the page content
|
||||
* @param committer the last committer
|
||||
* @param time the last modified time
|
||||
* @param id the latest commit id
|
||||
*/
|
||||
case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String)
|
||||
|
||||
/**
|
||||
* The model for wiki page history.
|
||||
*
|
||||
* @param name the page name
|
||||
* @param committer the committer the committer
|
||||
* @param message the commit message
|
||||
* @param date the commit date
|
||||
*/
|
||||
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
|
||||
|
||||
def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git")
|
||||
|
||||
def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) =
|
||||
repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git")
|
||||
}
|
||||
|
||||
trait WikiService {
|
||||
import WikiService._
|
||||
|
||||
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit =
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
|
||||
if(!dir.exists){
|
||||
JGitUtil.initRepository(dir)
|
||||
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the wiki page.
|
||||
*/
|
||||
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
if(!JGitUtil.isEmpty(git)){
|
||||
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
|
||||
WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes),
|
||||
file.committer, file.time, file.commitId)
|
||||
}
|
||||
} else None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of the specified file.
|
||||
*/
|
||||
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
if(!JGitUtil.isEmpty(git)){
|
||||
val index = path.lastIndexOf('/')
|
||||
val parentPath = if(index < 0) "." else path.substring(0, index)
|
||||
val fileName = if(index < 0) path else path.substring(index + 1)
|
||||
|
||||
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
|
||||
git.getRepository.open(file.id).getBytes
|
||||
}
|
||||
} else None
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of wiki page names.
|
||||
*/
|
||||
def getWikiPageList(owner: String, repository: String): List[String] = {
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
JGitUtil.getFileList(git, "master", ".")
|
||||
.filter(_.name.endsWith(".md"))
|
||||
.map(_.name.stripSuffix(".md"))
|
||||
.sortBy(x => x)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts specified changes.
|
||||
*/
|
||||
def revertWikiPage(owner: String, repository: String, from: String, to: String,
|
||||
committer: model.Account, pageName: Option[String]): Boolean = {
|
||||
|
||||
case class RevertInfo(operation: String, filePath: String, source: String)
|
||||
|
||||
try {
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
|
||||
val reader = git.getRepository.newObjectReader
|
||||
val oldTreeIter = new CanonicalTreeParser
|
||||
oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
|
||||
|
||||
val newTreeIter = new CanonicalTreeParser
|
||||
newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
|
||||
|
||||
val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff =>
|
||||
pageName match {
|
||||
case Some(x) => diff.getNewPath == x + ".md"
|
||||
case None => true
|
||||
}
|
||||
}
|
||||
|
||||
val patch = using(new java.io.ByteArrayOutputStream()){ out =>
|
||||
val formatter = new DiffFormatter(out)
|
||||
formatter.setRepository(git.getRepository)
|
||||
formatter.format(diffs.asJava)
|
||||
new String(out.toByteArray, "UTF-8")
|
||||
}
|
||||
|
||||
val p = new Patch()
|
||||
p.parse(new ByteArrayInputStream(patch.getBytes("UTF-8")))
|
||||
if(!p.getErrors.isEmpty){
|
||||
throw new PatchFormatException(p.getErrors())
|
||||
}
|
||||
val revertInfo = (p.getFiles.asScala.map { fh =>
|
||||
fh.getChangeType match {
|
||||
case DiffEntry.ChangeType.MODIFY => {
|
||||
val source = getWikiPage(owner, repository, fh.getNewPath.stripSuffix(".md")).map(_.content).getOrElse("")
|
||||
val applied = PatchUtil.apply(source, patch, fh)
|
||||
if(applied != null){
|
||||
Seq(RevertInfo("ADD", fh.getNewPath, applied))
|
||||
} else Nil
|
||||
}
|
||||
case DiffEntry.ChangeType.ADD => {
|
||||
val applied = PatchUtil.apply("", patch, fh)
|
||||
if(applied != null){
|
||||
Seq(RevertInfo("ADD", fh.getNewPath, applied))
|
||||
} else Nil
|
||||
}
|
||||
case DiffEntry.ChangeType.DELETE => {
|
||||
Seq(RevertInfo("DELETE", fh.getNewPath, ""))
|
||||
}
|
||||
case DiffEntry.ChangeType.RENAME => {
|
||||
val applied = PatchUtil.apply("", patch, fh)
|
||||
if(applied != null){
|
||||
Seq(RevertInfo("DELETE", fh.getOldPath, ""), RevertInfo("ADD", fh.getNewPath, applied))
|
||||
} else {
|
||||
Seq(RevertInfo("DELETE", fh.getOldPath, ""))
|
||||
}
|
||||
}
|
||||
case _ => Nil
|
||||
}
|
||||
}).flatten
|
||||
|
||||
if(revertInfo.nonEmpty){
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||
|
||||
JGitUtil.processTree(git, headId){ (path, tree) =>
|
||||
if(revertInfo.find(x => x.filePath == path).isEmpty){
|
||||
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||
}
|
||||
}
|
||||
|
||||
revertInfo.filter(_.operation == "ADD").foreach { x =>
|
||||
builder.add(JGitUtil.createDirCacheEntry(x.filePath, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, x.source.getBytes("UTF-8"))))
|
||||
}
|
||||
builder.finish()
|
||||
|
||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
|
||||
pageName match {
|
||||
case Some(x) => s"Revert ${from} ... ${to} on ${x}"
|
||||
case None => s"Revert ${from} ... ${to}"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the wiki page.
|
||||
*/
|
||||
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
|
||||
content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||
var created = true
|
||||
var updated = false
|
||||
var removed = false
|
||||
|
||||
if(headId != null){
|
||||
JGitUtil.processTree(git, headId){ (path, tree) =>
|
||||
if(path == currentPageName + ".md" && currentPageName != newPageName){
|
||||
removed = true
|
||||
} else if(path != newPageName + ".md"){
|
||||
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||
} else {
|
||||
created = false
|
||||
updated = JGitUtil.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(created || updated || removed){
|
||||
builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||
builder.finish()
|
||||
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
|
||||
if(message.trim.length == 0) {
|
||||
if(removed){
|
||||
s"Rename ${currentPageName} to ${newPageName}"
|
||||
} else if(created){
|
||||
s"Created ${newPageName}"
|
||||
} else {
|
||||
s"Updated ${newPageName}"
|
||||
}
|
||||
} else {
|
||||
message
|
||||
})
|
||||
|
||||
Some(newHeadId.getName)
|
||||
} else None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the wiki page.
|
||||
*/
|
||||
def deleteWikiPage(owner: String, repository: String, pageName: String,
|
||||
committer: String, mailAddress: String, message: String): Unit = {
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||
var removed = false
|
||||
|
||||
JGitUtil.processTree(git, headId){ (path, tree) =>
|
||||
if(path != pageName + ".md"){
|
||||
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||
} else {
|
||||
removed = true
|
||||
}
|
||||
}
|
||||
if(removed){
|
||||
builder.finish()
|
||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
package service
|
||||
|
||||
import java.util.Date
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.apache.commons.io.FileUtils
|
||||
import util._
|
||||
import _root_.util.ControlUtil._
|
||||
import org.eclipse.jgit.treewalk.{TreeWalk, CanonicalTreeParser}
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.eclipse.jgit.dircache.{DirCache, DirCacheEntry}
|
||||
import org.eclipse.jgit.revwalk.RevWalk
|
||||
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter}
|
||||
import java.io.ByteArrayInputStream
|
||||
import org.eclipse.jgit.patch._
|
||||
import org.eclipse.jgit.api.errors.PatchFormatException
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.Some
|
||||
import service.RepositoryService.RepositoryInfo
|
||||
|
||||
|
||||
object WikiService {
|
||||
|
||||
/**
|
||||
* The model for wiki page.
|
||||
*
|
||||
* @param name the page name
|
||||
* @param content the page content
|
||||
* @param committer the last committer
|
||||
* @param time the last modified time
|
||||
* @param id the latest commit id
|
||||
*/
|
||||
case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String)
|
||||
|
||||
/**
|
||||
* The model for wiki page history.
|
||||
*
|
||||
* @param name the page name
|
||||
* @param committer the committer the committer
|
||||
* @param message the commit message
|
||||
* @param date the commit date
|
||||
*/
|
||||
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
|
||||
|
||||
def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git")
|
||||
|
||||
def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) =
|
||||
repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git")
|
||||
}
|
||||
|
||||
trait WikiService {
|
||||
import WikiService._
|
||||
|
||||
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit =
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
|
||||
if(!dir.exists){
|
||||
JGitUtil.initRepository(dir)
|
||||
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the wiki page.
|
||||
*/
|
||||
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
if(!JGitUtil.isEmpty(git)){
|
||||
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
|
||||
WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes),
|
||||
file.committer, file.time, file.commitId)
|
||||
}
|
||||
} else None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of the specified file.
|
||||
*/
|
||||
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
if(!JGitUtil.isEmpty(git)){
|
||||
val index = path.lastIndexOf('/')
|
||||
val parentPath = if(index < 0) "." else path.substring(0, index)
|
||||
val fileName = if(index < 0) path else path.substring(index + 1)
|
||||
|
||||
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
|
||||
git.getRepository.open(file.id).getBytes
|
||||
}
|
||||
} else None
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of wiki page names.
|
||||
*/
|
||||
def getWikiPageList(owner: String, repository: String): List[String] = {
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
JGitUtil.getFileList(git, "master", ".")
|
||||
.filter(_.name.endsWith(".md"))
|
||||
.map(_.name.stripSuffix(".md"))
|
||||
.sortBy(x => x)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts specified changes.
|
||||
*/
|
||||
def revertWikiPage(owner: String, repository: String, from: String, to: String,
|
||||
committer: model.Account, pageName: Option[String]): Boolean = {
|
||||
|
||||
case class RevertInfo(operation: String, filePath: String, source: String)
|
||||
|
||||
try {
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
|
||||
val reader = git.getRepository.newObjectReader
|
||||
val oldTreeIter = new CanonicalTreeParser
|
||||
oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
|
||||
|
||||
val newTreeIter = new CanonicalTreeParser
|
||||
newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
|
||||
|
||||
val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff =>
|
||||
pageName match {
|
||||
case Some(x) => diff.getNewPath == x + ".md"
|
||||
case None => true
|
||||
}
|
||||
}
|
||||
|
||||
val patch = using(new java.io.ByteArrayOutputStream()){ out =>
|
||||
val formatter = new DiffFormatter(out)
|
||||
formatter.setRepository(git.getRepository)
|
||||
formatter.format(diffs.asJava)
|
||||
new String(out.toByteArray, "UTF-8")
|
||||
}
|
||||
|
||||
val p = new Patch()
|
||||
p.parse(new ByteArrayInputStream(patch.getBytes("UTF-8")))
|
||||
if(!p.getErrors.isEmpty){
|
||||
throw new PatchFormatException(p.getErrors())
|
||||
}
|
||||
val revertInfo = (p.getFiles.asScala.map { fh =>
|
||||
fh.getChangeType match {
|
||||
case DiffEntry.ChangeType.MODIFY => {
|
||||
val source = getWikiPage(owner, repository, fh.getNewPath.stripSuffix(".md")).map(_.content).getOrElse("")
|
||||
val applied = PatchUtil.apply(source, patch, fh)
|
||||
if(applied != null){
|
||||
Seq(RevertInfo("ADD", fh.getNewPath, applied))
|
||||
} else Nil
|
||||
}
|
||||
case DiffEntry.ChangeType.ADD => {
|
||||
val applied = PatchUtil.apply("", patch, fh)
|
||||
if(applied != null){
|
||||
Seq(RevertInfo("ADD", fh.getNewPath, applied))
|
||||
} else Nil
|
||||
}
|
||||
case DiffEntry.ChangeType.DELETE => {
|
||||
Seq(RevertInfo("DELETE", fh.getNewPath, ""))
|
||||
}
|
||||
case DiffEntry.ChangeType.RENAME => {
|
||||
val applied = PatchUtil.apply("", patch, fh)
|
||||
if(applied != null){
|
||||
Seq(RevertInfo("DELETE", fh.getOldPath, ""), RevertInfo("ADD", fh.getNewPath, applied))
|
||||
} else {
|
||||
Seq(RevertInfo("DELETE", fh.getOldPath, ""))
|
||||
}
|
||||
}
|
||||
case _ => Nil
|
||||
}
|
||||
}).flatten
|
||||
|
||||
if(revertInfo.nonEmpty){
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||
|
||||
JGitUtil.processTree(git, headId){ (path, tree) =>
|
||||
if(revertInfo.find(x => x.filePath == path).isEmpty){
|
||||
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||
}
|
||||
}
|
||||
|
||||
revertInfo.filter(_.operation == "ADD").foreach { x =>
|
||||
builder.add(JGitUtil.createDirCacheEntry(x.filePath, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, x.source.getBytes("UTF-8"))))
|
||||
}
|
||||
builder.finish()
|
||||
|
||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
|
||||
pageName match {
|
||||
case Some(x) => s"Revert ${from} ... ${to} on ${x}"
|
||||
case None => s"Revert ${from} ... ${to}"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the wiki page.
|
||||
*/
|
||||
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
|
||||
content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||
var created = true
|
||||
var updated = false
|
||||
var removed = false
|
||||
|
||||
if(headId != null){
|
||||
JGitUtil.processTree(git, headId){ (path, tree) =>
|
||||
if(path == currentPageName + ".md" && currentPageName != newPageName){
|
||||
removed = true
|
||||
} else if(path != newPageName + ".md"){
|
||||
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||
} else {
|
||||
created = false
|
||||
updated = JGitUtil.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(created || updated || removed){
|
||||
builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||
builder.finish()
|
||||
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
|
||||
if(message.trim.length == 0) {
|
||||
if(removed){
|
||||
s"Rename ${currentPageName} to ${newPageName}"
|
||||
} else if(created){
|
||||
s"Created ${newPageName}"
|
||||
} else {
|
||||
s"Updated ${newPageName}"
|
||||
}
|
||||
} else {
|
||||
message
|
||||
})
|
||||
|
||||
Some(newHeadId.getName)
|
||||
} else None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the wiki page.
|
||||
*/
|
||||
def deleteWikiPage(owner: String, repository: String, pageName: String,
|
||||
committer: String, mailAddress: String, message: String): Unit = {
|
||||
LockUtil.lock(s"${owner}/${repository}/wiki"){
|
||||
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||
val builder = DirCache.newInCore.builder()
|
||||
val inserter = git.getRepository.newObjectInserter()
|
||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||
var removed = false
|
||||
|
||||
JGitUtil.processTree(git, headId){ (path, tree) =>
|
||||
if(path != pageName + ".md"){
|
||||
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||
} else {
|
||||
removed = true
|
||||
}
|
||||
}
|
||||
if(removed){
|
||||
builder.finish()
|
||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user