mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-08 22:45:51 +01:00
Merge branch '#3_repository-search'
Conflicts: src/main/scala/app/UserManagementController.scala src/main/scala/service/IssuesService.scala src/main/twirl/issues/issue.scala.html
This commit is contained in:
@@ -5,7 +5,6 @@ import util.{JGitUtil, UsersAuthenticator}
|
||||
import service._
|
||||
import java.io.File
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.apache.commons.io._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
package app
|
||||
|
||||
import util._
|
||||
import service._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
class IndexController extends IndexControllerBase
|
||||
with RepositoryService with AccountService with SystemSettingsService with ActivityService
|
||||
with RepositorySearchService with IssuesService
|
||||
with ReferrerAuthenticator
|
||||
|
||||
trait IndexControllerBase extends ControllerBase { self: RepositoryService
|
||||
with SystemSettingsService with ActivityService =>
|
||||
|
||||
with SystemSettingsService with ActivityService with RepositorySearchService
|
||||
with ReferrerAuthenticator =>
|
||||
|
||||
val searchForm = mapping(
|
||||
"query" -> trim(text(required)),
|
||||
"owner" -> trim(text(required)),
|
||||
"repository" -> trim(text(required))
|
||||
)(SearchForm.apply)
|
||||
|
||||
case class SearchForm(query: String, owner: String, repository: String)
|
||||
|
||||
get("/"){
|
||||
val loginAccount = context.loginAccount
|
||||
|
||||
@@ -18,4 +31,31 @@ trait IndexControllerBase extends ControllerBase { self: RepositoryService
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
post("/search", searchForm){ form =>
|
||||
redirect(s"${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
|
||||
}
|
||||
|
||||
get("/:owner/:repository/search")(referrersOnly { repository =>
|
||||
val query = params("q").trim
|
||||
val target = params.getOrElse("type", "code")
|
||||
val page = try {
|
||||
val i = params.getOrElse("page", "1").toInt
|
||||
if(i <= 0) 1 else i
|
||||
} catch {
|
||||
case e: NumberFormatException => 1
|
||||
}
|
||||
|
||||
target.toLowerCase match {
|
||||
case "issue" => search.html.issues(
|
||||
searchIssues(repository.owner, repository.name, query),
|
||||
countFiles(repository.owner, repository.name, query),
|
||||
query, page, repository)
|
||||
|
||||
case _ => search.html.code(
|
||||
searchFiles(repository.owner, repository.name, query),
|
||||
countIssues(repository.owner, repository.name, query),
|
||||
query, page, repository)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package app
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
|
||||
import service._
|
||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, UsersAuthenticator}
|
||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
|
||||
|
||||
class MilestonesController extends MilestonesControllerBase
|
||||
with MilestonesService with RepositoryService with AccountService
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package app
|
||||
|
||||
import service._
|
||||
import util.{FileUtil, AdminAuthenticator}
|
||||
import util.AdminAuthenticator
|
||||
import util.StringUtil._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import util.Directory._
|
||||
import scala.Some
|
||||
|
||||
class UserManagementController extends UserManagementControllerBase with AccountService with AdminAuthenticator
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import scala.slick.jdbc.{StaticQuery => Q}
|
||||
import Q.interpolation
|
||||
|
||||
import model._
|
||||
import util.Implicits._
|
||||
import util.StringUtil
|
||||
|
||||
trait IssuesService {
|
||||
import IssuesService._
|
||||
@@ -235,6 +237,52 @@ trait IssuesService {
|
||||
}
|
||||
.update (closed, currentDate)
|
||||
|
||||
/**
|
||||
* Search issues by keyword.
|
||||
*
|
||||
* @param owner the repository owner
|
||||
* @param repository the repository name
|
||||
* @param query the keywords separated by whitespace.
|
||||
* @return issues with comment count and matched content of issue or comment
|
||||
*/
|
||||
def searchIssuesByKeyword(owner: String, repository: String, query: String): List[(Issue, Int, String)] = {
|
||||
import scala.slick.driver.H2Driver.likeEncode
|
||||
val keywords = StringUtil.splitWords(query.toLowerCase)
|
||||
|
||||
// Search Issue
|
||||
val issues = Query(Issues).filter { t =>
|
||||
keywords.map { keyword =>
|
||||
(t.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) || (t.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^'))
|
||||
} .reduceLeft(_ && _)
|
||||
}.map { t => (t, 0, t.content) }
|
||||
|
||||
// Search IssueComment
|
||||
val comments = Query(IssueComments).innerJoin(Issues).on { case (t1, t2) =>
|
||||
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
||||
}.filter { case (t1, t2) =>
|
||||
keywords.map { query =>
|
||||
t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^')
|
||||
}.reduceLeft(_ && _)
|
||||
}.map { case (t1, t2) => (t2, t1.commentId, t1.content) }
|
||||
|
||||
// TODO Excludes some actions which should be ignored.
|
||||
def getCommentCount(issue: Issue): Int = {
|
||||
Query(IssueComments)
|
||||
.filter(_.byIssue(issue.userName, issue.repositoryName, issue.issueId))
|
||||
.map(_.issueId)
|
||||
.list.length
|
||||
}
|
||||
|
||||
issues.union(comments).sortBy { case (issue, commentId, _) =>
|
||||
issue.issueId ~ commentId
|
||||
}.list.splitWith { case ((issue1, _, _), (issue2, _, _)) =>
|
||||
issue1.issueId == issue2.issueId
|
||||
}.map { result =>
|
||||
val (issue, _, content) = result.head
|
||||
(issue, getCommentCount(issue) , content)
|
||||
}.toList
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object IssuesService {
|
||||
@@ -281,4 +329,5 @@ object IssuesService {
|
||||
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
|
||||
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
121
src/main/scala/service/RepositorySearchService.scala
Normal file
121
src/main/scala/service/RepositorySearchService.scala
Normal file
@@ -0,0 +1,121 @@
|
||||
package service
|
||||
|
||||
import model.Issue
|
||||
import util.{FileUtil, StringUtil, JGitUtil}
|
||||
import util.Directory._
|
||||
import model.Issue
|
||||
import org.eclipse.jgit.revwalk.RevWalk
|
||||
import org.eclipse.jgit.treewalk.TreeWalk
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import org.eclipse.jgit.lib.FileMode
|
||||
import org.eclipse.jgit.api.Git
|
||||
|
||||
trait RepositorySearchService { self: IssuesService =>
|
||||
import RepositorySearchService._
|
||||
|
||||
def countIssues(owner: String, repository: String, query: String): Int =
|
||||
searchIssuesByKeyword(owner, repository, query).length
|
||||
|
||||
def searchIssues(owner: String, repository: String, query: String): List[IssueSearchResult] =
|
||||
searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
|
||||
IssueSearchResult(
|
||||
issue.issueId,
|
||||
issue.title,
|
||||
issue.openedUserName,
|
||||
issue.registeredDate,
|
||||
commentCount,
|
||||
getHighlightText(content, query)._1)
|
||||
}
|
||||
|
||||
def countFiles(owner: String, repository: String, query: String): Int =
|
||||
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git =>
|
||||
searchRepositoryFiles(git, query).length
|
||||
}
|
||||
|
||||
def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] =
|
||||
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git =>
|
||||
val files = searchRepositoryFiles(git, query)
|
||||
val commits = JGitUtil.getLatestCommitFromPaths(git, files.toList.map(_._1), "HEAD")
|
||||
files.map { case (path, text) =>
|
||||
val (highlightText, lineNumber) = getHighlightText(text, query)
|
||||
FileSearchResult(
|
||||
path,
|
||||
commits(path).getCommitterIdent.getWhen,
|
||||
highlightText,
|
||||
lineNumber)
|
||||
}
|
||||
}
|
||||
|
||||
private def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = {
|
||||
val revWalk = new RevWalk(git.getRepository)
|
||||
val objectId = git.getRepository.resolve("HEAD")
|
||||
val revCommit = revWalk.parseCommit(objectId)
|
||||
val treeWalk = new TreeWalk(git.getRepository)
|
||||
treeWalk.setRecursive(true)
|
||||
treeWalk.addTree(revCommit.getTree)
|
||||
|
||||
val keywords = StringUtil.splitWords(query.toLowerCase)
|
||||
val list = new ListBuffer[(String, String)]
|
||||
|
||||
while (treeWalk.next()) {
|
||||
if(treeWalk.getFileMode(0) != FileMode.TREE){
|
||||
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes =>
|
||||
if(FileUtil.isText(bytes)){
|
||||
val text = new String(bytes, "UTF-8")
|
||||
val lowerText = text.toLowerCase
|
||||
val indices = keywords.map(lowerText.indexOf _)
|
||||
if(!indices.exists(_ < 0)){
|
||||
list.append((treeWalk.getPathString, text))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
treeWalk.release
|
||||
revWalk.release
|
||||
|
||||
list.toList
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object RepositorySearchService {
|
||||
|
||||
val CodeLimit = 10
|
||||
val IssueLimit = 10
|
||||
|
||||
def getHighlightText(content: String, query: String): (String, Int) = {
|
||||
val keywords = StringUtil.splitWords(query.toLowerCase)
|
||||
val lowerText = content.toLowerCase
|
||||
val indices = keywords.map(lowerText.indexOf _)
|
||||
|
||||
if(!indices.exists(_ < 0)){
|
||||
val lineNumber = content.substring(0, indices.min).split("\n").size - 1
|
||||
val highlightText = StringUtil.escapeHtml(content.split("\n").drop(lineNumber).take(5).mkString("\n"))
|
||||
.replaceAll("(?i)(" + keywords.map("\\Q" + _ + "\\E").mkString("|") + ")",
|
||||
"<span style=\"background-color: #ffff88;;\">$1</span>")
|
||||
(highlightText, lineNumber + 1)
|
||||
} else {
|
||||
(content.split("\n").take(5).mkString("\n"), 1)
|
||||
}
|
||||
}
|
||||
|
||||
case class SearchResult(
|
||||
files : List[(String, String)],
|
||||
issues: List[(Issue, Int, String)])
|
||||
|
||||
case class IssueSearchResult(
|
||||
issueId: Int,
|
||||
title: String,
|
||||
openedUserName: String,
|
||||
registeredDate: java.util.Date,
|
||||
commentCount: Int,
|
||||
highlightText: String)
|
||||
|
||||
case class FileSearchResult(
|
||||
path: String,
|
||||
lastModified: java.util.Date,
|
||||
highlightText: String,
|
||||
highlightLineNumber: Int)
|
||||
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import org.eclipse.jgit.api.Git
|
||||
import org.apache.commons.io.FileUtils
|
||||
import util.JGitUtil.DiffInfo
|
||||
import util.{Directory, JGitUtil}
|
||||
import org.eclipse.jgit.lib.RepositoryBuilder
|
||||
import org.eclipse.jgit.treewalk.CanonicalTreeParser
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package util
|
||||
|
||||
import java.io.File
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.Ref
|
||||
|
||||
/**
|
||||
* Provides directories used by GitBucket.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package util
|
||||
|
||||
import org.apache.commons.io.{IOUtils, FileUtils, FilenameUtils}
|
||||
import org.apache.commons.io.{IOUtils, FileUtils}
|
||||
import java.net.URLConnection
|
||||
import java.io.File
|
||||
import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream}
|
||||
|
||||
@@ -20,4 +20,9 @@ object StringUtil {
|
||||
|
||||
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")
|
||||
|
||||
def splitWords(value: String): Array[String] = value.split("[ \\t ]+")
|
||||
|
||||
def escapeHtml(value: String): String =
|
||||
value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package util
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import scala.Some
|
||||
|
||||
trait Validations {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user