diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 00000000..f777c9f3 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,12 @@ +version = 1 + +test_patterns = [ + "*_test.go" +] + +[[analyzers]] +name = "go" +enabled = true + + [analyzers.meta] + import_path = "github.com/zaquestion/lab" diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 22292794..c2bc7ef5 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,6 @@ # These are supported funding model platforms -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: zaquestion patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username diff --git a/.goreleaser.yml b/.goreleaser.yml index a1f0ab39..4ff15841 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -30,7 +30,7 @@ scoop: email: goreleaser@carlosbecker.com homepage: "https://github.com/zaquestion/lab" description: "Lab wraps Git or Hub, making it simple to clone, fork, and interact with repositories on GitLab" - license: Unlicense + license: CC0 builds: - goos: - linux diff --git a/.travis.yml b/.travis.yml index 247a6e16..82403fe1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ os: - linux go: - - 1.11.x + - 1.13.x # use containers which run faster and have cache sudo: false diff --git a/LICENSE b/LICENSE index 0e259d42..104f586b 100644 --- a/LICENSE +++ b/LICENSE @@ -2,15 +2,6 @@ Creative Commons Legal Code CC0 1.0 Universal - CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE - LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN - ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS - INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES - REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS - PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM - THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED - HEREUNDER. - Statement of Purpose The laws of most jurisdictions throughout the world automatically confer diff --git a/README.md b/README.md index d166cc5b..fd84f61f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ lab will look for hub and uses that as your git binary when available so you don $ lab version git version 2.11.0 hub version 2.3.0-pre9 -lab version 0.16.0 +lab version 0.17.2 ``` # Inspiration @@ -42,12 +42,20 @@ scoop bucket add zaquestion https://github.com/zaquestion/scoop-bucket.git scoop install lab ``` +### Alpine +``` +apk add lab +``` + ### Bash Installs lab into `/usr/local/bin/` ``` -curl -s https://raw.githubusercontent.com/zaquestion/lab/master/install.sh | bash +curl -s https://raw.githubusercontent.com/zaquestion/lab/master/install.sh | sudo bash ``` +NOTE: Please take care when executing scripts in this fashion. Make sure you +trust the developer providing the script and consider peaking at the install +script itself (ours is pretty simply ;) ### PreBuilt Binaries @@ -56,7 +64,7 @@ Head to the [releases](https://github.com/zaquestion/lab/releases) page and down ### Source Required -* [Go 1.11+](https://golang.org/doc/install) +* [Go 1.12+](https://golang.org/doc/install) ``` git clone git@github.com:zaquestion/lab @@ -78,11 +86,11 @@ See the [contribution guide](CONTRIBUTING.md). `lab` needs your GitLab information in order to interact with to your GitLab instance. There are several ways to provide this information to `lab`: -1. Environment variables: `LAB_CORE_HOST`, `LAB_CORE_USER`, `LAB_CORE_TOKEN` -2. Environment variables: `CI_PROJECT_URL`, `CI_REGISTRY_USER`, `CI_JOB_TOKEN` +1. environment variables: `LAB_CORE_HOST`, `LAB_CORE_TOKEN`; +2. environment variables: `CI_PROJECT_URL`, `CI_JOB_TOKEN`; - Note: these are meant for when `lab` is running within a GitLab CI pipeline -3. HCL config file: `./lab.hcl` -4. HCL config file: `~/.config/lab.hcl` +3. directory-specific configuration file in [HashiCorp configuration language (HCL)](https://github.com/hashicorp/hcl): `./lab.hcl`; +4. user-specific configuration file in HCL: `~/.config/lab.hcl`. These are checked in order. If no suitable config values are found, `lab` will prompt for your GitLab information and save it into `~/.config/lab.hcl`. @@ -90,7 +98,6 @@ For example: ``` $ lab Enter default GitLab host (default: https://gitlab.com): -Enter default GitLab user: zaq Enter default GitLab token: ``` # Completions @@ -134,4 +141,8 @@ alias git=lab Zaq? Wiedmann has waived all copyright and related or neighboring rights to Lab. + This work is published from: + + United States.

