mirror of
https://github.com/Jguer/yay.git
synced 2026-09-17 20:59:28 -05:00
fix(query): ignore unknown search filter results Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
464 lines
12 KiB
Go
464 lines
12 KiB
Go
package query
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/Jguer/aur"
|
|
alpm "github.com/Jguer/dyalpm"
|
|
"github.com/adrg/strutil"
|
|
"github.com/adrg/strutil/metrics"
|
|
mapset "github.com/deckarep/golang-set/v2"
|
|
"github.com/leonelquinteros/gotext"
|
|
|
|
"github.com/Jguer/yay/v13/pkg/db"
|
|
"github.com/Jguer/yay/v13/pkg/intrange"
|
|
settingslua "github.com/Jguer/yay/v13/pkg/settings/lua"
|
|
"github.com/Jguer/yay/v13/pkg/settings/parser"
|
|
"github.com/Jguer/yay/v13/pkg/text"
|
|
)
|
|
|
|
type SearchVerbosity int
|
|
|
|
// Verbosity settings for search.
|
|
const (
|
|
NumberMenu SearchVerbosity = iota
|
|
Detailed
|
|
Minimal
|
|
)
|
|
|
|
type Builder interface {
|
|
Len() int
|
|
Execute(ctx context.Context, dbExecutor db.Executor, pkgS []string)
|
|
Results(dbExecutor db.Executor, verboseSearch SearchVerbosity) error
|
|
GetTargets(include, exclude intrange.IntRanges, otherExclude mapset.Set[string]) ([]string, error)
|
|
SetLua(engine *settingslua.Engine)
|
|
}
|
|
|
|
type SortFunc func(pkgA, pkgB abstractResult) int
|
|
|
|
type SourceQueryBuilder struct {
|
|
results []abstractResult
|
|
sortBy string
|
|
searchBy string
|
|
targetMode parser.TargetMode
|
|
aurQueryMap map[string]*aur.Pkg
|
|
repoQueryMap map[string]alpm.Package
|
|
bottomUp bool
|
|
singleLineResults bool
|
|
separateSources bool
|
|
|
|
aurClient aur.QueryClient
|
|
logger *text.Logger
|
|
lua *settingslua.Engine
|
|
}
|
|
|
|
func NewSourceQueryBuilder(
|
|
aurClient aur.QueryClient,
|
|
logger *text.Logger,
|
|
sortBy string,
|
|
targetMode parser.TargetMode,
|
|
searchBy string,
|
|
bottomUp,
|
|
singleLineResults bool,
|
|
separateSources bool,
|
|
) *SourceQueryBuilder {
|
|
return &SourceQueryBuilder{
|
|
aurClient: aurClient,
|
|
logger: logger,
|
|
bottomUp: bottomUp,
|
|
sortBy: sortBy,
|
|
targetMode: targetMode,
|
|
searchBy: searchBy,
|
|
singleLineResults: singleLineResults,
|
|
separateSources: separateSources,
|
|
aurQueryMap: map[string]*aur.Pkg{},
|
|
repoQueryMap: map[string]alpm.Package{},
|
|
results: make([]abstractResult, 0, 100),
|
|
}
|
|
}
|
|
|
|
func (s *SourceQueryBuilder) SetLua(engine *settingslua.Engine) {
|
|
s.lua = engine
|
|
}
|
|
|
|
type abstractResult struct {
|
|
source string
|
|
name string
|
|
description string
|
|
packageBase string
|
|
votes int
|
|
popularity float64
|
|
firstSubmitted int
|
|
lastModified int
|
|
provides []string
|
|
metricScore float64
|
|
}
|
|
|
|
type abstractResults struct {
|
|
results []abstractResult
|
|
search string
|
|
metric strutil.StringMetric
|
|
separateSources bool
|
|
sortByFunc SortFunc
|
|
repoOrder []string
|
|
|
|
separateSourceCache map[string]float64
|
|
}
|
|
|
|
func (a *abstractResults) GetSortFunc(sortBy string, bottomUp bool) SortFunc {
|
|
var sortFunc SortFunc
|
|
|
|
// Primary sort
|
|
switch sortBy {
|
|
case "base":
|
|
sortFunc = func(pkgA, pkgB abstractResult) int {
|
|
return cmp.Compare(pkgA.packageBase, pkgB.packageBase)
|
|
}
|
|
case "modified":
|
|
sortFunc = func(pkgA, pkgB abstractResult) int {
|
|
return cmp.Compare(pkgA.lastModified, pkgB.lastModified)
|
|
}
|
|
case "name":
|
|
sortFunc = func(pkgA, pkgB abstractResult) int {
|
|
return cmp.Compare(pkgA.name, pkgB.name)
|
|
}
|
|
case "popularity":
|
|
sortFunc = func(pkgA, pkgB abstractResult) int {
|
|
return cmp.Compare(pkgA.popularity, pkgB.popularity)
|
|
}
|
|
case "submitted":
|
|
sortFunc = func(pkgA, pkgB abstractResult) int {
|
|
return cmp.Compare(pkgA.firstSubmitted, pkgB.firstSubmitted)
|
|
}
|
|
case "votes":
|
|
sortFunc = func(pkgA, pkgB abstractResult) int {
|
|
return cmp.Compare(pkgA.votes, pkgB.votes)
|
|
}
|
|
default:
|
|
sortFunc = func(pkgA, pkgB abstractResult) int {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// Sort by metric as a tie-breaker. Also handle separating sources when not a tie
|
|
{
|
|
originalSortFunc := sortFunc
|
|
sortFunc = func(pkgA, pkgB abstractResult) int {
|
|
if cmpResult := originalSortFunc(pkgA, pkgB); cmpResult != 0 {
|
|
if a.separateSources {
|
|
if cmpSources := cmp.Compare(pkgA.source, pkgB.source); cmpSources != 0 {
|
|
return cmpSources
|
|
}
|
|
}
|
|
return cmpResult
|
|
}
|
|
|
|
return cmp.Compare(pkgA.metricScore, pkgB.metricScore)
|
|
}
|
|
}
|
|
|
|
if bottomUp {
|
|
// Invert sort for bottom-up sorting
|
|
originalSortFunc := sortFunc
|
|
sortFunc = func(pkgA, pkgB abstractResult) int {
|
|
return -originalSortFunc(pkgA, pkgB)
|
|
}
|
|
}
|
|
|
|
return sortFunc
|
|
}
|
|
|
|
// prepareMetrics computes the expensive Jaro-Winkler-based rank once per result
|
|
// before sorting so the comparator stays cheap.
|
|
func (a *abstractResults) prepareMetrics() {
|
|
a.separateSourceCache = make(map[string]float64, len(a.repoOrder))
|
|
|
|
for i := range a.results {
|
|
a.results[i].metricScore = a.calculateMetric(&a.results[i])
|
|
}
|
|
}
|
|
|
|
func (s *SourceQueryBuilder) Execute(ctx context.Context, dbExecutor db.Executor, pkgS []string) {
|
|
var aurErr error
|
|
|
|
pkgS = RemoveInvalidTargets(s.logger, pkgS, s.targetMode)
|
|
|
|
metric := &metrics.JaroWinkler{
|
|
// Case-sensitive: we lower-case the corpus once in GetMetric, search string normalized in Execute.
|
|
CaseSensitive: true,
|
|
}
|
|
|
|
sortableResults := &abstractResults{
|
|
results: []abstractResult{},
|
|
search: strings.ToLower(strings.Join(pkgS, " ")),
|
|
metric: metric,
|
|
separateSources: s.separateSources,
|
|
repoOrder: dbExecutor.Repos(),
|
|
}
|
|
sortableResults.sortByFunc = sortableResults.GetSortFunc(s.sortBy, s.bottomUp)
|
|
|
|
var repoResults []alpm.Package
|
|
if s.targetMode.AtLeastRepo() {
|
|
repoResults = dbExecutor.SyncPackages(pkgS...)
|
|
|
|
for i := range repoResults {
|
|
dbName := repoResults[i].DB().Name()
|
|
s.repoQueryMap[dbName+"/"+repoResults[i].Name()] = repoResults[i]
|
|
|
|
rawProvides := repoResults[i].Provides()
|
|
|
|
provides := make([]string, len(rawProvides))
|
|
for j := range rawProvides {
|
|
provides[j] = rawProvides[j].Name
|
|
}
|
|
|
|
sortableResults.results = append(sortableResults.results, abstractResult{
|
|
source: dbName,
|
|
name: repoResults[i].Name(),
|
|
description: repoResults[i].Description(),
|
|
packageBase: repoResults[i].Base(),
|
|
votes: -1,
|
|
popularity: -1,
|
|
firstSubmitted: -1,
|
|
lastModified: -1,
|
|
provides: provides,
|
|
})
|
|
}
|
|
}
|
|
|
|
if s.targetMode.AtLeastAUR() {
|
|
var aurResults []aur.Pkg
|
|
aurResults, aurErr = queryAUR(ctx, s.aurClient, pkgS, s.searchBy)
|
|
dbName := "aur"
|
|
searchTerms := normalizeSearchTerms(pkgS)
|
|
|
|
for i := range aurResults {
|
|
by := getSearchBy(s.searchBy)
|
|
if (by == aur.NameDesc || by == aur.None || by == aur.Name) &&
|
|
!matchesSearch(&aurResults[i], searchTerms) {
|
|
continue
|
|
}
|
|
|
|
s.aurQueryMap[aurResults[i].Name] = &aurResults[i]
|
|
|
|
sortableResults.results = append(sortableResults.results, abstractResult{
|
|
source: dbName,
|
|
name: aurResults[i].Name,
|
|
description: aurResults[i].Description,
|
|
packageBase: aurResults[i].PackageBase,
|
|
votes: aurResults[i].NumVotes,
|
|
popularity: aurResults[i].Popularity,
|
|
firstSubmitted: aurResults[i].FirstSubmitted,
|
|
lastModified: aurResults[i].LastModified,
|
|
provides: aurResults[i].Provides,
|
|
})
|
|
}
|
|
}
|
|
sortableResults.prepareMetrics()
|
|
|
|
slices.SortFunc(sortableResults.results, func(a, b abstractResult) int {
|
|
return sortableResults.sortByFunc(b, a)
|
|
})
|
|
s.results = s.applySearchFilter(sortableResults.results)
|
|
|
|
if aurErr != nil {
|
|
s.logger.Errorln(ErrAURSearch{inner: aurErr})
|
|
|
|
if len(repoResults) != 0 {
|
|
s.logger.Warnln(gotext.Get("Showing repo packages only"))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *SourceQueryBuilder) Results(dbExecutor db.Executor, verboseSearch SearchVerbosity) error {
|
|
for i := range s.results {
|
|
if verboseSearch == Minimal {
|
|
s.logger.Println(s.results[i].name)
|
|
continue
|
|
}
|
|
|
|
var toPrint string
|
|
|
|
if verboseSearch == NumberMenu {
|
|
if s.bottomUp {
|
|
toPrint += text.Magenta(strconv.Itoa(len(s.results)-i)) + " "
|
|
} else {
|
|
toPrint += text.Magenta(strconv.Itoa(i+1)) + " "
|
|
}
|
|
}
|
|
|
|
if s.results[i].source == "aur" {
|
|
pkg := s.aurQueryMap[s.results[i].name]
|
|
toPrint += s.renderAUR(pkg, dbExecutor)
|
|
} else {
|
|
pkg := s.repoQueryMap[s.results[i].source+"/"+s.results[i].name]
|
|
toPrint += s.renderSync(pkg, dbExecutor)
|
|
}
|
|
|
|
s.logger.Println(toPrint)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SourceQueryBuilder) Len() int {
|
|
return len(s.results)
|
|
}
|
|
|
|
func (s *SourceQueryBuilder) GetTargets(include, exclude intrange.IntRanges,
|
|
otherExclude mapset.Set[string],
|
|
) ([]string, error) {
|
|
var (
|
|
isInclude = len(exclude) == 0 && otherExclude.Cardinality() == 0
|
|
targets []string
|
|
lenRes = len(s.results)
|
|
)
|
|
|
|
for i := 1; i <= s.Len(); i++ {
|
|
target := i - 1
|
|
if s.bottomUp {
|
|
target = lenRes - i
|
|
}
|
|
|
|
if (isInclude && include.Get(i)) || (!isInclude && !exclude.Get(i)) {
|
|
targets = append(targets, s.results[target].source+"/"+s.results[target].name)
|
|
}
|
|
}
|
|
|
|
return targets, nil
|
|
}
|
|
|
|
func normalizeSearchTerms(terms []string) []string {
|
|
normalized := make([]string, len(terms))
|
|
for i, term := range terms {
|
|
normalized[i] = strings.ToLower(term)
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
func matchesSearch(pkg *aur.Pkg, terms []string) bool {
|
|
if len(terms) <= 1 {
|
|
return true
|
|
}
|
|
|
|
name := strings.ToLower(pkg.Name)
|
|
desc := strings.ToLower(pkg.Description)
|
|
for _, pkgN := range terms {
|
|
if strings.ContainsFunc(pkgN, unicode.IsSymbol) {
|
|
continue
|
|
}
|
|
|
|
if !strings.Contains(name, pkgN) && !strings.Contains(desc, pkgN) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (s *SourceQueryBuilder) applySearchFilter(results []abstractResult) []abstractResult {
|
|
if s.lua == nil || !s.lua.HasAutocmd(settingslua.EventSearchFilter) {
|
|
return results
|
|
}
|
|
|
|
pkgs := make([]settingslua.SearchResultPackage, len(results))
|
|
for i := range results {
|
|
pkgs[i] = settingslua.SearchResultPackage{
|
|
Source: results[i].source,
|
|
Name: results[i].name,
|
|
Description: results[i].description,
|
|
Base: results[i].packageBase,
|
|
Votes: results[i].votes,
|
|
Popularity: results[i].popularity,
|
|
FirstSubmitted: results[i].firstSubmitted,
|
|
LastModified: results[i].lastModified,
|
|
}
|
|
}
|
|
|
|
refs, err := s.lua.RunSearchFilter(&settingslua.SearchFilterEvent{Results: pkgs})
|
|
if err != nil {
|
|
s.logger.Errorln(err)
|
|
return results
|
|
}
|
|
|
|
if refs == nil {
|
|
return results
|
|
}
|
|
|
|
byRef := make(map[settingslua.SearchResultRef]abstractResult, len(results))
|
|
for i := range results {
|
|
byRef[settingslua.SearchResultRef{Source: results[i].source, Name: results[i].name}] = results[i]
|
|
}
|
|
|
|
filtered := make([]abstractResult, 0, len(refs))
|
|
for _, ref := range refs {
|
|
if result, ok := byRef[ref]; ok {
|
|
filtered = append(filtered, result)
|
|
}
|
|
}
|
|
|
|
return filtered
|
|
}
|
|
|
|
func (s *SourceQueryBuilder) renderAUR(pkg *aur.Pkg, dbExecutor db.Executor) string {
|
|
var localVersion string
|
|
if localPkg := dbExecutor.LocalPackage(pkg.Name); localPkg != nil {
|
|
localVersion = localPkg.Version()
|
|
}
|
|
|
|
if s.lua != nil && s.lua.HasAutocmd(settingslua.EventRenderAUR) {
|
|
rendered, ok, err := s.lua.RunRenderAUR(&settingslua.RenderAUREvent{
|
|
Name: pkg.Name,
|
|
Version: pkg.Version,
|
|
Description: pkg.Description,
|
|
Base: pkg.PackageBase,
|
|
Votes: pkg.NumVotes,
|
|
Popularity: pkg.Popularity,
|
|
Maintainer: pkg.Maintainer,
|
|
OutOfDate: pkg.OutOfDate,
|
|
FirstSubmitted: pkg.FirstSubmitted,
|
|
LastModified: pkg.LastModified,
|
|
LocalVersion: localVersion,
|
|
})
|
|
if err != nil {
|
|
s.logger.Errorln(err)
|
|
} else if ok {
|
|
return rendered
|
|
}
|
|
}
|
|
|
|
return aurPkgSearchStringResolved(pkg, localVersion, s.singleLineResults)
|
|
}
|
|
|
|
func (s *SourceQueryBuilder) renderSync(pkg alpm.Package, dbExecutor db.Executor) string {
|
|
var localVersion string
|
|
if localPkg := dbExecutor.LocalPackage(pkg.Name()); localPkg != nil {
|
|
localVersion = localPkg.Version()
|
|
}
|
|
|
|
groups := dbExecutor.PackageGroups(pkg)
|
|
|
|
if s.lua != nil && s.lua.HasAutocmd(settingslua.EventRenderSync) {
|
|
rendered, ok, err := s.lua.RunRenderSync(&settingslua.RenderSyncEvent{
|
|
Repository: pkg.DB().Name(),
|
|
Name: pkg.Name(),
|
|
Description: pkg.Description(),
|
|
Version: pkg.Version(),
|
|
Groups: groups,
|
|
LocalVersion: localVersion,
|
|
})
|
|
if err != nil {
|
|
s.logger.Errorln(err)
|
|
} else if ok {
|
|
return rendered
|
|
}
|
|
}
|
|
|
|
return syncPkgSearchStringResolved(pkg, groups, localVersion, s.singleLineResults)
|
|
}
|