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:
takezoe
2013-07-20 03:00:16 +09:00
36 changed files with 411 additions and 80 deletions

View File

@@ -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._

View File

@@ -1,13 +1,26 @@
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)
}
})
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"))
} }
} }

View 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)
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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}

View File

@@ -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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
} }

View File

@@ -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 {

View File

@@ -1,32 +1,32 @@
@(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>
@if(page == 1){ @if(page == 1){
<li class="disabled"><span>&#9664;</span></li> <li class="disabled"><span>&#9664;</span></li>
} else { } else {
<li><a href="@baseURL&page=@(page - 1)">&#9664;</a></li> <li><a href="@baseURL&page=@(page - 1)">&#9664;</a></li>
} }
@for(i <- 1 to p.max){ @for(i <- 1 to p.max){
@if(i == p.max && p.omitRight){ @if(i == p.max && p.omitRight){
<li><span>&hellip;</span></li> <li><span>&hellip;</span></li>
} }
@if(i == page){ @if(i == page){
<li class="active"><span>@i</span></li> <li class="active"><span>@i</span></li>
} else { } else {
@if(p.visibleFor(i)){ @if(p.visibleFor(i)){
<li><a href="@baseURL&page=@i">@i</a></li> <li><a href="@baseURL&page=@i">@i</a></li>
} }
} }
@if(i == 1 && p.omitLeft){ @if(i == 1 && p.omitLeft){
<li><span>&hellip;</span></li> <li><span>&hellip;</span></li>
} }
} }
@if(page == p.max){ @if(page == p.max){
<li class="disabled"><span>&#9654;</span></li> <li class="disabled"><span>&#9654;</span></li>
} else { } else {
<li><a href="@baseURL&page=@(page + 1)">&#9654;</a></li> <li><a href="@baseURL&page=@(page + 1)">&#9654;</a></li>
} }
</ul> </ul>
</div> </div>

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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,30 +29,37 @@
<script src="@assets/zclip/ZeroClipboard.min.js"></script> <script src="@assets/zclip/ZeroClipboard.min.js"></script>
</head> </head>
<body> <body>
<div class="navbar"> <form id="search" action="@path/search" method="POST">
<div class="navbar-inner"> <div class="navbar">
<div class="container"> <div class="navbar-inner">
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"> <div class="container">
<span class="icon-bar"></span> <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> <span class="icon-bar"></span>
<a class="brand" href="@path/">GitBucket</a> </button>
<div class="nav-collapse collapse pull-right"> <a class="brand" href="@path/">GitBucket</a>
@if(loginAccount.isDefined){ <div class="nav-collapse collapse pull-right header-menu">
<a href="@url(loginAccount.get.userName)" class="username menu">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</a> @repository.map { repository =>
<a href="@path/new" class="menu" data-toggle="tooltip" data-placement="bottom" title="Create a new repo"><i class="icon-plus"></i></a> <input type="text" name="query" style="width: 300px; margin-bottom: 0px;" placeholder="Search this repository"/>
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a> <input type="hidden" name="owner" value="@repository.owner"/>
@if(loginAccount.get.isAdmin){ <input type="hidden" name="repository" value="@repository.name"/>
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>
} }
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a> @if(loginAccount.isDefined){
} else { <a href="@url(loginAccount.get.userName)" class="username menu">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</a>
<a href="@path/signin?@currentUrl" class="btn btn-last">Sign in</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="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a>
</div><!--/.nav-collapse --> @if(loginAccount.get.isAdmin){
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>
}
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
} else {
<a href="@path/signin?@currentUrl" class="btn btn-last">Sign in</a>
}
</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>

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View 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")
}
}

View 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){
&nbsp;&nbsp;<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")
}
}

View 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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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)

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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;