diff --git a/commands/alias/set/alias_set.go b/commands/alias/set/alias_set.go index 9e7dc7b544791100c016e74ba9b54d7ee9732d9d..7f55d899826690bd6f5b35e73692c564eaf5c1eb 100644 --- a/commands/alias/set/alias_set.go +++ b/commands/alias/set/alias_set.go @@ -50,7 +50,7 @@ func NewCmdSet(f *cmdutils.Factory, runF func(*SetOptions) error) *cobra.Command Example: heredoc.Doc(` $ glab alias set mrv 'mr view' $ glab mrv -w 123 - #=> glab mr view -w 123 + # glab mr view -w 123 $ glab alias set createissue 'glab create issue --title "$1"' $ glab createissue "My Issue" --description "Something is broken." @@ -58,7 +58,7 @@ func NewCmdSet(f *cmdutils.Factory, runF func(*SetOptions) error) *cobra.Command $ glab alias set --shell igrep 'glab issue list --assignee="$1" | grep $2' $ glab igrep user foo - #=> glab issue list --assignee="user" | grep "foo" + # glab issue list --assignee="user" | grep "foo" `), Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/commands/ci/ci.go b/commands/ci/ci.go index c6d50ce123702ff8496b513dfa3870a133ce71da..e49470f5e066651e8f2c378f243315511d4b6269 100644 --- a/commands/ci/ci.go +++ b/commands/ci/ci.go @@ -28,7 +28,7 @@ func NewCmdCI(f *cmdutils.Factory) *cobra.Command { cmdutils.EnableRepoOverride(ciCmd, f) ciCmd.AddCommand(legacyCICmd.NewCmdCI(f)) - ciCmd.AddCommand(ciTraceCmd.NewCmdTrace(f, nil)) + ciCmd.AddCommand(ciTraceCmd.NewCmdTrace(f)) ciCmd.AddCommand(ciViewCmd.NewCmdView(f)) ciCmd.AddCommand(ciLintCmd.NewCmdLint(f)) ciCmd.AddCommand(pipeDeleteCmd.NewCmdDelete(f)) diff --git a/commands/ci/ciutils/utils.go b/commands/ci/ciutils/utils.go index 6fff9f3bf17e45083197458a11b0f3a931de9dd2..0a50f780ef3f65211f2cb180a00722b385db128d 100644 --- a/commands/ci/ciutils/utils.go +++ b/commands/ci/ciutils/utils.go @@ -4,15 +4,22 @@ import ( "context" "fmt" "io" + "regexp" + "strconv" "sync" "time" + "gitlab.com/gitlab-org/cli/internal/glrepo" + "gitlab.com/gitlab-org/cli/pkg/git" "gitlab.com/gitlab-org/cli/pkg/iostreams" + "gitlab.com/gitlab-org/cli/pkg/prompt" "gitlab.com/gitlab-org/cli/api" "gitlab.com/gitlab-org/cli/pkg/tableprinter" "gitlab.com/gitlab-org/cli/pkg/utils" + "github.com/AlecAivazis/survey/v2" + "github.com/AlecAivazis/survey/v2/terminal" "github.com/pkg/errors" "github.com/xanzy/go-gitlab" ) @@ -78,17 +85,17 @@ func RunTraceSha(ctx context.Context, apiClient *gitlab.Client, w io.Writer, pid if err != nil || job == nil { return errors.Wrap(err, "failed to find job") } - return RunTrace(ctx, apiClient, w, pid, job, name) + return runTrace(ctx, apiClient, w, pid, job.ID) } -func RunTrace(ctx context.Context, apiClient *gitlab.Client, w io.Writer, pid interface{}, job *gitlab.Job, name string) error { +func runTrace(ctx context.Context, apiClient *gitlab.Client, w io.Writer, pid interface{}, jobId int) error { fmt.Fprintln(w, "Getting job trace...") for range time.NewTicker(time.Second * 3).C { if ctx.Err() == context.Canceled { break } - trace, _, err := apiClient.Jobs.GetTraceFile(pid, job.ID) - if err != nil || trace == nil { + job, _, err := apiClient.Jobs.GetJob(pid, jobId) + if err != nil { return errors.Wrap(err, "failed to find job") } switch job.Status { @@ -102,11 +109,12 @@ func RunTrace(ctx context.Context, apiClient *gitlab.Client, w io.Writer, pid in fmt.Fprintf(w, "%s has been skipped\n", job.Name) } once.Do(func() { - if name == "" { - name = job.Name - } fmt.Fprintf(w, "Showing logs for %s job #%d\n", job.Name, job.ID) }) + trace, _, err := apiClient.Jobs.GetTraceFile(pid, jobId) + if err != nil || trace == nil { + return errors.Wrap(err, "failed to find job") + } _, _ = io.CopyN(io.Discard, trace, offset) lenT, err := io.Copy(w, trace) if err != nil { @@ -122,3 +130,158 @@ func RunTrace(ctx context.Context, apiClient *gitlab.Client, w io.Writer, pid in } return nil } + +func GetJobId(inputs *JobInputs, opts *JobOptions) (int, error) { + // If the user hasn't supplied an argument, we display the jobs list interactively. + if inputs.JobName == "" { + return getJobIdInteractive(inputs, opts) + } + + // If the user supplied a job ID, we can use it directly. + if jobID, err := strconv.Atoi(inputs.JobName); err == nil { + return jobID, nil + } + + // Otherwise, we try to find the latest job ID based on the job name. + pipelineId, err := getPipelineId(inputs, opts) + if err != nil { + return 0, fmt.Errorf("get pipeline: %w", err) + } + + jobs, _, err := opts.ApiClient.Jobs.ListPipelineJobs(opts.Repo.FullName(), pipelineId, nil) + if err != nil { + return 0, fmt.Errorf("list pipeline jobs: %w", err) + } + + for _, job := range jobs { + if job.Name == inputs.JobName { + return job.ID, nil + } + } + + return 0, fmt.Errorf("pipeline %d contains no jobs", pipelineId) +} + +func getPipelineId(inputs *JobInputs, opts *JobOptions) (int, error) { + if inputs.PipelineId != 0 { + return inputs.PipelineId, nil + } + + branch, err := getBranch(inputs.Branch, opts) + if err != nil { + return 0, fmt.Errorf("get branch: %w", err) + } + + pipeline, err := api.GetLastPipeline(opts.ApiClient, opts.Repo.FullName(), branch) + if err != nil { + return 0, fmt.Errorf("get last pipeline: %w", err) + } + return pipeline.ID, err +} + +func getBranch(branch string, opts *JobOptions) (string, error) { + if branch != "" { + return branch, nil + } + + branch, err := git.CurrentBranch() + if err != nil { + return "", err + } + + return branch, nil +} + +func getJobIdInteractive(inputs *JobInputs, opts *JobOptions) (int, error) { + pipelineId, err := getPipelineId(inputs, opts) + if err != nil { + return 0, err + } + + fmt.Fprintf(opts.IO.StdOut, "Getting jobs for pipeline %d...\n\n", pipelineId) + + jobs, err := api.GetPipelineJobs(opts.ApiClient, pipelineId, opts.Repo.FullName()) + if err != nil { + return 0, err + } + + var jobOptions []string + var selectedJob string + + for _, job := range jobs { + jobOptions = append(jobOptions, fmt.Sprintf("%s (%d) - %s", job.Name, job.ID, job.Status)) + } + + promptOpts := &survey.Select{ + Message: "Select pipeline job to trace:", + Options: jobOptions, + } + + err = prompt.AskOne(promptOpts, &selectedJob) + if err != nil { + if errors.Is(err, terminal.InterruptErr) { + return 0, nil + } + + return 0, err + } + + if selectedJob != "" { + re := regexp.MustCompile(`(?s)\((.*)\)`) + m := re.FindAllStringSubmatch(selectedJob, -1) + return utils.StringToInt(m[0][1]), nil + } else if len(jobs) > 0 { + return jobs[0].ID, nil + } else { + pipeline, err := api.GetPipeline(opts.ApiClient, pipelineId, nil, opts.Repo.FullName()) + if err != nil { + return 0, err + } + // use commit statuses to show external jobs + cs, err := api.GetCommitStatuses(opts.ApiClient, opts.Repo.FullName(), pipeline.SHA) + if err != nil { + return 0, nil + } + + c := opts.IO.Color() + + fmt.Fprint(opts.IO.StdOut, "Getting external jobs...") + for _, status := range cs { + var s string + + switch status.Status { + case "success": + s = c.Green(status.Status) + case "error": + s = c.Red(status.Status) + default: + s = c.Gray(status.Status) + } + fmt.Fprintf(opts.IO.StdOut, "(%s) %s\nURL: %s\n\n", s, c.Bold(status.Name), c.Gray(status.TargetURL)) + } + + return 0, nil + } +} + +type JobInputs struct { + JobName string + Branch string + PipelineId int +} + +type JobOptions struct { + ApiClient *gitlab.Client + Repo glrepo.Interface + IO *iostreams.IOStreams +} + +func TraceJob(inputs *JobInputs, opts *JobOptions) error { + jobID, err := GetJobId(inputs, opts) + if err != nil { + fmt.Fprintln(opts.IO.StdErr, "invalid job ID:", inputs.JobName) + return err + } + fmt.Fprintln(opts.IO.StdOut) + return runTrace(context.Background(), opts.ApiClient, opts.IO.StdOut, opts.Repo.FullName(), jobID) +} diff --git a/commands/ci/ciutils/utils_test.go b/commands/ci/ciutils/utils_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c24d46c5846bd9723021958c76349a7f75f6741b --- /dev/null +++ b/commands/ci/ciutils/utils_test.go @@ -0,0 +1,349 @@ +package ciutils + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/cli/commands/cmdtest" + "gitlab.com/gitlab-org/cli/pkg/httpmock" + "gitlab.com/gitlab-org/cli/pkg/iostreams" + "gitlab.com/gitlab-org/cli/pkg/prompt" +) + +func TestGetJobId(t *testing.T) { + type httpMock struct { + method string + path string + status int + body string + } + + tests := []struct { + name string + jobName string + pipelineId int + httpMocks []httpMock + askOneStubs []string + expectedOut int + expectedError string + }{ + { + name: "when getJobId with integer is requested", + jobName: "1122", + expectedOut: 1122, + httpMocks: []httpMock{}, + }, { + name: "when getJobId with name and pipelineId is requested", + jobName: "lint", + pipelineId: 123, + expectedOut: 1122, + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs", + http.StatusOK, + `[{ + "id": 1122, + "name": "lint", + "status": "failed" + }, { + "id": 1124, + "name": "publish", + "status": "failed" + }]`, + }, + }, + }, { + name: "when getJobId with name and pipelineId is requested and listJobs throws error", + jobName: "lint", + pipelineId: 123, + expectedError: "list pipeline jobs: GET https://gitlab.com/api/v4/projects/OWNER/REPO/pipelines/123/jobs: 403 ", + expectedOut: 0, + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs", + http.StatusForbidden, + `{}`, + }, + }, + }, { + name: "when getJobId with name and last pipeline is requested", + jobName: "lint", + pipelineId: 0, + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/repository/commits/main", + http.StatusOK, + `{ + "last_pipeline" : { + "id": 123 + } + }`, + }, + { + http.MethodGet, + "/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs", + http.StatusOK, + `[{ + "id": 1122, + "name": "lint", + "status": "failed" + }, { + "id": 1124, + "name": "publish", + "status": "failed" + }]`, + }, + }, + expectedOut: 1122, + }, { + name: "when getJobId with name and last pipeline is requested and getCommits throws error", + jobName: "lint", + pipelineId: 0, + expectedError: "get pipeline: get last pipeline: GET https://gitlab.com/api/v4/projects/OWNER/REPO/repository/commits/main: 403 ", + expectedOut: 0, + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/repository/commits/main", + http.StatusForbidden, + `{}`, + }, + }, + }, { + name: "when getJobId with name and last pipeline is requested and getJobs throws error", + jobName: "lint", + pipelineId: 0, + expectedError: "list pipeline jobs: GET https://gitlab.com/api/v4/projects/OWNER/REPO/pipelines/123/jobs: 403 ", + expectedOut: 0, + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/repository/commits/main", + http.StatusOK, + `{ + "last_pipeline" : { + "id": 123 + } + }`, + }, + { + http.MethodGet, + "/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs", + http.StatusForbidden, + `{}`, + }, + }, + }, { + name: "when getJobId with pipelineId is requested, ask for job and answer", + jobName: "", + pipelineId: 123, + expectedOut: 1122, + askOneStubs: []string{"lint (1122) - failed"}, + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs?per_page=100", + http.StatusOK, + `[{ + "id": 1122, + "name": "lint", + "status": "failed" + }, { + "id": 1124, + "name": "publish", + "status": "failed" + }]`, + }, + }, + }, { + name: "when getJobId with pipelineId is requested, ask for job and give no answer", + jobName: "", + pipelineId: 123, + expectedOut: 1122, + askOneStubs: []string{""}, + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs?per_page=100", + http.StatusOK, + `[{ + "id": 1122, + "name": "lint", + "status": "failed" + }, { + "id": 1124, + "name": "publish", + "status": "failed" + }]`, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + fakeHTTP := &httpmock.Mocker{ + MatchURL: httpmock.PathAndQuerystring, + } + + if tc.askOneStubs != nil { + as, restoreAsk := prompt.InitAskStubber() + defer restoreAsk() + for _, value := range tc.askOneStubs { + as.StubOne(value) + } + } + + defer fakeHTTP.Verify(t) + + for _, mock := range tc.httpMocks { + fakeHTTP.RegisterResponder(mock.method, mock.path, httpmock.NewStringResponse(mock.status, mock.body)) + } + + ios, _, _, _ := iostreams.Test() + f := cmdtest.InitFactory(ios, fakeHTTP) + + _, _ = f.HttpClient() + + apiClient, _ := f.HttpClient() + repo, _ := f.BaseRepo() + + output, err := GetJobId(&JobInputs{ + JobName: tc.jobName, + PipelineId: tc.pipelineId, + Branch: "main", + }, &JobOptions{ + IO: f.IO, + Repo: repo, + ApiClient: apiClient, + }) + + if tc.expectedError == "" { + require.Nil(t, err) + } else { + require.NotNil(t, err) + require.Equal(t, tc.expectedError, err.Error()) + } + assert.Equal(t, tc.expectedOut, output) + }) + } +} + +func TestTraceJob(t *testing.T) { + type httpMock struct { + method string + path string + status int + body string + } + + tests := []struct { + name string + jobName string + pipelineId int + httpMocks []httpMock + expectedError string + }{ + { + name: "when traceJob is requested", + jobName: "1122", + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/jobs/1122/trace", + http.StatusOK, + `Lorem ipsum`, + }, + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/jobs/1122", + http.StatusOK, + `{ + "id": 1122, + "name": "lint", + "status": "success" + }`, + }, + }, + }, + { + name: "when traceJob is requested and getJob throws error", + jobName: "1122", + expectedError: "failed to find job: GET https://gitlab.com/api/v4/projects/OWNER/REPO/jobs/1122: 403 ", + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/jobs/1122", + http.StatusForbidden, + `{}`, + }, + }, + }, + { + name: "when traceJob is requested and getJob throws error", + jobName: "1122", + expectedError: "failed to find job: GET https://gitlab.com/api/v4/projects/OWNER/REPO/jobs/1122/trace: 403 ", + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/jobs/1122/trace", + http.StatusForbidden, + `{}`, + }, + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/jobs/1122", + http.StatusOK, + `{ + "id": 1122, + "name": "lint", + "status": "success" + }`, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + fakeHTTP := &httpmock.Mocker{ + MatchURL: httpmock.PathAndQuerystring, + } + defer fakeHTTP.Verify(t) + + for _, mock := range tc.httpMocks { + fakeHTTP.RegisterResponder(mock.method, mock.path, httpmock.NewStringResponse(mock.status, mock.body)) + } + + ios, _, _, _ := iostreams.Test() + f := cmdtest.InitFactory(ios, fakeHTTP) + + _, _ = f.HttpClient() + + apiClient, _ := f.HttpClient() + repo, _ := f.BaseRepo() + + err := TraceJob(&JobInputs{ + JobName: tc.jobName, + PipelineId: tc.pipelineId, + Branch: "main", + }, &JobOptions{ + IO: f.IO, + Repo: repo, + ApiClient: apiClient, + }) + + if tc.expectedError == "" { + require.Nil(t, err) + } else { + require.NotNil(t, err) + require.Equal(t, tc.expectedError, err.Error()) + } + }) + } +} diff --git a/commands/ci/legacyci/pipeline_ci.go b/commands/ci/legacyci/pipeline_ci.go index 2e7981e6725c7dd28de09b02fcc6caed947942a8..87c4de0046f90974ab6079088c6a4a2bcc8d1c99 100644 --- a/commands/ci/legacyci/pipeline_ci.go +++ b/commands/ci/legacyci/pipeline_ci.go @@ -22,7 +22,7 @@ func NewCmdCI(f *cmdutils.Factory) *cobra.Command { `), } - pipelineCICmd.AddCommand(ciTraceCmd.NewCmdTrace(f, nil)) + pipelineCICmd.AddCommand(ciTraceCmd.NewCmdTrace(f)) pipelineCICmd.AddCommand(ciViewCmd.NewCmdView(f)) pipelineCICmd.AddCommand(ciLintCmd.NewCmdLint(f)) pipelineCICmd.Deprecated = "This command is deprecated. All the commands under it has been moved to `ci` or `pipeline` command. See https://gitlab.com/gitlab-org/cli/issues/372 for more info." diff --git a/commands/ci/lint/lint.go b/commands/ci/lint/lint.go index 719eb8ebd0e566e2ea815483dc8fc1ebdb0c8c80..6c74729cc2a440b1cf3ba0179ccb03c4c17ca335 100644 --- a/commands/ci/lint/lint.go +++ b/commands/ci/lint/lint.go @@ -22,7 +22,7 @@ func NewCmdLint(f *cmdutils.Factory) *cobra.Command { Args: cobra.MaximumNArgs(1), Example: heredoc.Doc(` $ glab ci lint - #=> Uses .gitlab-ci.yml in the current directory + # Uses .gitlab-ci.yml in the current directory $ glab ci lint .gitlab-ci.yml diff --git a/commands/ci/retry/retry.go b/commands/ci/retry/retry.go index 2da83ac2d8a8bbce07c2a6e9f6197aff231ce9a9..d383c50beb9bcaad7fcc270ce037accb2c3953ec 100644 --- a/commands/ci/retry/retry.go +++ b/commands/ci/retry/retry.go @@ -4,8 +4,8 @@ import ( "fmt" "gitlab.com/gitlab-org/cli/api" + "gitlab.com/gitlab-org/cli/commands/ci/ciutils" "gitlab.com/gitlab-org/cli/commands/cmdutils" - "gitlab.com/gitlab-org/cli/pkg/utils" "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" @@ -17,39 +17,60 @@ func NewCmdRetry(f *cmdutils.Factory) *cobra.Command { Short: `Retry a CI/CD job`, Aliases: []string{}, Example: heredoc.Doc(` - glab ci retry 871528 + $ glab ci retry + # Interactively select a job to retry + + $ glab ci retry 224356863 + # Retry job with ID 224356863 + + $ glab ci retry lint + # Retry job with the name 'lint' `), Long: ``, - Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { var err error - apiClient, err := f.HttpClient() + repo, err := f.BaseRepo() if err != nil { return err } - - repo, err := f.BaseRepo() + apiClient, err := f.HttpClient() if err != nil { return err } - jobID := utils.StringToInt(args[0]) + jobName := "" + if len(args) != 0 { + jobName = args[0] + } + branch, _ := cmd.Flags().GetString("branch") + pipelineId, _ := cmd.Flags().GetInt("pipeline-id") - if jobID < 1 { - fmt.Fprintln(f.IO.StdErr, "invalid job id:", args[0]) - return cmdutils.SilentError + jobID, err := ciutils.GetJobId(&ciutils.JobInputs{ + JobName: jobName, + Branch: branch, + PipelineId: pipelineId, + }, &ciutils.JobOptions{ + ApiClient: apiClient, + IO: f.IO, + Repo: repo, + }) + if err != nil { + fmt.Fprintln(f.IO.StdErr, "invalid job ID:", args[0]) + return err } job, err := api.RetryPipelineJob(apiClient, jobID, repo.FullName()) if err != nil { return cmdutils.WrapError(err, fmt.Sprintf("Could not retry job with ID: %d", jobID)) } - fmt.Fprintln(f.IO.StdOut, "Retried job (id:", job.ID, "), status:", job.Status, ", ref:", job.Ref, ", weburl: ", job.WebURL, ")") + fmt.Fprintln(f.IO.StdOut, "Retried job (ID:", job.ID, "), status:", job.Status, ", ref:", job.Ref, ", weburl: ", job.WebURL, ")") return nil }, } + pipelineRetryCmd.Flags().StringP("branch", "b", "", "The branch to search for the job. (Default: current branch)") + pipelineRetryCmd.Flags().IntP("pipeline-id", "p", 0, "The pipeline ID to search for the job.") return pipelineRetryCmd } diff --git a/commands/ci/retry/retry_test.go b/commands/ci/retry/retry_test.go index 3c464e885223498d3cfc156aefc075c3b8a3a3a1..71c9d6226770febee2d91e82bc9fa9cabe8c2b61 100644 --- a/commands/ci/retry/retry_test.go +++ b/commands/ci/retry/retry_test.go @@ -4,17 +4,16 @@ import ( "net/http" "testing" - "gitlab.com/gitlab-org/cli/pkg/iostreams" - - "github.com/MakeNowJust/heredoc" + "gitlab.com/gitlab-org/cli/commands/cmdtest" "github.com/stretchr/testify/assert" - "gitlab.com/gitlab-org/cli/commands/cmdtest" + "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/cli/pkg/httpmock" + "gitlab.com/gitlab-org/cli/pkg/iostreams" "gitlab.com/gitlab-org/cli/test" ) -func runCommand(rt http.RoundTripper, cli string) (*test.CmdOut, error) { +func runCommand(rt http.RoundTripper, args string) (*test.CmdOut, error) { ios, _, stdout, stderr := iostreams.Test() factory := cmdtest.InitFactory(ios, rt) @@ -22,40 +21,191 @@ func runCommand(rt http.RoundTripper, cli string) (*test.CmdOut, error) { cmd := NewCmdRetry(factory) - return cmdtest.ExecuteCommand(cmd, cli, stdout, stderr) + return cmdtest.ExecuteCommand(cmd, args, stdout, stderr) } func TestCiRetry(t *testing.T) { - fakeHTTP := httpmock.New() - defer fakeHTTP.Verify(t) + type httpMock struct { + method string + path string + status int + body string + } - // test will fail with unmatched HTTP stub if this POST is not performed - fakeHTTP.RegisterResponder(http.MethodPost, "/projects/OWNER/REPO/jobs/1122/retry", - httpmock.NewStringResponse(http.StatusCreated, ` + tests := []struct { + name string + args string + httpMocks []httpMock + expectedError string + expectedStderr string + expectedOut string + }{ + { + name: "when retry with job-id", + args: "1122", + expectedOut: "Retried job (ID: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 )\n", + httpMocks: []httpMock{ + { + http.MethodPost, + "/api/v4/projects/OWNER/REPO/jobs/1122/retry", + http.StatusCreated, + `{ + "id": 1123, + "status": "pending", + "stage": "build", + "name": "build-job", + "ref": "branch-name", + "tag": false, + "coverage": null, + "allow_failure": false, + "created_at": "2022-12-01T05:13:13.703Z", + "web_url": "https://gitlab.com/OWNER/REPO/-/jobs/1123" + }`, + }, + }, + }, { - "id": 1123, - "status": "pending", - "stage": "build", - "name": "build-job", - "ref": "branch-name", - "tag": false, - "coverage": null, - "allow_failure": false, - "created_at": "2022-12-01T05:13:13.703Z", - "web_url": "https://gitlab.com/OWNER/REPO/-/jobs/1123" - } - `)) - - jobId := "1122" - output, err := runCommand(fakeHTTP, jobId) - if err != nil { - t.Errorf("error running command `ci retry %s`: %v", jobId, err) + name: "when retry with job-id throws error", + args: "1122", + expectedError: "POST https://gitlab.com/api/v4/projects/OWNER/REPO/jobs/1122/retry: 403 ", + expectedOut: "", + httpMocks: []httpMock{ + { + http.MethodPost, + "/api/v4/projects/OWNER/REPO/jobs/1122/retry", + http.StatusForbidden, + `{}`, + }, + }, + }, + { + name: "when retry with job-name", + args: "lint -b main -p 123", + expectedOut: "Retried job (ID: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 )\n", + httpMocks: []httpMock{ + { + http.MethodPost, + "/api/v4/projects/OWNER/REPO/jobs/1122/retry", + http.StatusCreated, + `{ + "id": 1123, + "status": "pending", + "stage": "build", + "name": "build-job", + "ref": "branch-name", + "tag": false, + "coverage": null, + "allow_failure": false, + "created_at": "2022-12-01T05:13:13.703Z", + "web_url": "https://gitlab.com/OWNER/REPO/-/jobs/1123" + }`, + }, + { + http.MethodGet, + "/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs", + http.StatusOK, + `[{ + "id": 1122, + "name": "lint", + "status": "failed" + }, { + "id": 1124, + "name": "publish", + "status": "failed" + }]`, + }, + }, + }, + { + name: "when retry with job-name throws error", + args: "lint -b main -p 123", + expectedError: "list pipeline jobs: GET https://gitlab.com/api/v4/projects/OWNER/REPO/pipelines/123/jobs: 403 ", + expectedStderr: "invalid job ID: lint\n", + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs", + http.StatusForbidden, + `{}`, + }, + }, + }, + { + name: "when retry with job-name and last pipeline", + args: "lint -b main", + expectedOut: "Retried job (ID: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 )\n", + httpMocks: []httpMock{ + { + http.MethodPost, + "/api/v4/projects/OWNER/REPO/jobs/1122/retry", + http.StatusCreated, + `{ + "id": 1123, + "status": "pending", + "stage": "build", + "name": "build-job", + "ref": "branch-name", + "tag": false, + "coverage": null, + "allow_failure": false, + "created_at": "2022-12-01T05:13:13.703Z", + "web_url": "https://gitlab.com/OWNER/REPO/-/jobs/1123" + }`, + }, + { + http.MethodGet, + "/api/v4/projects/OWNER%2FREPO/repository/commits/main", + http.StatusOK, + `{ + "last_pipeline" : { + "id": 123 + } + }`, + }, + { + http.MethodGet, + "/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs", + http.StatusOK, + `[{ + "id": 1122, + "name": "lint", + "status": "failed" + }, { + "id": 1124, + "name": "publish", + "status": "failed" + }]`, + }, + }, + }, } - out := output.String() + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + fakeHTTP := &httpmock.Mocker{ + MatchURL: httpmock.PathAndQuerystring, + } + defer fakeHTTP.Verify(t) + + for _, mock := range tc.httpMocks { + fakeHTTP.RegisterResponder(mock.method, mock.path, httpmock.NewStringResponse(mock.status, mock.body)) + } - assert.Equal(t, heredoc.Doc(` - Retried job (id: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 ) -`), out) - assert.Empty(t, output.Stderr()) + output, err := runCommand(fakeHTTP, tc.args) + + if tc.expectedError == "" { + require.Nil(t, err) + } else { + require.NotNil(t, err) + require.Equal(t, tc.expectedError, err.Error()) + } + + assert.Equal(t, tc.expectedOut, output.String()) + if tc.expectedStderr != "" { + assert.Equal(t, tc.expectedStderr, output.Stderr()) + } else { + assert.Empty(t, output.Stderr()) + } + }) + } } diff --git a/commands/ci/status/status.go b/commands/ci/status/status.go index 623df22764c8fa34fb2c475a95c0ef82229d3d8b..c053485bd5f4b28eb834d3cf75c9642fe8b17a45 100644 --- a/commands/ci/status/status.go +++ b/commands/ci/status/status.go @@ -5,7 +5,7 @@ import ( "time" "gitlab.com/gitlab-org/cli/api" - ciTraceCmd "gitlab.com/gitlab-org/cli/commands/ci/trace" + "gitlab.com/gitlab-org/cli/commands/ci/ciutils" "gitlab.com/gitlab-org/cli/commands/cmdutils" "gitlab.com/gitlab-org/cli/pkg/git" "gitlab.com/gitlab-org/cli/pkg/utils" @@ -144,16 +144,13 @@ func NewCmdStatus(f *cmdutils.Factory) *cobra.Command { isRunning = false } } - if retry == "View Logs" { - // ToDo: bad idea to call another sub-command. should be fixed to avoid cyclo imports - // and the a shared function placed in the ciutils sub-module - return ciTraceCmd.TraceRun(&ciTraceCmd.TraceOpts{ - Branch: branch, - JobID: 0, - BaseRepo: f.BaseRepo, - HTTPClient: f.HttpClient, - IO: f.IO, + return ciutils.TraceJob(&ciutils.JobInputs{ + Branch: branch, + }, &ciutils.JobOptions{ + Repo: repo, + ApiClient: apiClient, + IO: f.IO, }) } } diff --git a/commands/ci/trace/trace.go b/commands/ci/trace/trace.go index 563365aa807cdd822aa96a0f89e055976eeba8a3..8139c9f2477d22dc62758d1470162e91dc2ad15d 100644 --- a/commands/ci/trace/trace.go +++ b/commands/ci/trace/trace.go @@ -1,174 +1,57 @@ package trace import ( - "context" - "errors" - "fmt" - "regexp" - - "gitlab.com/gitlab-org/cli/pkg/iostreams" - - "gitlab.com/gitlab-org/cli/internal/glrepo" - "gitlab.com/gitlab-org/cli/pkg/prompt" - - "gitlab.com/gitlab-org/cli/api" "gitlab.com/gitlab-org/cli/commands/ci/ciutils" "gitlab.com/gitlab-org/cli/commands/cmdutils" - "gitlab.com/gitlab-org/cli/pkg/git" - "gitlab.com/gitlab-org/cli/pkg/utils" - "github.com/AlecAivazis/survey/v2" - "github.com/AlecAivazis/survey/v2/terminal" "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" - "github.com/xanzy/go-gitlab" ) -type TraceOpts struct { - Branch string - JobID int - - BaseRepo func() (glrepo.Interface, error) - HTTPClient func() (*gitlab.Client, error) - IO *iostreams.IOStreams -} - -func NewCmdTrace(f *cmdutils.Factory, runE func(traceOpts *TraceOpts) error) *cobra.Command { - opts := &TraceOpts{ - IO: f.IO, - } +func NewCmdTrace(f *cmdutils.Factory) *cobra.Command { pipelineCITraceCmd := &cobra.Command{ Use: "trace [] [flags]", Short: `Trace a CI/CD job log in real time`, Example: heredoc.Doc(` $ glab ci trace - #=> interactively select a job to trace + # Interactively select a job to trace $ glab ci trace 224356863 - #=> trace job with id 224356863 + # Trace job with ID 224356863 + + $ glab ci trace lint + # Trace job with the name 'lint' `), RunE: func(cmd *cobra.Command, args []string) error { var err error - - // support `-R, --repo` override - // - // NOTE: it is important to assign the BaseRepo and HTTPClient in RunE because - // they are overridden in a PersistentRun hook (when `-R, --repo` is specified) - // which runs before RunE is executed - opts.BaseRepo = f.BaseRepo - opts.HTTPClient = f.HttpClient - - if len(args) != 0 { - opts.JobID = utils.StringToInt(args[0]) + repo, err := f.BaseRepo() + if err != nil { + return err } - if opts.Branch == "" { - opts.Branch, err = git.CurrentBranch() - if err != nil { - return err - } + apiClient, err := f.HttpClient() + if err != nil { + return err } - if runE != nil { - return runE(opts) + jobName := "" + if len(args) != 0 { + jobName = args[0] } - return TraceRun(opts) + branch, _ := cmd.Flags().GetString("branch") + pipelineId, _ := cmd.Flags().GetInt("pipeline-id") + + return ciutils.TraceJob(&ciutils.JobInputs{ + JobName: jobName, + Branch: branch, + PipelineId: pipelineId, + }, &ciutils.JobOptions{ + ApiClient: apiClient, + IO: f.IO, + Repo: repo, + }) }, } - pipelineCITraceCmd.Flags().StringVarP(&opts.Branch, "branch", "b", "", "Check pipeline status for a branch. (Default is the current branch)") + pipelineCITraceCmd.Flags().StringP("branch", "b", "", "The branch to search for the job. (Default: current branch)") + pipelineCITraceCmd.Flags().IntP("pipeline-id", "p", 0, "The pipeline ID to search for the job.") return pipelineCITraceCmd } - -func TraceRun(opts *TraceOpts) error { - apiClient, err := opts.HTTPClient() - if err != nil { - return err - } - - repo, err := opts.BaseRepo() - if err != nil { - return err - } - - if opts.JobID < 1 { - fmt.Fprintf(opts.IO.StdOut, "\nSearching for latest pipeline on %s...\n", opts.Branch) - - pipeline, err := api.GetLastPipeline(apiClient, repo.FullName(), opts.Branch) - if err != nil { - return err - } - - fmt.Fprintf(opts.IO.StdOut, "Getting jobs for pipeline %d...\n\n", pipeline.ID) - - jobs, err := api.GetPipelineJobs(apiClient, pipeline.ID, repo.FullName()) - if err != nil { - return err - } - - var jobOptions []string - var selectedJob string - - for _, job := range jobs { - jobOptions = append(jobOptions, fmt.Sprintf("%s (%d) - %s", job.Name, job.ID, job.Status)) - } - - promptOpts := &survey.Select{ - Message: "Select pipeline job to trace:", - Options: jobOptions, - } - - err = prompt.AskOne(promptOpts, &selectedJob) - if err != nil { - if errors.Is(err, terminal.InterruptErr) { - return nil - } - - return err - } - - if selectedJob != "" { - re := regexp.MustCompile(`(?s)\((.*)\)`) - m := re.FindAllStringSubmatch(selectedJob, -1) - opts.JobID = utils.StringToInt(m[0][1]) - } else if len(jobs) > 0 { - opts.JobID = jobs[0].ID - } else { - // use commit statuses to show external jobs - cs, err := api.GetCommitStatuses(apiClient, repo.FullName(), pipeline.SHA) - if err != nil { - return nil - } - - c := opts.IO.Color() - - fmt.Fprint(opts.IO.StdOut, "Getting external jobs...") - for _, status := range cs { - var s string - - switch status.Status { - case "success": - s = c.Green(status.Status) - case "error": - s = c.Red(status.Status) - default: - s = c.Gray(status.Status) - } - fmt.Fprintf(opts.IO.StdOut, "(%s) %s\nURL: %s\n\n", s, c.Bold(status.Name), c.Gray(status.TargetURL)) - } - - return nil - } - } - - job, err := api.GetPipelineJob(apiClient, opts.JobID, repo.FullName()) - if err != nil { - return err - } - fmt.Fprintln(opts.IO.StdOut) - - err = ciutils.RunTrace(context.Background(), apiClient, opts.IO.StdOut, repo.FullName(), job, job.Name) - if err != nil { - return err - } - - return nil -} diff --git a/commands/ci/trace/trace_integration_test.go b/commands/ci/trace/trace_integration_test.go deleted file mode 100644 index 8b3318a186a61121827094f671c83b109c1ed693..0000000000000000000000000000000000000000 --- a/commands/ci/trace/trace_integration_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package trace - -import ( - "bytes" - "testing" - - "gitlab.com/gitlab-org/cli/test" - - "gitlab.com/gitlab-org/cli/pkg/iostreams" - - "github.com/google/shlex" - "github.com/stretchr/testify/assert" - "gitlab.com/gitlab-org/cli/pkg/prompt" - - "github.com/spf13/cobra" - "gitlab.com/gitlab-org/cli/commands/cmdutils" - "gitlab.com/gitlab-org/cli/internal/config" - - "gitlab.com/gitlab-org/cli/commands/cmdtest" -) - -var ( - stubFactory *cmdutils.Factory - cmd *cobra.Command - stdout *bytes.Buffer -) - -func TestMain(m *testing.M) { - cmdtest.InitTest(m, "ci_trace_test") -} - -func TestNewCmdTrace_Integration(t *testing.T) { - glTestHost := test.GetHostOrSkip(t) - - defer config.StubConfig(`--- -git_protocol: https -hosts: - gitlab.com: - username: root -`, "")() - - var io *iostreams.IOStreams - io, _, stdout, _ = iostreams.Test() - stubFactory, _ = cmdtest.StubFactoryWithConfig(glTestHost + "/cli-automated-testing/test.git") - stubFactory.IO = io - stubFactory.IO.IsaTTY = true - stubFactory.IO.IsErrTTY = true - - tests := []struct { - name string - args string - wantOpts *TraceOpts - }{ - { - name: "Has no arg", - args: ``, - wantOpts: &TraceOpts{ - Branch: "master", - JobID: 0, - }, - }, - { - name: "Has arg with job-id", - args: `3509632552`, - wantOpts: &TraceOpts{ - Branch: "master", - JobID: 3509632552, - }, - }, - { - name: "On a specified repo with job ID", - args: "3509632552 -X cli-automated-testing/test", - wantOpts: &TraceOpts{ - Branch: "master", - JobID: 3509632552, - }, - }, - } - - var actualOpts *TraceOpts - cmd = NewCmdTrace(stubFactory, func(opts *TraceOpts) error { - actualOpts = opts - return nil - }) - cmd.Flags().StringP("repo", "X", "", "") - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.wantOpts.IO = stubFactory.IO - - argv, err := shlex.Split(tt.args) - if err != nil { - t.Fatal(err) - } - cmd.SetArgs(argv) - _, err = cmd.ExecuteC() - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, tt.wantOpts.JobID, actualOpts.JobID) - assert.Equal(t, tt.wantOpts.Branch, actualOpts.Branch) - assert.Equal(t, tt.wantOpts.Branch, actualOpts.Branch) - assert.Equal(t, tt.wantOpts.IO, actualOpts.IO) - }) - } -} - -func TestTraceRun(t *testing.T) { - glTestHost := test.GetHostOrSkip(t) - - var io *iostreams.IOStreams - io, _, stdout, _ = iostreams.Test() - stubFactory = cmdtest.StubFactory(glTestHost + "/cli-automated-testing/test.git") - stubFactory.IO = io - stubFactory.IO.IsaTTY = true - stubFactory.IO.IsErrTTY = true - - tests := []struct { - desc string - args string - assertContains func(t *testing.T, out string) - }{ - { - desc: "Has no arg", - args: ``, - assertContains: func(t *testing.T, out string) { - assert.Contains(t, out, "Getting job trace...") - assert.Contains(t, out, "Showing logs for ") - assert.Contains(t, out, `Preparing the "docker+machine"`) - assert.Contains(t, out, `$ echo "This is a after script section"`) - assert.Contains(t, out, "Job succeeded") - }, - }, - { - desc: "Has arg with job-id", - args: `3509632552`, - assertContains: func(t *testing.T, out string) { - assert.Contains(t, out, "Getting job trace...\n") - }, - }, - { - desc: "On a specified repo with job ID", - args: "3509632552 -X cli-automated-testing/test", - assertContains: func(t *testing.T, out string) { - assert.Contains(t, out, "Getting job trace...\n") - }, - }, - } - - cmd = NewCmdTrace(stubFactory, nil) - cmd.Flags().StringP("repo", "X", "", "") - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - if tt.args == "" { - as, teardown := prompt.InitAskStubber() - defer teardown() - - as.StubOne("cleanup4 (3509632552) - success") - } - argv, err := shlex.Split(tt.args) - if err != nil { - t.Fatal(err) - } - cmd.SetArgs(argv) - _, err = cmd.ExecuteC() - if err != nil { - t.Fatal(err) - } - tt.assertContains(t, stdout.String()) - }) - } -} diff --git a/commands/ci/trace/trace_test.go b/commands/ci/trace/trace_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a4345e858b1a6bab441d1cbee711b34e2c1b26f8 --- /dev/null +++ b/commands/ci/trace/trace_test.go @@ -0,0 +1,213 @@ +package trace + +import ( + "net/http" + "testing" + + "gitlab.com/gitlab-org/cli/commands/cmdtest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/cli/pkg/httpmock" + "gitlab.com/gitlab-org/cli/pkg/iostreams" + "gitlab.com/gitlab-org/cli/test" +) + +func runCommand(rt http.RoundTripper, args string) (*test.CmdOut, error) { + ios, _, stdout, stderr := iostreams.Test() + factory := cmdtest.InitFactory(ios, rt) + + _, _ = factory.HttpClient() + + cmd := NewCmdTrace(factory) + + return cmdtest.ExecuteCommand(cmd, args, stdout, stderr) +} + +func TestCiTrace(t *testing.T) { + type httpMock struct { + method string + path string + status int + body string + } + + tests := []struct { + name string + args string + httpMocks []httpMock + expectedOut string + expectedError string + }{ + { + name: "when trace for job-id is requested", + args: "1122", + expectedOut: "\nGetting job trace...\nShowing logs for lint job #1122\nLorem ipsum", + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/jobs/1122/trace", + http.StatusOK, + `Lorem ipsum`, + }, + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/jobs/1122", + http.StatusOK, + `{ + "id": 1122, + "name": "lint", + "status": "success" + }`, + }, + }, + }, + { + name: "when trace for job-id is requested and getTrace throws error", + args: "1122", + expectedError: "failed to find job: GET https://gitlab.com/api/v4/projects/OWNER/REPO/jobs/1122/trace: 403 ", + expectedOut: "\nGetting job trace...\n", + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/jobs/1122", + http.StatusOK, + `{ + "id": 1122, + "name": "lint", + "status": "success" + }`, + }, + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/jobs/1122/trace", + http.StatusForbidden, + `{}`, + }, + }, + }, + { + name: "when trace for job-id is requested and getJob throws error", + args: "1122", + expectedError: "failed to find job: GET https://gitlab.com/api/v4/projects/OWNER/REPO/jobs/1122: 403 ", + expectedOut: "\nGetting job trace...\n", + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/jobs/1122", + http.StatusForbidden, + `{}`, + }, + }, + }, + { + name: "when trace for job-name is requested", + args: "lint -b main -p 123", + expectedOut: "\nGetting job trace...\n", + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/jobs/1122/trace", + http.StatusOK, + `Lorem ipsum`, + }, + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/jobs/1122", + http.StatusOK, + `{ + "id": 1122, + "name": "lint", + "status": "success" + }`, + }, + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/pipelines/123/jobs", + http.StatusOK, + `[{ + "id": 1122, + "name": "lint", + "status": "failed" + }, { + "id": 1124, + "name": "publish", + "status": "failed" + }]`, + }, + }, + }, + { + name: "when trace for job-name and last pipeline is requested", + args: "lint -b main", + expectedOut: "\nGetting job trace...\n", + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/jobs/1122/trace", + http.StatusOK, + `Lorem ipsum`, + }, + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/jobs/1122", + http.StatusOK, + `{ + "id": 1122, + "name": "lint", + "status": "success" + }`, + }, + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/repository/commits/main", + http.StatusOK, + `{ + "last_pipeline" : { + "id": 123 + } + }`, + }, + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/pipelines/123/jobs", + http.StatusOK, + `[{ + "id": 1122, + "name": "lint", + "status": "failed" + }, { + "id": 1124, + "name": "publish", + "status": "failed" + }]`, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + fakeHTTP := &httpmock.Mocker{ + MatchURL: httpmock.PathAndQuerystring, + } + defer fakeHTTP.Verify(t) + + for _, mock := range tc.httpMocks { + fakeHTTP.RegisterResponder(mock.method, mock.path, httpmock.NewStringResponse(mock.status, mock.body)) + } + + output, err := runCommand(fakeHTTP, tc.args) + + if tc.expectedError == "" { + require.Nil(t, err) + } else { + require.NotNil(t, err) + require.Equal(t, tc.expectedError, err.Error()) + } + + assert.Equal(t, tc.expectedOut, output.String()) + assert.Empty(t, output.Stderr()) + }) + } +} diff --git a/commands/project/contributors/repo_contributors.go b/commands/project/contributors/repo_contributors.go index d3a7e8fb236f7d8a791f9b7a8651a50665acf1b5..ca16f070d4c667402a8b140c4aa6234b4c1c25d0 100644 --- a/commands/project/contributors/repo_contributors.go +++ b/commands/project/contributors/repo_contributors.go @@ -36,7 +36,7 @@ func NewCmdContributors(f *cmdutils.Factory) *cobra.Command { $ glab repo contributors $ glab repo contributors -R gitlab-com/www-gitlab-com - #=> Supports repo override + # Supports repo override `), Args: cobra.ExactArgs(0), Aliases: []string{"users"}, diff --git a/docs/source/alias/set.md b/docs/source/alias/set.md index 6b2e2848c994fd080c4fc78af72b028bf1db7459..7fa387b5249947e693a1279825acd15618adc9ce 100644 --- a/docs/source/alias/set.md +++ b/docs/source/alias/set.md @@ -39,7 +39,7 @@ glab alias set '' [flags] ```plaintext $ glab alias set mrv 'mr view' $ glab mrv -w 123 -#=> glab mr view -w 123 +# glab mr view -w 123 $ glab alias set createissue 'glab create issue --title "$1"' $ glab createissue "My Issue" --description "Something is broken." @@ -47,7 +47,7 @@ $ glab createissue "My Issue" --description "Something is broken." $ glab alias set --shell igrep 'glab issue list --assignee="$1" | grep $2' $ glab igrep user foo -#=> glab issue list --assignee="user" | grep "foo" +# glab issue list --assignee="user" | grep "foo" ``` diff --git a/docs/source/ci/ci/lint.md b/docs/source/ci/ci/lint.md index 6695247e056930f2020c1623b4a1a8f80b51dad8..5081ce5d2afcb26093979f442738b4c3467d7671 100644 --- a/docs/source/ci/ci/lint.md +++ b/docs/source/ci/ci/lint.md @@ -21,7 +21,7 @@ glab ci ci lint [flags] ```plaintext $ glab ci lint -#=> Uses .gitlab-ci.yml in the current directory +# Uses .gitlab-ci.yml in the current directory $ glab ci lint .gitlab-ci.yml diff --git a/docs/source/ci/ci/trace.md b/docs/source/ci/ci/trace.md index b7da375b1836704c41eca4e30e15a0d397176dc7..f569f888966f39219fa876d3ed7ef628f8dfd368 100644 --- a/docs/source/ci/ci/trace.md +++ b/docs/source/ci/ci/trace.md @@ -21,17 +21,21 @@ glab ci ci trace [] [flags] ```plaintext $ glab ci trace -#=> interactively select a job to trace +# Interactively select a job to trace $ glab ci trace 224356863 -#=> trace job with id 224356863 +# Trace job with ID 224356863 + +$ glab ci trace lint +# Trace job with the name 'lint' ``` ## Options ```plaintext - -b, --branch string Check pipeline status for a branch. (Default is the current branch) + -b, --branch string The branch to search for the job. (Default: current branch) + -p, --pipeline-id int The pipeline ID to search for the job. ``` ## Options inherited from parent commands diff --git a/docs/source/ci/lint.md b/docs/source/ci/lint.md index cc74d6619f93556abaaf918c88154cd843467563..15cc9a69fb94bbf51cf38e674d0562882080cc52 100644 --- a/docs/source/ci/lint.md +++ b/docs/source/ci/lint.md @@ -21,7 +21,7 @@ glab ci lint [flags] ```plaintext $ glab ci lint -#=> Uses .gitlab-ci.yml in the current directory +# Uses .gitlab-ci.yml in the current directory $ glab ci lint .gitlab-ci.yml diff --git a/docs/source/ci/retry.md b/docs/source/ci/retry.md index 44541a3f50dd1edd932d65e949295a8338bd6a1d..cd98b66ca327187ffadd72c380b991ecb7800496 100644 --- a/docs/source/ci/retry.md +++ b/docs/source/ci/retry.md @@ -20,8 +20,22 @@ glab ci retry [flags] ## Examples ```plaintext -glab ci retry 871528 +$ glab ci retry +# Interactively select a job to retry +$ glab ci retry 224356863 +# Retry job with ID 224356863 + +$ glab ci retry lint +# Retry job with the name 'lint' + +``` + +## Options + +```plaintext + -b, --branch string The branch to search for the job. (Default: current branch) + -p, --pipeline-id int The pipeline ID to search for the job. ``` ## Options inherited from parent commands diff --git a/docs/source/ci/trace.md b/docs/source/ci/trace.md index b92b44672b85f63c31be0bef670ec386b6262896..29fd68a6daac007a57bba6ee9f234aeb011578fe 100644 --- a/docs/source/ci/trace.md +++ b/docs/source/ci/trace.md @@ -21,17 +21,21 @@ glab ci trace [] [flags] ```plaintext $ glab ci trace -#=> interactively select a job to trace +# Interactively select a job to trace $ glab ci trace 224356863 -#=> trace job with id 224356863 +# Trace job with ID 224356863 + +$ glab ci trace lint +# Trace job with the name 'lint' ``` ## Options ```plaintext - -b, --branch string Check pipeline status for a branch. (Default is the current branch) + -b, --branch string The branch to search for the job. (Default: current branch) + -p, --pipeline-id int The pipeline ID to search for the job. ``` ## Options inherited from parent commands diff --git a/docs/source/repo/contributors.md b/docs/source/repo/contributors.md index ead22dde51f581735d58407be80b5c75e63de209..19aba7d90efd1165d217bddd6c7a1395c61f479f 100644 --- a/docs/source/repo/contributors.md +++ b/docs/source/repo/contributors.md @@ -29,7 +29,7 @@ users $ glab repo contributors $ glab repo contributors -R gitlab-com/www-gitlab-com -#=> Supports repo override +# Supports repo override ```