diff --git a/internal/cmd/sprint/close/close.go b/internal/cmd/sprint/close/close.go new file mode 100644 index 00000000..93346269 --- /dev/null +++ b/internal/cmd/sprint/close/close.go @@ -0,0 +1,101 @@ +package close + +import ( + "fmt" + "strconv" + + "github.com/AlecAivazis/survey/v2" + "github.com/ankitpokhrel/jira-cli/api" + "github.com/ankitpokhrel/jira-cli/internal/cmdutil" + "github.com/ankitpokhrel/jira-cli/internal/query" + "github.com/spf13/cobra" +) + +const ( + helpText = `Close sprint.` + examples = `$ jira sprint close SPRINT_ID` +) + +// NewCmdClose is an add command. +func NewCmdClose() *cobra.Command { + return &cobra.Command{ + Use: "close SPRINT_ID", + Short: "Close sprint", + Long: helpText, + Example: examples, + Aliases: []string{"complete"}, + Annotations: map[string]string{ + "help:args": "SPRINT_ID\t\tID of the sprint on which you want to assign issues to, eg: 123\n", + }, + Run: closeSprint, + } +} + +func closeSprint(cmd *cobra.Command, args []string) { + params := parseFlags(cmd.Flags(), args) + client := api.DefaultClient(params.debug) + + qs := getQuestions(params) + if len(qs) > 0 { + ans := struct { + SprintID string + }{} + err := survey.Ask(qs, &ans) + cmdutil.ExitIfError(err) + + if params.sprintID == "" { + params.sprintID = ans.SprintID + } + } + + err := func() error { + s := cmdutil.Info("Closing sprint...\n") + defer s.Stop() + + sprintID, err := strconv.Atoi(params.sprintID) + if err != nil { + return err + } + + return client.EndSprint(sprintID) + }() + cmdutil.ExitIfError(err) + + cmdutil.Success(fmt.Sprintf("Sprint %s has been closed.", params.sprintID)) +} + +func parseFlags(flags query.FlagParser, args []string) *addParams { + var sprintID string + + nArgs := len(args) + if nArgs > 0 { + sprintID = args[0] + } + + debug, err := flags.GetBool("debug") + cmdutil.ExitIfError(err) + + return &addParams{ + sprintID: sprintID, + debug: debug, + } +} + +func getQuestions(params *addParams) []*survey.Question { + var qs []*survey.Question + + if params.sprintID == "" { + qs = append(qs, &survey.Question{ + Name: "sprintID", + Prompt: &survey.Input{Message: "Sprint ID"}, + Validate: survey.Required, + }) + } + + return qs +} + +type addParams struct { + sprintID string + debug bool +} diff --git a/internal/cmd/sprint/sprint.go b/internal/cmd/sprint/sprint.go index 88232f8b..148cf461 100644 --- a/internal/cmd/sprint/sprint.go +++ b/internal/cmd/sprint/sprint.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" "github.com/ankitpokhrel/jira-cli/internal/cmd/sprint/add" + "github.com/ankitpokhrel/jira-cli/internal/cmd/sprint/close" "github.com/ankitpokhrel/jira-cli/internal/cmd/sprint/list" ) @@ -22,8 +23,9 @@ func NewCmdSprint() *cobra.Command { lc := list.NewCmdList() ac := add.NewCmdAdd() + cc := close.NewCmdClose() - cmd.AddCommand(lc, ac) + cmd.AddCommand(lc, ac, cc) list.SetFlags(lc) diff --git a/pkg/jira/client.go b/pkg/jira/client.go index 91d837f8..039bf16e 100644 --- a/pkg/jira/client.go +++ b/pkg/jira/client.go @@ -234,6 +234,11 @@ func (c *Client) PutV2(ctx context.Context, path string, body []byte, headers He return c.request(ctx, http.MethodPut, c.server+baseURLv2+path, body, headers) } +// PutV1 sends PUT request to v1 version of the jira api. +func (c *Client) PutV1(ctx context.Context, path string, body []byte, headers Header) (*http.Response, error) { + return c.request(ctx, http.MethodPut, c.server+baseURLv1+path, body, headers) +} + // DeleteV2 sends DELETE request to v2 version of the jira api. func (c *Client) DeleteV2(ctx context.Context, path string, headers Header) (*http.Response, error) { return c.request(ctx, http.MethodDelete, c.server+baseURLv2+path, nil, headers) diff --git a/pkg/jira/sprint.go b/pkg/jira/sprint.go index 6e831644..cd468dac 100644 --- a/pkg/jira/sprint.go +++ b/pkg/jira/sprint.go @@ -51,6 +51,75 @@ func (c *Client) Sprints(boardID int, qp string, from, limit int) (*SprintResult return &out, err } +// GetSprint returns a single sprint given an ID. +func (c *Client) GetSprint(sprintID int) (*Sprint, error) { + res, err := c.GetV1( + context.Background(), + fmt.Sprintf("/sprint/%d", sprintID), + nil, + ) + if err != nil { + return nil, err + } + if res == nil { + return nil, ErrEmptyResponse + } + defer func() { _ = res.Body.Close() }() + + if res.StatusCode != http.StatusOK { + return nil, formatUnexpectedResponse(res) + } + + var s Sprint + err = json.NewDecoder(res.Body).Decode(&s) + if err != nil { + return nil, err + } + + return &s, nil +} + +// EndSprint queries the existence of the sprint and +// full updates the sprint with new status of closed. +// Default behavior is all open tasks are sent to backlog. +func (c *Client) EndSprint(sprintID int) error { + // get the sprint + sprint, err := c.GetSprint(sprintID) + if err != nil { + return err + } + + // update to closed and format for PUT + sprint.Status = SprintStateClosed + body, err := json.Marshal(sprint) + if err != nil { + return err + } + + res, err := c.PutV1( + context.Background(), + fmt.Sprintf("/sprint/%d", sprintID), + body, + Header{ + "Accept": "application/json", + "Content-Type": "application/json", + }, + ) + if err != nil { + return err + } + if res == nil { + return ErrEmptyResponse + } + defer func() { _ = res.Body.Close() }() + + if res.StatusCode != http.StatusOK { + return formatUnexpectedResponse(res) + } + + return nil +} + // SprintsInBoards fetches sprints across given board IDs. // // qp is an additional query parameters in key, value pair format, eg: state=closed.