Code/repo search (#2582)

Indexed search of repository contents (for default branch only)
This commit is contained in:
Ethan Koenig
2017-10-26 23:10:54 -07:00
committed by Lauris BH
parent 762f1d7237
commit 5866eb2321
33 changed files with 1214 additions and 31 deletions

4
conf/app.ini vendored
View File

@ -192,7 +192,11 @@ ITERATE_BUFFER_SIZE = 50
[indexer]
ISSUE_INDEXER_PATH = indexers/issues.bleve
; repo indexer by default disabled, since it uses a lot of disk space
REPO_INDEXER_ENABLED = false
REPO_INDEXER_PATH = indexers/repos.bleve
UPDATE_BUFFER_LEN = 20
MAX_FILE_SIZE = 1048576
[admin]
; Disable regular (non-admin) users to create organizations

View File

@ -63,6 +63,10 @@ func TestMain(m *testing.M) {
fmt.Printf("os.RemoveAll: %v\n", err)
os.Exit(1)
}
if err = os.RemoveAll(setting.Indexer.RepoPath); err != nil {
fmt.Printf("Unable to remove repo indexer: %v\n", err)
os.Exit(1)
}
os.Exit(exitCode)
}

View File

@ -12,6 +12,8 @@ PATH = data/gitea.db
[indexer]
ISSUE_INDEXER_PATH = integrations/indexers-mysql/issues.bleve
REPO_INDEXER_ENABLED = true
REPO_INDEXER_PATH = integrations/indexers-mysql/repos.bleve
[repository]
ROOT = integrations/gitea-integration-mysql/gitea-repositories

View File

@ -12,6 +12,8 @@ PATH = data/gitea.db
[indexer]
ISSUE_INDEXER_PATH = integrations/indexers-pgsql/issues.bleve
REPO_INDEXER_ENABLED = true
REPO_INDEXER_PATH = integrations/indexers-pgsql/repos.bleve
[repository]
ROOT = integrations/gitea-integration-pgsql/gitea-repositories

View File

@ -0,0 +1,35 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package integrations
import (
"net/http"
"testing"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
)
func resultFilenames(t testing.TB, doc *HTMLDoc) []string {
resultsSelection := doc.doc.Find(".repository.search")
assert.EqualValues(t, 1, resultsSelection.Length(),
"Invalid template (repo search template has changed?)")
filenameSelections := resultsSelection.Find(".repo-search-result").Find(".header").Find("span.file")
result := make([]string, filenameSelections.Length())
filenameSelections.Each(func(i int, selection *goquery.Selection) {
result[i] = selection.Text()
})
return result
}
func TestSearchRepo(t *testing.T) {
prepareTestEnv(t)
req := NewRequestf(t, "GET", "/user2/repo1/search?q=Description&page=1")
resp := MakeRequest(t, req, http.StatusOK)
filenames := resultFilenames(t, NewHTMLParser(t, resp.Body))
assert.EqualValues(t, []string{"README.md"}, filenames)
}

View File

@ -7,6 +7,8 @@ PATH = :memory:
[indexer]
ISSUE_INDEXER_PATH = integrations/indexers-sqlite/issues.bleve
REPO_INDEXER_ENABLED = true
REPO_INDEXER_PATH = integrations/indexers-sqlite/repos.bleve
[repository]
ROOT = integrations/gitea-integration-sqlite/gitea-repositories

View File

@ -0,0 +1 @@
[] # empty

View File

@ -144,6 +144,8 @@ var migrations = []Migration{
NewMigration("remove organization watch repositories", removeOrganizationWatchRepo),
// v47 -> v48
NewMigration("add deleted branches", addDeletedBranch),
// v48 -> v49
NewMigration("add repo indexer status", addRepoIndexerStatus),
}
// Migrate database to current version

25
models/migrations/v48.go Normal file
View File

@ -0,0 +1,25 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package migrations
import (
"fmt"
"github.com/go-xorm/xorm"
)
func addRepoIndexerStatus(x *xorm.Engine) error {
// RepoIndexerStatus see models/repo_indexer.go
type RepoIndexerStatus struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL"`
CommitSha string `xorm:"VARCHAR(40)"`
}
if err := x.Sync2(new(RepoIndexerStatus)); err != nil {
return fmt.Errorf("Sync2: %v", err)
}
return nil
}

