feat(incident): add incident reopen command

Delivers: #1314
This commit is contained in:
Vitali Tatarintev 2023-04-25 11:47:00 +02:00
parent 25c6ec70a3
commit a5154f5343
13 changed files with 404 additions and 29 deletions

View File

@ -6,6 +6,7 @@ import (
incidentCloseCmd "gitlab.com/gitlab-org/cli/commands/incident/close"
incidentListCmd "gitlab.com/gitlab-org/cli/commands/incident/list"
incidentReopenCmd "gitlab.com/gitlab-org/cli/commands/incident/reopen"
incidentViewCmd "gitlab.com/gitlab-org/cli/commands/incident/view"
"github.com/spf13/cobra"
@ -33,5 +34,6 @@ func NewCmdIncident(f *cmdutils.Factory) *cobra.Command {
incidentCmd.AddCommand(incidentListCmd.NewCmdList(f, nil))
incidentCmd.AddCommand(incidentViewCmd.NewCmdView(f))
incidentCmd.AddCommand(incidentCloseCmd.NewCmdClose(f))
incidentCmd.AddCommand(incidentReopenCmd.NewCmdReopen(f))
return incidentCmd
}

View File

@ -0,0 +1,14 @@
package reopen
import (
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/commands/issuable"
"github.com/spf13/cobra"
issuableReopenCmd "gitlab.com/gitlab-org/cli/commands/issuable/reopen"
)
func NewCmdReopen(f *cmdutils.Factory) *cobra.Command {
return issuableReopenCmd.NewCmdReopen(f, issuable.TypeIncident)
}

View File

@ -59,13 +59,9 @@ func NewCmdClose(f *cmdutils.Factory, issueType issuable.IssueType) *cobra.Comma
c := f.IO.Color()
for _, issue := range issues {
// Issues and incidents are the same kind, but with different issueType.
// `issue close` can close issues of all types including incidents
// `incident close` on the other hand, should close only incidents, and treat all other issue types as not found
//
// When using `incident close` with non incident's IDs, print an error.
if issueType == issuable.TypeIncident && *issue.IssueType != string(issuable.TypeIncident) {
fmt.Fprintln(f.IO.StdOut, "Incident not found, but an issue with the provided ID exists. Run `glab issue close <id>` to close it.")
valid, msg := issuable.ValidateIncidentCmd(issueType, "close", issue)
if !valid {
fmt.Fprintln(f.IO.StdOut, msg)
continue
}

View File

@ -1,8 +1,32 @@
package issuable
import (
"fmt"
"github.com/xanzy/go-gitlab"
)
type IssueType string
const (
TypeIssue IssueType = "issue"
TypeIncident IssueType = "incident"
)
// ValidateIncidentCmd returns an error when incident command is used with non-incident's IDs.
//
// Issues and incidents are the same kind, but with different issueType.
//
// For example:
// `issue view` can view issues of all types including incidents
// `incident view` on the other hand, should view only incidents, and treat all other issue types as not found
func ValidateIncidentCmd(cmd IssueType, subcmd string, issue *gitlab.Issue) (bool, string) {
if cmd == TypeIncident && *issue.IssueType != string(TypeIncident) {
return false, fmt.Sprintf(
"Incident not found, but an issue with the provided ID exists. Run `glab issue %[1]s <id>` to %[1]s it.",
subcmd,
)
}
return true, ""
}

View File

@ -0,0 +1,73 @@
package issuable
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/xanzy/go-gitlab"
)
func TestValidateIncidentCmd(t *testing.T) {
issueTypeIssue := "issue"
issueTypeIncident := "incident"
tests := []struct {
name string
cmd IssueType
subcmd string
issue *gitlab.Issue
valid bool
}{
{
name: "valid_incident_view_command",
cmd: TypeIncident,
subcmd: "view",
issue: &gitlab.Issue{
IssueType: &issueTypeIncident,
},
valid: true,
},
{
name: "invalid_incident_view_command",
cmd: TypeIncident,
subcmd: "view",
issue: &gitlab.Issue{
IssueType: &issueTypeIssue,
},
valid: false,
},
{
name: "valid_issue_view_command_for_issue",
cmd: TypeIssue,
subcmd: "view",
issue: &gitlab.Issue{
IssueType: &issueTypeIssue,
},
valid: true,
},
{
name: "valid_issue_view_command_for_incident",
cmd: TypeIssue,
subcmd: "view",
issue: &gitlab.Issue{
IssueType: &issueTypeIncident,
},
valid: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
valid, msg := ValidateIncidentCmd(tt.cmd, tt.subcmd, tt.issue)
assert.Equal(t, tt.valid, valid)
if !valid {
assert.Equal(
t,
fmt.Sprintf("Incident not found, but an issue with the provided ID exists. Run `glab issue %[1]s <id>` to %[1]s it.", tt.subcmd),
msg,
)
}
})
}
}

View File

@ -6,23 +6,47 @@ import (
"github.com/MakeNowJust/heredoc"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/commands/issuable"
"gitlab.com/gitlab-org/cli/commands/issue/issueutils"
"github.com/spf13/cobra"
"github.com/xanzy/go-gitlab"
)
func NewCmdReopen(f *cmdutils.Factory) *cobra.Command {
var (
description = map[issuable.IssueType]string{
issuable.TypeIssue: "Reopen a closed issue",
issuable.TypeIncident: "Reopen a resolved incident",
}
reopeningMessage = map[issuable.IssueType]string{
issuable.TypeIssue: "Reopening Issue",
issuable.TypeIncident: "Reopening Incident",
}
reopenedMessage = map[issuable.IssueType]string{
issuable.TypeIssue: "Reopened Issue",
issuable.TypeIncident: "Reopened Incident",
}
)
func NewCmdReopen(f *cmdutils.Factory, issueType issuable.IssueType) *cobra.Command {
examplePath := "issues/123"
if issueType == issuable.TypeIncident {
examplePath = "issues/incident/123"
}
issueReopenCmd := &cobra.Command{
Use: "reopen <id>",
Short: `Reopen a closed issue`,
Use: "reopen [<id> | <url>] [flags]",
Short: description[issueType],
Long: ``,
Aliases: []string{"open"},
Example: heredoc.Doc(`
glab issue reopen 123
glab issue open 123
glab issue reopen https://gitlab.com/profclems/glab/-/issues/123
`),
Example: heredoc.Doc(fmt.Sprintf(`
glab %[1]s reopen 123
glab %[1]s open 123
glab %[1]s reopen https://gitlab.com/NAMESPACE/REPO/-/%s
`, issueType, examplePath)),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var err error
@ -43,16 +67,19 @@ func NewCmdReopen(f *cmdutils.Factory) *cobra.Command {
l.StateEvent = gitlab.String("reopen")
for _, issue := range issues {
if f.IO.IsaTTY && f.IO.IsErrTTY {
fmt.Fprintln(out, "- Reopening Issue...")
valid, msg := issuable.ValidateIncidentCmd(issueType, "reopen", issue)
if !valid {
fmt.Fprintln(f.IO.StdOut, msg)
continue
}
fmt.Fprintf(out, "- %s...\n", reopeningMessage[issueType])
issue, err := api.UpdateIssue(apiClient, repo.FullName(), issue.IID, l)
if err != nil {
return err
}
fmt.Fprintf(out, "%s Reopened Issue #%d\n", c.GreenCheck(), issue.IID)
fmt.Fprintf(out, "%s %s #%d\n", c.GreenCheck(), reopenedMessage[issueType], issue.IID)
fmt.Fprintln(out, issueutils.DisplayIssue(c, issue, f.IO.IsaTTY))
}
return nil

View File

@ -0,0 +1,188 @@
package reopen
import (
"fmt"
"io"
"net/http"
"testing"
"github.com/MakeNowJust/heredoc"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
"github.com/xanzy/go-gitlab"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/commands/issuable"
"gitlab.com/gitlab-org/cli/internal/glrepo"
"gitlab.com/gitlab-org/cli/pkg/httpmock"
"gitlab.com/gitlab-org/cli/pkg/iostreams"
"gitlab.com/gitlab-org/cli/test"
)
func mockAllResponses(t *testing.T, fakeHTTP *httpmock.Mocker) {
fakeHTTP.RegisterResponder(http.MethodGet, "/projects/OWNER/REPO/issues/1",
httpmock.NewStringResponse(http.StatusOK, `{
"id": 1,
"iid": 1,
"title": "test issue",
"state": "closed",
"issue_type": "issue",
"created_at": "2023-04-05T10:51:26.371Z"
}`),
)
fakeHTTP.RegisterResponder(http.MethodPut, "/projects/OWNER/REPO/issues/1",
func(req *http.Request) (*http.Response, error) {
rb, _ := io.ReadAll(req.Body)
assert.Contains(t, string(rb), `"state_event":"reopen"`)
resp, _ := httpmock.NewStringResponse(http.StatusOK, `{
"id": 1,
"iid": 1,
"state": "open",
"issue_type": "issue",
"created_at": "2023-04-05T10:51:26.371Z"
}`)(req)
return resp, nil
},
)
fakeHTTP.RegisterResponder(http.MethodGet, "/projects/OWNER/REPO/issues/2",
httpmock.NewStringResponse(http.StatusOK, `{
"id": 2,
"iid": 2,
"title": "test incident",
"state": "closed",
"issue_type": "incident",
"created_at": "2023-04-05T10:51:26.371Z"
}`),
)
fakeHTTP.RegisterResponder(http.MethodPut, "/projects/OWNER/REPO/issues/2",
func(req *http.Request) (*http.Response, error) {
rb, _ := io.ReadAll(req.Body)
assert.Contains(t, string(rb), `"state_event":"reopen"`)
resp, _ := httpmock.NewStringResponse(http.StatusOK, `{
"id": 2,
"iid": 2,
"state": "opened",
"issue_type": "incident",
"created_at": "2023-04-05T10:51:26.371Z"
}`)(req)
return resp, nil
},
)
fakeHTTP.RegisterResponder(http.MethodGet, "/projects/OWNER/REPO/issues/404",
httpmock.NewStringResponse(http.StatusNotFound, `{"message": "404 not found"}`),
)
}
func runCommand(rt http.RoundTripper, issuableID string, issueType issuable.IssueType) (*test.CmdOut, error) {
ios, _, stdout, stderr := iostreams.Test()
factory := &cmdutils.Factory{
IO: ios,
HttpClient: func() (*gitlab.Client, error) {
a, err := api.TestClient(&http.Client{Transport: rt}, "", "", false)
if err != nil {
return nil, err
}
return a.Lab(), err
},
BaseRepo: func() (glrepo.Interface, error) {
return glrepo.New("OWNER", "REPO"), nil
},
}
_, _ = factory.HttpClient()
cmd := NewCmdReopen(factory, issueType)
argv, err := shlex.Split(issuableID)
if err != nil {
return nil, err
}
cmd.SetArgs(argv)
_, err = cmd.ExecuteC()
return &test.CmdOut{
OutBuf: stdout,
ErrBuf: stderr,
}, err
}
func TestIssuableReopen(t *testing.T) {
tests := []struct {
iid int
name string
issueType issuable.IssueType
wantOutput string
wantErr bool
}{
{
iid: 1,
name: "issue_reopen",
issueType: issuable.TypeIssue,
wantOutput: heredoc.Doc(`
- Reopening Issue...
Reopened Issue #1
`),
},
{
iid: 2,
name: "incident_reopen",
issueType: issuable.TypeIncident,
wantOutput: heredoc.Doc(`
- Reopening Incident...
Reopened Incident #2
`),
},
{
iid: 2,
name: "incident_reopen_using_issue_command",
issueType: issuable.TypeIssue,
wantOutput: heredoc.Doc(`
- Reopening Issue...
Reopened Issue #2
`),
},
{
iid: 1,
name: "issue_reopen_using_incident_command",
issueType: issuable.TypeIncident,
wantOutput: "Incident not found, but an issue with the provided ID exists. Run `glab issue reopen <id>` to reopen it.\n",
},
{
iid: 404,
name: "issue_not_found",
issueType: issuable.TypeIssue,
wantOutput: "404 not found",
wantErr: true,
},
}
for _, tt := range tests {
fakeHTTP := httpmock.New()
mockAllResponses(t, fakeHTTP)
t.Run(tt.name, func(t *testing.T) {
output, err := runCommand(fakeHTTP, fmt.Sprint(tt.iid), tt.issueType)
if tt.wantErr {
assert.Contains(t, err.Error(), tt.wantOutput)
return
}
assert.NoErrorf(t, err, "error running command `%s reopen %d`", tt.issueType, tt.iid)
assert.Equal(t, tt.wantOutput, output.String())
assert.Empty(t, output.Stderr())
})
}
}

View File

@ -70,13 +70,9 @@ func NewCmdView(f *cmdutils.Factory, issueType issuable.IssueType) *cobra.Comman
opts.Issue = issue
// Issues and incidents are the same kind, but with different issueType.
// `issue view` can display issues of all types including incidents
// `incident view` on the other hand, should display only incidents, and treat all other issue types as not found
//
// When using `incident view` with non incident's IDs, print an error.
if issueType == issuable.TypeIncident && *opts.Issue.IssueType != string(issuable.TypeIncident) {
fmt.Fprintln(opts.IO.StdErr, "Incident not found, but an issue with the provided ID exists. Run `glab issue view <id>` to view it.")
valid, msg := issuable.ValidateIncidentCmd(issueType, "view", opts.Issue)
if !valid {
fmt.Fprintln(opts.IO.StdErr, msg)
return nil
}

View File

@ -3,13 +3,13 @@ package issue
import (
"github.com/MakeNowJust/heredoc"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
issuableReopenCmd "gitlab.com/gitlab-org/cli/commands/issuable/reopen"
issueBoardCmd "gitlab.com/gitlab-org/cli/commands/issue/board"
issueCloseCmd "gitlab.com/gitlab-org/cli/commands/issue/close"
issueCreateCmd "gitlab.com/gitlab-org/cli/commands/issue/create"
issueDeleteCmd "gitlab.com/gitlab-org/cli/commands/issue/delete"
issueListCmd "gitlab.com/gitlab-org/cli/commands/issue/list"
issueNoteCmd "gitlab.com/gitlab-org/cli/commands/issue/note"
issueReopenCmd "gitlab.com/gitlab-org/cli/commands/issue/reopen"
issueSubscribeCmd "gitlab.com/gitlab-org/cli/commands/issue/subscribe"
issueUnsubscribeCmd "gitlab.com/gitlab-org/cli/commands/issue/unsubscribe"
issueUpdateCmd "gitlab.com/gitlab-org/cli/commands/issue/update"
@ -46,7 +46,7 @@ func NewCmdIssue(f *cmdutils.Factory) *cobra.Command {
issueCmd.AddCommand(issueDeleteCmd.NewCmdDelete(f))
issueCmd.AddCommand(issueListCmd.NewCmdList(f, nil))
issueCmd.AddCommand(issueNoteCmd.NewCmdNote(f))
issueCmd.AddCommand(issuableReopenCmd.NewCmdReopen(f))
issueCmd.AddCommand(issueReopenCmd.NewCmdReopen(f))
issueCmd.AddCommand(issueViewCmd.NewCmdView(f))
issueCmd.AddCommand(issueSubscribeCmd.NewCmdSubscribe(f))
issueCmd.AddCommand(issueUnsubscribeCmd.NewCmdUnsubscribe(f))

View File

@ -0,0 +1,14 @@
package reopen
import (
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/commands/issuable"
"github.com/spf13/cobra"
issuableReopenCmd "gitlab.com/gitlab-org/cli/commands/issuable/reopen"
)
func NewCmdReopen(f *cmdutils.Factory) *cobra.Command {
return issuableReopenCmd.NewCmdReopen(f, issuable.TypeIssue)
}

View File

@ -36,4 +36,5 @@ glab incident list
- [close](close.md)
- [list](list.md)
- [reopen](reopen.md)
- [view](view.md)

View File

@ -0,0 +1,40 @@
---
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
---
<!--
This documentation is auto generated by a script.
Please do not edit this file directly. Run `make gen-docs` instead.
-->
# `glab incident reopen`
Reopen a resolved incident
```plaintext
glab incident reopen [<id> | <url>] [flags]
```
## Aliases
```plaintext
open
```
## Examples
```plaintext
glab incident reopen 123
glab incident open 123
glab incident reopen https://gitlab.com/NAMESPACE/REPO/-/issues/incident/123
```
## Options inherited from parent commands
```plaintext
--help Show help for command
-R, --repo OWNER/REPO Select another repository using the OWNER/REPO or `GROUP/NAMESPACE/REPO` format or full URL or git URL
```

View File

@ -14,7 +14,7 @@ Please do not edit this file directly. Run `make gen-docs` instead.
Reopen a closed issue
```plaintext
glab issue reopen <id> [flags]
glab issue reopen [<id> | <url>] [flags]
```
## Aliases
@ -28,7 +28,7 @@ open
```plaintext
glab issue reopen 123
glab issue open 123
glab issue reopen https://gitlab.com/profclems/glab/-/issues/123
glab issue reopen https://gitlab.com/NAMESPACE/REPO/-/issues/123
```