diff --git a/api/todo.go b/api/todo.go new file mode 100644 index 0000000000000000000000000000000000000000..dffd7224415dc356b8ed656bbf4591418c3e625c --- /dev/null +++ b/api/todo.go @@ -0,0 +1,19 @@ +package api + +import "github.com/xanzy/go-gitlab" + +var ListTodos = func(client *gitlab.Client, opts *gitlab.ListTodosOptions) ([]*gitlab.Todo, *gitlab.Response, error) { + if client == nil { + client = apiClient.Lab() + } + + if opts.PerPage == 0 { + opts.PerPage = DefaultListLimit + } + + todos, resp, err := client.Todos.ListTodos(opts) + if err != nil { + return nil, nil, err + } + return todos, resp, nil +} diff --git a/commands/root.go b/commands/root.go index 69b6ace5347e0d3adcb5722d03a97e47e5351ff8..442d8bd27802e2e482fccb4621da8277f8047126 100644 --- a/commands/root.go +++ b/commands/root.go @@ -26,6 +26,7 @@ import ( scheduleCmd "gitlab.com/gitlab-org/cli/commands/schedule" snippetCmd "gitlab.com/gitlab-org/cli/commands/snippet" sshCmd "gitlab.com/gitlab-org/cli/commands/ssh-key" + todoCmd "gitlab.com/gitlab-org/cli/commands/todo" updateCmd "gitlab.com/gitlab-org/cli/commands/update" userCmd "gitlab.com/gitlab-org/cli/commands/user" variableCmd "gitlab.com/gitlab-org/cli/commands/variable" @@ -126,6 +127,7 @@ func NewCmdRoot(f *cmdutils.Factory, version, buildDate string) *cobra.Command { rootCmd.AddCommand(scheduleCmd.NewCmdSchedule(f)) rootCmd.AddCommand(snippetCmd.NewCmdSnippet(f)) rootCmd.AddCommand(askCmd.NewCmd(f)) + rootCmd.AddCommand(todoCmd.NewCmdTodo(f)) rootCmd.Flags().BoolP("version", "v", false, "show glab version information") return rootCmd diff --git a/commands/todo/list/todo_list.go b/commands/todo/list/todo_list.go new file mode 100644 index 0000000000000000000000000000000000000000..e8fee42a9a2964b564728a37ad9cb7f9100877bb --- /dev/null +++ b/commands/todo/list/todo_list.go @@ -0,0 +1,109 @@ +package list + +import ( + "fmt" + + "gitlab.com/gitlab-org/cli/api" + "gitlab.com/gitlab-org/cli/commands/cmdutils" + "gitlab.com/gitlab-org/cli/pkg/iostreams" + "gitlab.com/gitlab-org/cli/pkg/tableprinter" + "gitlab.com/gitlab-org/cli/pkg/utils" + + "github.com/MakeNowJust/heredoc" + "github.com/spf13/cobra" + "github.com/xanzy/go-gitlab" + + "gitlab.com/gitlab-org/cli/commands/todo/todoutils" +) + +type ListOptions struct { + State string + + // Pagination + Page int + PerPage int + + // display opts + TitleQualifier string + + IO *iostreams.IOStreams + HTTPClient func() (*gitlab.Client, error) +} + +func DisplayAllTodos(streams *iostreams.IOStreams, todos []*gitlab.Todo) string { + table := tableprinter.NewTablePrinter() + table.SetIsTTY(streams.IsOutputTTY()) + for _, todo := range todos { + table.AddCell(todoutils.TodoActionName(todo)) + table.AddCell(streams.Hyperlink(fmt.Sprintf("%s%s", todo.Project.PathWithNamespace, todo.Target.Reference), todo.TargetURL)) + table.AddCell(todo.Body) + table.EndRow() + } + + return table.Render() +} + +func NewCmdList(f *cmdutils.Factory, runE func(opts *ListOptions) error) *cobra.Command { + opts := &ListOptions{ + IO: f.IO, + } + + todoListCmd := &cobra.Command{ + Use: "list [flags]", + Short: `List your todos`, + Long: ``, + Aliases: []string{"ls"}, + Example: heredoc.Doc(` + glab todo list + glab todo list --state done + `), + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + var err error + + opts.HTTPClient = f.HttpClient + + apiClient, err := opts.HTTPClient() + if err != nil { + return err + } + + l := &gitlab.ListTodosOptions{} + + if p, _ := cmd.Flags().GetInt("page"); p != 0 { + opts.Page = p + l.Page = p + } + + if p, _ := cmd.Flags().GetInt("per-page"); p != 0 { + opts.PerPage = p + l.PerPage = p + } + + if state, _ := cmd.Flags().GetString("state"); state != "" { + opts.State = state + l.State = gitlab.String(state) + } + + title := utils.NewListTitle(opts.TitleQualifier + " todo") + todos, _, err := api.ListTodos(apiClient, l) + + if err != nil { + return err + } + + title.Page = l.Page + title.CurrentPageTotal = len(todos) + + fmt.Fprintf(opts.IO.StdOut, "%s\n%s\n", title.Describe(), DisplayAllTodos(opts.IO, todos)) + + return nil + }, + } + + todoListCmd.Flags().IntP("page", "p", 1, "Page number") + todoListCmd.Flags().IntP("per-page", "P", 30, "Number of items to list per page") + todoListCmd.Flags().StringP("state", "s", "pending", "State of todo") + + return todoListCmd +} diff --git a/commands/todo/list/todo_list_test.go b/commands/todo/list/todo_list_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a3de5d2f8170ac21b451185a85d116b61170479d --- /dev/null +++ b/commands/todo/list/todo_list_test.go @@ -0,0 +1,79 @@ +package list + +import ( + "net/http" + "testing" + + "gitlab.com/gitlab-org/cli/pkg/iostreams" + + "github.com/MakeNowJust/heredoc" + + "github.com/stretchr/testify/assert" + "gitlab.com/gitlab-org/cli/commands/cmdtest" + "gitlab.com/gitlab-org/cli/pkg/httpmock" + "gitlab.com/gitlab-org/cli/test" +) + +func runCommand(rt http.RoundTripper) (*test.CmdOut, error) { + ios, _, stdout, stderr := iostreams.Test() + factory := cmdtest.InitFactory(ios, rt) + + _, _ = factory.HttpClient() + + cmd := NewCmdList(factory, nil) + + _, err := cmd.ExecuteC() + return &test.CmdOut{ + OutBuf: stdout, + ErrBuf: stderr, + }, err +} + +func TestTodoList(t *testing.T) { + fakeHTTP := &httpmock.Mocker{} + defer fakeHTTP.Verify(t) + + fakeHTTP.RegisterResponder(http.MethodGet, "/api/v4/todos", + httpmock.NewStringResponse(http.StatusOK, ` + [ + { + "id": 102, + "project": { + "path_with_namespace": "gitlab-org/gitlab-foss" + }, + "action_name": "marked", + "target": { + "reference": "!1" + }, + "target_url": "https://gitlab.example.com/gitlab-org/gitlab-foss/-/merge_requests/7" + }, + { + "id": 102, + "project": { + "path_with_namespace": "gitlab-org/gitlab-foss" + }, + "action_name": "build_failed", + "target": { + "reference": "!1" + }, + "target_url": "https://gitlab.example.com/gitlab-org/gitlab-foss/-/merge_requests/7" + } + ] + `)) + + output, err := runCommand(fakeHTTP) + if err != nil { + t.Errorf("error running command `todo list`: %v", err) + } + + out := output.String() + + assert.Equal(t, heredoc.Doc(` + Showing 2 todos (Page 1) + + Added todo gitlab-org/gitlab-foss!1 + Pipeline failed gitlab-org/gitlab-foss!1 + + `), out) + assert.Empty(t, output.Stderr()) +} diff --git a/commands/todo/todo.go b/commands/todo/todo.go new file mode 100644 index 0000000000000000000000000000000000000000..45bb917baf2aa1c275570f14917f566f56c65a6a --- /dev/null +++ b/commands/todo/todo.go @@ -0,0 +1,18 @@ +package todo + +import ( + "github.com/spf13/cobra" + "gitlab.com/gitlab-org/cli/commands/cmdutils" + todoListCmd "gitlab.com/gitlab-org/cli/commands/todo/list" +) + +func NewCmdTodo(f *cmdutils.Factory) *cobra.Command { + todoCmd := &cobra.Command{ + Use: "todo [flags]", + Short: `List todos`, + Long: ``, + } + + todoCmd.AddCommand(todoListCmd.NewCmdList(f, nil)) + return todoCmd +} diff --git a/commands/todo/todo_test.go b/commands/todo/todo_test.go new file mode 100644 index 0000000000000000000000000000000000000000..13de5d80edc2bc47a5f569b5852d99bcee189dcf --- /dev/null +++ b/commands/todo/todo_test.go @@ -0,0 +1,32 @@ +package todo + +import ( + "bytes" + "io" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "gitlab.com/gitlab-org/cli/commands/cmdutils" +) + +func TestNewCmdTodo(t *testing.T) { + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + assert.Nil(t, NewCmdTodo(&cmdutils.Factory{}).Execute()) + + outC := make(chan string) + go func() { + var buf bytes.Buffer + _, _ = io.Copy(&buf, r) + outC <- buf.String() + }() + + w.Close() + os.Stdout = old + out := <-outC + + assert.Contains(t, out, " \"todo [command]") +} diff --git a/commands/todo/todoutils/utils.go b/commands/todo/todoutils/utils.go new file mode 100644 index 0000000000000000000000000000000000000000..e002a9311da1aa6ee3def7b87d10936af3bbac93 --- /dev/null +++ b/commands/todo/todoutils/utils.go @@ -0,0 +1,29 @@ +package todoutils + +import ( + "github.com/xanzy/go-gitlab" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +func TodoActionName(todo *gitlab.Todo) string { + switch todo.ActionName { + case "approval_required": + return "Approval required" + case "build_failed": + return "Pipeline failed" + case "directly_addressed": + return "Mentioned" + case "marked": + return "Added todo" + case "merge_train_removed": + return "Removed from merge train" + case "review_requested": + return "Review requested" + case "review_submitted": + return "Review submitted" + default: + return cases.Title(language.English, cases.NoLower).String(string(todo.ActionName)) + } +} diff --git a/docs/source/todo/help.md b/docs/source/todo/help.md new file mode 100644 index 0000000000000000000000000000000000000000..dc3ab7ccc791d527805120f752dc5276729e1fe5 --- /dev/null +++ b/docs/source/todo/help.md @@ -0,0 +1,24 @@ +--- +stage: Create +group: Code Review +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + + + +# `glab todo help` + +Help about any command + +```plaintext +glab todo help [command] [flags] +``` + +## Options inherited from parent commands + +```plaintext + --help Show help for command +``` diff --git a/docs/source/todo/index.md b/docs/source/todo/index.md new file mode 100644 index 0000000000000000000000000000000000000000..15afea901472f75042b404f768bab037529210e1 --- /dev/null +++ b/docs/source/todo/index.md @@ -0,0 +1,24 @@ +--- +stage: Create +group: Code Review +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + + + +# `glab todo` + +List todos + +## Options inherited from parent commands + +```plaintext + --help Show help for command +``` + +## Subcommands + +- [`list`](list.md) diff --git a/docs/source/todo/list.md b/docs/source/todo/list.md new file mode 100644 index 0000000000000000000000000000000000000000..db6ffde35e15cdf2c7edacb3fb106e74a542c77f --- /dev/null +++ b/docs/source/todo/list.md @@ -0,0 +1,46 @@ +--- +stage: Create +group: Code Review +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + + + +# `glab todo list` + +List your todos + +```plaintext +glab todo list [flags] +``` + +## Aliases + +```plaintext +ls +``` + +## Examples + +```plaintext +glab todo list +glab todo list --state done + +``` + +## Options + +```plaintext + -p, --page int Page number (default 1) + -P, --per-page int Number of items to list per page (default 30) + -s, --state string State of todo (default "pending") +``` + +## Options inherited from parent commands + +```plaintext + --help Show help for command +``` diff --git a/pkg/utils/display.go b/pkg/utils/display.go index a13c360af601cfb6570bc987d889f25bd540ff3c..90dad8a7e6da03682dcc91223e3994f8a1f6c324 100644 --- a/pkg/utils/display.go +++ b/pkg/utils/display.go @@ -67,7 +67,11 @@ func (opts *ListTitleOptions) Describe() string { } if opts.CurrentPageTotal > 0 { - return fmt.Sprintf("Showing %s %s on %s %s\n", pageNumInfo, opts.Name, opts.RepoName, pageInfo) + if opts.RepoName == "" { + return fmt.Sprintf("Showing %s %s %s\n", pageNumInfo, opts.Name, pageInfo) + } else { + return fmt.Sprintf("Showing %s %s on %s %s\n", pageNumInfo, opts.Name, opts.RepoName, pageInfo) + } } emptyMessage := opts.EmptyMessage