mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
fix(issue): reject unknown milestones in issue list and search filters
Gitea drops milestone filter values it cannot resolve and lists every issue when none is left, so tea issue list --milestone nope printed all issues and exited 0, and a pipeline closing the result would act on all of them. issue list and search issue|pr -R now resolve each --milestone value by ID or title and fail with "milestone "nope" not found in alice/project". Instance-wide search cannot check the names and says so on stderr. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
4 files changed
+125
-12
No files matched your search
@@ -37,6 +37,10 @@ func (a *application) discussionSearchCommand(pulls bool) *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
milestones, err := nameFilter("milestone", opts.Milestones)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target, err := a.resolve(cmd.Context(), false)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -50,13 +54,15 @@ func (a *application) discussionSearchCommand(pulls bool) *cobra.Command {
|
||||
if opts.Team != "" && target.Repo != "" {
|
||||
return clierr.Wrap(clierr.Usage, errors.New("--team requires instance-wide search; omit --repo or TEA_REPO"))
|
||||
}
|
||||
if len(labelNames) > 0 {
|
||||
if target.Repo != "" {
|
||||
if err := (services.Labels{Client: target.client, Repository: target.Repo}).RequireFilterNames(cmd.Context(), labelNames); err != nil {
|
||||
return err
|
||||
}
|
||||
if target.Repo != "" {
|
||||
if opts.Labels, opts.Milestones, err = issueFilters(cmd, target, labelNames, milestones); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if len(labelNames)+len(milestones) > 0 {
|
||||
opts.Labels, opts.Milestones = strings.Join(labelNames, ","), strings.Join(milestones, ",")
|
||||
if _, err := fmt.Fprintln(cmd.ErrOrStderr(), "warning: instance-wide search ignores --label and --milestone values that match nothing; pass --repo to check them"); err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Labels = strings.Join(labelNames, ",")
|
||||
}
|
||||
opts.Query = strings.Join(args, " ")
|
||||
items, err := (services.Issues{Client: target.client, Repository: target.Repo}).Search(cmd.Context(), opts)
|
||||
|
||||
@@ -128,3 +128,56 @@ func TestIssueListWithoutRepoPointsToSearch(t *testing.T) {
|
||||
require.ErrorContains(t, err, "tea search issue")
|
||||
require.Empty(t, out.String())
|
||||
}
|
||||
|
||||
func TestIssueMilestoneFiltersRejectUnknownMilestones(t *testing.T) {
|
||||
var listed []string
|
||||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/repos/alice/project/milestones":
|
||||
require.Equal(t, "all", r.URL.Query().Get("state"))
|
||||
if r.URL.Query().Get("page") == "1" {
|
||||
_, _ = w.Write([]byte(`[{"id":1,"title":"v1.0"},{"id":2,"title":"2026","state":"closed"}]`))
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(`[]`))
|
||||
case "/api/v1/repos/alice/project/issues", "/api/v1/repos/issues/search":
|
||||
listed = append(listed, r.URL.Path+"?milestones="+r.URL.Query().Get("milestones"))
|
||||
_, _ = w.Write([]byte(`[]`))
|
||||
default:
|
||||
t.Errorf("unexpected %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
deps, out, diagnostics := serverDeps(t, server.URL)
|
||||
for _, test := range []struct {
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{[]string{"issue", "list", "--milestone", "nope"}, `milestone "nope" not found in alice/project`},
|
||||
{[]string{"issue", "list", "--milestone", "999"}, "milestone 999 not found in alice/project"},
|
||||
{[]string{"issue", "list", "--milestone", "v1.0,nope"}, `milestone "nope" not found`},
|
||||
{[]string{"search", "issue", "--milestone", "nope"}, `milestone "nope" not found in alice/project`},
|
||||
{[]string{"search", "pr", "--milestone", "nope"}, `milestone "nope" not found in alice/project`},
|
||||
} {
|
||||
err := Execute(context.Background(), deps, append(test.args, "-R", "alice/project", "--json"))
|
||||
require.Equal(t, clierr.Failure, clierr.ExitCode(err), "%v", err)
|
||||
require.ErrorContains(t, err, test.want)
|
||||
}
|
||||
require.Empty(t, listed)
|
||||
require.Empty(t, out.String())
|
||||
for _, args := range [][]string{
|
||||
{"issue", "list", "-R", "alice/project", "--milestone", "v1.0,2026"},
|
||||
{"issue", "list", "-R", "alice/project", "--milestone", "1"},
|
||||
{"search", "issue", "-R", "alice/project", "--milestone", "V1.0"},
|
||||
{"search", "issue", "--milestone", "nope"},
|
||||
} {
|
||||
require.NoError(t, Execute(context.Background(), deps, append(args, "--json")))
|
||||
}
|
||||
require.Equal(t, []string{
|
||||
"/api/v1/repos/alice/project/issues?milestones=1,2",
|
||||
"/api/v1/repos/alice/project/issues?milestones=1",
|
||||
"/api/v1/repos/alice/project/issues?milestones=1",
|
||||
"/api/v1/repos/issues/search?milestones=nope",
|
||||
}, listed)
|
||||
require.Contains(t, diagnostics.String(), "pass --repo to check them")
|
||||
}
|
||||
+33
-5
@@ -114,6 +114,33 @@ func labelChoices(items []services.Label) []terminal.IssueLabelChoice {
|
||||
|
||||
func issueLabel(n int64) string { return fmt.Sprintf("issue %d", n) }
|
||||
|
||||
// nameFilter splits a comma-separated filter of kind, trimming entries and rejecting blanks.
|
||||
func nameFilter(kind, raw string) ([]string, error) {
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
names := strings.Split(raw, ",")
|
||||
return names, requireNames(kind, "", names)
|
||||
}
|
||||
|
||||
// issueFilters checks the label and milestone filters of an issue listing in the target repository
|
||||
// and returns them as query values. Gitea ignores labels and milestones it cannot find, and lists
|
||||
// every issue when no filter value is left, so unknown names are errors.
|
||||
func issueFilters(cmd *cobra.Command, t target, labels, milestones []string) (labelQuery, milestoneQuery string, err error) {
|
||||
if len(labels) > 0 {
|
||||
if err := (services.Labels{Client: t.client, Repository: t.Repo}).RequireFilterNames(cmd.Context(), labels); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
var ids []string
|
||||
if len(milestones) > 0 {
|
||||
if ids, err = (services.Milestones{Client: t.client, Repository: t.Repo}).FilterIDs(cmd.Context(), milestones); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
return strings.Join(labels, ","), strings.Join(ids, ","), nil
|
||||
}
|
||||
|
||||
// metadataFlags are the label and assignee flags shared by issue and pull request edits.
|
||||
type metadataFlags struct {
|
||||
setLabels, addLabels, removeLabels []string
|
||||
@@ -206,6 +233,10 @@ func (a *application) issueCommand() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
milestones, err := nameFilter("milestone", opts.Milestones)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target, err := a.resolve(cmd.Context(), true)
|
||||
if err != nil {
|
||||
if errors.Is(err, errRepositoryRequired) {
|
||||
@@ -213,11 +244,8 @@ func (a *application) issueCommand() *cobra.Command {
|
||||
}
|
||||
return err
|
||||
}
|
||||
if len(labelNames) > 0 {
|
||||
if err := (services.Labels{Client: target.client, Repository: target.Repo}).RequireFilterNames(cmd.Context(), labelNames); err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Labels = strings.Join(labelNames, ",")
|
||||
if opts.Labels, opts.Milestones, err = issueFilters(cmd, target, labelNames, milestones); err != nil {
|
||||
return err
|
||||
}
|
||||
items, more, err := (services.Issues{Client: target.client, Repository: target.Repo}).List(cmd.Context(), opts)
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user