fix(label): sort labels by open issue count, not total (#39464)

The labels page shows open issue counts, but "Most issues" and "Least
issues" sorted by the total including closed issues, so a label with no
open issues could land in the middle of the list.

- Sort repository and organization labels by their open issue count
- Sort organization labels on a repository's labels page by their open
issues in that repository, which is the count they display

Fixes: https://github.com/go-gitea/gitea/issues/39346

---------

Signed-off-by: piyush295 <mr.piyush295@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
Piyush Kumar
2026-10-04 14:18:18 +05:30
committed by GitHub
parent eb4468ff4e
commit a9ac8c0afd
4 changed files with 46 additions and 16 deletions
+6 -4
View File
@@ -433,12 +433,13 @@ func GetLabelsByRepoID(ctx context.Context, repoID int64, sortType string, listO
case "reversealphabetically":
sess.Desc("name")
case "leastissues":
sess.Asc("num_issues")
sess.OrderBy("num_issues - num_closed_issues ASC")
case "mostissues":
sess.Desc("num_issues")
sess.OrderBy("num_issues - num_closed_issues DESC")
default:
sess.Asc("name")
}
sess.Asc("id")
if listOptions.Page > 0 {
db.SetSessionPagination(sess, &listOptions)
@@ -508,12 +509,13 @@ func GetLabelsByOrgID(ctx context.Context, orgID int64, sortType string, listOpt
case "reversealphabetically":
sess.Desc("name")
case "leastissues":
sess.Asc("num_issues")
sess.OrderBy("num_issues - num_closed_issues ASC")
case "mostissues":
sess.Desc("num_issues")
sess.OrderBy("num_issues - num_closed_issues DESC")
default:
sess.Asc("name")
}
sess.Asc("id")
if listOptions.Page > 0 {
db.SetSessionPagination(sess, &listOptions)
+5 -1
View File
@@ -191,6 +191,8 @@ func TestGetLabelsInRepoByIDs(t *testing.T) {
func TestGetLabelsByRepoID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
_, err := db.GetEngine(t.Context()).ID(2).Cols("num_issues", "num_closed_issues").Update(&issues_model.Label{NumIssues: 3, NumClosedIssues: 3})
assert.NoError(t, err)
testSuccess := func(repoID int64, sortType string, expectedIssueIDs []int64) {
labels, err := issues_model.GetLabelsByRepoID(t.Context(), repoID, sortType, db.ListOptions{})
assert.NoError(t, err)
@@ -258,6 +260,8 @@ func TestGetLabelsInOrgByIDs(t *testing.T) {
func TestGetLabelsByOrgID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
_, err := db.GetEngine(t.Context()).ID(3).Cols("num_issues", "num_closed_issues").Update(&issues_model.Label{NumIssues: 3, NumClosedIssues: 3})
assert.NoError(t, err)
testSuccess := func(orgID int64, sortType string, expectedIssueIDs []int64) {
labels, err := issues_model.GetLabelsByOrgID(t.Context(), orgID, sortType, db.ListOptions{})
assert.NoError(t, err)
@@ -271,7 +275,7 @@ func TestGetLabelsByOrgID(t *testing.T) {
testSuccess(3, "reversealphabetically", []int64{4, 3})
testSuccess(3, "default", []int64{3, 4})
_, err := issues_model.GetLabelsByOrgID(t.Context(), 0, "leastissues", db.ListOptions{})
_, err = issues_model.GetLabelsByOrgID(t.Context(), 0, "leastissues", db.ListOptions{})
assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
_, err = issues_model.GetLabelsByOrgID(t.Context(), -1, "leastissues", db.ListOptions{})
+12
View File
@@ -4,8 +4,10 @@
package repo
import (
"cmp"
"errors"
"net/http"
"slices"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
@@ -77,6 +79,16 @@ func RetrieveLabelsForList(ctx *context.Context) {
for _, l := range orgLabels {
l.CalOpenOrgIssues(ctx, ctx.Repo.Repository.ID, l.ID)
}
switch ctx.FormString("sort") {
case "leastissues":
slices.SortStableFunc(orgLabels, func(label1, label2 *issues_model.Label) int {
return cmp.Compare(label1.NumOpenRepoIssues, label2.NumOpenRepoIssues)
})
case "mostissues":
slices.SortStableFunc(orgLabels, func(label1, label2 *issues_model.Label) int {
return cmp.Compare(label2.NumOpenRepoIssues, label1.NumOpenRepoIssues)
})
}
ctx.Data["OrgLabels"] = orgLabels
org, err := organization.GetOrgByName(ctx, ctx.Repo.Owner.LowerName)
+23 -11
View File
@@ -9,6 +9,7 @@ import (
"strings"
"testing"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/unittest"
"gitea.dev/modules/base"
@@ -51,14 +52,23 @@ func testInitializeLabels(t *testing.T) {
}
func testRetrieveLabels(t *testing.T) {
issueLabel := &issues_model.IssueLabel{IssueID: 6, LabelID: 3}
assert.NoError(t, db.Insert(t.Context(), issueLabel))
defer func() {
_, err := db.DeleteByBean(t.Context(), issueLabel)
assert.NoError(t, err)
}()
for _, testCase := range []struct {
RepoID int64
Sort string
ExpectedLabelIDs []int64
RepoID int64
Sort string
ExpectedLabelIDs []int64
ExpectedOrgLabelIDs []int64
}{
{1, "", []int64{1, 2}},
{1, "leastissues", []int64{2, 1}},
{2, "", []int64{}},
{1, "", []int64{1, 2}, nil},
{1, "leastissues", []int64{2, 1}, nil},
{2, "", nil, nil},
{3, "mostissues", []int64{10}, []int64{3, 4}},
} {
ctx, _ := contexttest.MockContext(t, "user/repo/issues")
contexttest.LoadUser(t, ctx, 2)
@@ -66,13 +76,15 @@ func testRetrieveLabels(t *testing.T) {
ctx.Req.Form.Set("sort", testCase.Sort)
RetrieveLabelsForList(ctx)
assert.False(t, ctx.Written())
labels, ok := ctx.Data["Labels"].([]*issues_model.Label)
assert.True(t, ok)
if assert.Len(t, labels, len(testCase.ExpectedLabelIDs)) {
for i, label := range labels {
assert.Equal(t, testCase.ExpectedLabelIDs[i], label.ID)
labelIDs := func(key string) (ids []int64) {
labels, _ := ctx.Data[key].([]*issues_model.Label)
for _, label := range labels {
ids = append(ids, label.ID)
}
return ids
}
assert.Equal(t, testCase.ExpectedLabelIDs, labelIDs("Labels"))
assert.Equal(t, testCase.ExpectedOrgLabelIDs, labelIDs("OrgLabels"))
}
}