0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-04-03 23:22:39 +02:00

Add archived repository filters to global code search

This commit is contained in:
Maxim 2026-03-24 01:21:17 +03:00
parent 4f9f0fc4b8
commit 4034e6d709
11 changed files with 215 additions and 13 deletions

View File

@ -97,7 +97,7 @@ func UpdateIndexerStatus(ctx context.Context, repo *Repository, indexerType Repo
return fmt.Errorf("UpdateIndexerStatus: Unable to getIndexerStatus for repo: %s Error: %w", repo.FullName(), err)
}
if len(status.CommitSha) == 0 {
if status.ID == 0 {
status.CommitSha = sha
if err := db.Insert(ctx, status); err != nil {
return fmt.Errorf("UpdateIndexerStatus: Unable to insert repoIndexerStatus for repo: %s Sha: %s Error: %w", repo.FullName(), sha, err)

View File

@ -54,6 +54,7 @@ func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error {
// RepoIndexerData data stored in the repo indexer
type RepoIndexerData struct {
RepoID int64
Archived bool
CommitID string
Content string
Filename string
@ -71,7 +72,7 @@ const (
filenameIndexerAnalyzer = "filenameIndexerAnalyzer"
filenameIndexerTokenizer = "filenameIndexerTokenizer"
repoIndexerDocType = "repoIndexerDocType"
repoIndexerLatestVersion = 9
repoIndexerLatestVersion = 10
)
// generateBleveIndexMapping generates a bleve index mapping for the repo indexer
@ -81,6 +82,10 @@ func generateBleveIndexMapping() (mapping.IndexMapping, error) {
numericFieldMapping.IncludeInAll = false
docMapping.AddFieldMappingsAt("RepoID", numericFieldMapping)
boolFieldMapping := bleve.NewBooleanFieldMapping()
boolFieldMapping.IncludeInAll = false
docMapping.AddFieldMappingsAt("Archived", boolFieldMapping)
textFieldMapping := bleve.NewTextFieldMapping()
textFieldMapping.IncludeInAll = false
docMapping.AddFieldMappingsAt("Content", textFieldMapping)
@ -195,6 +200,7 @@ func (b *Indexer) addUpdate(ctx context.Context, catFileBatch git.CatFileBatch,
id := internal.FilenameIndexerID(repo.ID, update.Filename)
return batch.Index(id, &RepoIndexerData{
RepoID: repo.ID,
Archived: repo.IsArchived,
CommitID: commitSha,
Filename: update.Filename,
Content: string(charset.ToUTF8DropErrors(fileContents)),
@ -298,6 +304,13 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int
indexerQuery = keywordQuery
}
if opts.Archived.Has() {
indexerQuery = bleve.NewConjunctionQuery(
indexerQuery,
inner_bleve.BoolFieldQuery(opts.Archived.Value(), "Archived"),
)
}
// Save for reuse without language filter
facetQuery := indexerQuery
if len(opts.Language) > 0 {

View File

@ -32,7 +32,7 @@ import (
)
const (
esRepoIndexerLatestVersion = 3
esRepoIndexerLatestVersion = 4
// multi-match-types, currently only 2 types are used
// Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types
esMultiMatchTypeBestFields = "best_fields"
@ -100,6 +100,10 @@ const (
"type": "long",
"index": true
},
"archived": {
"type": "boolean",
"index": true
},
"filename": {
"type": "text",
"term_vector": "with_positions_offsets",
@ -185,6 +189,7 @@ func (b *Indexer) addUpdate(ctx context.Context, catFileBatch git.CatFileBatch,
Id(id).
Doc(map[string]any{
"repo_id": repo.ID,
"archived": repo.IsArchived,
"filename": update.Filename,
"content": string(charset.ToUTF8DropErrors(fileContents)),
"commit_id": sha,
@ -377,6 +382,9 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int
repoQuery := elastic.NewTermsQuery("repo_id", repoStrs...)
query = query.Must(repoQuery)
}
if opts.Archived.Has() {
query = query.Must(elastic.NewTermQuery("archived", opts.Archived.Value()))
}
var (
start, pageSize = opts.GetSkipTake()

View File

@ -10,11 +10,13 @@ import (
"testing"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
indexer_module "code.gitea.io/gitea/modules/indexer"
"code.gitea.io/gitea/modules/indexer/code/bleve"
"code.gitea.io/gitea/modules/indexer/code/elasticsearch"
"code.gitea.io/gitea/modules/indexer/code/internal"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
@ -280,6 +282,110 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) {
assert.NoError(t, tearDownRepositoryIndexes(t.Context(), indexer))
})
t.Run(name+"_archived_filter", func(t *testing.T) {
assert.NoError(t, setupRepositoryIndexes(t.Context(), indexer))
t.Cleanup(func() {
assert.NoError(t, tearDownRepositoryIndexes(context.Background(), indexer))
})
repo1, err := repo_model.GetRepositoryByID(t.Context(), 1)
require.NoError(t, err)
repo62, err := repo_model.GetRepositoryByID(t.Context(), 62)
require.NoError(t, err)
originalRepo1Archived := repo1.IsArchived
originalRepo62Archived := repo62.IsArchived
t.Cleanup(func() {
ctx := context.Background()
require.NoError(t, repo_model.SetArchiveRepoState(ctx, repo1, originalRepo1Archived))
require.NoError(t, repo_model.SetArchiveRepoState(ctx, repo62, originalRepo62Archived))
require.NoError(t, repo_model.UpdateIndexerStatus(ctx, repo1, repo_model.RepoIndexerTypeCode, ""))
require.NoError(t, repo_model.UpdateIndexerStatus(ctx, repo62, repo_model.RepoIndexerTypeCode, ""))
require.NoError(t, index(ctx, indexer, repo1.ID))
require.NoError(t, index(ctx, indexer, repo62.ID))
})
require.NoError(t, repo_model.SetArchiveRepoState(t.Context(), repo1, false))
require.NoError(t, repo_model.SetArchiveRepoState(t.Context(), repo62, true))
require.NoError(t, repo_model.UpdateIndexerStatus(t.Context(), repo1, repo_model.RepoIndexerTypeCode, ""))
require.NoError(t, repo_model.UpdateIndexerStatus(t.Context(), repo62, repo_model.RepoIndexerTypeCode, ""))
require.NoError(t, index(t.Context(), indexer, repo1.ID))
require.NoError(t, index(t.Context(), indexer, repo62.ID))
testCases := []struct {
name string
keyword string
archived optional.Option[bool]
total int64
filenames []string
}{
{
name: "exclude_archived_repo_results",
keyword: "cucumber",
archived: optional.Some(false),
total: 0,
filenames: []string{},
},
{
name: "include_archived_repo_results",
keyword: "cucumber",
archived: optional.None[bool](),
total: 2,
filenames: []string{"cucumber.md", "avocado.md"},
},
{
name: "only_archived_repo_results",
keyword: "cucumber",
archived: optional.Some(true),
total: 2,
filenames: []string{"cucumber.md", "avocado.md"},
},
{
name: "exclude_keeps_non_archived_repo_results",
keyword: "Description",
archived: optional.Some(false),
total: 1,
filenames: []string{"README.md"},
},
{
name: "include_keeps_non_archived_repo_results",
keyword: "Description",
archived: optional.None[bool](),
total: 1,
filenames: []string{"README.md"},
},
{
name: "only_excludes_non_archived_repo_results",
keyword: "Description",
archived: optional.Some(true),
total: 0,
filenames: []string{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
total, res, _, err := indexer.Search(t.Context(), &internal.SearchOptions{
Keyword: tc.keyword,
SearchMode: indexer_module.SearchModeWords,
Archived: tc.archived,
Paginator: &db.ListOptions{
Page: 1,
PageSize: 10,
},
})
require.NoError(t, err)
assert.Equal(t, tc.total, total)
filenames := make([]string, 0, len(res))
for _, hit := range res {
filenames = append(filenames, hit.Filename)
}
assert.Equal(t, tc.filenames, filenames)
})
}
})
}
func TestBleveIndexAndSearch(t *testing.T) {
@ -332,6 +438,13 @@ func tearDownRepositoryIndexes(ctx context.Context, indexer internal.Indexer) er
if err := indexer.Delete(ctx, repoID); err != nil {
return err
}
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
return err
}
if err := repo_model.UpdateIndexerStatus(ctx, repo, repo_model.RepoIndexerTypeCode, ""); err != nil {
return err
}
}
return nil
}

View File

@ -11,6 +11,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/indexer"
"code.gitea.io/gitea/modules/indexer/internal"
"code.gitea.io/gitea/modules/optional"
)
// Indexer defines an interface to index and search code contents
@ -28,6 +29,7 @@ type SearchOptions struct {
Language string
SearchMode indexer.SearchModeType
Archived optional.Option[bool]
db.Paginator
}

View File

@ -161,6 +161,8 @@
"search.type_tooltip": "Search type",
"search.fuzzy": "Fuzzy",
"search.fuzzy_tooltip": "Include results that closely match the search term",
"search.code_archived_filter.all": "Archived and not archived",
"search.code_archived_filter.not_archived": "Not Archived",
"search.words": "Words",
"search.words_tooltip": "Include only results that match the search term words",
"search.regexp": "Regexp",

View File

@ -22,6 +22,7 @@ import (
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/label"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
@ -1014,6 +1015,12 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
if err := actions_service.CleanRepoScheduleTasks(ctx, repo); err != nil {
log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
}
if setting.Indexer.RepoIndexerEnabled {
if err := repo_model.UpdateIndexerStatus(ctx, repo, repo_model.RepoIndexerTypeCode, ""); err != nil {
log.Error("Reset code indexer status for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
}
code_indexer.UpdateRepoIndexer(repo)
}
log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
} else {
if err := repo_model.SetArchiveRepoState(ctx, repo, *opts.Archived); err != nil {
@ -1026,6 +1033,12 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
}
}
if setting.Indexer.RepoIndexerEnabled {
if err := repo_model.UpdateIndexerStatus(ctx, repo, repo_model.RepoIndexerTypeCode, ""); err != nil {
log.Error("Reset code indexer status for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
}
code_indexer.UpdateRepoIndexer(repo)
}
log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
}
}

View File

@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/routers/common"
@ -19,8 +20,27 @@ import (
const (
// tplExploreCode explore code page template
tplExploreCode templates.TplName = "explore/code"
archivedModeExclude = "exclude"
archivedModeInclude = "include"
archivedModeOnly = "only"
)
func parseArchivedMode(value string) (string, optional.Option[bool]) {
switch value {
case archivedModeInclude:
return archivedModeInclude, optional.None[bool]()
case "":
return archivedModeInclude, optional.None[bool]()
case archivedModeOnly:
return archivedModeOnly, optional.Some(true)
case archivedModeExclude:
return archivedModeExclude, optional.Some(false)
default:
return archivedModeInclude, optional.None[bool]()
}
}
// Code render explore code page
func Code(ctx *context.Context) {
if !setting.Indexer.RepoIndexerEnabled || setting.Service.Explore.DisableCodePage {
@ -36,6 +56,10 @@ func Code(ctx *context.Context) {
ctx.Data["PageIsExploreCode"] = true
ctx.Data["PageIsViewCode"] = true
archivedMode, archivedFilter := parseArchivedMode(ctx.FormTrim("archived"))
ctx.Data["ArchivedMode"] = archivedMode
ctx.Data["IsArchived"] = archivedFilter
prepareSearch := common.PrepareCodeSearch(ctx)
if prepareSearch.Keyword == "" {
ctx.HTML(http.StatusOK, tplExploreCode)
@ -77,6 +101,7 @@ func Code(ctx *context.Context) {
Keyword: prepareSearch.Keyword,
SearchMode: prepareSearch.SearchMode,
Language: prepareSearch.Language,
Archived: archivedFilter,
Paginator: &db.ListOptions{
Page: page,
PageSize: setting.UI.RepoSearchPagingNum,

View File

@ -962,6 +962,12 @@ func handleSettingsPostArchive(ctx *context.Context) {
// update issue indexer
issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
if setting.Indexer.RepoIndexerEnabled {
if err := repo_model.UpdateIndexerStatus(ctx, repo, repo_model.RepoIndexerTypeCode, ""); err != nil {
log.Error("Reset code indexer status for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
}
code.UpdateRepoIndexer(repo)
}
ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
@ -991,6 +997,12 @@ func handleSettingsPostUnarchive(ctx *context.Context) {
// update issue indexer
issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
if setting.Indexer.RepoIndexerEnabled {
if err := repo_model.UpdateIndexerStatus(ctx, repo, repo_model.RepoIndexerTypeCode, ""); err != nil {
log.Error("Reset code indexer status for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
}
code.UpdateRepoIndexer(repo)
}
ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))

View File

@ -1,7 +1,7 @@
<div class="flex-text-block tw-flex-wrap">
{{range $term := .SearchResultLanguages}}
<a class="ui {{if eq $.Language $term.Language}}primary{{end}} basic label tw-m-0"
href="?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}&search_mode={{$.SelectedSearchMode}}">
href="?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}{{if $.SelectedSearchMode}}&search_mode={{$.SelectedSearchMode}}{{end}}{{if $.ArchivedMode}}&archived={{$.ArchivedMode}}{{end}}">
<i class="color-icon tw-mr-2" style="background-color: {{$term.Color}}"></i>
{{$term.Language}}
<div class="detail">{{$term.Count}}</div>

View File

@ -1,12 +1,26 @@
<form class="ui form ignore-dirty">
{{template "shared/search/combo" (dict
"Disabled" .CodeIndexerUnavailable
"Value" .Keyword
"Placeholder" (ctx.Locale.Tr "search.code_kind")
"SearchModes" .SearchModes
"SelectedSearchMode" .SelectedSearchMode
)}}
</form>
<div class="ui small secondary filter menu">
<form id="repo-search-form" class="ui form ignore-dirty tw-flex-1 tw-flex tw-items-center tw-gap-x-2">
{{if .Language}}<input type="hidden" name="l" value="{{.Language}}">{{end}}
<div class="tw-flex-1">
{{template "shared/search/combo" (dict
"Disabled" .CodeIndexerUnavailable
"Value" .Keyword
"Placeholder" (ctx.Locale.Tr "search.code_kind")
"SearchModes" .SearchModes
"SelectedSearchMode" .SelectedSearchMode
)}}
</div>
<div class="item ui small dropdown jump">
<span class="text">{{ctx.Locale.Tr "filter_title"}}</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu flex-items-menu">
<label class="item"><input type="radio" name="archived" {{if eq .ArchivedMode "include"}}checked{{end}} value="include"> {{ctx.Locale.Tr "search.code_archived_filter.all"}}</label>
<label class="item"><input type="radio" name="archived" {{if eq .ArchivedMode "exclude"}}checked{{end}} value="exclude"> {{ctx.Locale.Tr "search.code_archived_filter.not_archived"}}</label>
<label class="item"><input type="radio" name="archived" {{if .IsArchived.Value}}checked{{end}} value="only"> {{ctx.Locale.Tr "filter.is_archived"}}</label>
</div>
</div>
</form>
</div>
<div class="divider"></div>
<div class="ui list">
{{template "base/alert" .}}