Merge branch 'add-incident-close-command' into 'main'

feat(incident): add `incident close` command

Closes #1295

See merge request https://gitlab.com/gitlab-org/cli/-/merge_requests/1219

Merged-by: Jay McCure <jmccure@gitlab.com>
Approved-by: Halil Coban <hcoban@gitlab.com>
Approved-by: Jay McCure <jmccure@gitlab.com>
Reviewed-by: Jay McCure <jmccure@gitlab.com>
Reviewed-by: Halil Coban <hcoban@gitlab.com>
Co-authored-by: Vitali Tatarintev <vtatarintev@gitlab.com>
This commit is contained in:
Jay McCure 2023-04-06 07:20:29 +00:00
commit ae77206daf
10 changed files with 333 additions and 144 deletions

View File

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

View File

@ -4,6 +4,7 @@ import (
"github.com/MakeNowJust/heredoc"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
incidentCloseCmd "gitlab.com/gitlab-org/cli/commands/incident/close"
incidentListCmd "gitlab.com/gitlab-org/cli/commands/incident/list"
incidentViewCmd "gitlab.com/gitlab-org/cli/commands/incident/view"
@ -31,5 +32,6 @@ func NewCmdIncident(f *cmdutils.Factory) *cobra.Command {
incidentCmd.AddCommand(incidentListCmd.NewCmdList(f, nil))
incidentCmd.AddCommand(incidentViewCmd.NewCmdView(f))
incidentCmd.AddCommand(incidentCloseCmd.NewCmdClose(f))
return incidentCmd
}

View File

@ -0,0 +1,84 @@
package close
import (
"fmt"
"github.com/MakeNowJust/heredoc"
"gitlab.com/gitlab-org/cli/api"
"github.com/spf13/cobra"
"github.com/xanzy/go-gitlab"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/commands/issuable"
"gitlab.com/gitlab-org/cli/commands/issue/issueutils"
)
var closingMessage = map[issuable.IssueType]string{
issuable.TypeIssue: "Closing Issue",
issuable.TypeIncident: "Resolving Incident",
}
var closedMessage = map[issuable.IssueType]string{
issuable.TypeIssue: "Closed Issue",
issuable.TypeIncident: "Resolved Incident",
}
func NewCmdClose(f *cmdutils.Factory, issueType issuable.IssueType) *cobra.Command {
examplePath := "issues/123"
aliases := []string{}
if issueType == issuable.TypeIncident {
examplePath = "issues/incident/123"
aliases = []string{"resolve"}
}
issueCloseCmd := &cobra.Command{
Use: "close [<id> | <url>] [flags]",
Short: fmt.Sprintf(`Close an %s`, issueType),
Long: ``,
Aliases: aliases,
Example: heredoc.Doc(fmt.Sprintf(`
glab %[1]s close 123
glab %[1]s close https://gitlab.com/NAMESPACE/REPO/-/%s
`, issueType, examplePath)),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
apiClient, err := f.HttpClient()
if err != nil {
return err
}
issues, repo, err := issueutils.IssuesFromArgs(apiClient, f.BaseRepo, args)
if err != nil {
return err
}
l := &gitlab.UpdateIssueOptions{}
l.StateEvent = gitlab.String("close")
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.")
continue
}
fmt.Fprintf(f.IO.StdOut, "- %s...\n", closingMessage[issueType])
issue, err := api.UpdateIssue(apiClient, repo.FullName(), issue.IID, l)
if err != nil {
return err
}
fmt.Fprintf(f.IO.StdOut, "%s %s #%d\n", c.RedCheck(), closedMessage[issueType], issue.IID)
fmt.Fprintln(f.IO.StdOut, issueutils.DisplayIssue(c, issue, f.IO.IsaTTY))
}
return nil
},
}
return issueCloseCmd
}

View File

@ -0,0 +1,187 @@
package close
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": "opened",
"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":"close"`)
resp, _ := httpmock.NewStringResponse(http.StatusOK, `{
"id": 1,
"iid": 1,
"state": "closed",
"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": "opened",
"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":"close"`)
resp, _ := httpmock.NewStringResponse(http.StatusOK, `{
"id": 2,
"iid": 2,
"state": "closed",
"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 := NewCmdClose(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 TestIssuableClose(t *testing.T) {
tests := []struct {
iid int
name string
issueType issuable.IssueType
wantOutput string
wantErr bool
}{
{
iid: 1,
name: "issue_close",
issueType: issuable.TypeIssue,
wantOutput: heredoc.Doc(`
- Closing Issue...
Closed Issue #1
`),
},
{
iid: 2,
name: "incident_close",
issueType: issuable.TypeIncident,
wantOutput: heredoc.Doc(`
- Resolving Incident...
Resolved Incident #2
`),
},
{
iid: 2,
name: "incident_close_using_issue_command",
issueType: issuable.TypeIssue,
wantOutput: heredoc.Doc(`
- Closing Issue...
Closed Issue #2
`),
},
{
iid: 1,
name: "issue_close_using_incident_command",
issueType: issuable.TypeIncident,
wantOutput: "Incident not found, but an issue with the provided ID exists. Run `glab issue close <id>` to close 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)
} else {
assert.NoErrorf(t, err, "error running command `issue close %d`", tt.iid)
assert.Equal(t, tt.wantOutput, output.String())
assert.Empty(t, output.Stderr())
}
})
}
}

View File

