mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-03 12:05:59 +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 service._
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import org.eclipse.jgit.lib._
|
|
||||||
import org.apache.commons.io._
|
import org.apache.commons.io._
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,25 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
|
import util._
|
||||||
import service._
|
import service._
|
||||||
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
|
||||||
class IndexController extends IndexControllerBase
|
class IndexController extends IndexControllerBase
|
||||||
with RepositoryService with AccountService with SystemSettingsService with ActivityService
|
with RepositoryService with AccountService with SystemSettingsService with ActivityService
|
||||||
|
with RepositorySearchService with IssuesService
|
||||||
|
with ReferrerAuthenticator
|
||||||
|
|
||||||
trait IndexControllerBase extends ControllerBase { self: RepositoryService
|
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("/"){
|
get("/"){
|
||||||
val loginAccount = context.loginAccount
|
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 jp.sf.amateras.scalatra.forms._
|
||||||
|
|
||||||
import service._
|
import service._
|
||||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, UsersAuthenticator}
|
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
|
||||||
|
|
||||||
class MilestonesController extends MilestonesControllerBase
|
class MilestonesController extends MilestonesControllerBase
|
||||||
with MilestonesService with RepositoryService with AccountService
|
with MilestonesService with RepositoryService with AccountService
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import service._
|
import service._
|
||||||
import util.{FileUtil, AdminAuthenticator}
|
import util.AdminAuthenticator
|
||||||
import util.StringUtil._
|
import util.StringUtil._
|
||||||
import jp.sf.amateras.scalatra.forms._
|
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
|
class UserManagementController extends UserManagementControllerBase with AccountService with AdminAuthenticator
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import scala.slick.jdbc.{StaticQuery => Q}
|
|||||||
import Q.interpolation
|
import Q.interpolation
|
||||||
|
|
||||||
import model._
|
import model._
|
||||||
|
import util.Implicits._
|
||||||
|
import util.StringUtil
|
||||||
|
|
||||||
trait IssuesService {
|
trait IssuesService {
|
||||||
import IssuesService._
|
import IssuesService._
|
||||||
@@ -235,6 +237,52 @@ trait IssuesService {
|
|||||||
}
|
}
|
||||||
.update (closed, currentDate)
|
.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 {
|
object IssuesService {
|
||||||
@@ -281,4 +329,5 @@ object IssuesService {
|
|||||||
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
|
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
|
||||||
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
|
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 org.apache.commons.io.FileUtils
|
||||||
import util.JGitUtil.DiffInfo
|
import util.JGitUtil.DiffInfo
|
||||||
import util.{Directory, JGitUtil}
|
import util.{Directory, JGitUtil}
|
||||||
import org.eclipse.jgit.lib.RepositoryBuilder
|
|
||||||
import org.eclipse.jgit.treewalk.CanonicalTreeParser
|
import org.eclipse.jgit.treewalk.CanonicalTreeParser
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import org.eclipse.jgit.api.Git
|
|
||||||
import org.eclipse.jgit.lib.Ref
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides directories used by GitBucket.
|
* Provides directories used by GitBucket.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import org.apache.commons.io.{IOUtils, FileUtils, FilenameUtils}
|
import org.apache.commons.io.{IOUtils, FileUtils}
|
||||||
import java.net.URLConnection
|
import java.net.URLConnection
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream}
|
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 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
|
package util
|
||||||
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
import scala.Some
|
|
||||||
|
|
||||||
trait Validations {
|
trait Validations {
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@(page: Int, count: Int, limit: Int, width: Int, baseURL: String)
|
@(page: Int, count: Int, limit: Int, width: Int, baseURL: String)
|
||||||
@defining(view.Pagination(page, count, service.IssuesService.IssueLimit, width)){ p =>
|
@defining(view.Pagination(page, count, limit, width)){ p =>
|
||||||
@if(p.count > p.limit){
|
@if(p.count > p.limit){
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main("New Issue - " + repository.owner + "/" + repository.name){
|
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
|
||||||
@html.header("issues", repository)
|
@html.header("issues", repository)
|
||||||
@tab("", repository)
|
@tab("", repository)
|
||||||
<form action="@url(repository)/issues/new" method="POST" validate="true">
|
<form action="@url(repository)/issues/new" method="POST" validate="true">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}"){
|
@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
|
||||||
@html.header("issues", repository)
|
@html.header("issues", repository)
|
||||||
@tab("issues", repository)
|
@tab("issues", repository)
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
hasWritePermission: Boolean)(implicit context: app.Context)
|
hasWritePermission: Boolean)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main("Issues - " + repository.owner + "/" + repository.name){
|
@html.main(s"Issues - ${repository.owner}/${repository.name}", Some(repository)){
|
||||||
@html.header("issues", repository)
|
@html.header("issues", repository)
|
||||||
@tab("issues", repository)
|
@tab("issues", repository)
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@(milestone: Option[model.Milestone], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
@(milestone: Option[model.Milestone], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main("Milestones - " + repository.owner + "/" + repository.name){
|
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
|
||||||
@html.header("milestones", repository)
|
@html.header("milestones", repository)
|
||||||
@issues.html.tab("milestones", repository)
|
@issues.html.tab("milestones", repository)
|
||||||
<form method="POST" action="@url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true">
|
<form method="POST" action="@url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
hasWritePermission: Boolean)(implicit context: app.Context)
|
hasWritePermission: Boolean)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main("Milestones - " + repository.owner + "/" + repository.name){
|
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
|
||||||
@html.header("milestones", repository)
|
@html.header("milestones", repository)
|
||||||
@issues.html.tab("milestones", repository)
|
@issues.html.tab("milestones", repository)
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@(title: String)(body: Html)(implicit context: app.Context)
|
@(title: String, repository: Option[service.RepositoryService.RepositoryInfo] = None)(body: Html)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
<script src="@assets/zclip/ZeroClipboard.min.js"></script>
|
<script src="@assets/zclip/ZeroClipboard.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<form id="search" action="@path/search" method="POST">
|
||||||
<div class="navbar">
|
<div class="navbar">
|
||||||
<div class="navbar-inner">
|
<div class="navbar-inner">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -38,7 +39,12 @@
|
|||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
</button>
|
</button>
|
||||||
<a class="brand" href="@path/">GitBucket</a>
|
<a class="brand" href="@path/">GitBucket</a>
|
||||||
<div class="nav-collapse collapse pull-right">
|
<div class="nav-collapse collapse pull-right header-menu">
|
||||||
|
@repository.map { repository =>
|
||||||
|
<input type="text" name="query" style="width: 300px; margin-bottom: 0px;" placeholder="Search this repository"/>
|
||||||
|
<input type="hidden" name="owner" value="@repository.owner"/>
|
||||||
|
<input type="hidden" name="repository" value="@repository.name"/>
|
||||||
|
}
|
||||||
@if(loginAccount.isDefined){
|
@if(loginAccount.isDefined){
|
||||||
<a href="@url(loginAccount.get.userName)" class="username menu">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</a>
|
<a href="@url(loginAccount.get.userName)" class="username menu">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</a>
|
||||||
<a href="@path/new" class="menu" data-toggle="tooltip" data-placement="bottom" title="Create a new repo"><i class="icon-plus"></i></a>
|
<a href="@path/new" class="menu" data-toggle="tooltip" data-placement="bottom" title="Create a new repo"><i class="icon-plus"></i></a>
|
||||||
@@ -53,6 +59,7 @@
|
|||||||
</div><!--/.nav-collapse -->
|
</div><!--/.nav-collapse -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
@defining(servlet.AutoUpdate.getCurrentVersion){ version =>
|
@defining(servlet.AutoUpdate.getCurrentVersion){ version =>
|
||||||
<div class="gitbucket-version">version @version.majorVersion.@version.minorVersion</div>
|
<div class="gitbucket-version">version @version.majorVersion.@version.minorVersion</div>
|
||||||
}
|
}
|
||||||
@@ -60,5 +67,12 @@
|
|||||||
<div class="container body">
|
<div class="container body">
|
||||||
@body
|
@body
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
$(function(){
|
||||||
|
$('#search').submit(function(){
|
||||||
|
return $.trim($(this).find('input[name=query]').val()) != '';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
latestCommit: util.JGitUtil.CommitInfo)(implicit context: app.Context)
|
latestCommit: util.JGitUtil.CommitInfo)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main(repository.owner+"/"+repository.name) {
|
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
||||||
@html.header("code", repository)
|
@html.header("code", repository)
|
||||||
@tab(branch, repository, "files")
|
@tab(branch, repository, "files")
|
||||||
<div class="head">
|
<div class="head">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@import util.Implicits._
|
@import util.Implicits._
|
||||||
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
||||||
@html.main(commit.shortMessage){
|
@html.main(commit.shortMessage, Some(repository)){
|
||||||
@html.header("code", repository)
|
@html.header("code", repository)
|
||||||
@tab(commitId, repository, "commits")
|
@tab(commitId, repository, "commits")
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
hasNext: Boolean)(implicit context: app.Context)
|
hasNext: Boolean)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main(repository.owner+"/"+repository.name) {
|
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
||||||
@html.header("code", repository)
|
@html.header("code", repository)
|
||||||
@tab(branch, repository, if(pathList.isEmpty) "commits" else "files")
|
@tab(branch, repository, if(pathList.isEmpty) "commits" else "files")
|
||||||
<div class="head">
|
<div class="head">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
readme: Option[String])(implicit context: app.Context)
|
readme: Option[String])(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main(repository.owner + "/" + repository.name) {
|
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
||||||
@html.header("code", repository)
|
@html.header("code", repository)
|
||||||
@tab(branch, repository, "files")
|
@tab(branch, repository, "files")
|
||||||
<div class="head">
|
<div class="head">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main(repository.owner + "/" + repository.name) {
|
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
||||||
@html.header("code", repository)
|
@html.header("code", repository)
|
||||||
<h3 style="margin-top: 30px;">Create a new repository on the command line</h3>
|
<h3 style="margin-top: 30px;">Create a new repository on the command line</h3>
|
||||||
<pre>
|
<pre>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main(repository.owner + "/" + repository.name) {
|
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
||||||
@html.header("code", repository)
|
@html.header("code", repository)
|
||||||
@tab(repository.repository.defaultBranch, repository, "tags", true)
|
@tab(repository.repository.defaultBranch, repository, "tags", true)
|
||||||
<h1>Tags</h1>
|
<h1>Tags</h1>
|
||||||
|
|||||||
26
src/main/twirl/search/code.scala.html
Normal file
26
src/main/twirl/search/code.scala.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
@(files: List[service.RepositorySearchService.FileSearchResult],
|
||||||
|
issueCount: Int,
|
||||||
|
query: String,
|
||||||
|
page: Int,
|
||||||
|
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
@import view.helpers._
|
||||||
|
@import service.RepositorySearchService._
|
||||||
|
@html.main("Search Results", Some(repository)){
|
||||||
|
@menu("code", files.size, issueCount, query, repository){
|
||||||
|
@if(files.isEmpty){
|
||||||
|
<h4>We couldn't find any code matching '@query'</h4>
|
||||||
|
} else {
|
||||||
|
<h4>We've found @files.size code @plural(files.size, "result")</h4>
|
||||||
|
}
|
||||||
|
@files.drop((page - 1) * CodeLimit).take(CodeLimit).map { file =>
|
||||||
|
<div>
|
||||||
|
<h5><a href="@url(repository)/blob/@repository.repository.defaultBranch/@file.path">@file.path</a></h5>
|
||||||
|
<div class="small muted">Latest commit at @datetime(file.lastModified)</div>
|
||||||
|
<pre class="prettyprint linenums:@file.highlightLineNumber" style="padding-left: 25px;">@Html(file.highlightText)</pre>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@helper.html.paginator(page, files.size, CodeLimit, 10,
|
||||||
|
s"${url(repository)}/search?q=${urlEncode(query)}&type=code")
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/main/twirl/search/issues.scala.html
Normal file
33
src/main/twirl/search/issues.scala.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
@(issues: List[service.RepositorySearchService.IssueSearchResult],
|
||||||
|
fileCount: Int,
|
||||||
|
query: String,
|
||||||
|
page: Int,
|
||||||
|
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
@import view.helpers._
|
||||||
|
@import service.RepositorySearchService._
|
||||||
|
@html.main("Search Results", Some(repository)){
|
||||||
|
@menu("issue", fileCount, issues.size, query, repository){
|
||||||
|
@if(issues.isEmpty){
|
||||||
|
<h4>We couldn't find any code matching '@query'</h4>
|
||||||
|
} else {
|
||||||
|
<h4>We've found @issues.size code @plural(issues.size, "result")</h4>
|
||||||
|
}
|
||||||
|
@issues.drop((page - 1) * IssueLimit).take(IssueLimit).map { issue =>
|
||||||
|
<div class="block">
|
||||||
|
<div class="pull-right muted">#@issue.issueId</div>
|
||||||
|
<h4 style="margin-top: 0px;"><a href="@url(repository)/issues/@issue.issueId">@issue.title</a></h4>
|
||||||
|
<pre>@Html(issue.highlightText)</pre>
|
||||||
|
<div class="small muted">
|
||||||
|
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a>
|
||||||
|
at @datetime(issue.registeredDate)
|
||||||
|
@if(issue.commentCount > 0){
|
||||||
|
<i class="icon-comment"></i><strong>@issue.commentCount</strong> @plural(issue.commentCount, "comment")
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@helper.html.paginator(page, issues.size, IssueLimit, 10,
|
||||||
|
s"${url(repository)}/search?q=${urlEncode(query)}&type=issue")
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/main/twirl/search/menu.scala.html
Normal file
37
src/main/twirl/search/menu.scala.html
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
@(active: String, fileCount: Int, issueCount: Int, query: String,
|
||||||
|
repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
@import view.helpers._
|
||||||
|
@html.header("", repository)
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="span3">
|
||||||
|
<div class="box">
|
||||||
|
<ul class="nav nav-tabs nav-stacked side-menu">
|
||||||
|
<li@if(active=="code"){ class="active"}>
|
||||||
|
<a href="@url(repository)/search?q=@urlEncode(query)&type=code">
|
||||||
|
@if(fileCount != 0){
|
||||||
|
<span class="badge pull-right">@fileCount</span>
|
||||||
|
}
|
||||||
|
Code
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li@if(active=="issue"){ class="active"}>
|
||||||
|
<a href="@url(repository)/search?q=@urlEncode(query)&type=issue">
|
||||||
|
@if(issueCount != 0){
|
||||||
|
<span class="badge pull-right">@issueCount</span>
|
||||||
|
}
|
||||||
|
Issue
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="span9">
|
||||||
|
<form action="@url(repository)/search" method="GET">
|
||||||
|
<input type="text" name="q" value="@query" style="width: 80%; margin-bottom: 0px;"/>
|
||||||
|
<input type="submit" value="Search" class="btn" style="width: 15%;"/>
|
||||||
|
<input type="hidden" name="type" value="@active"/>
|
||||||
|
</form>
|
||||||
|
@body
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
@(collaborators: List[String], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
@(collaborators: List[String], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main("Settings"){
|
@html.main("Settings", Some(repository)){
|
||||||
@html.header("settings", repository)
|
@html.header("settings", repository)
|
||||||
@menu("collaborators", repository){
|
@menu("collaborators", repository){
|
||||||
<h3>Manage Collaborators</h3>
|
<h3>Manage Collaborators</h3>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main("Delete Repository"){
|
@html.main("Delete Repository", Some(repository)){
|
||||||
@html.header("settings", repository)
|
@html.header("settings", repository)
|
||||||
@menu("delete", repository){
|
@menu("delete", repository){
|
||||||
<form id="form" method="post" action="@url(repository)/settings/delete">
|
<form id="form" method="post" action="@url(repository)/settings/delete">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@(repository: service.RepositoryService.RepositoryInfo, info: Option[Any])(implicit context: app.Context)
|
@(repository: service.RepositoryService.RepositoryInfo, info: Option[Any])(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main("Settings"){
|
@html.main("Settings", Some(repository)){
|
||||||
@html.header("settings", repository)
|
@html.header("settings", repository)
|
||||||
@menu("options", repository){
|
@menu("options", repository){
|
||||||
@helper.html.information(info)
|
@helper.html.information(info)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
||||||
@html.main("Compare Revisions - " + repository.owner + "/" + repository.name){
|
@html.main(s"Compare Revisions - ${repository.owner}/${repository.name}", Some(repository)){
|
||||||
@html.header("wiki", repository)
|
@html.header("wiki", repository)
|
||||||
@tab("history", repository)
|
@tab("history", repository)
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main((if(pageName == "") "New Page" else pageName) + " - " + repository.owner + "/" + repository.name){
|
@html.main(s"${if(pageName == "") "New Page" else pageName} - ${repository.owner}/${repository.name}", Some(repository)){
|
||||||
@html.header("wiki", repository)
|
@html.header("wiki", repository)
|
||||||
@tab("", repository)
|
@tab("", repository)
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main("History - " + repository.owner + "/" + repository.name){
|
@html.main(s"History - ${repository.owner}/${repository.name}", Some(repository)){
|
||||||
@html.header("wiki", repository)
|
@html.header("wiki", repository)
|
||||||
@tab(if(pageName.isEmpty) "history" else "", repository)
|
@tab(if(pageName.isEmpty) "history" else "", repository)
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
hasWritePermission: Boolean)(implicit context: app.Context)
|
hasWritePermission: Boolean)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main(pageName + " - " + repository.owner + "/" + repository.name){
|
@html.main(s"${pageName} - ${repository.owner}/${repository.name}", Some(repository)){
|
||||||
@html.header("wiki", repository)
|
@html.header("wiki", repository)
|
||||||
@tab((if(pageName == "Home") "home" else ""), repository)
|
@tab((if(pageName == "Home") "home" else ""), repository)
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@(pages: List[String], repository: service.RepositoryService.RepositoryInfo, hasWritePermission: Boolean)(implicit context: app.Context)
|
@(pages: List[String], repository: service.RepositoryService.RepositoryInfo, hasWritePermission: Boolean)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main("Pages - " + repository.owner + "/" + repository.name){
|
@html.main(s"Pages - ${repository.owner}/${repository.name}", Some(repository)){
|
||||||
@html.header("wiki", repository)
|
@html.header("wiki", repository)
|
||||||
@tab("pages", repository)
|
@tab("pages", repository)
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ body {
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ======================================================================== */
|
||||||
|
/* Global Header */
|
||||||
|
/* ======================================================================== */
|
||||||
div.navbar-inner {
|
div.navbar-inner {
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
-webkit-border-radius: 0px;
|
-webkit-border-radius: 0px;
|
||||||
@@ -16,18 +19,23 @@ div.navbar-inner {
|
|||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.header-menu {
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.header-menu input,
|
||||||
|
div.header-menu a.btn {
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
div.nav-collapse a.menu {
|
div.nav-collapse a.menu {
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
line-height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.nav-collapse a.btn-last {
|
|
||||||
margin-right: 30px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.nav-collapse a.btn-last,
|
||||||
div.nav-collapse a.menu-last {
|
div.nav-collapse a.menu-last {
|
||||||
margin-right: 30px;
|
margin-right: 30px;
|
||||||
line-height: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.gitbucket-version {
|
div.gitbucket-version {
|
||||||
@@ -37,6 +45,9 @@ div.gitbucket-version {
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ======================================================================== */
|
||||||
|
/* Repository Header */
|
||||||
|
/* ======================================================================== */
|
||||||
table.global-nav {
|
table.global-nav {
|
||||||
width: 920px;
|
width: 920px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
@@ -59,6 +70,9 @@ table.global-nav th a:link, table.global-nav th a:hover, table.global-nav th a:v
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ======================================================================== */
|
||||||
|
/* General Styles */
|
||||||
|
/* ======================================================================== */
|
||||||
div.head {
|
div.head {
|
||||||
font-size: large;
|
font-size: large;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|||||||
Reference in New Issue
Block a user