From ce6bcbd775372fc2e06eaa9b68bb2286df370f2f Mon Sep 17 00:00:00 2001 From: Pujitha Paladugu Date: Sat, 26 Sep 2026 14:56:01 -0700 Subject: [PATCH] fix(query): keep pacman.conf repo order for exact name matches (#2987) Motivation With separatesources enabled (the default), a package with the exact searched name that exists in several sync repos can be listed in the wrong order. For example, if [cachyos-extra-v3] comes before [extra] in pacman.conf, extra/chromium can still show up as the top pick instead of cachyos-extra-v3/chromium. pacman prefers the repo listed first. separateSourceScore returned a flat 50 for every exact match, whatever its source. As a result, all exact matches got the same metric (50 + 1.0). The unstable slices.SortFunc then left their relative order unspecified, so the ordering added for non-exact results did not apply to exact ones. Approach Move the repo-order scoring into a repoOrderScore helper (same logic and cache as before). For exact matches, add repoOrderScore/separateSourceMax to the 50: a bonus between 0 and 1. Exact matches still outrank every non-exact result (which scores at most 45 + a metric below 1). Among exact matches, repos earlier in pacman.conf now come first. Exact AUR matches keep 50, so an exact sync-repo match now ranks strictly above an exact AUR match instead of tying with it. Scoring for non-exact results is unchanged, and nothing changes when separatesources is disabled. Validation - go test -race ./pkg/query/... -count=1: passes. - New tests TestSourceQueryBuilderExactMatchSortsByRepoOrder (top-down and bottom-up) and TestSeparateSourceScore_ExactMatchKeepsRepoOrder fail with the previous metric.go and pass with this change. - golangci-lint run ./pkg/query/...: 0 issues; gofmt clean. - go test ./... on macOS has failures that also occur on the unchanged base: Linux-only syscall fields in pkg/settings/exe, and a timezone-dependent test in pkg/text. Not tested against a real pacman setup with custom repos. Report: https://github.com/Jguer/yay/issues/2986 Assisted-by: claude-opus-5-5 (via Claude Code) Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Co-authored-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> --- pkg/query/metric.go | 15 +++++-- pkg/query/metric_test.go | 20 +++++++++ pkg/query/query_builder_test.go | 77 +++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/pkg/query/metric.go b/pkg/query/metric.go index 2fcc78db..e337fc60 100644 --- a/pkg/query/metric.go +++ b/pkg/query/metric.go @@ -57,10 +57,21 @@ func (a *abstractResults) separateSourceScore(source string, score float64) floa return 0 } + sourceScore := a.repoOrderScore(source) + + // Exact matches always rank above everything else; among them, keep + // pacman.conf repo order by adding a bonus in [0, 1]. if score == 1.0 { - return 50 + return 50 + sourceScore/separateSourceMax } + return sourceScore +} + +// repoOrderScore scores 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. +// AUR and unknown sources get 0. +func (a *abstractResults) repoOrderScore(source string) float64 { if v, ok := a.separateSourceCache[source]; ok { return v } @@ -70,8 +81,6 @@ func (a *abstractResults) separateSourceScore(source string, score float64) floa return 0 } - // 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 diff --git a/pkg/query/metric_test.go b/pkg/query/metric_test.go index 52206ee2..5328214a 100644 --- a/pkg/query/metric_test.go +++ b/pkg/query/metric_test.go @@ -43,3 +43,23 @@ func TestSeparateSourceScore_UsesRepoOrderEvenlyDistributed(t *testing.T) { assert.Equal(t, 0.0, a.separateSourceScore("aur", sim)) }) } + +func TestSeparateSourceScore_ExactMatchKeepsRepoOrder(t *testing.T) { + t.Parallel() + + a := &abstractResults{ + separateSources: true, + repoOrder: []string{"cachyos-extra-v3", "core", "extra"}, + separateSourceCache: map[string]float64{}, + } + + custom := a.separateSourceScore("cachyos-extra-v3", 1.0) + extra := a.separateSourceScore("extra", 1.0) + aurScore := a.separateSourceScore("aur", 1.0) + + assert.Greater(t, custom, extra) + assert.Greater(t, extra, aurScore) + assert.Equal(t, 50.0, aurScore) + // Exact matches must still outrank any non-exact match. + assert.Greater(t, extra+1.0, a.separateSourceScore("cachyos-extra-v3", 0.99)+0.99) +} diff --git a/pkg/query/query_builder_test.go b/pkg/query/query_builder_test.go index 509d53ae..7dcd5f2f 100644 --- a/pkg/query/query_builder_test.go +++ b/pkg/query/query_builder_test.go @@ -445,6 +445,83 @@ func TestSourceQueryBuilderTieSortsByRepoOrder(t *testing.T) { } } +func TestSourceQueryBuilderExactMatchSortsByRepoOrder(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + bottomUp bool + repoOrder []string + dbOrder []string + wantSources []string + }{ + { + desc: "topdown custom repo first", + repoOrder: []string{"cachyos-extra-v3", "core", "extra"}, + dbOrder: []string{"extra", "cachyos-extra-v3"}, + wantSources: []string{"cachyos-extra-v3", "extra", "aur"}, + }, + { + desc: "topdown extra first", + repoOrder: []string{"core", "extra", "cachyos-extra-v3"}, + dbOrder: []string{"cachyos-extra-v3", "extra"}, + wantSources: []string{"extra", "cachyos-extra-v3", "aur"}, + }, + { + desc: "bottomup custom repo first", + bottomUp: true, + repoOrder: []string{"cachyos-extra-v3", "core", "extra"}, + dbOrder: []string{"extra", "cachyos-extra-v3"}, + wantSources: []string{"aur", "extra", "cachyos-extra-v3"}, + }, + } + + mockAUR := &mockaur.MockAUR{ + GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) { + return []aur.Pkg{{Name: "chromium", PackageBase: "chromium", Version: "1-1"}}, nil + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + + mockDB := &mock.DBExecutor{ + ReposFn: func() []string { return tc.repoOrder }, + SyncPackagesFn: func(pkgs ...string) []mock.IPackage { + res := make([]mock.IPackage, 0, len(tc.dbOrder)) + for _, dbName := range tc.dbOrder { + res = append(res, &mock.Package{ + PBase: "chromium", + PName: "chromium", + PVersion: "153.0.8010.52-1", + PDescription: "A web browser built for speed, simplicity, and security", + PDB: mock.NewDB(dbName), + PArchitecture: "x86_64", + }) + } + return res + }, + LocalPackageFn: func(string) mock.IPackage { return nil }, + } + + queryBuilder := NewSourceQueryBuilder(mockAUR, + text.NewLogger(io.Discard, io.Discard, strings.NewReader(""), false, "test"), + "", parser.ModeAny, "", tc.bottomUp, + false, true) + + queryBuilder.Execute(t.Context(), mockDB, []string{"chromium"}) + + gotSources := make([]string, len(queryBuilder.results)) + for i, result := range queryBuilder.results { + gotSources[i] = result.source + } + + assert.Equal(t, tc.wantSources, gotSources) + }) + } +} + func TestSourceQueryBuilderTieDoesNotSeparateSources(t *testing.T) { t.Parallel()