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:
J Guerreiro 2022-03-06 23:48:15 +00:00 committed by GitHub
parent ae01f8e4a0
commit e4a1f018ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 628 additions and 148 deletions

View file

@ -68,6 +68,13 @@ linters:
- whitespace
- wsl
- godot
# - maligned
# - interfacer
# - nilerr
# - nlreturn
# - exhaustivestruct
# - errname
# - forbidigo
run:

18
cmd.go
View file

@ -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 {

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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)

View file

@ -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++

View file

@ -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
View 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
}

View 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)
})
}
}

View file

@ -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
View 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)
})
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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,
}
}

View file

@ -446,6 +446,7 @@ func isArg(arg string) bool {
case "currentconfig":
case "singlelineresults":
case "doublelineresults":
case "separatesources", "noseparatesources":
default:
return false
}

View file

@ -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

View file

@ -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++