@ -1,55 +1,13 @@
package close
import (
"fmt"
"github.com/MakeNowJust/heredoc"
"gitlab.com/gitlab-org/cli/api"
"github.com/spf13/cobra"
"github.com/xanzy/go-gitlab"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/commands/issue/issueutils"
"gitlab.com/gitlab-org/cli/commands/issuable"
issuableCloseCmd "gitlab.com/gitlab-org/cli/commands/issuable/close"
)
func NewCmdClose(f *cmdutils.Factory) *cobra.Command {
issueCloseCmd := &cobra.Command{
Use: "close <id>",
Short: `Close an issue`,
Long: ``,
Example: heredoc.Doc(`
glab issue close 123
glab issue close https://gitlab.com/profclems/glab/-/issues/123
`),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var err error
c := f.IO.Color()
apiClient, err := f.HttpClient()
if err != nil {
return err
}
issues, repo, err := issueutils.IssuesFromArgs(apiClient, f.BaseRepo, args)
if err != nil {
return err
}
l := &gitlab.UpdateIssueOptions{}
l.StateEvent = gitlab.String("close")
for _, issue := range issues {
fmt.Fprintln(f.IO.StdOut, "- Closing Issue...")
issue, err := api.UpdateIssue(apiClient, repo.FullName(), issue.IID, l)
if err != nil {
return err
}
fmt.Fprintf(f.IO.StdOut, "%s Closed Issue #%d\n", c.RedCheck(), issue.IID)
fmt.Fprintln(f.IO.StdOut, issueutils.DisplayIssue(c, issue, f.IO.IsaTTY))
}
return nil
},
}
return issueCloseCmd
return issuableCloseCmd.NewCmdClose(f, issuable.TypeIssue)
}

View File

@ -1,95 +0,0 @@
package close
import (
"fmt"
"testing"
"time"
"gitlab.com/gitlab-org/cli/test"
"gitlab.com/gitlab-org/cli/pkg/iostreams"
"github.com/acarl005/stripansi"
"github.com/stretchr/testify/require"
"github.com/xanzy/go-gitlab"
"gitlab.com/gitlab-org/cli/api"
"github.com/stretchr/testify/assert"
"gitlab.com/gitlab-org/cli/commands/cmdtest"
)
func Test_issueClose_Integration(t *testing.T) {
glTestHost := test.GetHostOrSkip(t)
t.Parallel()
oldUpdateIssue := api.UpdateIssue
timer, _ := time.Parse(time.RFC3339, "2014-11-12T11:45:26.371Z")
api.UpdateIssue = func(client *gitlab.Client, projectID interface{}, issueID int, opts *gitlab.UpdateIssueOptions) (*gitlab.Issue, error) {
if projectID == "" || projectID == "WRONG_REPO" || projectID == "expected_err" || issueID == 0 {
return nil, fmt.Errorf("error expected")
}
return &gitlab.Issue{
ID: issueID,
IID: issueID,
State: "closed",
Description: "Dummy description for issue " + string(rune(issueID)),
Author: &gitlab.IssueAuthor{
ID: 1,
Name: "John Dev Wick",
Username: "jdwick",
},
CreatedAt: &timer,
}, nil
}
testCases := []struct {
Name string
Issue string
ExpectedMsg []string
wantErr bool
}{
{
Name: "Issue Exists",
Issue: "1",
ExpectedMsg: []string{"Closing Issue...", "Closed Issue #1"},
},
{
Name: "Issue Does Not Exist",
Issue: "0",
ExpectedMsg: []string{"Closing Issue", "404 Not found"},
wantErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
io, _, stdout, stderr := iostreams.Test()
f := cmdtest.StubFactory(glTestHost + "/cli-automated-testing/test")
f.IO = io
f.IO.IsaTTY = true
f.IO.IsErrTTY = true
cmd := NewCmdClose(f)
cmd.SetArgs([]string{tc.Issue})
cmd.SetOut(stdout)
cmd.SetErr(stderr)
_, err := cmd.ExecuteC()
if tc.wantErr {
require.Error(t, err)
return
} else {
require.NoError(t, err)
}
out := stripansi.Strip(stdout.String())
for _, msg := range tc.ExpectedMsg {
assert.Contains(t, out, msg)
}
})
}
api.UpdateIssue = oldUpdateIssue
}

View File

@ -54,7 +54,7 @@ func runCommand(rt http.RoundTripper, cli string) (*test.CmdOut, error) {
}, err
}
func TestMrApprove(t *testing.T) {
func TestMrClose(t *testing.T) {
fakeHTTP := httpmock.New()
defer fakeHTTP.Verify(t)

View File

@ -0,0 +1,39 @@
---
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 close`
Close an incident
```plaintext
glab incident close [<id> | <url>] [flags]
```
## Aliases
```plaintext
resolve
```
## Examples
```plaintext
glab incident close 123
glab incident close 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

@ -34,5 +34,6 @@ glab incident list
## Subcommands
- [close](close.md)
- [list](list.md)
- [view](view.md)

View File

@ -14,14 +14,14 @@ Please do not edit this file directly. Run `make gen-docs` instead.
Close an issue
```plaintext
glab issue close <id> [flags]
glab issue close [<id> | <url>] [flags]
```
## Examples
```plaintext
glab issue close 123
glab issue close https://gitlab.com/profclems/glab/-/issues/123
glab issue close https://gitlab.com/NAMESPACE/REPO/-/issues/123
```