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