feat(repo): Sort repository results by pacman.conf repo order (#2740)

* ci(yay): update packages on builder before building release

* respect other repos in order

* ensure repo ends on top in case of tie

* revert dockerfile change
This commit is contained in:
Jo
2025-12-19 21:13:47 +01:00
committed by GitHub
parent 44dfda05dd
commit df80f397af
5 changed files with 112 additions and 50 deletions

View File

@@ -167,7 +167,8 @@ func (t *DBExecutor) Repos() []string {
if t.ReposFn != nil { if t.ReposFn != nil {
return t.ReposFn() return t.ReposFn()
} }
panic("implement me") // Tests that don't care about repo ordering shouldn't need to stub this out.
return nil
} }
func (t *DBExecutor) SatisfierFromDB(s, s2 string) (IPackage, error) { func (t *DBExecutor) SatisfierFromDB(s, s2 string) (IPackage, error) {

View File

@@ -1,13 +1,16 @@
package query package query
import ( import (
"hash/fnv"
"strings" "strings"
"github.com/adrg/strutil" "github.com/adrg/strutil"
) )
const minVotes = 30 const minVotes = 30
const (
separateSourceMax = 45.0
separateSourceMin = 5.0
)
// TODO: Add support for Popularity and LastModified // TODO: Add support for Popularity and LastModified
func (a *abstractResults) aurSortByMetric(pkg *abstractResult) float64 { func (a *abstractResults) aurSortByMetric(pkg *abstractResult) float64 {
@@ -58,29 +61,35 @@ func (a *abstractResults) separateSourceScore(source string, score float64) floa
return 50 return 50
} }
switch source {
case sourceAUR:
return 0
case "core":
return 40
case "extra":
return 30
case "community":
return 20
case "multilib":
return 10
}
if v, ok := a.separateSourceCache[source]; ok { if v, ok := a.separateSourceCache[source]; ok {
return v return v
} }
h := fnv.New32a() // AUR is always lowest priority
h.Write([]byte(source)) if source == sourceAUR {
sourceScore := float64(int(h.Sum32())%9 + 2) return 0
a.separateSourceCache[source] = sourceScore }
return sourceScore // Score sync repositories based on pacman.conf order (as reflected by dbExecutor.Repos()).
// First repo gets max, last repo gets min, evenly distributed across the range.
for i, repo := range a.repoOrder {
if repo != source {
continue
}
n := len(a.repoOrder)
if n == 1 {
a.separateSourceCache[source] = separateSourceMax
return separateSourceMax
}
step := (separateSourceMax - separateSourceMin) / float64(n-1)
sourceScore := separateSourceMax - (float64(i) * step)
a.separateSourceCache[source] = sourceScore
return sourceScore
}
return 0
} }
func (a *abstractResults) calculateMetric(pkg *abstractResult) float64 { func (a *abstractResults) calculateMetric(pkg *abstractResult) float64 {

47
pkg/query/metric_test.go Normal file
View File

@@ -0,0 +1,47 @@
//go:build !integration
// +build !integration
package query
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSeparateSourceScore_UsesRepoOrderEvenlyDistributed(t *testing.T) {
t.Parallel()
// Any non-1.0 score avoids the special-case 50 return.
const sim = 0.5
const delta = 1e-1
t.Run("arch repos (core/extra/community/multilib)", func(t *testing.T) {
a := &abstractResults{
separateSources: true,
repoOrder: []string{"core", "extra", "community", "multilib"},
separateSourceCache: map[string]float64{},
}
assert.InDelta(t, 45.0, a.separateSourceScore("core", sim), delta)
assert.InDelta(t, 31.6, a.separateSourceScore("extra", sim), delta)
assert.InDelta(t, 18.3, a.separateSourceScore("community", sim), delta)
assert.InDelta(t, 5.0, a.separateSourceScore("multilib", sim), delta)
assert.Equal(t, 0.0, a.separateSourceScore(sourceAUR, sim))
})
t.Run("arch arm repos (core/extra/alarm/aur)", func(t *testing.T) {
a := &abstractResults{
separateSources: true,
repoOrder: []string{"core", "extra", "alarm", "aur"},
separateSourceCache: map[string]float64{},
}
// Note: AUR is not a sync repository; it is always lowest priority (0) regardless of repo order.
assert.InDelta(t, 45.0, a.separateSourceScore("core", sim), delta)
assert.InDelta(t, 31.6, a.separateSourceScore("extra", sim), delta)
assert.InDelta(t, 18.3, a.separateSourceScore("alarm", sim), delta)
assert.InDelta(t, 5.0, a.separateSourceScore("aur", sim), delta)
assert.Equal(t, 0.0, a.separateSourceScore(sourceAUR, sim))
})
}

View File

@@ -20,7 +20,7 @@ import (
"github.com/Jguer/yay/v12/pkg/text" "github.com/Jguer/yay/v12/pkg/text"
) )
const sourceAUR = "aur" const sourceAUR = "AUR"
type SearchVerbosity int type SearchVerbosity int
@@ -91,6 +91,7 @@ type abstractResults struct {
metric strutil.StringMetric metric strutil.StringMetric
separateSources bool separateSources bool
sortBy string sortBy string
repoOrder []string
distanceCache map[string]float64 distanceCache map[string]float64
separateSourceCache map[string]float64 separateSourceCache map[string]float64
@@ -143,9 +144,38 @@ func (s *SourceQueryBuilder) Execute(ctx context.Context, dbExecutor db.Executor
metric: metric, metric: metric,
separateSources: s.separateSources, separateSources: s.separateSources,
sortBy: s.sortBy, sortBy: s.sortBy,
repoOrder: dbExecutor.Repos(),
distanceCache: map[string]float64{}, distanceCache: map[string]float64{},
separateSourceCache: map[string]float64{}, separateSourceCache: map[string]float64{},
} }
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]any{}
}
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,
})
}
}
if s.targetMode.AtLeastAUR() { if s.targetMode.AtLeastAUR() {
var aurResults []aur.Pkg var aurResults []aur.Pkg
@@ -175,35 +205,6 @@ func (s *SourceQueryBuilder) Execute(ctx context.Context, dbExecutor db.Executor
} }
} }
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]any{}
}
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) sort.Sort(sortableResults)
s.results = sortableResults.results s.results = sortableResults.results

View File

@@ -289,6 +289,10 @@ func TestSourceQueryBuilder(t *testing.T) {
} }
mockDB := &mock.DBExecutor{ mockDB := &mock.DBExecutor{
ReposFn: func() []string {
// Match pacman.conf parsing order for source separation.
return []string{"core"}
},
SyncPackagesFn: func(pkgs ...string) []mock.IPackage { SyncPackagesFn: func(pkgs ...string) []mock.IPackage {
mockDB := mock.NewDB("core") mockDB := mock.NewDB("core")
return []mock.IPackage{ return []mock.IPackage{