View File

@ -13,6 +13,10 @@ import (
"path"
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
// Needed for the MySQL driver
_ "github.com/go-sql-driver/mysql"
"github.com/go-xorm/core"
@ -23,9 +27,6 @@ import (
// Needed for the MSSSQL driver
_ "github.com/denisenkom/go-mssqldb"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
// Engine represents a xorm engine or session.
@ -115,6 +116,7 @@ func init() {
new(Stopwatch),
new(TrackedTime),
new(DeletedBranch),
new(RepoIndexerStatus),
)
gonicNames := []string{"SSL", "UID"}
@ -150,8 +152,13 @@ func LoadConfigs() {
DbCfg.Timeout = sec.Key("SQLITE_TIMEOUT").MustInt(500)
sec = setting.Cfg.Section("indexer")
setting.Indexer.IssuePath = sec.Key("ISSUE_INDEXER_PATH").MustString("indexers/issues.bleve")
setting.Indexer.IssuePath = absolutePath(
sec.Key("ISSUE_INDEXER_PATH").MustString("indexers/issues.bleve"))
setting.Indexer.RepoIndexerEnabled = sec.Key("REPO_INDEXER_ENABLED").MustBool(false)
setting.Indexer.RepoPath = absolutePath(
sec.Key("REPO_INDEXER_PATH").MustString("indexers/repos.bleve"))
setting.Indexer.UpdateQueueLength = sec.Key("UPDATE_BUFFER_LEN").MustInt(20)
setting.Indexer.MaxIndexerFileSize = sec.Key("MAX_FILE_SIZE").MustInt64(512 * 1024 * 1024)
}
// parsePostgreSQLHostPort parses given input in various forms defined in
@ -336,3 +343,12 @@ func DumpDatabase(filePath string, dbType string) error {
}
return x.DumpTablesToFile(tbs, filePath)
}
// absolutePath make path absolute if it is relative
func absolutePath(path string) string {
workDir, err := setting.WorkDir()
if err != nil {
log.Fatal(4, "Failed to get work directory: %v", err)
}
return util.EnsureAbsolutePath(path, workDir)
}

View File

@ -205,10 +205,11 @@ type Repository struct {
ExternalMetas map[string]string `xorm:"-"`
Units []*RepoUnit `xorm:"-"`
IsFork bool `xorm:"INDEX NOT NULL DEFAULT false"`
ForkID int64 `xorm:"INDEX"`
BaseRepo *Repository `xorm:"-"`
Size int64 `xorm:"NOT NULL DEFAULT 0"`
IsFork bool `xorm:"INDEX NOT NULL DEFAULT false"`
ForkID int64 `xorm:"INDEX"`
BaseRepo *Repository `xorm:"-"`
Size int64 `xorm:"NOT NULL DEFAULT 0"`
IndexerStatus *RepoIndexerStatus `xorm:"-"`
Created time.Time `xorm:"-"`
CreatedUnix int64 `xorm:"INDEX created"`
@ -782,8 +783,10 @@ func UpdateLocalCopyBranch(repoPath, localPath, branch string) error {
if err != nil {
return fmt.Errorf("git fetch origin: %v", err)
}
if err := git.ResetHEAD(localPath, true, "origin/"+branch); err != nil {
return fmt.Errorf("git reset --hard origin/%s: %v", branch, err)
if len(branch) > 0 {
if err := git.ResetHEAD(localPath, true, "origin/"+branch); err != nil {
return fmt.Errorf("git reset --hard origin/%s: %v", branch, err)
}
}
}
return nil
@ -989,6 +992,7 @@ func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, err
if err = SyncReleasesWithTags(repo, gitRepo); err != nil {
log.Error(4, "Failed to synchronize tags to releases for repository: %v", err)
}
UpdateRepoIndexer(repo)
}
if err = repo.UpdateSize(); err != nil {
@ -1883,6 +1887,7 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
go HookQueue.Add(repo.ID)
}
DeleteRepoFromIndexer(repo)
return nil
}

View File

@ -178,6 +178,8 @@ func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (
if err != nil {
return fmt.Errorf("PushUpdate: %v", err)
}
UpdateRepoIndexer(repo)
return nil
}

302
models/repo_indexer.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -263,6 +263,10 @@ func pushUpdate(opts PushUpdateOptions) (repo *Repository, err error) {
commits = ListToPushCommits(l)
}
if opts.RefFullName == git.BranchPrefix+repo.DefaultBranch {
UpdateRepoIndexer(repo)
}
if err := CommitRepoAction(CommitRepoActionOptions{
PusherName: opts.PusherName,
RepoOwnerID: owner.ID,

View File

@ -367,6 +367,7 @@ func RepoAssignment() macaron.Handler {
ctx.Data["DisableSSH"] = setting.SSH.Disabled
ctx.Data["ExposeAnonSSH"] = setting.SSH.ExposeAnonymous
ctx.Data["DisableHTTP"] = setting.Repository.DisableHTTPGit
ctx.Data["RepoSearchEnabled"] = setting.Indexer.RepoIndexerEnabled
ctx.Data["CloneLink"] = repo.CloneLink()
ctx.Data["WikiCloneLink"] = repo.WikiCloneLink()

199
modules/indexer/repo.go Normal file
View File

@ -0,0 +1,199 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package indexer
import (
"os"
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/blevesearch/bleve"
"github.com/blevesearch/bleve/analysis/analyzer/custom"
"github.com/blevesearch/bleve/analysis/token/camelcase"
"github.com/blevesearch/bleve/analysis/token/lowercase"
"github.com/blevesearch/bleve/analysis/tokenizer/unicode"
)
const repoIndexerAnalyzer = "repoIndexerAnalyzer"
// repoIndexer (thread-safe) index for repository contents
var repoIndexer bleve.Index
// RepoIndexerOp type of operation to perform on repo indexer
type RepoIndexerOp int
const (
// RepoIndexerOpUpdate add/update a file's contents
RepoIndexerOpUpdate = iota
// RepoIndexerOpDelete delete a file
RepoIndexerOpDelete
)
// RepoIndexerData data stored in the repo indexer
type RepoIndexerData struct {
RepoID int64
Content string
}
// RepoIndexerUpdate an update to the repo indexer
type RepoIndexerUpdate struct {
Filepath string
Op RepoIndexerOp
Data *RepoIndexerData
}
func (update RepoIndexerUpdate) addToBatch(batch *bleve.Batch) error {
id := filenameIndexerID(update.Data.RepoID, update.Filepath)
switch update.Op {
case RepoIndexerOpUpdate:
return batch.Index(id, update.Data)
case RepoIndexerOpDelete:
batch.Delete(id)
default:
log.Error(4, "Unrecognized repo indexer op: %d", update.Op)
}
return nil
}
// InitRepoIndexer initialize repo indexer
func InitRepoIndexer(populateIndexer func() error) {
_, err := os.Stat(setting.Indexer.RepoPath)
if err != nil {
if os.IsNotExist(err) {
if err = createRepoIndexer(); err != nil {
log.Fatal(4, "CreateRepoIndexer: %v", err)
}
if err = populateIndexer(); err != nil {
log.Fatal(4, "PopulateRepoIndex: %v", err)
}
} else {
log.Fatal(4, "InitRepoIndexer: %v", err)
}
} else {
repoIndexer, err = bleve.Open(setting.Indexer.RepoPath)
if err != nil {
log.Fatal(4, "InitRepoIndexer, open index: %v", err)
}
}
}
// createRepoIndexer create a repo indexer if one does not already exist
func createRepoIndexer() error {
docMapping := bleve.NewDocumentMapping()
docMapping.AddFieldMappingsAt("RepoID", bleve.NewNumericFieldMapping())
textFieldMapping := bleve.NewTextFieldMapping()
docMapping.AddFieldMappingsAt("Content", textFieldMapping)
mapping := bleve.NewIndexMapping()
if err := addUnicodeNormalizeTokenFilter(mapping); err != nil {
return err
} else if err := mapping.AddCustomAnalyzer(repoIndexerAnalyzer, map[string]interface{}{
"type": custom.Name,
"char_filters": []string{},
"tokenizer": unicode.Name,
"token_filters": []string{unicodeNormalizeName, camelcase.Name, lowercase.Name},
}); err != nil {
return err
}
mapping.DefaultAnalyzer = repoIndexerAnalyzer
mapping.AddDocumentMapping("repo", docMapping)
var err error
repoIndexer, err = bleve.New(setting.Indexer.RepoPath, mapping)
return err
}
func filenameIndexerID(repoID int64, filename string) string {
return indexerID(repoID) + "_" + filename
}
func filenameOfIndexerID(indexerID string) string {
index := strings.IndexByte(indexerID, '_')
if index == -1 {
log.Error(4, "Unexpected ID in repo indexer: %s", indexerID)
}
return indexerID[index+1:]
}
// RepoIndexerBatch batch to add updates to
func RepoIndexerBatch() *Batch {
return &Batch{
batch: repoIndexer.NewBatch(),
index: repoIndexer,
}
}
// DeleteRepoFromIndexer delete all of a repo's files from indexer
func DeleteRepoFromIndexer(repoID int64) error {
query := numericEqualityQuery(repoID, "RepoID")
searchRequest := bleve.NewSearchRequestOptions(query, 2147483647, 0, false)
result, err := repoIndexer.Search(searchRequest)
if err != nil {
return err
}
batch := RepoIndexerBatch()
for _, hit := range result.Hits {
batch.batch.Delete(hit.ID)
if err = batch.flushIfFull(); err != nil {
return err
}
}
return batch.Flush()
}
// RepoSearchResult result of performing a search in a repo
type RepoSearchResult struct {
StartIndex int
EndIndex int
Filename string
Content string
}
// SearchRepoByKeyword searches for files in the specified repo.
// Returns the matching file-paths
func SearchRepoByKeyword(repoID int64, keyword string, page, pageSize int) (int64, []*RepoSearchResult, error) {
phraseQuery := bleve.NewMatchPhraseQuery(keyword)
phraseQuery.FieldVal = "Content"
phraseQuery.Analyzer = repoIndexerAnalyzer
indexerQuery := bleve.NewConjunctionQuery(
numericEqualityQuery(repoID, "RepoID"),
phraseQuery,
)
from := (page - 1) * pageSize
searchRequest := bleve.NewSearchRequestOptions(indexerQuery, pageSize, from, false)
searchRequest.Fields = []string{"Content"}
searchRequest.IncludeLocations = true
result, err := repoIndexer.Search(searchRequest)
if err != nil {
return 0, nil, err
}
searchResults := make([]*RepoSearchResult, len(result.Hits))
for i, hit := range result.Hits {
var startIndex, endIndex int = -1, -1
for _, locations := range hit.Locations["Content"] {
location := locations[0]
locationStart := int(location.Start)
locationEnd := int(location.End)
if startIndex < 0 || locationStart < startIndex {
startIndex = locationStart
}
if endIndex < 0 || locationEnd > endIndex {
endIndex = locationEnd
}
}
searchResults[i] = &RepoSearchResult{
StartIndex: startIndex,
EndIndex: endIndex,
Filename: filenameOfIndexerID(hit.ID),
Content: hit.Fields["Content"].(string),
}
}
return int64(result.Total), searchResults, nil
}

128
modules/search/search.go Normal file
View File

@ -0,0 +1,128 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package search
import (
"bytes"
gotemplate "html/template"
"strings"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/indexer"
"code.gitea.io/gitea/modules/util"
)
// Result a search result to display
type Result struct {
Filename string
HighlightClass string
LineNumbers []int
FormattedLines gotemplate.HTML
}
func indices(content string, selectionStartIndex, selectionEndIndex int) (int, int) {
startIndex := selectionStartIndex
numLinesBefore := 0
for ; startIndex > 0; startIndex-- {
if content[startIndex-1] == '\n' {
if numLinesBefore == 1 {
break
}
numLinesBefore++
}
}
endIndex := selectionEndIndex
numLinesAfter := 0
for ; endIndex < len(content); endIndex++ {
if content[endIndex] == '\n' {
if numLinesAfter == 1 {
break
}
numLinesAfter++
}
}
return startIndex, endIndex
}
func writeStrings(buf *bytes.Buffer, strs ...string) error {
for _, s := range strs {
_, err := buf.WriteString(s)
if err != nil {
return err
}
}
return nil
}
func searchResult(result *indexer.RepoSearchResult, startIndex, endIndex int) (*Result, error) {
startLineNum := 1 + strings.Count(result.Content[:startIndex], "\n")
var formattedLinesBuffer bytes.Buffer
contentLines := strings.SplitAfter(result.Content[startIndex:endIndex], "\n")
lineNumbers := make([]int, len(contentLines))
index := startIndex
for i, line := range contentLines {
var err error
if index < result.EndIndex &&
result.StartIndex < index+len(line) &&
result.StartIndex < result.EndIndex {
openActiveIndex := util.Max(result.StartIndex-index, 0)
closeActiveIndex := util.Min(result.EndIndex-index, len(line))
err = writeStrings(&formattedLinesBuffer,
`<li>`,
line[:openActiveIndex],
`<span class='active'>`,
line[openActiveIndex:closeActiveIndex],
`</span>`,
line[closeActiveIndex:],
`</li>`,
)
} else {
err = writeStrings(&formattedLinesBuffer,
`<li>`,
line,
`</li>`,
)
}
if err != nil {
return nil, err
}
lineNumbers[i] = startLineNum + i
index += len(line)
}
return &Result{
Filename: result.Filename,
HighlightClass: highlight.FileNameToHighlightClass(result.Filename),
LineNumbers: lineNumbers,
FormattedLines: gotemplate.HTML(formattedLinesBuffer.String()),
}, nil
}
// PerformSearch perform a search on a repository
func PerformSearch(repoID int64, keyword string, page, pageSize int) (int, []*Result, error) {
if len(keyword) == 0 {
return 0, nil, nil
}
total, results, err := indexer.SearchRepoByKeyword(repoID, keyword, page, pageSize)
if err != nil {
return 0, nil, err
}
displayResults := make([]*Result, len(results))
for i, result := range results {
startIndex, endIndex := indices(result.Content, result.StartIndex, result.EndIndex)
displayResults[i], err = searchResult(result, startIndex, endIndex)
if err != nil {
return 0, nil, err
}
}
return int(total), displayResults, nil
}

View File

@ -140,8 +140,11 @@ var (
// Indexer settings
Indexer struct {
IssuePath string
UpdateQueueLength int
IssuePath string
RepoIndexerEnabled bool
RepoPath string
UpdateQueueLength int
MaxIndexerFileSize int64
}
// Webhook settings
@ -234,12 +237,13 @@ var (
// UI settings
UI = struct {
ExplorePagingNum int
IssuePagingNum int
FeedMaxCommitNum int
ThemeColorMetaTag string
MaxDisplayFileSize int64
ShowUserEmail bool
ExplorePagingNum int
IssuePagingNum int
RepoSearchPagingNum int
FeedMaxCommitNum int
ThemeColorMetaTag string
MaxDisplayFileSize int64
ShowUserEmail bool
Admin struct {
UserPagingNum int
@ -256,11 +260,12 @@ var (
Keywords string
} `ini:"ui.meta"`
}{
ExplorePagingNum: 20,
IssuePagingNum: 10,
FeedMaxCommitNum: 5,
ThemeColorMetaTag: `#6cc644`,
MaxDisplayFileSize: 8388608,
ExplorePagingNum: 20,
IssuePagingNum: 10,
RepoSearchPagingNum: 10,
FeedMaxCommitNum: 5,
ThemeColorMetaTag: `#6cc644`,
MaxDisplayFileSize: 8388608,
Admin: struct {
UserPagingNum int
RepoPagingNum int

16
modules/util/path.go Normal file
View File

@ -0,0 +1,16 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package util
import "path/filepath"
// EnsureAbsolutePath ensure that a path is absolute, making it
// relative to absoluteBase if necessary
func EnsureAbsolutePath(path string, absoluteBase string) string {
if filepath.IsAbs(path) {
return path
}
return filepath.Join(absoluteBase, path)
}

View File

@ -38,3 +38,19 @@ func OptionalBoolOf(b bool) OptionalBool {
}
return OptionalBoolFalse
}
// Max max of two ints
func Max(a, b int) int {
if a < b {
return b
}
return a
}
// Min min of two ints
func Min(a, b int) int {
if a > b {
return b
}
return a
}

View File

@ -848,6 +848,10 @@ activity.title.releases_n = %d Releases
activity.title.releases_published_by = %s published by %s
activity.published_release_label = Published
search = Search
search.search_repo = Search repository
search.results = Search results for "%s" in <a href="%s">%s</a>
settings = Settings
settings.desc = Settings is where you can manage the settings for the repository
settings.options = Options

File diff suppressed because one or more lines are too long

View File

@ -158,6 +158,11 @@
}
&.file.list {
.repo-description {
display: flex;
justify-content: space-between;
align-items: center;
}
#repo-desc {
font-size: 1.2em;
}
@ -226,7 +231,7 @@
}
}
#file-content {
.non-diff-file-content {
.header {
.icon {
font-size: 1em;
@ -244,7 +249,7 @@
background: transparent;
border: 0;
outline: none;
}
}
.btn-octicon:hover {
color: #4078c0;
}
@ -323,6 +328,9 @@
}
}
}
.active {
background: #ffffdd;
}
}
}
@ -1038,6 +1046,13 @@
overflow-x: auto;
overflow-y: hidden;
}
.repo-search-result {
padding-top: 10px;
padding-bottom: 10px;
.lines-num a {
color: inherit;
}
}
&.quickstart {
.guide {

View File

@ -66,6 +66,7 @@ func GlobalInit() {
// Booting long running goroutines.
cron.NewContext()
models.InitIssueIndexer()
models.InitRepoIndexer()
models.InitSyncMirrors()
models.InitDeliverHooks()
models.InitTestPullRequests()

46
routers/repo/search.go Normal file
View File

@ -0,0 +1,46 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package repo
import (
"path"
"strings"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/search"
"code.gitea.io/gitea/modules/setting"
"github.com/Unknwon/paginater"
)
const tplSearch base.TplName = "repo/search"
// Search render repository search page
func Search(ctx *context.Context) {
if !setting.Indexer.RepoIndexerEnabled {
ctx.Redirect(ctx.Repo.RepoLink, 302)
return
}
keyword := strings.TrimSpace(ctx.Query("q"))
page := ctx.QueryInt("page")
if page <= 0 {
page = 1
}
total, searchResults, err := search.PerformSearch(ctx.Repo.Repository.ID, keyword, page, setting.UI.RepoSearchPagingNum)
if err != nil {
ctx.Handle(500, "SearchResults", err)
return
}
ctx.Data["Keyword"] = keyword
pager := paginater.New(total, setting.UI.RepoSearchPagingNum, page, 5)
ctx.Data["Page"] = pager
ctx.Data["SourcePath"] = setting.AppSubURL + "/" +
path.Join(ctx.Repo.Repository.Owner.Name, ctx.Repo.Repository.Name, "src", ctx.Repo.Repository.DefaultBranch)
ctx.Data["SearchResults"] = searchResults
ctx.Data["RequireHighlightJS"] = true
ctx.Data["PageIsViewCode"] = true
ctx.HTML(200, tplSearch)
}

View File

@ -649,6 +649,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Group("/:username/:reponame", func() {
m.Get("/stars", repo.Stars)
m.Get("/watchers", repo.Watchers)
m.Get("/search", context.CheckUnit(models.UnitTypeCode), repo.Search)
}, ignSignIn, context.RepoAssignment(), context.RepoRef(), context.UnitTypes(), context.LoadRepoUnits())
m.Group("/:username", func() {

View File

@ -3,10 +3,26 @@
{{template "repo/header" .}}
<div class="ui container">
{{template "base/alert" .}}
<p id="repo-desc">
{{if .Repository.DescriptionHTML}}<span class="description has-emoji">{{.Repository.DescriptionHTML}}</span>{{else if .IsRepositoryAdmin}}<span class="no-description text-italic">{{.i18n.Tr "repo.no_desc"}}</span>{{end}}
<a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a>
</p>
<div class="ui repo-description">
<div id="repo-desc">
{{if .Repository.DescriptionHTML}}<span class="description has-emoji">{{.Repository.DescriptionHTML}}</span>{{else if .IsRepositoryAdmin}}<span class="no-description text-italic">{{.i18n.Tr "repo.no_desc"}}</span>{{end}}
<a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a>
</div>
{{if .RepoSearchEnabled}}
<div class="ui repo-search">
<form class="ui form" action="{{.RepoLink}}/search" method="get">
<div class="field">
<div class="ui action input">
<input name="q" value="{{.Keyword}}" placeholder="{{.i18n.Tr "repo.search.search_repo"}}">
<button class="ui icon button" type="submit">
<i class="search icon"></i>
</button>
</div>
</div>
</form>
</div>
{{end}}
</div>
{{template "repo/sub_menu" .}}
<div class="ui secondary menu">
{{if .PullRequestCtx.Allowed}}

View File

@ -0,0 +1,49 @@
{{template "base/head" .}}
<div class="repository file list">
{{template "repo/header" .}}
<div class="ui container">
<div class="ui repo-search">
<form class="ui form" method="get">
<div class="ui fluid action input">
<input name="q" value="{{.Keyword}}" placeholder="{{.i18n.Tr "repo.search.search_repo"}}">
<button class="ui button" type="submit">
<i class="search icon"></i>
</button>
</div>
</form>
</div>
{{if .Keyword}}
<h3>
{{.i18n.Tr "repo.search.results" .Keyword .RepoLink .RepoName | Str2html}}
</h3>
<div class="repository search">
{{range $result := .SearchResults}}
<div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result">
<h4 class="ui top attached normal header">
<span class="file">{{.Filename}}</span>
<a class="ui basic grey tiny button" rel="nofollow" href="{{EscapePound $.SourcePath}}/{{EscapePound .Filename}}">{{$.i18n.Tr "repo.diff.view_file"}}</a>
</h4>
<div class="ui attached table segment">
<div class="file-body file-code code-view">
<table>
<tbody>
<tr>
<td class="lines-num">
{{range .LineNumbers}}
<a href="{{EscapePound $.SourcePath}}/{{EscapePound $result.Filename}}#L{{.}}"><span>{{.}}</span></a>
{{end}}
</td>
<td class="lines-code"><pre><code class="{{.HighlightClass}}"><ol class="linenums">{{.FormattedLines}}</ol></code></pre></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
{{end}}
</div>
{{template "base/paginate" .}}
{{end}}
</div>
</div>
{{template "base/footer" .}}

View File

@ -1,4 +1,4 @@
<div id="file-content" class="{{TabSizeClass .Editorconfig .FileName}}">
<div class="{{TabSizeClass .Editorconfig .FileName}} non-diff-file-content">
<h4 class="ui top attached header" id="{{if .ReadmeExist}}repo-readme{{else}}repo-read-file{{end}}">
{{if .ReadmeExist}}
<i class="book icon ui left"></i>

View File

@ -0,0 +1,78 @@
// Copyright (c) 2016 Couchbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package camelcase
import (
"bytes"
"unicode/utf8"
"github.com/blevesearch/bleve/analysis"
"github.com/blevesearch/bleve/registry"
)
const Name = "camelCase"
// CamelCaseFilter splits a given token into a set of tokens where each resulting token
// falls into one the following classes:
// 1) Upper case followed by lower case letters.
// Terminated by a number, an upper case letter, and a non alpha-numeric symbol.
// 2) Upper case followed by upper case letters.
// Terminated by a number, an upper case followed by a lower case letter, and a non alpha-numeric symbol.
// 3) Lower case followed by lower case letters.
// Terminated by a number, an upper case letter, and a non alpha-numeric symbol.
// 4) Number followed by numbers.
// Terminated by a letter, and a non alpha-numeric symbol.
// 5) Non alpha-numeric symbol followed by non alpha-numeric symbols.
// Terminated by a number, and a letter.
//
// It does a one-time sequential pass over an input token, from left to right.
// The scan is greedy and generates the longest substring that fits into one of the classes.
//
// See the test file for examples of classes and their parsings.
type CamelCaseFilter struct{}
func NewCamelCaseFilter() *CamelCaseFilter {
return &CamelCaseFilter{}
}
func (f *CamelCaseFilter) Filter(input analysis.TokenStream) analysis.TokenStream {
rv := make(analysis.TokenStream, 0, len(input))
nextPosition := 1
for _, token := range input {
runeCount := utf8.RuneCount(token.Term)
runes := bytes.Runes(token.Term)
p := NewParser(runeCount, nextPosition, token.Start)
for i := 0; i < runeCount; i++ {
if i+1 >= runeCount {
p.Push(runes[i], nil)
} else {
p.Push(runes[i], &runes[i+1])
}
}
rv = append(rv, p.FlushTokens()...)
nextPosition = p.NextPosition()
}
return rv
}
func CamelCaseFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) {
return NewCamelCaseFilter(), nil
}
func init() {
registry.RegisterTokenFilter(Name, CamelCaseFilterConstructor)
}

Some files were not shown because too many files have changed in this diff Show More