diff --git a/cmd/ci_run_test.go b/cmd/ci_run_test.go index 22ed6e6f..6c3782e3 100644 --- a/cmd/ci_run_test.go +++ b/cmd/ci_run_test.go @@ -123,7 +123,7 @@ func Test_getCIRunOptions(t *testing.T) { []string{}, nil, // https://gitlab.com/zaquestion/test project ID "", - "gitlab project not found", + "gitlab project not found, verify you have access to the requested resource", }, } diff --git a/cmd/ci_status.go b/cmd/ci_status.go index a76fe25f..71c8a1f7 100644 --- a/cmd/ci_status.go +++ b/cmd/ci_status.go @@ -5,9 +5,11 @@ import ( "log" "os" "text/tabwriter" + "time" "github.com/pkg/errors" "github.com/spf13/cobra" + gitlab "github.com/xanzy/go-gitlab" "github.com/zaquestion/lab/internal/git" lab "github.com/zaquestion/lab/internal/gitlab" ) @@ -45,37 +47,50 @@ lab ci status --wait`, pid := rn w := tabwriter.NewWriter(os.Stdout, 2, 4, 1, byte(' '), 0) - jobs, err := lab.CIJobs(pid, branch) - if err != nil { - log.Fatal(errors.Wrap(err, "failed to find ci jobs")) - } - jobs = latestJobs(jobs) - - if len(jobs) == 0 { - return - } wait, err := cmd.Flags().GetBool("wait") if err != nil { log.Fatal(err) } + var jobs []*gitlab.Job + fmt.Fprintln(w, "Stage:\tName\t-\tStatus") for { + // fetch all of the CI Jobs from the API + jobs, err = lab.CIJobs(pid, branch) + if err != nil { + log.Fatal(errors.Wrap(err, "failed to find ci jobs")) + } + + // filter out old jobs + jobs = latestJobs(jobs) + + if len(jobs) == 0 { + log.Fatal("no CI jobs found for branch ", branch, " on remote ", remote) + return + } + + // print the status of all current jobs for _, job := range jobs { fmt.Fprintf(w, "%s:\t%s\t-\t%s\n", job.Stage, job.Name, job.Status) } - if !wait { - break - } - if jobs[0].Pipeline.Status != "pending" && - jobs[0].Pipeline.Status != "running" { + + dontWaitForJobsToFinish := !wait || + (jobs[0].Pipeline.Status != "pending" && + jobs[0].Pipeline.Status != "running") + if dontWaitForJobsToFinish { break } + fmt.Fprintln(w) + + // don't spam the api TOO much + time.Sleep(1 * time.Second) } fmt.Fprintf(w, "\nPipeline Status: %s\n", jobs[0].Pipeline.Status) + // exit w/ status code 1 to indicate a job failure if wait && jobs[0].Pipeline.Status != "success" { os.Exit(1) } diff --git a/cmd/ci_view.go b/cmd/ci_view.go index f98e3653..14f39b61 100644 --- a/cmd/ci_view.go +++ b/cmd/ci_view.go @@ -82,28 +82,44 @@ Feedback Encouraged!: https://github.com/zaquestion/lab/issues`, boxes = make(map[string]*tview.TextView) jobsCh := make(chan []*gitlab.Job) + inputCh := make(chan struct{}) + + screen, err := tcell.NewScreen() + if err != nil { + log.Fatal(err) + } + screen.Init() var navi navigator - a.SetInputCapture(inputCapture(a, root, navi)) + a.SetInputCapture(inputCapture(a, root, navi, inputCh)) go updateJobs(a, jobsCh, project.ID, branch) - go refreshScreen(a, root) - if err := a.SetRoot(root, true).SetBeforeDrawFunc(jobsView(a, jobsCh, root)).SetAfterDrawFunc(connectJobsView(a)).Run(); err != nil { + go func() { + defer recoverPanic(a) + for { + a.SetFocus(root) + jobsView(a, jobsCh, inputCh, root) + a.Draw() + } + }() + if err := a.SetScreen(screen).SetRoot(root, true).SetAfterDrawFunc(connectJobsView(a)).Run(); err != nil { log.Fatal(err) } }, } -func inputCapture(a *tview.Application, root *tview.Pages, navi navigator) func(event *tcell.EventKey) *tcell.EventKey { +func inputCapture(a *tview.Application, root *tview.Pages, navi navigator, inputCh chan struct{}) func(event *tcell.EventKey) *tcell.EventKey { return func(event *tcell.EventKey) *tcell.EventKey { if event.Rune() == 'q' || event.Key() == tcell.KeyEscape { switch { case modalVisible: modalVisible = !modalVisible root.HidePage("yesno") + inputCh <- struct{}{} case logsVisible: logsVisible = !logsVisible root.HidePage("logs-" + curJob.Name) - a.Draw() + inputCh <- struct{}{} + a.ForceDraw() default: a.Stop() return nil @@ -111,6 +127,9 @@ func inputCapture(a *tview.Application, root *tview.Pages, navi navigator) func( } if !modalVisible && !logsVisible { curJob = navi.Navigate(jobs, event) + root.SendToFront("jobs-" + curJob.Name) + // update jobs view on input changes + inputCh <- struct{}{} } switch event.Rune() { case 'c': @@ -121,7 +140,8 @@ func inputCapture(a *tview.Application, root *tview.Pages, navi navigator) func( } curJob = job root.RemovePage("logs-" + curJob.Name) - a.Draw() + inputCh <- struct{}{} + a.ForceDraw() case 'p', 'r': if modalVisible { break @@ -134,11 +154,11 @@ func inputCapture(a *tview.Application, root *tview.Pages, navi navigator) func( modalVisible = false root.RemovePage("yesno") if buttonLabel == "No" { - a.Draw() + a.ForceDraw() return } root.RemovePage("logs-" + curJob.Name) - a.Draw() + a.ForceDraw() job, err := lab.CIPlayOrRetry(projectID, curJob.ID, curJob.Status) if err != nil { @@ -147,18 +167,20 @@ func inputCapture(a *tview.Application, root *tview.Pages, navi navigator) func( } if job != nil { curJob = job - a.Draw() + a.ForceDraw() } }) root.AddAndSwitchToPage("yesno", modal, false) - a.Draw() + inputCh <- struct{}{} + a.ForceDraw() return nil case 't': logsVisible = !logsVisible if !logsVisible { root.HidePage("logs-" + curJob.Name) } - a.Draw() + inputCh <- struct{}{} + a.ForceDraw() return nil case 'T': a.Suspend(func() { @@ -186,8 +208,10 @@ func inputCapture(a *tview.Application, root *tview.Pages, navi navigator) func( } } }) + inputCh <- struct{}{} return nil } + inputCh <- struct{}{} return event } } @@ -298,151 +322,137 @@ func adjacentStages(jobs []*gitlab.Job, s string) (p, n string) { return } -func jobsView(app *tview.Application, jobsCh chan []*gitlab.Job, root *tview.Pages) func(screen tcell.Screen) bool { - return func(screen tcell.Screen) bool { - defer recoverPanic(app) - screen.Clear() - select { - case jobs = <-jobsCh: - default: - if len(jobs) == 0 { - jobs = <-jobsCh - } - } - if curJob == nil && len(jobs) > 0 { - curJob = jobs[0] - } - if modalVisible { - return false - } - if logsVisible { - logsKey := "logs-" + curJob.Name - if !root.SwitchToPage(logsKey).HasPage(logsKey) { - tv := tview.NewTextView() - tv.SetDynamicColors(true) - tv.SetBorderPadding(0, 0, 1, 1).SetBorder(true) - - go func() { - err := doTrace(context.Background(), vtclean.NewWriter(tview.ANSIWriter(tv), true), projectID, branch, curJob.Name) - if err != nil { - app.Stop() - log.Fatal(err) - } - }() - root.AddAndSwitchToPage("logs-"+curJob.Name, tv, true) - } - return false - } - px, _, maxX, maxY := root.GetInnerRect() - var ( - stages = 0 - lastStage = "" - ) - // get the number of stages - for _, j := range jobs { - if j.Stage != lastStage { - lastStage = j.Stage - stages++ - } +func jobsView(app *tview.Application, jobsCh chan []*gitlab.Job, inputCh chan struct{}, root *tview.Pages) { + select { + case jobs = <-jobsCh: + case <-inputCh: + case <-time.NewTicker(time.Second * 1).C: + } + if jobs == nil { + jobs = <-jobsCh + } + if curJob == nil && len(jobs) > 0 { + curJob = jobs[0] + } + if modalVisible { + return + } + if logsVisible { + logsKey := "logs-" + curJob.Name + if !root.SwitchToPage(logsKey).HasPage(logsKey) { + tv := tview.NewTextView() + tv.SetDynamicColors(true) + tv.SetBorderPadding(0, 0, 1, 1).SetBorder(true) + + go func() { + err := doTrace(context.Background(), vtclean.NewWriter(tview.ANSIWriter(tv), true), projectID, branch, curJob.Name) + if err != nil { + app.Stop() + log.Fatal(err) + } + }() + root.AddAndSwitchToPage("logs-"+curJob.Name, tv, true) } + return + } + px, _, maxX, maxY := root.GetInnerRect() + var ( + stages = 0 lastStage = "" - var ( - rowIdx = 0 - stageIdx = 0 - maxTitle = 20 - ) - for _, j := range jobs { - boxX := px + (maxX / stages * stageIdx) - if j.Stage != lastStage { - rowIdx = 0 - stageIdx++ - lastStage = j.Stage - key := "stage-" + j.Stage - - x, y, w, h := boxX, maxY/6-4, maxTitle+2, 3 - b := box(root, key, x, y, w, h) - b.SetText(strings.Title(j.Stage)) - b.SetTextAlign(tview.AlignCenter) - - } + ) + // get the number of stages + for _, j := range jobs { + if j.Stage != lastStage { + lastStage = j.Stage + stages++ } - lastStage = jobs[0].Stage - rowIdx = 0 + } + lastStage = "" + var ( + rowIdx = 0 stageIdx = 0 - for _, j := range jobs { - if j.Stage != lastStage { - rowIdx = 0 - lastStage = j.Stage - stageIdx++ - } - boxX := px + (maxX / stages * stageIdx) - - key := "jobs-" + j.Name - x, y, w, h := boxX, maxY/6+(rowIdx*5), maxTitle+2, 4 + maxTitle = 20 + ) + for _, j := range jobs { + boxX := px + (maxX / stages * stageIdx) + if j.Stage != lastStage { + rowIdx = 0 + stageIdx++ + lastStage = j.Stage + key := "stage-" + j.Stage + + x, y, w, h := boxX, maxY/6-4, maxTitle+2, 3 b := box(root, key, x, y, w, h) - b.SetTitle(j.Name) - // The scope of jobs to show, one or array of: created, pending, running, - // failed, success, canceled, skipped; showing all jobs if none provided - var statChar rune - switch j.Status { - case "success": - b.SetBorderColor(tcell.ColorGreen) - statChar = '✔' - case "failed": - b.SetBorderColor(tcell.ColorRed) - statChar = '✘' - case "running": - b.SetBorderColor(tcell.ColorBlue) - statChar = '●' - case "pending": - b.SetBorderColor(tcell.ColorYellow) - statChar = '●' - case "manual": - b.SetBorderColor(tcell.ColorGrey) - statChar = '●' - } - // retryChar := '⟳' - title := fmt.Sprintf("%c %s", statChar, j.Name) - if statChar == '✔' { - // I don't understand why, but upon updating - // rivo/tview the '✔' rune now gets printed - // with an extra space for the title, so I'm - // remove the space that I add here to obviate - // this. - title = fmt.Sprintf("%c%s", statChar, j.Name) - } - // trim the suffix if it matches the stage, I've seen - // the pattern in 2 different places to handle - // different stages for the same service and it tends - // to make the title spill over the max - title = strings.TrimSuffix(title, ":"+j.Stage) - b.SetTitle(title) - // tview default aligns center, which is nice, but if - // the title is too long we want to bias towards seeing - // the beginning of it - if tview.TaggedStringWidth(title) > maxTitle { - b.SetTitleAlign(tview.AlignLeft) - } - if j.StartedAt != nil { - end := time.Now() - if j.FinishedAt != nil { - end = *j.FinishedAt - } - b.SetText("\n" + fmtDuration(end.Sub(*j.StartedAt))) - b.SetTextAlign(tview.AlignRight) - } else { - b.SetText("") - } - rowIdx++ + + b.SetText(strings.Title(j.Stage)) + b.SetTextAlign(tview.AlignCenter) } - // last box keeps getting focus'd some how - for _, b := range boxes { - b.Blur() + } + lastStage = jobs[0].Stage + rowIdx = 0 + stageIdx = 0 + for _, j := range jobs { + if j.Stage != lastStage { + rowIdx = 0 + lastStage = j.Stage + stageIdx++ + } + boxX := px + (maxX / stages * stageIdx) + + key := "jobs-" + j.Name + x, y, w, h := boxX, maxY/6+(rowIdx*5), maxTitle+2, 4 + b := box(root, key, x, y, w, h) + b.SetTitle(j.Name) + // The scope of jobs to show, one or array of: created, pending, running, + // failed, success, canceled, skipped; showing all jobs if none provided + var statChar rune + switch j.Status { + case "success": + b.SetBorderColor(tcell.ColorGreen) + statChar = '✔' + case "failed": + b.SetBorderColor(tcell.ColorRed) + statChar = '✘' + case "running": + b.SetBorderColor(tcell.ColorBlue) + statChar = '●' + case "pending": + b.SetBorderColor(tcell.ColorYellow) + statChar = '●' + case "manual": + b.SetBorderColor(tcell.ColorGrey) + statChar = '●' + } + // retryChar := '⟳' + title := fmt.Sprintf("%c %s", statChar, j.Name) + // trim the suffix if it matches the stage, I've seen + // the pattern in 2 different places to handle + // different stages for the same service and it tends + // to make the title spill over the max + title = strings.TrimSuffix(title, ":"+j.Stage) + b.SetTitle(title) + // tview default aligns center, which is nice, but if + // the title is too long we want to bias towards seeing + // the beginning of it + if tview.TaggedStringWidth(title) > maxTitle { + b.SetTitleAlign(tview.AlignLeft) + } + if j.StartedAt != nil { + end := time.Now() + if j.FinishedAt != nil { + end = *j.FinishedAt + } + b.SetText("\n" + fmtDuration(end.Sub(*j.StartedAt))) + b.SetTextAlign(tview.AlignRight) + } else { + b.SetText("") } - boxes["jobs-"+curJob.Name].Focus(nil) - return false + rowIdx++ + } + root.SendToFront("jobs-" + curJob.Name) + } func fmtDuration(d time.Duration) string { d = d.Round(time.Second) @@ -471,14 +481,6 @@ func recoverPanic(app *tview.Application) { } } -func refreshScreen(app *tview.Application, root *tview.Pages) { - defer recoverPanic(app) - for { - app.Draw() - time.Sleep(time.Second * 1) - } -} - func updateJobs(app *tview.Application, jobsCh chan []*gitlab.Job, pid interface{}, branch string) { defer recoverPanic(app) for { diff --git a/cmd/ci_view_test.go b/cmd/ci_view_test.go index 7f16bd89..eeea367e 100644 --- a/cmd/ci_view_test.go +++ b/cmd/ci_view_test.go @@ -499,6 +499,7 @@ func Test_jobsView(t *testing.T) { boxes = make(map[string]*tview.TextView) jobsCh := make(chan []*gitlab.Job) + inputCh := make(chan struct{}) root := tview.NewPages() root.SetBorderPadding(1, 1, 2, 2) @@ -515,7 +516,9 @@ func Test_jobsView(t *testing.T) { go func() { jobsCh <- jobs }() - jobsView(nil, jobsCh, root)(screen) + root.Box.Focus(nil) + jobsView(nil, jobsCh, inputCh, root) + root.Focus(func(p tview.Primitive) { p.Focus(nil) }) root.Draw(screen) connectJobsView(nil)(screen) screen.Sync() diff --git a/cmd/issue.go b/cmd/issue.go index 115d3dda..515c31d8 100644 --- a/cmd/issue.go +++ b/cmd/issue.go @@ -37,5 +37,6 @@ func init() { issueCmd.Flags().BoolP("list", "l", false, "List issues on a remote") issueCmd.Flags().BoolP("browse", "b", false, "View issue in a browser") issueCmd.Flags().StringP("close", "d", "", "Close issue on remote") + issueCmd.Flags().BoolP("no-markdown", "M", false, "Don't use markdown renderer to print the issue description") RootCmd.AddCommand(issueCmd) } diff --git a/cmd/issue_create.go b/cmd/issue_create.go index 40dbebed..0fba31af 100644 --- a/cmd/issue_create.go +++ b/cmd/issue_create.go @@ -71,7 +71,7 @@ var issueCreateCmd = &cobra.Command{ issueURL, err := lab.IssueCreate(rn, &gitlab.CreateIssueOptions{ Title: &title, Description: &body, - Labels: gitlab.Labels(labels), + Labels: lab.Labels(labels), AssigneeIDs: assigneeIDs, }) if err != nil { diff --git a/cmd/issue_create_test.go b/cmd/issue_create_test.go index 520d18ca..70239371 100644 --- a/cmd/issue_create_test.go +++ b/cmd/issue_create_test.go @@ -22,7 +22,7 @@ func Test_issueCreate(t *testing.T) { } out := getAppOutput(b)[0] - require.Contains(t, out, "https://gitlab.com/lab-testing/test/issues/") + require.Contains(t, out, "https://gitlab.com/lab-testing/test/-/issues/") // Get the issue ID from the returned URL and close the issue. u, err := url.Parse(out) diff --git a/cmd/issue_edit.go b/cmd/issue_edit.go index 4ae16a83..21decb1a 100644 --- a/cmd/issue_edit.go +++ b/cmd/issue_edit.go @@ -70,7 +70,7 @@ lab issue edit -l newlabel --unlabel oldlabel # relabel issue`, } if labelsChanged { - opts.Labels = gitlab.Labels(labels) + opts.Labels = lab.Labels(labels) } if assigneesChanged { diff --git a/cmd/issue_show.go b/cmd/issue_show.go index b39f7a50..67412085 100644 --- a/cmd/issue_show.go +++ b/cmd/issue_show.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/charmbracelet/glamour" "github.com/spf13/cobra" gitlab "github.com/xanzy/go-gitlab" lab "github.com/zaquestion/lab/internal/gitlab" @@ -28,7 +29,13 @@ var issueShowCmd = &cobra.Command{ log.Fatal(err) } - printIssue(issue, rn) + noMarkdown, _ := cmd.Flags().GetBool("no-markdown") + if err != nil { + log.Fatal(err) + } + renderMarkdown := !noMarkdown + + printIssue(issue, rn, renderMarkdown) showComments, _ := cmd.Flags().GetBool("comments") if showComments { @@ -42,7 +49,7 @@ var issueShowCmd = &cobra.Command{ }, } -func printIssue(issue *gitlab.Issue, project string) { +func printIssue(issue *gitlab.Issue, project string, renderMarkdown bool) { milestone := "None" timestats := "None" dueDate := "None" @@ -70,6 +77,14 @@ func printIssue(issue *gitlab.Issue, project string) { } } + if renderMarkdown { + r, _ := glamour.NewTermRenderer( + glamour.WithStandardStyle("auto"), + ) + + issue.Description, _ = r.Render(issue.Description) + } + fmt.Printf(` #%d %s =================================== @@ -133,6 +148,7 @@ func init() { issueShowCmd.MarkZshCompPositionalArgumentCustom(1, "__lab_completion_remote") issueShowCmd.MarkZshCompPositionalArgumentCustom(2, "__lab_completion_issue $words[2]") issueShowCmd.MarkZshCompPositionalArgumentCustom(1, "__lab_completion_issue") + issueShowCmd.Flags().BoolP("no-markdown", "M", false, "Don't use markdown renderer to print the issue description") issueShowCmd.Flags().BoolP("comments", "c", false, "Show comments for the issue") issueCmd.AddCommand(issueShowCmd) } diff --git a/cmd/issue_show_test.go b/cmd/issue_show_test.go index 7403558d..1cf44605 100644 --- a/cmd/issue_show_test.go +++ b/cmd/issue_show_test.go @@ -4,6 +4,7 @@ import ( "os/exec" "testing" + "github.com/acarl005/stripansi" "github.com/stretchr/testify/require" ) @@ -19,10 +20,15 @@ func Test_issueShow(t *testing.T) { t.Error(err) } - require.Contains(t, string(b), ` + out := string(b) + out = stripansi.Strip(out) // This is required because glamour adds a lot of ansi chars + + require.Contains(t, out, ` #1 test issue for lab list =================================== + + ----------------------------------- Project: zaquestion/test Status: Open @@ -32,7 +38,7 @@ Milestone: 1.0 Due Date: 2018-01-01 00:00:00 +0000 UTC Time Stats: Estimated 1w, Spent 1d Labels: bug -WebURL: https://gitlab.com/zaquestion/test/issues/1 +WebURL: https://gitlab.com/zaquestion/test/-/issues/1 `) require.Contains(t, string(b), `commented at`) diff --git a/cmd/issue_test.go b/cmd/issue_test.go index ce1162ac..a0b67eed 100644 --- a/cmd/issue_test.go +++ b/cmd/issue_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/acarl005/stripansi" "github.com/stretchr/testify/require" ) @@ -28,10 +29,10 @@ func Test_issueCmd(t *testing.T) { } out := string(b) - require.Contains(t, out, "https://gitlab.com/lab-testing/test/issues/") + require.Contains(t, out, "https://gitlab.com/lab-testing/test/-/issues/") i := strings.Index(out, "\n") - issueID = strings.TrimPrefix(out[:i], "https://gitlab.com/lab-testing/test/issues/") + issueID = strings.TrimPrefix(out[:i], "https://gitlab.com/lab-testing/test/-/issues/") t.Log(issueID) }) t.Run("show", func(t *testing.T) { @@ -48,11 +49,13 @@ func Test_issueCmd(t *testing.T) { t.Fatal(err) } out := string(b) + outStripped := stripansi.Strip(out) // This is required because glamour adds a lot of ansi chars require.Contains(t, out, "Project: lab-testing/test\n") require.Contains(t, out, "Status: Open\n") require.Contains(t, out, "Assignees: lab-testing\n") require.Contains(t, out, fmt.Sprintf("#%s issue title", issueID)) - require.Contains(t, out, "===================================\nissue description") + require.Contains(t, out, "===================================\n") + require.Contains(t, outStripped, "issue description") require.Contains(t, out, "Labels: bug, critical\n") require.Contains(t, out, fmt.Sprintf("WebURL: https://gitlab.com/lab-testing/test/issues/%s", issueID)) }) diff --git a/cmd/mr.go b/cmd/mr.go index 4c372b69..b3339e37 100644 --- a/cmd/mr.go +++ b/cmd/mr.go @@ -38,5 +38,6 @@ func init() { mrCmd.Flags().BoolP("list", "l", false, "List merge requests on a remote") mrCmd.Flags().BoolP("browse", "b", false, "View merge request in a browser") mrCmd.Flags().StringP("close", "d", "", "Close merge request on remote") + mrCmd.Flags().BoolP("no-markdown", "M", false, "Don't use markdown renderer to print the issue description") RootCmd.AddCommand(mrCmd) } diff --git a/cmd/mr_browse.go b/cmd/mr_browse.go index 476ff165..eafb7086 100644 --- a/cmd/mr_browse.go +++ b/cmd/mr_browse.go @@ -40,7 +40,7 @@ var mrBrowseCmd = &cobra.Command{ ListOptions: gitlab.ListOptions{ PerPage: 10, }, - Labels: mrLabels, + Labels: lab.Labels(mrLabels), State: &mrState, OrderBy: gitlab.String("updated_at"), SourceBranch: gitlab.String(currentBranch), diff --git a/cmd/mr_create.go b/cmd/mr_create.go index a11fa94f..2f278b1c 100644 --- a/cmd/mr_create.go +++ b/cmd/mr_create.go @@ -31,7 +31,7 @@ var mrCreateCmd = &cobra.Command{ func init() { mrCreateCmd.Flags().StringSliceP("message", "m", []string{}, "Use the given ; multiple -m are concatenated as separate paragraphs") - mrCreateCmd.Flags().StringP("assignee", "a", "", "Set assignee by username") + mrCreateCmd.Flags().StringSliceP("assignee", "a", []string{}, "Set assignee by username; can be specified multiple times for multiple assignees") mrCreateCmd.Flags().StringSliceP("label", "l", []string{}, "Add label