diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index 5b7cb5ed957..a4e1efa9452 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -475,7 +475,7 @@ func SearchRepositoryCondition(opts SearchRepoOptions) builder.Cond { pieces := strings.Split(opts.Keyword, "/") ownerName := pieces[0] repoName := pieces[1] - likes = likes.Or(builder.And(builder.Like{"owner_name", strings.ToLower(ownerName)}, builder.Like{"lower_name", strings.ToLower(repoName)})) + likes = likes.Or(builder.And(db.BuildCaseInsensitiveLike("owner_name", ownerName), builder.Like{"lower_name", strings.ToLower(repoName)})) } if opts.IncludeDescription { @@ -609,10 +609,12 @@ func searchRepositoryByCondition(ctx context.Context, opts SearchRepoOptions, co orderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_id = ? THEN 0 ELSE owner_id END, %s", orderBy)) args = append(args, opts.PriorityOwnerID) } else if strings.Count(opts.Keyword, "/") == 1 { - // With "owner/repo" search times, prioritise results which match the owner field - orgName, _, _ := strings.Cut(opts.Keyword, "/") - orderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_name LIKE ? THEN 0 ELSE 1 END, %s", orderBy)) - args = append(args, orgName) + ownerName, repoName, _ := strings.Cut(strings.ToLower(opts.Keyword), "/") + orderBy = db.SearchOrderBy(fmt.Sprintf("CASE LOWER(owner_name) WHEN ? THEN CASE lower_name WHEN ? THEN 0 ELSE 1 END ELSE 2 END, %s", orderBy)) + args = append(args, ownerName, repoName) + } else if len(opts.OrderBy) == 0 && opts.Keyword != "" && !strings.ContainsAny(opts.Keyword, ",/") { // unlike "owner/repo", a bare name never overrides an explicit sort + orderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN lower_name = ? THEN 0 ELSE 1 END, %s", orderBy)) + args = append(args, strings.ToLower(opts.Keyword)) } sess := db.GetEngine(ctx) diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go index 1cdd5cd4605..c0b13a016ff 100644 --- a/models/repo/repo_list_test.go +++ b/models/repo/repo_list_test.go @@ -195,6 +195,25 @@ func TestSearchRepository(t *testing.T) { t.Run("SearchRepositoryWithInDescription", testSearchRepositoryWithInDescription) t.Run("SearchRepositoryNotInDescription", testSearchRepositoryNotInDescription) t.Run("SearchRepositoryCases", testSearchRepositoryCases) + t.Run("SearchRepositoryRanksExactMatchesCaseInsensitivelyUnlessSorted", func(t *testing.T) { + afoo := &repo_model.Repository{OwnerID: 2, OwnerName: "User2", LowerName: "afoo", Name: "afoo"} + aafoo := &repo_model.Repository{OwnerID: 20, OwnerName: "user20", LowerName: "aafoo", Name: "aafoo"} + foo := &repo_model.Repository{OwnerID: 2, OwnerName: "User2", LowerName: "foo", Name: "foo"} + require.NoError(t, db.Insert(t.Context(), afoo, aafoo, foo)) + for _, tc := range []struct { + keyword string + orderBy db.SearchOrderBy + want []int64 + }{ + {"Foo", "", []int64{foo.ID, aafoo.ID, afoo.ID}}, + {"Foo", db.SearchOrderByID, []int64{afoo.ID, aafoo.ID, foo.ID}}, + {"user2/foo", "", []int64{foo.ID, afoo.ID, aafoo.ID}}, + } { + repos, _, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{Keyword: tc.keyword, OrderBy: tc.orderBy}) + require.NoError(t, err) + assert.Equal(t, tc.want, repoIDs(repos), "%s %s", tc.keyword, tc.orderBy) + } + }) } func testSearchRepositoryPublic(t *testing.T) {