feat(issue): list issues without an assignee with --no-assignee

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-18 17:17:11 +02:00
1 parent bae885a4d2
commit 3a61db6533
7 files changed
+128 -44

No files matched your search

+31
View File
@@ -7,6 +7,7 @@ import (
"context"
"net/http"
"strconv"
"strings"
"testing"
"time"
@@ -155,6 +156,8 @@ func TestIssueListWebOpensTheFilteredPage(t *testing.T) {
deps.Browser = browser
require.NoError(t, Execute(context.Background(), deps, []string{"issue", "list", "-R", "alice/project", "-w", "-s", "closed", "-S", "login bug"}))
require.Equal(t, server.URL+"/alice/project/issues?q=login+bug&state=closed", browser.resource)
require.NoError(t, Execute(context.Background(), deps, []string{"issue", "list", "-R", "alice/project", "-w", "--no-assignee"}))
require.Equal(t, server.URL+"/alice/project/issues?assignee=%28none%29&state=open", browser.resource)
}
func TestIssueListKindFilter(t *testing.T) {
@@ -270,3 +273,31 @@ func TestIssueCommandsComplete(t *testing.T) {
require.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive&cobra.ShellCompDirectiveNoFileComp, test.args)
}
}
func TestIssueListNoAssigneeFiltersEveryPageRead(t *testing.T) {
var pages []string
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
require.Empty(t, query.Get("assigned_by"))
require.Equal(t, "50", query.Get("limit"))
pages = append(pages, query.Get("page"))
start, _ := strconv.Atoi(query.Get("page"))
issues := []string{}
for n := (start-1)*50 + 1; n <= min(start*50, 60); n++ {
assignees := `[{"login":"bob"}]`
if n%20 == 0 {
assignees = `[]`
}
issues = append(issues, `{"number":`+strconv.Itoa(n)+`,"state":"open","assignees":`+assignees+`}`)
}
_, _ = w.Write([]byte("[" + strings.Join(issues, ",") + "]"))
}))
defer server.Close()
deps, out, _ := serverDeps(t, server.URL)
require.NoError(t, Execute(context.Background(), deps, []string{"issue", "list", "-R", "alice/project", "--no-assignee", "--json", "number"}))
require.JSONEq(t, `[{"number":20},{"number":40},{"number":60}]`, out.String())
require.Equal(t, []string{"1", "2"}, pages)
err := Execute(context.Background(), deps, []string{"issue", "list", "-R", "alice/project", "--no-assignee", "--assignee", "bob"})
require.Equal(t, clierr.Usage, clierr.ExitCode(err))
require.Len(t, pages, 2)
}
+10 -2
View File
@@ -42,12 +42,16 @@ func (a *application) issueListCommand() *cobra.Command {
cmd := &cobra.Command{Use: "list", Short: "List issues in a repository", Long: `List the issues of a repository, newest first. Filters combine: an issue must match all of them.
Labels and milestones must exist in the repository.`, Example: `$ tea issue list --label bug --state all
$ tea issue list --assignee @me --json number,title
$ tea issue list --no-assignee --label bug
# Issues and pull requests in a milestone, as tab-separated values
$ tea issue list --kind all --milestone v1.0 | cut -f1,2`, Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error {
var err error
if opts.Since, opts.Before, err = updated.parse(a.deps.Now()); err != nil {
return err
}
if opts.NoAssignee && opts.Assignee != "" {
return clierr.Wrap(clierr.Usage, errors.New("--assignee and --no-assignee are mutually exclusive"))
}
if err := requireNames("label", "", labels); err != nil {
return err
}
@@ -66,7 +70,7 @@ $ tea issue list --kind all --milestone v1.0 | cut -f1,2`, Args: argsExact(0), R
if web {
return a.openBrowser(cmd, issuesURL(target, opts))
}
filtered := opts.Search+opts.Assignee+opts.Author+opts.Mentions != "" || len(labels)+len(milestones) > 0 || opts.Since != nil || opts.Before != nil
filtered := opts.Search+opts.Assignee+opts.Author+opts.Mentions != "" || len(labels)+len(milestones) > 0 || opts.Since != nil || opts.Before != nil || opts.NoAssignee
if opts.Labels, opts.Milestones, err = issueFilters(cmd, target, labels, milestones); err != nil {
return err
}
@@ -82,6 +86,7 @@ $ tea issue list --kind all --milestone v1.0 | cut -f1,2`, Args: argsExact(0), R
flags := cmd.Flags()
flags.StringVarP(&opts.Search, "search", "S", "", "Search issue titles and bodies")
flags.StringVarP(&opts.Assignee, "assignee", "a", "", "Filter by assignee; @me for yourself")
flags.BoolVar(&opts.NoAssignee, "no-assignee", false, "Only issues without an assignee")
flags.StringVarP(&opts.Author, "author", "A", "", "Filter by author; @me for yourself")
flags.StringVar(&opts.Mentions, "mentions", "", "Filter by mentioned user; @me for yourself")
flags.StringSliceVarP(&labels, "label", "l", nil, "Filter by label `names`")
@@ -97,7 +102,7 @@ $ tea issue list --kind all --milestone v1.0 | cut -f1,2`, Args: argsExact(0), R
}
// issuesURL is the web page of the repository's issues or pull requests in state, searched for
// the --search text.
// the --search text and limited to unassigned ones for --no-assignee.
func issuesURL(t target, opts services.ListOptions) string {
section := "issues"
if opts.Kind == "pulls" {
@@ -107,6 +112,9 @@ func issuesURL(t target, opts services.ListOptions) string {
if opts.Search != "" {
query.Set("q", opts.Search)
}
if opts.NoAssignee {
query.Set("assignee", "(none)")
}
return repositoryURL(t) + "/" + section + "?" + query.Encode()
}
+4
View File
@@ -45,6 +45,9 @@ each pull request, which tea only makes when --json names them.`, Args: argsExac
if id, err := strconv.ParseInt(milestone, 10, 64); err == nil && id < 1 {
return clierr.Wrap(clierr.Usage, errors.New("--milestone must be a milestone title or a positive ID"))
}
if options.NoAssignee && options.Assignee != "" {
return clierr.Wrap(clierr.Usage, errors.New("--assignee and --no-assignee are mutually exclusive"))
}
t, err := a.resolve(cmd.Context(), true)
if err != nil {
return err
@@ -106,6 +109,7 @@ each pull request, which tea only makes when --json names them.`, Args: argsExac
pageFlags(cmd, &limit, &page)
cmd.Flags().StringVarP(&options.Author, "author", "A", "", "Filter by author; @me for yourself")
cmd.Flags().StringVarP(&options.Assignee, "assignee", "a", "", "Filter by assignee; @me for yourself")
cmd.Flags().BoolVar(&options.NoAssignee, "no-assignee", false, "Only pull requests without an assignee")
cmd.Flags().StringVarP(&options.Base, "base", "B", "", "Filter by base branch")
cmd.Flags().StringVarP(&options.Head, "head", "H", "", "Filter by head branch, or OWNER:BRANCH for a fork")
cmd.Flags().BoolVarP(&options.Draft, "draft", "d", false, "Show only drafts")