mirror of https://github.com/go-gitea/gitea.git
feat(search): support code search by zoekt
Signed-off-by: ZheNing Hu <adlternative@gmail.com>
parent
1e777f92c7
commit
77c7d4274a
File diff suppressed because one or more lines are too long
@ -0,0 +1,49 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package zoekt
|
||||
|
||||
import "unicode/utf8"
|
||||
|
||||
// Bitmap used by func special to check whether a character needs to be escaped.
|
||||
var specialBytes [16]byte
|
||||
|
||||
// special reports whether byte b needs to be escaped by QuoteMeta.
|
||||
func special(b byte) bool {
|
||||
return b < utf8.RuneSelf && specialBytes[b%16]&(1<<(b/16)) != 0
|
||||
}
|
||||
|
||||
func init() {
|
||||
for _, b := range []byte(`-:\.+*?()|[]{}^$`) {
|
||||
specialBytes[b%16] |= 1 << (b / 16)
|
||||
}
|
||||
}
|
||||
|
||||
func QuoteMeta(s string) string {
|
||||
// A byte loop is correct because all metacharacters are ASCII.
|
||||
var i int
|
||||
for i = 0; i < len(s); i++ {
|
||||
if special(s[i]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
// No meta characters found, so return original string.
|
||||
if i >= len(s) {
|
||||
return s
|
||||
}
|
||||
|
||||
b := make([]byte, 3*len(s)-2*i)
|
||||
copy(b, s[:i])
|
||||
j := i
|
||||
for ; i < len(s); i++ {
|
||||
if special(s[i]) {
|
||||
b[j] = '\\'
|
||||
j++
|
||||
b[j] = '\\'
|
||||
j++
|
||||
}
|
||||
b[j] = s[i]
|
||||
j++
|
||||
}
|
||||
return string(b[:j])
|
||||
}
|
||||
@ -0,0 +1,395 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build unix
|
||||
|
||||
package zoekt
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/analyze"
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/indexer"
|
||||
"code.gitea.io/gitea/modules/indexer/code/internal"
|
||||
indexer_internal "code.gitea.io/gitea/modules/indexer/internal"
|
||||
inner_zoekt "code.gitea.io/gitea/modules/indexer/internal/zoekt"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
|
||||
"github.com/go-enry/go-enry/v2"
|
||||
"github.com/sourcegraph/zoekt"
|
||||
"github.com/sourcegraph/zoekt/index"
|
||||
"github.com/sourcegraph/zoekt/query"
|
||||
)
|
||||
|
||||
const repoIndexerLatestVersion = 1
|
||||
|
||||
type Indexer struct {
|
||||
indexer_internal.Indexer // do not composite inner_zoekt.Indexer directly to avoid exposing too much
|
||||
inner *inner_zoekt.Indexer
|
||||
indexDir string
|
||||
}
|
||||
|
||||
func (b *Indexer) SupportedSearchModes() []indexer.SearchMode {
|
||||
return indexer.ZoektSearchModes()
|
||||
}
|
||||
|
||||
func NewIndexer(indexDir string) *Indexer {
|
||||
idxer := inner_zoekt.NewIndexer(indexDir, repoIndexerLatestVersion)
|
||||
return &Indexer{
|
||||
Indexer: idxer,
|
||||
inner: idxer,
|
||||
indexDir: indexDir,
|
||||
}
|
||||
}
|
||||
|
||||
func newZoektIndexBuilder(indexDir string, repo *repo_model.Repository, targetSHA string) (*index.Builder, error) {
|
||||
opts := index.Options{
|
||||
IndexDir: indexDir,
|
||||
SizeMax: int(setting.Indexer.MaxIndexerFileSize),
|
||||
IsDelta: true,
|
||||
RepositoryDescription: zoekt.Repository{
|
||||
ID: uint32(repo.ID),
|
||||
Name: strconv.FormatInt(repo.ID, 10),
|
||||
Branches: []zoekt.RepositoryBranch{
|
||||
{
|
||||
Name: "HEAD",
|
||||
Version: targetSHA,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if opts.IncrementalSkipIndexing() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
opts.SetDefaults()
|
||||
|
||||
builder, err := index.NewBuilder(opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("index.newZoektIndexBuilder: %w", err)
|
||||
}
|
||||
|
||||
return builder, nil
|
||||
}
|
||||
|
||||
func (b *Indexer) addDelete(builder *index.Builder, filename string) {
|
||||
builder.MarkFileAsChangedOrRemoved(filename)
|
||||
}
|
||||
|
||||
func (b *Indexer) addUpdate(ctx context.Context, builder *index.Builder, batchWriter git.WriteCloserError, batchReader *bufio.Reader, update internal.FileUpdate, repo *repo_model.Repository) error {
|
||||
// Ignore vendored files in code search
|
||||
if setting.Indexer.ExcludeVendored && analyze.IsVendor(update.Filename) {
|
||||
return nil
|
||||
}
|
||||
|
||||
size := update.Size
|
||||
var err error
|
||||
if !update.Sized {
|
||||
var stdout string
|
||||
stdout, _, err = gitcmd.NewCommand("cat-file", "-s").AddDynamicArguments(update.BlobSha).WithDir(repo.RepoPath()).RunStdString(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if size, err = strconv.ParseInt(strings.TrimSpace(stdout), 10, 64); err != nil {
|
||||
return fmt.Errorf("misformatted git cat-file output: %w", err)
|
||||
}
|
||||
}
|
||||
if size > setting.Indexer.MaxIndexerFileSize {
|
||||
b.addDelete(builder, update.Filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := batchWriter.Write([]byte(update.BlobSha + "\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _, size, err = git.ReadBatchLine(batchReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileContents, err := io.ReadAll(io.LimitReader(batchReader, size))
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !typesniffer.DetectContentType(fileContents).IsText() {
|
||||
// FIXME: UTF-16 files will probably fail here
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err = batchReader.Discard(1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
builder.MarkFileAsChangedOrRemoved(update.Filename)
|
||||
|
||||
// branches := []string{repo.DefaultBranch}
|
||||
branches := []string{"HEAD"}
|
||||
|
||||
err = builder.Add(
|
||||
index.Document{
|
||||
Name: update.Filename,
|
||||
Content: charset.ToUTF8DropErrors(fileContents, charset.ConvertOpts{}),
|
||||
Branches: branches,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding document with name %s: %w", update.Filename, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Index will save the index data
|
||||
func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error {
|
||||
builder, err := newZoektIndexBuilder(b.indexDir, repo, sha)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating builder: %w", err)
|
||||
}
|
||||
|
||||
if builder == nil {
|
||||
// skip indexing when there is no change
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(changes.Updates) > 0 {
|
||||
gitBatch, err := git.NewBatch(ctx, repo.RepoPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gitBatch.Close()
|
||||
|
||||
for _, update := range changes.Updates {
|
||||
err := b.addUpdate(ctx, builder, gitBatch.Writer, gitBatch.Reader, update, repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, filename := range changes.RemovedFilenames {
|
||||
b.addDelete(builder, filename)
|
||||
}
|
||||
|
||||
return builder.Finish()
|
||||
}
|
||||
|
||||
// Delete entries by repoId
|
||||
func (b *Indexer) Delete(ctx context.Context, repoID int64) error {
|
||||
repoPathPrefix := strconv.FormatInt(repoID, 10)
|
||||
|
||||
// remove all {repoId}_v{N}.{X}.zoekt or {repoId}_v{N}.{X}.zoekt.meta where X is %05d formatted int in b.indexDir
|
||||
pattern := repoPathPrefix + "_v*.[0-9][0-9][0-9][0-9][0-9].zoekt*"
|
||||
matches, err := filepath.Glob(filepath.Join(b.indexDir, pattern))
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding files to delete: %w", err)
|
||||
}
|
||||
|
||||
for _, filePath := range matches {
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
log.Error("failed to delete %s: %v", filePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
tmpPattern := repoPathPrefix + "_v*.tmp"
|
||||
tmpMatches, err := filepath.Glob(filepath.Join(b.indexDir, tmpPattern))
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding temp files to delete: %w", err)
|
||||
}
|
||||
|
||||
for _, filePath := range tmpMatches {
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
log.Error("failed to delete temp file %s: %v", filePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TransToZoektContentQueryString(s string) string {
|
||||
return fmt.Sprintf("content:\"%s\"", s)
|
||||
}
|
||||
|
||||
// generateZoektQuery creates a Zoekt query object based on search options
|
||||
func (b *Indexer) generateZoektQuery(_ context.Context, opts *internal.SearchOptions) (query.Q, error) {
|
||||
keyword := opts.Keyword
|
||||
|
||||
// Build base content query according to search mode
|
||||
var contentQuery query.Q
|
||||
var err error
|
||||
|
||||
switch opts.SearchMode {
|
||||
case indexer.SearchModeRegexp:
|
||||
// Regular expression search mode
|
||||
contentQuery, err = query.Parse(TransToZoektContentQueryString(keyword))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse regexp keyword %q: %w", keyword, err)
|
||||
}
|
||||
|
||||
case indexer.SearchModeWords:
|
||||
// Multi-word search mode - words are combined with OR
|
||||
fields := strings.Fields(keyword)
|
||||
if len(fields) == 0 {
|
||||
return nil, errors.New("empty keyword")
|
||||
}
|
||||
|
||||
// Process first word
|
||||
contentQuery, err = query.Parse(TransToZoektContentQueryString(QuoteMeta(fields[0])))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse word keyword %q: %w", fields[0], err)
|
||||
}
|
||||
|
||||
// Process remaining words, connecting with OR
|
||||
for _, field := range fields[1:] {
|
||||
fieldQuery, err := query.Parse(TransToZoektContentQueryString(QuoteMeta(field)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse word keyword %q: %w", field, err)
|
||||
}
|
||||
contentQuery = query.NewOr(contentQuery, fieldQuery)
|
||||
}
|
||||
|
||||
case indexer.SearchModeZoekt:
|
||||
// Zoekt search mode - use zoekt syntax
|
||||
contentQuery, err = query.Parse(keyword)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse zoekt keyword %q: %w", keyword, err)
|
||||
}
|
||||
case indexer.SearchModeExact:
|
||||
fallthrough
|
||||
default:
|
||||
// Exact match mode (default)
|
||||
contentQuery, err = query.Parse(TransToZoektContentQueryString(QuoteMeta(keyword)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse exact keyword %q: %w", keyword, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Build final query by combining with all filters
|
||||
finalQuery := contentQuery
|
||||
|
||||
// Add repository ID filter
|
||||
if len(opts.RepoIDs) > 0 {
|
||||
repoIDs := make([]uint32, 0, len(opts.RepoIDs))
|
||||
for _, repoID := range opts.RepoIDs {
|
||||
repoIDs = append(repoIDs, uint32(repoID))
|
||||
}
|
||||
finalQuery = query.NewAnd(finalQuery, query.NewRepoIDs(repoIDs...))
|
||||
}
|
||||
|
||||
// Add language filter
|
||||
if opts.Language != "" {
|
||||
langQueryStr := "lang:" + opts.Language
|
||||
langQuery, err := query.Parse(langQueryStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse language filter %q: %w", langQueryStr, err)
|
||||
}
|
||||
finalQuery = query.NewAnd(finalQuery, langQuery)
|
||||
}
|
||||
|
||||
log.Info("Search query: %s", finalQuery.String())
|
||||
|
||||
return finalQuery, nil
|
||||
}
|
||||
|
||||
func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
|
||||
var searchResults []*internal.SearchResult
|
||||
|
||||
q, err := b.generateZoektQuery(ctx, opts)
|
||||
if err != nil {
|
||||
return 0, nil, nil, err
|
||||
}
|
||||
|
||||
result, err := b.inner.Searcher.Search(ctx, q, &zoekt.SearchOptions{
|
||||
Whole: true,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, nil, nil, err
|
||||
}
|
||||
log.Info("len of (result): %d", len(result.Files))
|
||||
|
||||
// remove filename match items from the result
|
||||
for i := 0; i < len(result.Files); i++ {
|
||||
result.Files[i].LineMatches = slices.DeleteFunc(result.Files[i].LineMatches, func(line zoekt.LineMatch) bool {
|
||||
return line.FileName
|
||||
})
|
||||
}
|
||||
result.Files = slices.DeleteFunc(result.Files, func(file zoekt.FileMatch) bool {
|
||||
return len(file.LineMatches) == 0
|
||||
})
|
||||
|
||||
searchResultsLanguages := getSearchResultLanguages(result)
|
||||
|
||||
// pagination
|
||||
if opts.Paginator != nil {
|
||||
page, pageSize := opts.GetSkipTake()
|
||||
|
||||
pageStart := min(page*pageSize, len(result.Files))
|
||||
pageEnd := min((page+1)*pageSize, len(result.Files))
|
||||
result.Files = result.Files[pageStart:pageEnd]
|
||||
}
|
||||
|
||||
// calculate highlight range
|
||||
for _, file := range result.Files {
|
||||
startIndex, endIndex := -1, -1
|
||||
for _, line := range file.LineMatches {
|
||||
for _, frag := range line.LineFragments {
|
||||
fragStart := (int)(frag.Offset)
|
||||
fragEnd := (int)(frag.Offset) + frag.MatchLength
|
||||
if startIndex < 0 || fragStart < startIndex {
|
||||
startIndex = fragStart
|
||||
}
|
||||
if endIndex < 0 || fragEnd > endIndex {
|
||||
endIndex = fragEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
searchResults = append(searchResults, &internal.SearchResult{
|
||||
Filename: file.FileName,
|
||||
Content: string(file.Content),
|
||||
RepoID: int64(file.RepositoryID),
|
||||
StartIndex: startIndex,
|
||||
EndIndex: endIndex,
|
||||
Language: file.Language,
|
||||
Color: enry.GetColor(file.Language),
|
||||
CommitID: file.Version,
|
||||
// UpdatedUnix: not supported yet,
|
||||
})
|
||||
}
|
||||
|
||||
return int64(result.Stats.FileCount), searchResults, searchResultsLanguages, nil
|
||||
}
|
||||
|
||||
func getSearchResultLanguages(searchResult *zoekt.SearchResult) []*internal.SearchResultLanguages {
|
||||
languages := make(map[string]int)
|
||||
|
||||
for _, file := range searchResult.Files {
|
||||
languages[file.Language]++
|
||||
}
|
||||
|
||||
searchResultLanguages := make([]*internal.SearchResultLanguages, 0, len(languages))
|
||||
|
||||
for lang, count := range languages {
|
||||
searchResultLanguages = append(searchResultLanguages, &internal.SearchResultLanguages{
|
||||
Language: lang,
|
||||
Count: count,
|
||||
Color: enry.GetColor(lang),
|
||||
})
|
||||
}
|
||||
|
||||
return searchResultLanguages
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !unix
|
||||
|
||||
package zoekt
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/indexer"
|
||||
"code.gitea.io/gitea/modules/indexer/code/internal"
|
||||
indexer_internal "code.gitea.io/gitea/modules/indexer/internal"
|
||||
inner_zoekt "code.gitea.io/gitea/modules/indexer/internal/zoekt"
|
||||
)
|
||||
|
||||
type Indexer struct {
|
||||
indexer_internal.Indexer // do not composite inner_zoekt.Indexer directly to avoid exposing too much
|
||||
inner *inner_zoekt.Indexer
|
||||
}
|
||||
|
||||
func (b *Indexer) SupportedSearchModes() []indexer.SearchMode {
|
||||
return indexer.ZoektSearchModes()
|
||||
}
|
||||
|
||||
func NewIndexer(_ string) *Indexer {
|
||||
idxer := inner_zoekt.NewIndexer()
|
||||
return &Indexer{
|
||||
Indexer: idxer,
|
||||
inner: idxer,
|
||||
}
|
||||
}
|
||||
|
||||
// Index will save the index data
|
||||
func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error {
|
||||
return inner_zoekt.ErrNotImplemented
|
||||
}
|
||||
|
||||
// Delete entries by repoId
|
||||
func (b *Indexer) Delete(_ context.Context, repoID int64) error {
|
||||
return inner_zoekt.ErrNotImplemented
|
||||
}
|
||||
|
||||
func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
|
||||
return 0, nil, nil, inner_zoekt.ErrNotImplemented
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build unix
|
||||
|
||||
package zoekt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"code.gitea.io/gitea/modules/indexer/internal/zoekt/meta"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/sourcegraph/zoekt"
|
||||
"github.com/sourcegraph/zoekt/search"
|
||||
)
|
||||
|
||||
type Indexer struct {
|
||||
indexDir string
|
||||
Searcher zoekt.Streamer
|
||||
version int
|
||||
}
|
||||
|
||||
func NewIndexer(indexDir string, version int) *Indexer {
|
||||
return &Indexer{
|
||||
indexDir: indexDir,
|
||||
version: version,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Indexer) Init(_ context.Context) (bool, error) {
|
||||
exists := true
|
||||
|
||||
if _, err := os.Stat(i.indexDir); err != nil && os.IsNotExist(err) {
|
||||
exists = false
|
||||
err = os.MkdirAll(i.indexDir, 0o755)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create index directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Initializing zoekt indexer at %s", i.indexDir)
|
||||
metadata, err := meta.ReadIndexMetadata(i.indexDir)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
if err = meta.WriteIndexMetadata(i.indexDir, &meta.IndexMetadata{
|
||||
Version: i.version,
|
||||
}); err != nil {
|
||||
return false, fmt.Errorf("failed to write index metadata: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
if metadata.Version < i.version {
|
||||
log.Warn("Found older zoekt index with version %d, Gitea will remove it and rebuild", metadata.Version)
|
||||
|
||||
// the indexer is using a previous version, so we should delete it and
|
||||
// re-populate
|
||||
if err = util.RemoveAll(i.indexDir); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(i.indexDir); err != nil && os.IsNotExist(err) {
|
||||
exists = false
|
||||
err = os.MkdirAll(i.indexDir, 0o755)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create index directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = meta.WriteIndexMetadata(i.indexDir, &meta.IndexMetadata{
|
||||
Version: i.version,
|
||||
}); err != nil {
|
||||
return false, fmt.Errorf("failed to write index metadata: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: change to use shards.NewDirectorySearcherFast
|
||||
searcher, err := search.NewDirectorySearcher(i.indexDir)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
i.Searcher = searcher
|
||||
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
func (i *Indexer) Ping(_ context.Context) error {
|
||||
// NOTHING TO DO
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Indexer) Close() {
|
||||
if i.Searcher == nil {
|
||||
return
|
||||
}
|
||||
i.Searcher.Close()
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !unix
|
||||
|
||||
package zoekt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var ErrNotImplemented = errors.New("zoekt indexer is not supported on non-Unix systems")
|
||||
|
||||
type Indexer struct{}
|
||||
|
||||
func NewIndexer() *Indexer {
|
||||
return &Indexer{}
|
||||
}
|
||||
|
||||
func (i *Indexer) Init(_ context.Context) (bool, error) {
|
||||
return false, ErrNotImplemented
|
||||
}
|
||||
|
||||
func (i *Indexer) Ping(_ context.Context) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func (i *Indexer) Close() {
|
||||
// NOTHING TO DO
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build unix
|
||||
|
||||
package meta
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
)
|
||||
|
||||
const metaFilename = "zoekt_meta.json"
|
||||
|
||||
func indexMetadataPath(dir string) string {
|
||||
return filepath.Join(dir, metaFilename)
|
||||
}
|
||||
|
||||
type IndexMetadata struct {
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
func readJSON(path string, meta any) error {
|
||||
metaBytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(metaBytes, meta)
|
||||
}
|
||||
|
||||
func writeJSON(path string, meta any) error {
|
||||
metaBytes, err := json.Marshal(meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, metaBytes, 0o666)
|
||||
}
|
||||
|
||||
// ReadIndexMetadata returns the metadata for the index at the specified path.
|
||||
// If no such index metadata exists, an empty metadata and a nil error are
|
||||
// returned.
|
||||
func ReadIndexMetadata(path string) (*IndexMetadata, error) {
|
||||
meta := &IndexMetadata{}
|
||||
metaPath := indexMetadataPath(path)
|
||||
if _, err := os.Stat(metaPath); os.IsNotExist(err) {
|
||||
return meta, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return meta, readJSON(metaPath, meta)
|
||||
}
|
||||
|
||||
// WriteIndexMetadata writes metadata for the index at the specified path.
|
||||
func WriteIndexMetadata(path string, meta *IndexMetadata) error {
|
||||
return writeJSON(indexMetadataPath(path), meta)
|
||||
}
|
||||
Loading…
Reference in New Issue