mirror of
https://github.com/Jguer/yay
synced 2024-10-01 13:53:32 +00:00
feat(search): Add fuzzy name matching and mixed sources (#1719)
* fix(alpm): fix callback text * feat(yay): Add mixed search result * remove old result structs * add option for controlling query builder * only set query builder after parsing args * add parser args * update manpage * write test for results * write test for results * mixed source test * only sort 1 time with every mode
This commit is contained in:
parent
ae01f8e4a0
commit
e4a1f018ea
|
@ -68,6 +68,13 @@ linters:
|
|||
- whitespace
|
||||
- wsl
|
||||
- godot
|
||||
# - maligned
|
||||
# - interfacer
|
||||
# - nilerr
|
||||
# - nlreturn
|
||||
# - exhaustivestruct
|
||||
# - errname
|
||||
# - forbidigo
|
||||
|
||||
run:
|
||||
|
||||
|
|
18
cmd.go
18
cmd.go
|
@ -185,7 +185,7 @@ func handleCmd(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Exe
|
|||
case "P", "show":
|
||||
return handlePrint(ctx, cmdArgs, dbExecutor)
|
||||
case "Y", "--yay":
|
||||
return handleYay(ctx, cmdArgs, dbExecutor)
|
||||
return handleYay(ctx, cmdArgs, dbExecutor, config.Runtime.QueryBuilder)
|
||||
}
|
||||
|
||||
return errors.New(gotext.Get("unhandled operation"))
|
||||
|
@ -289,7 +289,7 @@ func handlePrint(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.E
|
|||
return nil
|
||||
}
|
||||
|
||||
func handleYay(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Executor) error {
|
||||
func handleYay(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Executor, queryBuilder query.Builder) error {
|
||||
switch {
|
||||
case cmdArgs.ExistsArg("gendb"):
|
||||
return createDevelDB(ctx, config, dbExecutor)
|
||||
|
@ -298,7 +298,7 @@ func handleYay(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Exe
|
|||
case cmdArgs.ExistsArg("c", "clean"):
|
||||
return cleanDependencies(ctx, cmdArgs, dbExecutor, false)
|
||||
case len(cmdArgs.Targets) > 0:
|
||||
return handleYogurt(ctx, cmdArgs, dbExecutor)
|
||||
return displayNumberMenu(ctx, cmdArgs.Targets, dbExecutor, queryBuilder, cmdArgs)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -312,16 +312,12 @@ func handleGetpkgbuild(ctx context.Context, cmdArgs *parser.Arguments, dbExecuto
|
|||
return getPkgbuilds(ctx, dbExecutor, config, cmdArgs.Targets, cmdArgs.ExistsArg("f", "force"))
|
||||
}
|
||||
|
||||
func handleYogurt(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Executor) error {
|
||||
return displayNumberMenu(ctx, cmdArgs.Targets, dbExecutor, cmdArgs)
|
||||
}
|
||||
|
||||
func handleSync(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Executor) error {
|
||||
targets := cmdArgs.Targets
|
||||
|
||||
switch {
|
||||
case cmdArgs.ExistsArg("s", "search"):
|
||||
return syncSearch(ctx, targets, config.Runtime.AURClient, dbExecutor, !cmdArgs.ExistsArg("q", "quiet"))
|
||||
return syncSearch(ctx, targets, config.Runtime.AURClient, dbExecutor, config.Runtime.QueryBuilder, !cmdArgs.ExistsArg("q", "quiet"))
|
||||
case cmdArgs.ExistsArg("p", "print", "print-format"):
|
||||
return config.Runtime.CmdBuilder.Show(config.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
|
||||
cmdArgs, config.Runtime.Mode, settings.NoConfirm))
|
||||
|
@ -355,9 +351,9 @@ func handleRemove(ctx context.Context, cmdArgs *parser.Arguments, localCache *vc
|
|||
}
|
||||
|
||||
// NumberMenu presents a CLI for selecting packages to install.
|
||||
func displayNumberMenu(ctx context.Context, pkgS []string, dbExecutor db.Executor, cmdArgs *parser.Arguments) error {
|
||||
queryBuilder := query.NewSourceQueryBuilder(config.SortBy, config.Runtime.Mode, config.SearchBy, config.BottomUp, config.SingleLineResults)
|
||||
|
||||
func displayNumberMenu(ctx context.Context, pkgS []string, dbExecutor db.Executor,
|
||||
queryBuilder query.Builder, cmdArgs *parser.Arguments,
|
||||
) error {
|
||||
queryBuilder.Execute(ctx, dbExecutor, config.Runtime.AURClient, pkgS)
|
||||
|
||||
if err := queryBuilder.Results(os.Stdout, dbExecutor, query.NumberMenu); err != nil {
|
||||
|
|
|
@ -382,6 +382,14 @@ the last modification time of each package's AUR page.
|
|||
.B \-\-notimeupdate
|
||||
Do not consider build times during sysupgrade.
|
||||
|
||||
.TP
|
||||
.B \-\-separatesources
|
||||
Separate query results by source, AUR and sync
|
||||
|
||||
.TP
|
||||
.B \-\-noseparatesources
|
||||
Do not separate query results by source for searching
|
||||
|
||||
.TP
|
||||
.B \-\-redownload
|
||||
Always download pkgbuilds of targets even when a copy is available in cache.
|
||||
|
|
1
go.mod
1
go.mod
|
@ -15,6 +15,7 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/adrg/strutil v0.2.3
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -6,6 +6,8 @@ github.com/Morganamilo/go-pacmanconf v0.0.0-20210502114700-cff030e927a5 h1:TMscP
|
|||
github.com/Morganamilo/go-pacmanconf v0.0.0-20210502114700-cff030e927a5/go.mod h1:Hk55m330jNiwxRodIlMCvw5iEyoRUCIY64W1p9D+tHc=
|
||||
github.com/Morganamilo/go-srcinfo v1.0.0 h1:Wh4nEF+HJWo+29hnxM18Q2hi+DUf0GejS13+Wg+dzmI=
|
||||
github.com/Morganamilo/go-srcinfo v1.0.0/go.mod h1:MP6VGY1NNpVUmYIEgoM9acix95KQqIRyqQ0hCLsyYUY=
|
||||
github.com/adrg/strutil v0.2.3 h1:WZVn3ItPBovFmP4wMHHVXUr8luRaHrbyIuLlHt32GZQ=
|
||||
github.com/adrg/strutil v0.2.3/go.mod h1:+SNxbiH6t+O+5SZqIj5n/9i5yUjR+S3XXVrjEcN2mxg=
|
||||
github.com/bradleyjkemp/cupaloy v2.3.0+incompatible h1:UafIjBvWQmS9i/xRg+CamMrnLTKNzo+bdmT/oH34c2Y=
|
||||
github.com/bradleyjkemp/cupaloy v2.3.0+incompatible/go.mod h1:Au1Xw1sgaJ5iSFktEhYsS0dbQiS1B0/XMXl+42y9Ilk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
|
9
main.go
9
main.go
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
"github.com/Jguer/yay/v11/pkg/db"
|
||||
"github.com/Jguer/yay/v11/pkg/db/ialpm"
|
||||
"github.com/Jguer/yay/v11/pkg/query"
|
||||
"github.com/Jguer/yay/v11/pkg/settings"
|
||||
"github.com/Jguer/yay/v11/pkg/settings/parser"
|
||||
"github.com/Jguer/yay/v11/pkg/text"
|
||||
|
@ -131,6 +132,14 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
if config.SeparateSources {
|
||||
config.Runtime.QueryBuilder = query.NewSourceQueryBuilder(config.SortBy,
|
||||
config.Runtime.Mode, config.SearchBy, config.BottomUp, config.SingleLineResults)
|
||||
} else {
|
||||
config.Runtime.QueryBuilder = query.NewMixedSourceQueryBuilder(config.SortBy,
|
||||
config.Runtime.Mode, config.SearchBy, config.BottomUp, config.SingleLineResults)
|
||||
}
|
||||
|
||||
var useColor bool
|
||||
|
||||
config.Runtime.PacmanConf, useColor, err = initAlpm(cmdArgs, config.PacmanConf)
|
||||
|
|
|
@ -160,7 +160,7 @@ func (ae *AlpmExecutor) questionCallback() func(question alpm.QuestionAny) {
|
|||
return nil
|
||||
})
|
||||
|
||||
str := text.Bold(gotext.Get("There are %d providers available for %s:\n", size, qp.Dep()))
|
||||
str := text.Bold(gotext.Get("There are %d providers available for %s:", size, qp.Dep()))
|
||||
|
||||
size = 1
|
||||
|
||||
|
@ -171,7 +171,8 @@ func (ae *AlpmExecutor) questionCallback() func(question alpm.QuestionAny) {
|
|||
|
||||
if dbName != thisDB {
|
||||
dbName = thisDB
|
||||
str += text.SprintOperationInfo(gotext.Get("Repository"), dbName, "\n ")
|
||||
str += "\n"
|
||||
str += text.SprintOperationInfo(gotext.Get("Repository"), " ", dbName, "\n ")
|
||||
}
|
||||
str += fmt.Sprintf("%d) %s ", size, pkg.Name())
|
||||
size++
|
||||
|
|
|
@ -148,7 +148,7 @@ func (p *Package) Packager() string {
|
|||
|
||||
// Provides returns DependList of packages provides by package.
|
||||
func (p *Package) Provides() alpm.DependList {
|
||||
panic("not implemented") // TODO: Implement
|
||||
return alpm.DependList{}
|
||||
}
|
||||
|
||||
// Origin returns package origin.
|
||||
|
|
262
pkg/query/mixed_sources.go
Normal file
262
pkg/query/mixed_sources.go
Normal file
|
@ -0,0 +1,262 @@
|
|||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Jguer/aur"
|
||||
"github.com/Jguer/go-alpm/v2"
|
||||
"github.com/adrg/strutil"
|
||||
"github.com/adrg/strutil/metrics"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/Jguer/yay/v11/pkg/db"
|
||||
"github.com/Jguer/yay/v11/pkg/intrange"
|
||||
"github.com/Jguer/yay/v11/pkg/settings/parser"
|
||||
"github.com/Jguer/yay/v11/pkg/stringset"
|
||||
"github.com/Jguer/yay/v11/pkg/text"
|
||||
)
|
||||
|
||||
const sourceAUR = "aur"
|
||||
|
||||
type Builder interface {
|
||||
Len() int
|
||||
Execute(ctx context.Context, dbExecutor db.Executor, aurClient *aur.Client, pkgS []string)
|
||||
Results(w io.Writer, dbExecutor db.Executor, verboseSearch SearchVerbosity) error
|
||||
GetTargets(include, exclude intrange.IntRanges, otherExclude stringset.StringSet) ([]string, error)
|
||||
}
|
||||
|
||||
type MixedSourceQueryBuilder struct {
|
||||
results []abstractResult
|
||||
sortBy string
|
||||
searchBy string
|
||||
targetMode parser.TargetMode
|
||||
queryMap map[string]map[string]interface{}
|
||||
bottomUp bool
|
||||
singleLineResults bool
|
||||
}
|
||||
|
||||
func NewMixedSourceQueryBuilder(
|
||||
sortBy string,
|
||||
targetMode parser.TargetMode,
|
||||
searchBy string,
|
||||
bottomUp,
|
||||
singleLineResults bool,
|
||||
) *MixedSourceQueryBuilder {
|
||||
return &MixedSourceQueryBuilder{
|
||||
bottomUp: bottomUp,
|
||||
sortBy: sortBy,
|
||||
targetMode: targetMode,
|
||||
searchBy: searchBy,
|
||||
singleLineResults: singleLineResults,
|
||||
queryMap: map[string]map[string]interface{}{},
|
||||
results: make([]abstractResult, 0, 100),
|
||||
}
|
||||
}
|
||||
|
||||
type abstractResult struct {
|
||||
source string
|
||||
name string
|
||||
description string
|
||||
votes int
|
||||
provides []string
|
||||
}
|
||||
|
||||
type abstractResults struct {
|
||||
results []abstractResult
|
||||
search string
|
||||
distanceCache map[string]float64
|
||||
bottomUp bool
|
||||
metric strutil.StringMetric
|
||||
}
|
||||
|
||||
func (a *abstractResults) Len() int { return len(a.results) }
|
||||
func (a *abstractResults) Swap(i, j int) { a.results[i], a.results[j] = a.results[j], a.results[i] }
|
||||
|
||||
func (a *abstractResults) GetMetric(pkg *abstractResult) float64 {
|
||||
if v, ok := a.distanceCache[pkg.name]; ok {
|
||||
return v
|
||||
}
|
||||
|
||||
sim := strutil.Similarity(pkg.name, a.search, a.metric)
|
||||
|
||||
for _, prov := range pkg.provides {
|
||||
// If the package provides search, it's a perfect match
|
||||
// AUR packages don't populate provides
|
||||
candidate := strutil.Similarity(prov, a.search, a.metric)
|
||||
if candidate > sim {
|
||||
sim = candidate
|
||||
}
|
||||
}
|
||||
|
||||
simDesc := strutil.Similarity(pkg.description, a.search, a.metric)
|
||||
|
||||
// slightly overweight sync sources by always giving them max popularity
|
||||
popularity := 1.0
|
||||
if pkg.source == sourceAUR {
|
||||
popularity = float64(pkg.votes) / float64(pkg.votes+60)
|
||||
}
|
||||
|
||||
sim = sim*0.6 + simDesc*0.2 + popularity*0.2
|
||||
|
||||
a.distanceCache[pkg.name] = sim
|
||||
|
||||
return sim
|
||||
}
|
||||
|
||||
func (a *abstractResults) Less(i, j int) bool {
|
||||
pkgA := a.results[i]
|
||||
pkgB := a.results[j]
|
||||
|
||||
simA := a.GetMetric(&pkgA)
|
||||
simB := a.GetMetric(&pkgB)
|
||||
|
||||
if a.bottomUp {
|
||||
return simA < simB
|
||||
}
|
||||
|
||||
return simA > simB
|
||||
}
|
||||
|
||||
func (s *MixedSourceQueryBuilder) Execute(ctx context.Context, dbExecutor db.Executor, aurClient *aur.Client, pkgS []string) {
|
||||
var aurErr error
|
||||
|
||||
pkgS = RemoveInvalidTargets(pkgS, s.targetMode)
|
||||
|
||||
metric := &metrics.JaroWinkler{
|
||||
CaseSensitive: false,
|
||||
}
|
||||
|
||||
sortableResults := &abstractResults{
|
||||
results: []abstractResult{},
|
||||
search: strings.Join(pkgS, ""),
|
||||
distanceCache: map[string]float64{},
|
||||
bottomUp: s.bottomUp,
|
||||
metric: metric,
|
||||
}
|
||||
|
||||
if s.targetMode.AtLeastAUR() {
|
||||
var aurResults aurQuery
|
||||
aurResults, aurErr = queryAUR(ctx, aurClient, pkgS, s.searchBy)
|
||||
dbName := sourceAUR
|
||||
|
||||
for i := range aurResults {
|
||||
if s.queryMap[dbName] == nil {
|
||||
s.queryMap[dbName] = map[string]interface{}{}
|
||||
}
|
||||
|
||||
s.queryMap[dbName][aurResults[i].Name] = aurResults[i]
|
||||
|
||||
sortableResults.results = append(sortableResults.results, abstractResult{
|
||||
source: dbName,
|
||||
name: aurResults[i].Name,
|
||||
description: aurResults[i].Description,
|
||||
provides: aurResults[i].Provides,
|
||||
votes: aurResults[i].NumVotes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var repoResults []alpm.IPackage
|
||||
if s.targetMode.AtLeastRepo() {
|
||||
repoResults = dbExecutor.SyncPackages(pkgS...)
|
||||
|
||||
for i := range repoResults {
|
||||
dbName := repoResults[i].DB().Name()
|
||||
if s.queryMap[dbName] == nil {
|
||||
s.queryMap[dbName] = map[string]interface{}{}
|
||||
}
|
||||
|
||||
s.queryMap[dbName][repoResults[i].Name()] = repoResults[i]
|
||||
|
||||
rawProvides := repoResults[i].Provides().Slice()
|
||||
|
||||
provides := make([]string, len(rawProvides))
|
||||
for j := range rawProvides {
|
||||
provides[j] = rawProvides[j].Name
|
||||
}
|
||||
|
||||
sortableResults.results = append(sortableResults.results, abstractResult{
|
||||
source: repoResults[i].DB().Name(),
|
||||
name: repoResults[i].Name(),
|
||||
description: repoResults[i].Description(),
|
||||
provides: provides,
|
||||
votes: -1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(sortableResults)
|
||||
s.results = sortableResults.results
|
||||
|
||||
if aurErr != nil {
|
||||
text.Errorln(ErrAURSearch{inner: aurErr})
|
||||
|
||||
if len(repoResults) != 0 {
|
||||
text.Warnln(gotext.Get("Showing repo packages only"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MixedSourceQueryBuilder) Results(w io.Writer, dbExecutor db.Executor, verboseSearch SearchVerbosity) error {
|
||||
for i := range s.results {
|
||||
if verboseSearch == Minimal {
|
||||
_, _ = fmt.Fprintln(w, 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)) + " "
|
||||
}
|
||||
}
|
||||
|
||||
pkg := s.queryMap[s.results[i].source][s.results[i].name]
|
||||
if s.results[i].source == sourceAUR {
|
||||
aurPkg := pkg.(aur.Pkg)
|
||||
toPrint += aurPkgSearchString(&aurPkg, dbExecutor, s.singleLineResults)
|
||||
} else {
|
||||
syncPkg := pkg.(alpm.IPackage)
|
||||
toPrint += syncPkgSearchString(syncPkg, dbExecutor, s.singleLineResults)
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, toPrint)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MixedSourceQueryBuilder) Len() int {
|
||||
return len(s.results)
|
||||
}
|
||||
|
||||
func (s *MixedSourceQueryBuilder) GetTargets(include, exclude intrange.IntRanges,
|
||||
otherExclude stringset.StringSet,
|
||||
) ([]string, error) {
|
||||
var (
|
||||
isInclude = len(exclude) == 0 && len(otherExclude) == 0
|
||||
targets []string
|
||||
lenRes = len(s.results)
|
||||
)
|
||||
|
||||
for i := 0; 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
|
||||
}
|
61
pkg/query/mixed_sources_test.go
Normal file
61
pkg/query/mixed_sources_test.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Jguer/yay/v11/pkg/settings/parser"
|
||||
|
||||
"github.com/Jguer/aur"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMixedSourceQueryBuilder(t *testing.T) {
|
||||
t.Parallel()
|
||||
type testCase struct {
|
||||
desc string
|
||||
bottomUp bool
|
||||
want string
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{desc: "bottomup", bottomUp: true, want: "\x1b[1m\x1b[34maur\x1b[0m\x1b[0m/\x1b[1mlinux-ck\x1b[0m \x1b[36m5.16.12-1\x1b[0m\x1b[1m (+450\x1b[0m \x1b[1m1.51) \x1b[0m\n The Linux-ck kernel and modules with ck's hrtimer patches\n\x1b[1m\x1b[33mcore\x1b[0m\x1b[0m/\x1b[1mlinux-zen\x1b[0m \x1b[36m5.16.0\x1b[0m\x1b[1m (1.0 B 1.0 B) \x1b[0m\n The Linux ZEN kernel and modules\n\x1b[1m\x1b[33mcore\x1b[0m\x1b[0m/\x1b[1mlinux\x1b[0m \x1b[36m5.16.0\x1b[0m\x1b[1m (1.0 B 1.0 B) \x1b[0m\n The Linux kernel and modules\n"},
|
||||
{
|
||||
desc: "topdown", bottomUp: false,
|
||||
want: "\x1b[1m\x1b[33mcore\x1b[0m\x1b[0m/\x1b[1mlinux\x1b[0m \x1b[36m5.16.0\x1b[0m\x1b[1m (1.0 B 1.0 B) \x1b[0m\n The Linux kernel and modules\n\x1b[1m\x1b[33mcore\x1b[0m\x1b[0m/\x1b[1mlinux-zen\x1b[0m \x1b[36m5.16.0\x1b[0m\x1b[1m (1.0 B 1.0 B) \x1b[0m\n The Linux ZEN kernel and modules\n\x1b[1m\x1b[34maur\x1b[0m\x1b[0m/\x1b[1mlinux-ck\x1b[0m \x1b[36m5.16.12-1\x1b[0m\x1b[1m (+450\x1b[0m \x1b[1m1.51) \x1b[0m\n The Linux-ck kernel and modules with ck's hrtimer patches\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
queryBuilder := NewMixedSourceQueryBuilder("votes", parser.ModeAny, "", tc.bottomUp, false)
|
||||
search := []string{"linux"}
|
||||
mockStore := &mockDB{}
|
||||
|
||||
client, err := aur.NewClient(aur.WithHTTPClient(&mockDoer{}))
|
||||
require.NoError(t, err)
|
||||
queryBuilder.Execute(context.Background(), mockStore, client, search)
|
||||
assert.Len(t, queryBuilder.results, 3)
|
||||
assert.Equal(t, 3, queryBuilder.Len())
|
||||
|
||||
if tc.bottomUp {
|
||||
assert.Equal(t, "linux-ck", queryBuilder.results[0].name)
|
||||
assert.Equal(t, "linux-zen", queryBuilder.results[1].name)
|
||||
assert.Equal(t, "linux", queryBuilder.results[2].name)
|
||||
} else {
|
||||
assert.Equal(t, "linux-ck", queryBuilder.results[2].name)
|
||||
assert.Equal(t, "linux-zen", queryBuilder.results[1].name)
|
||||
assert.Equal(t, "linux", queryBuilder.results[0].name)
|
||||
}
|
||||
|
||||
w := &strings.Builder{}
|
||||
queryBuilder.Results(w, mockStore, Detailed)
|
||||
|
||||
wString := w.String()
|
||||
require.GreaterOrEqual(t, len(wString), 1)
|
||||
assert.Equal(t, tc.want, wString)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -29,10 +29,10 @@ const (
|
|||
type SourceQueryBuilder struct {
|
||||
repoQuery
|
||||
aurQuery
|
||||
bottomUp bool
|
||||
sortBy string
|
||||
targetMode parser.TargetMode
|
||||
searchBy string
|
||||
targetMode parser.TargetMode
|
||||
bottomUp bool
|
||||
singleLineResults bool
|
||||
}
|
||||
|
||||
|
@ -60,11 +60,18 @@ func (s *SourceQueryBuilder) Execute(ctx context.Context, dbExecutor db.Executor
|
|||
pkgS = RemoveInvalidTargets(pkgS, s.targetMode)
|
||||
|
||||
if s.targetMode.AtLeastAUR() {
|
||||
s.aurQuery, aurErr = queryAUR(ctx, aurClient, pkgS, s.searchBy, s.bottomUp, s.sortBy)
|
||||
s.aurQuery, aurErr = queryAUR(ctx, aurClient, pkgS, s.searchBy)
|
||||
s.aurQuery = filterAURResults(pkgS, s.aurQuery)
|
||||
|
||||
sort.Sort(aurSortable{aurQuery: s.aurQuery, sortBy: s.sortBy, bottomUp: s.bottomUp})
|
||||
}
|
||||
|
||||
if s.targetMode.AtLeastRepo() {
|
||||
s.repoQuery = queryRepo(pkgS, dbExecutor, s.bottomUp)
|
||||
s.repoQuery = repoQuery(dbExecutor.SyncPackages(pkgS...))
|
||||
|
||||
if s.bottomUp {
|
||||
s.Reverse()
|
||||
}
|
||||
}
|
||||
|
||||
if aurErr != nil && len(s.repoQuery) != 0 {
|
||||
|
@ -104,7 +111,8 @@ func (s *SourceQueryBuilder) Len() int {
|
|||
}
|
||||
|
||||
func (s *SourceQueryBuilder) GetTargets(include, exclude intrange.IntRanges,
|
||||
otherExclude stringset.StringSet) ([]string, error) {
|
||||
otherExclude stringset.StringSet,
|
||||
) ([]string, error) {
|
||||
isInclude := len(exclude) == 0 && len(otherExclude) == 0
|
||||
|
||||
var targets []string
|
||||
|
@ -140,85 +148,48 @@ func (s *SourceQueryBuilder) GetTargets(include, exclude intrange.IntRanges,
|
|||
return targets, nil
|
||||
}
|
||||
|
||||
// queryRepo handles repo searches. Creates a RepoSearch struct.
|
||||
func queryRepo(pkgInputN []string, dbExecutor db.Executor, bottomUp bool) repoQuery {
|
||||
s := repoQuery(dbExecutor.SyncPackages(pkgInputN...))
|
||||
// filter AUR results to remove strings that don't contain all of the search terms.
|
||||
func filterAURResults(pkgS []string, results []aur.Pkg) []aur.Pkg {
|
||||
aurPkgs := make([]aur.Pkg, 0, len(results))
|
||||
|
||||
if bottomUp {
|
||||
s.Reverse()
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// queryAUR searches AUR and narrows based on subarguments.
|
||||
func queryAUR(ctx context.Context, aurClient *aur.Client, pkgS []string, searchBy string, bottomUp bool, sortBy string) (aurQuery, error) {
|
||||
var (
|
||||
r []aur.Pkg
|
||||
err error
|
||||
usedIndex int
|
||||
)
|
||||
|
||||
by := getSearchBy(searchBy)
|
||||
|
||||
if len(pkgS) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for i, word := range pkgS {
|
||||
r, err = aurClient.Search(ctx, word, by)
|
||||
if err == nil {
|
||||
usedIndex = i
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pkgS) == 1 {
|
||||
sort.Sort(aurSortable{
|
||||
aurQuery: r,
|
||||
sortBy: sortBy,
|
||||
bottomUp: bottomUp,
|
||||
})
|
||||
|
||||
return r, err
|
||||
}
|
||||
|
||||
aq := make(aurQuery, 0, len(r))
|
||||
|
||||
for i := range r {
|
||||
match := true
|
||||
|
||||
for j, pkgN := range pkgS {
|
||||
if usedIndex == j {
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.ToLower(r[i].Name)
|
||||
desc := strings.ToLower(r[i].Description)
|
||||
matchesSearchTerms := func(pkg *aur.Pkg, terms []string) bool {
|
||||
for _, pkgN := range terms {
|
||||
name := strings.ToLower(pkg.Name)
|
||||
desc := strings.ToLower(pkg.Description)
|
||||
targ := strings.ToLower(pkgN)
|
||||
|
||||
if !(strings.Contains(name, targ) || strings.Contains(desc, targ)) {
|
||||
match = false
|
||||
|
||||
break
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if match {
|
||||
aq = append(aq, r[i])
|
||||
return true
|
||||
}
|
||||
|
||||
for i := range results {
|
||||
if matchesSearchTerms(&results[i], pkgS) {
|
||||
aurPkgs = append(aurPkgs, results[i])
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(aurSortable{
|
||||
aurQuery: aq,
|
||||
sortBy: sortBy,
|
||||
bottomUp: bottomUp,
|
||||
})
|
||||
|
||||
return aq, err
|
||||
return aurPkgs
|
||||
}
|
||||
|
||||
// queryAUR searches AUR and narrows based on subarguments.
|
||||
func queryAUR(ctx context.Context, aurClient *aur.Client, pkgS []string, searchBy string) ([]aur.Pkg, error) {
|
||||
var (
|
||||
err error
|
||||
by = getSearchBy(searchBy)
|
||||
)
|
||||
|
||||
for _, word := range pkgS {
|
||||
var r []aur.Pkg
|
||||
|
||||
r, err = aurClient.Search(ctx, word, by)
|
||||
if err == nil {
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
|
136
pkg/query/source_test.go
Normal file
136
pkg/query/source_test.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
package query
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Jguer/yay/v11/pkg/db"
|
||||
"github.com/Jguer/yay/v11/pkg/db/mock"
|
||||
"github.com/Jguer/yay/v11/pkg/settings/parser"
|
||||
|
||||
"github.com/Jguer/aur"
|
||||
"github.com/Jguer/go-alpm/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const validPayload = `{
|
||||
"resultcount": 1,
|
||||
"results": [
|
||||
{
|
||||
"Description": "The Linux-ck kernel and modules with ck's hrtimer patches",
|
||||
"FirstSubmitted": 1311346274,
|
||||
"ID": 1045311,
|
||||
"LastModified": 1646250901,
|
||||
"Maintainer": "graysky",
|
||||
"Name": "linux-ck",
|
||||
"NumVotes": 450,
|
||||
"OutOfDate": null,
|
||||
"PackageBase": "linux-ck",
|
||||
"PackageBaseID": 50911,
|
||||
"Popularity": 1.511141,
|
||||
"URL": "https://wiki.archlinux.org/index.php/Linux-ck",
|
||||
"URLPath": "/cgit/aur.git/snapshot/linux-ck.tar.gz",
|
||||
"Version": "5.16.12-1"
|
||||
}
|
||||
],
|
||||
"type": "search",
|
||||
"version": 5
|
||||
}
|
||||
`
|
||||
|
||||
type mockDB struct {
|
||||
db.Executor
|
||||
}
|
||||
|
||||
func (m *mockDB) LocalPackage(string) alpm.IPackage {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockDB) PackageGroups(pkg alpm.IPackage) []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (m *mockDB) SyncPackages(...string) []alpm.IPackage {
|
||||
mockDB := mock.NewDB("core")
|
||||
linuxRepo := &mock.Package{
|
||||
PName: "linux",
|
||||
PVersion: "5.16.0",
|
||||
PDescription: "The Linux kernel and modules",
|
||||
PSize: 1,
|
||||
PISize: 1,
|
||||
PDB: mockDB,
|
||||
}
|
||||
|
||||
linuxZen := &mock.Package{
|
||||
PName: "linux-zen",
|
||||
PVersion: "5.16.0",
|
||||
PDescription: "The Linux ZEN kernel and modules",
|
||||
PSize: 1,
|
||||
PISize: 1,
|
||||
PDB: mockDB,
|
||||
}
|
||||
|
||||
return []alpm.IPackage{linuxRepo, linuxZen}
|
||||
}
|
||||
|
||||
type mockDoer struct{}
|
||||
|
||||
func (m *mockDoer) Do(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString(validPayload)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestSourceQueryBuilder(t *testing.T) {
|
||||
t.Parallel()
|
||||
type testCase struct {
|
||||
desc string
|
||||
bottomUp bool
|
||||
want string
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{desc: "bottomup", bottomUp: true, want: "\x1b[1m\x1b[34maur\x1b[0m\x1b[0m/\x1b[1mlinux-ck\x1b[0m \x1b[36m5.16.12-1\x1b[0m\x1b[1m (+450\x1b[0m \x1b[1m1.51) \x1b[0m\n The Linux-ck kernel and modules with ck's hrtimer patches\n\x1b[1m\x1b[33mcore\x1b[0m\x1b[0m/\x1b[1mlinux-zen\x1b[0m \x1b[36m5.16.0\x1b[0m\x1b[1m (1.0 B 1.0 B) \x1b[0m\n The Linux ZEN kernel and modules\n\x1b[1m\x1b[33mcore\x1b[0m\x1b[0m/\x1b[1mlinux\x1b[0m \x1b[36m5.16.0\x1b[0m\x1b[1m (1.0 B 1.0 B) \x1b[0m\n The Linux kernel and modules\n"},
|
||||
{
|
||||
desc: "topdown", bottomUp: false,
|
||||
want: "\x1b[1m\x1b[33mcore\x1b[0m\x1b[0m/\x1b[1mlinux\x1b[0m \x1b[36m5.16.0\x1b[0m\x1b[1m (1.0 B 1.0 B) \x1b[0m\n The Linux kernel and modules\n\x1b[1m\x1b[33mcore\x1b[0m\x1b[0m/\x1b[1mlinux-zen\x1b[0m \x1b[36m5.16.0\x1b[0m\x1b[1m (1.0 B 1.0 B) \x1b[0m\n The Linux ZEN kernel and modules\n\x1b[1m\x1b[34maur\x1b[0m\x1b[0m/\x1b[1mlinux-ck\x1b[0m \x1b[36m5.16.12-1\x1b[0m\x1b[1m (+450\x1b[0m \x1b[1m1.51) \x1b[0m\n The Linux-ck kernel and modules with ck's hrtimer patches\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
queryBuilder := NewSourceQueryBuilder("votes", parser.ModeAny, "", tc.bottomUp, false)
|
||||
search := []string{"linux"}
|
||||
mockStore := &mockDB{}
|
||||
|
||||
client, err := aur.NewClient(aur.WithHTTPClient(&mockDoer{}))
|
||||
require.NoError(t, err)
|
||||
queryBuilder.Execute(context.Background(), mockStore, client, search)
|
||||
assert.Len(t, queryBuilder.aurQuery, 1)
|
||||
assert.Len(t, queryBuilder.repoQuery, 2)
|
||||
assert.Equal(t, 3, queryBuilder.Len())
|
||||
assert.Equal(t, "linux-ck", queryBuilder.aurQuery[0].Name)
|
||||
|
||||
if tc.bottomUp {
|
||||
assert.Equal(t, "linux-zen", queryBuilder.repoQuery[0].Name())
|
||||
assert.Equal(t, "linux", queryBuilder.repoQuery[1].Name())
|
||||
} else {
|
||||
assert.Equal(t, "linux-zen", queryBuilder.repoQuery[1].Name())
|
||||
assert.Equal(t, "linux", queryBuilder.repoQuery[0].Name())
|
||||
}
|
||||
|
||||
w := &strings.Builder{}
|
||||
queryBuilder.Results(w, mockStore, Detailed)
|
||||
|
||||
wString := w.String()
|
||||
require.GreaterOrEqual(t, len(wString), 1)
|
||||
assert.Equal(t, tc.want, wString)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -115,38 +115,48 @@ func (q aurQuery) printSearch(
|
|||
}
|
||||
}
|
||||
|
||||
toprint += text.Bold(text.ColorHash("aur")) + "/" + text.Bold(q[i].Name) +
|
||||
" " + text.Cyan(q[i].Version) +
|
||||
text.Bold(" (+"+strconv.Itoa(q[i].NumVotes)) +
|
||||
" " + text.Bold(strconv.FormatFloat(q[i].Popularity, 'f', 2, 64)+") ")
|
||||
|
||||
if q[i].Maintainer == "" {
|
||||
toprint += text.Bold(text.Red(gotext.Get("(Orphaned)"))) + " "
|
||||
}
|
||||
|
||||
if q[i].OutOfDate != 0 {
|
||||
toprint += text.Bold(text.Red(gotext.Get("(Out-of-date: %s)", text.FormatTime(q[i].OutOfDate)))) + " "
|
||||
}
|
||||
|
||||
if pkg := dbExecutor.LocalPackage(q[i].Name); pkg != nil {
|
||||
if pkg.Version() != q[i].Version {
|
||||
toprint += text.Bold(text.Green(gotext.Get("(Installed: %s)", pkg.Version())))
|
||||
} else {
|
||||
toprint += text.Bold(text.Green(gotext.Get("(Installed)")))
|
||||
}
|
||||
}
|
||||
|
||||
if singleLineResults {
|
||||
toprint += "\t"
|
||||
} else {
|
||||
toprint += "\n "
|
||||
}
|
||||
|
||||
toprint += q[i].Description
|
||||
toprint += aurPkgSearchString(&q[i], dbExecutor, singleLineResults)
|
||||
_, _ = fmt.Fprintln(w, toprint)
|
||||
}
|
||||
}
|
||||
|
||||
func aurPkgSearchString(
|
||||
pkg *aur.Pkg,
|
||||
dbExecutor db.Executor,
|
||||
singleLineResults bool,
|
||||
) string {
|
||||
toPrint := text.Bold(text.ColorHash("aur")) + "/" + text.Bold(pkg.Name) +
|
||||
" " + text.Cyan(pkg.Version) +
|
||||
text.Bold(" (+"+strconv.Itoa(pkg.NumVotes)) +
|
||||
" " + text.Bold(strconv.FormatFloat(pkg.Popularity, 'f', 2, 64)+") ")
|
||||
|
||||
if pkg.Maintainer == "" {
|
||||
toPrint += text.Bold(text.Red(gotext.Get("(Orphaned)"))) + " "
|
||||
}
|
||||
|
||||
if pkg.OutOfDate != 0 {
|
||||
toPrint += text.Bold(text.Red(gotext.Get("(Out-of-date: %s)", text.FormatTime(pkg.OutOfDate)))) + " "
|
||||
}
|
||||
|
||||
if localPkg := dbExecutor.LocalPackage(pkg.Name); localPkg != nil {
|
||||
if localPkg.Version() != pkg.Version {
|
||||
toPrint += text.Bold(text.Green(gotext.Get("(Installed: %s)", localPkg.Version())))
|
||||
} else {
|
||||
toPrint += text.Bold(text.Green(gotext.Get("(Installed)")))
|
||||
}
|
||||
}
|
||||
|
||||
if singleLineResults {
|
||||
toPrint += "\t"
|
||||
} else {
|
||||
toPrint += "\n "
|
||||
}
|
||||
|
||||
toPrint += pkg.Description
|
||||
|
||||
return toPrint
|
||||
}
|
||||
|
||||
// PrintSearch receives a RepoSearch type and outputs pretty text.
|
||||
func (r repoQuery) printSearch(w io.Writer, dbExecutor db.Executor, searchMode SearchVerbosity, bottomUp, singleLineResults bool) {
|
||||
for i, res := range r {
|
||||
|
@ -165,32 +175,38 @@ func (r repoQuery) printSearch(w io.Writer, dbExecutor db.Executor, searchMode S
|
|||
}
|
||||
}
|
||||
|
||||
toprint += text.Bold(text.ColorHash(res.DB().Name())) + "/" + text.Bold(res.Name()) +
|
||||
" " + text.Cyan(res.Version()) +
|
||||
text.Bold(" ("+text.Human(res.Size())+
|
||||
" "+text.Human(res.ISize())+") ")
|
||||
|
||||
packageGroups := dbExecutor.PackageGroups(res)
|
||||
if len(packageGroups) != 0 {
|
||||
toprint += fmt.Sprint(packageGroups, " ")
|
||||
}
|
||||
|
||||
if pkg := dbExecutor.LocalPackage(res.Name()); pkg != nil {
|
||||
if pkg.Version() != res.Version() {
|
||||
toprint += text.Bold(text.Green(gotext.Get("(Installed: %s)", pkg.Version())))
|
||||
} else {
|
||||
toprint += text.Bold(text.Green(gotext.Get("(Installed)")))
|
||||
}
|
||||
}
|
||||
|
||||
if singleLineResults {
|
||||
toprint += "\t"
|
||||
} else {
|
||||
toprint += "\n "
|
||||
}
|
||||
|
||||
toprint += res.Description()
|
||||
|
||||
toprint += syncPkgSearchString(res, dbExecutor, singleLineResults)
|
||||
_, _ = fmt.Fprintln(w, toprint)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintSearch receives a RepoSearch type and outputs pretty text.
|
||||
func syncPkgSearchString(pkg alpm.IPackage, dbExecutor db.Executor, singleLineResults bool) string {
|
||||
toPrint := text.Bold(text.ColorHash(pkg.DB().Name())) + "/" + text.Bold(pkg.Name()) +
|
||||
" " + text.Cyan(pkg.Version()) +
|
||||
text.Bold(" ("+text.Human(pkg.Size())+
|
||||
" "+text.Human(pkg.ISize())+") ")
|
||||
|
||||
packageGroups := dbExecutor.PackageGroups(pkg)
|
||||
if len(packageGroups) != 0 {
|
||||
toPrint += fmt.Sprint(packageGroups, " ")
|
||||
}
|
||||
|
||||
if localPkg := dbExecutor.LocalPackage(pkg.Name()); localPkg != nil {
|
||||
if localPkg.Version() != pkg.Version() {
|
||||
toPrint += text.Bold(text.Green(gotext.Get("(Installed: %s)", localPkg.Version())))
|
||||
} else {
|
||||
toPrint += text.Bold(text.Green(gotext.Get("(Installed)")))
|
||||
}
|
||||
}
|
||||
|
||||
if singleLineResults {
|
||||
toPrint += "\t"
|
||||
} else {
|
||||
toPrint += "\n "
|
||||
}
|
||||
|
||||
toPrint += pkg.Description()
|
||||
|
||||
return toPrint
|
||||
}
|
||||
|
|
|
@ -183,6 +183,10 @@ func (c *Configuration) handleOption(option, value string) bool {
|
|||
c.RemoveMake = "no"
|
||||
case "askremovemake":
|
||||
c.RemoveMake = "ask"
|
||||
case "separatesources":
|
||||
c.SeparateSources = true
|
||||
case "noseparatesources":
|
||||
c.SeparateSources = false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ type Configuration struct {
|
|||
UseAsk bool `json:"useask"`
|
||||
BatchInstall bool `json:"batchinstall"`
|
||||
SingleLineResults bool `json:"singlelineresults"`
|
||||
SeparateSources bool `json:"separatesources"`
|
||||
Runtime *Runtime `json:"-"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
@ -216,6 +217,7 @@ func DefaultConfig(version string) *Configuration {
|
|||
EditMenu: false,
|
||||
UseAsk: false,
|
||||
CombinedUpgrade: false,
|
||||
SeparateSources: false,
|
||||
Version: version,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -446,6 +446,7 @@ func isArg(arg string) bool {
|
|||
case "currentconfig":
|
||||
case "singlelineresults":
|
||||
case "doublelineresults":
|
||||
case "separatesources", "noseparatesources":
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/Jguer/aur"
|
||||
|
||||
"github.com/Jguer/yay/v11/pkg/query"
|
||||
"github.com/Jguer/yay/v11/pkg/settings/exe"
|
||||
"github.com/Jguer/yay/v11/pkg/settings/parser"
|
||||
"github.com/Jguer/yay/v11/pkg/vcs"
|
||||
|
@ -14,6 +15,7 @@ import (
|
|||
|
||||
type Runtime struct {
|
||||
Mode parser.TargetMode
|
||||
QueryBuilder query.Builder
|
||||
Version string // current version of yay
|
||||
SaveConfig bool
|
||||
CompletionPath string
|
||||
|
|
9
query.go
9
query.go
|
@ -19,9 +19,9 @@ import (
|
|||
)
|
||||
|
||||
// SyncSearch presents a query to the local repos and to the AUR.
|
||||
func syncSearch(ctx context.Context, pkgS []string, aurClient *aur.Client, dbExecutor db.Executor, verbose bool) error {
|
||||
queryBuilder := query.NewSourceQueryBuilder(config.SortBy, config.Runtime.Mode, config.SearchBy, config.BottomUp, config.SingleLineResults)
|
||||
|
||||
func syncSearch(ctx context.Context, pkgS []string, aurClient *aur.Client,
|
||||
dbExecutor db.Executor, queryBuilder query.Builder, verbose bool,
|
||||
) error {
|
||||
queryBuilder.Execute(ctx, dbExecutor, aurClient, pkgS)
|
||||
|
||||
searchMode := query.Minimal
|
||||
|
@ -209,7 +209,8 @@ func statistics(dbExecutor db.Executor) (res struct {
|
|||
TotalSize int64
|
||||
pacmanCaches map[string]int64
|
||||
yayCache int64
|
||||
}) {
|
||||
},
|
||||
) {
|
||||
for _, pkg := range dbExecutor.LocalPackages() {
|
||||
res.TotalSize += pkg.ISize()
|
||||
res.Totaln++
|
||||
|
|
Loading…
Reference in a new issue