Skip to content

Commit 2ffb4fb

Browse files
committed
cmd/mr_list.go: Add 'fancy' output
I've always found the project merge_requests page useful. It shows the status of MRs, the CI and Approval statuses, etc. Let's start doing that in the 'mr list' command. I still like the default simple command, so let's add a '--show-state' to get fancy output. Note, that this requires a per-MR lookup to get the CI status. That slows the output of --show-state down, but it's worth the trade off. Add --show-state to output similar to the project merge_requests page. Signed-off-by: Prarit Bhargava <[email protected]>
1 parent 5309b44 commit 2ffb4fb

File tree

3 files changed

+206
-4
lines changed

3 files changed

+206
-4
lines changed

cmd/mr_list.go

Lines changed: 202 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@ package cmd
22

33
import (
44
"fmt"
5+
"os"
56
"strconv"
67
"strings"
78

89
"github.com/MakeNowJust/heredoc/v2"
10+
"github.com/fatih/color"
11+
gitlab "gitlab.com/gitlab-org/api/client-go"
912
"github.com/pkg/errors"
1013
"github.com/rsteube/carapace"
14+
"github.com/savioxavier/termlink"
1115
"github.com/spf13/cobra"
12-
gitlab "gitlab.com/gitlab-org/api/client-go"
1316
"github.com/zaquestion/lab/internal/action"
17+
"github.com/zaquestion/lab/internal/git"
1418
lab "github.com/zaquestion/lab/internal/gitlab"
19+
"golang.org/x/term"
1520
)
1621

1722
var (
@@ -39,6 +44,119 @@ var (
3944
mrReviewerID *gitlab.ReviewerIDValue
4045
)
4146

47+
func truncateText(s string, length int) (string) {
48+
if length > len(s) {
49+
return s
50+
}
51+
return s[:length]
52+
}
53+
54+
func printRED(text string, cols int) {
55+
fmt.Printf(color.RedString("%-"+fmt.Sprintf("%d", cols)+"s", text))
56+
}
57+
58+
func printGREEN(text string, cols int) {
59+
fmt.Printf(color.GreenString("%-"+fmt.Sprintf("%d", cols)+"s", text))
60+
}
61+
62+
func printYELLOW(text string, cols int) {
63+
fmt.Printf(color.YellowString("%-"+fmt.Sprintf("%d", cols)+"s", text))
64+
}
65+
66+
func printColumns(data [][]string) {
67+
spacing := 2 // adjust this variable to increase/decrease spacing between columns
68+
69+
columnWidths := make([]int, len(data[0]))
70+
for _, row := range data {
71+
for cellnum, cell := range row {
72+
if cellnum == 0 { // the 0th entry is the webURL and is not output
73+
continue
74+
}
75+
if len(cell) > columnWidths[cellnum] {
76+
columnWidths[cellnum] = len(cell)
77+
}
78+
}
79+
}
80+
81+
// Make sure the output fits on the screen. If it does not, then truncate
82+
// the title field.
83+
84+
// get the screen resolution (only width is needed)
85+
width, _, err := term.GetSize(int(os.Stdin.Fd()))
86+
if err != nil {
87+
log.Fatal(err)
88+
}
89+
90+
// Determine the output string length
91+
linelength := 0
92+
for _, col := range columnWidths {
93+
linelength += col
94+
}
95+
96+
// This is the actual line length. It is the columnWidths (calculated in the
97+
// for loop above), extra spacing in the middle columns (ie, 2 * (# of cols - 2)
98+
// and one extra character for the newline.
99+
linelength = linelength + (spacing * (len(columnWidths) - 2) + 1)
100+
101+
// If the line length is greater than the width of the terminal, truncate
102+
// the Title column. The title text itself is truncated in the switch statement below.
103+
delta := linelength - width
104+
if delta > 0 {
105+
columnWidths[2] = columnWidths[2] - delta
106+
}
107+
108+
// output the data to the screen
109+
for rownum, row := range data {
110+
weburl := row[0]
111+
for cellnum, cell := range row {
112+
if cellnum == 0 { // the 0th entry is the webURL and is not output
113+
continue
114+
}
115+
if rownum == 0 { // print out the header
116+
if cellnum < (len(row) - 1) {
117+
fmt.Printf("%-*s", columnWidths[cellnum]+spacing, cell)
118+
} else { // no spaces after last column
119+
fmt.Printf("%-*s", columnWidths[cellnum], cell)
120+
}
121+
continue
122+
}
123+
124+
switch cellnum {
125+
case 1: // MRID (and weburl link)
126+
// Requires initial offset of width+spacing-len(cell)
127+
link := termlink.Link(cell, weburl)
128+
fmt.Printf("%s%-"+fmt.Sprintf("%d", columnWidths[cellnum]+spacing-len(cell))+"s",link, "")
129+
130+
case 2: // MR Title
131+
fmt.Printf("%-*s", columnWidths[cellnum]+spacing, truncateText(cell, columnWidths[cellnum]))
132+
case 3: // CI Status
133+
switch cell {
134+
case "failed":
135+
printRED(cell, columnWidths[cellnum]+spacing)
136+
case "cancelled":
137+
printRED(cell, columnWidths[cellnum]+spacing)
138+
case "success":
139+
printGREEN(cell, columnWidths[cellnum]+spacing)
140+
case "running":
141+
printYELLOW(cell, columnWidths[cellnum]+spacing)
142+
default:
143+
printGREEN(cell, columnWidths[cellnum]+spacing)
144+
}
145+
146+
case 4: // MR Status
147+
// spacing is not added here as this is the last column
148+
switch cell {
149+
case "mergeable":
150+
printGREEN(cell, columnWidths[cellnum])
151+
default:
152+
printRED(cell, columnWidths[cellnum])
153+
}
154+
}
155+
}
156+
fmt.Println()
157+
}
158+
}
159+
42160
// listCmd represents the list command
43161
var listCmd = &cobra.Command{
44162
Use: "list [remote] [search]",
@@ -62,9 +180,15 @@ var listCmd = &cobra.Command{
62180
lab mr list --ready
63181
lab mr list --no-conflicts
64182
lab mr list -x 'test MR'
65-
lab mr list -r johndoe`),
183+
lab mr list -r johndoe
184+
lab mr list --show-status`),
66185
PersistentPreRun: labPersistentPreRun,
67186
Run: func(cmd *cobra.Command, args []string) {
187+
rn, err := git.PathWithNamespace(defaultRemote)
188+
if err != nil {
189+
return
190+
}
191+
68192
mrs, err := mrList(args)
69193
if err != nil {
70194
log.Fatal(err)
@@ -73,9 +197,82 @@ var listCmd = &cobra.Command{
73197
pager := newPager(cmd.Flags())
74198
defer pager.Close()
75199

200+
showstatus, _ := cmd.Flags().GetBool("show-status")
201+
202+
if !showstatus {
203+
for _, mr := range mrs {
204+
fmt.Printf("!%d %s\n", mr.IID, mr.Title)
205+
}
206+
return
207+
}
208+
209+
output := [][]string{{"", "MRID", "Title", "CIStatus", "MRStatus"}}
76210
for _, mr := range mrs {
77-
fmt.Printf("!%d %s\n", mr.IID, mr.Title)
211+
mrx, err := lab.MRGet(rn, int(mr.IID))
212+
if err != nil {
213+
log.Fatal(err)
214+
}
215+
216+
// In general we use the Detailed Merge Status. There are some
217+
// cases below where a custom status is used.
218+
detailedMergeStatus := strings.Replace(mr.DetailedMergeStatus, "_", " ", -1)
219+
220+
CIStatus := "no pipeline"
221+
if mrx.HeadPipeline != nil {
222+
CIStatus = mrx.HeadPipeline.Status
223+
}
224+
225+
// Custom MR Status: If the status is success/not approved, then
226+
// check to see if there are any threads that need to be resolved.
227+
// If there are report 'unresolved threads' as a status
228+
if detailedMergeStatus == "not approved" && CIStatus == "success" {
229+
discussions, err := lab.MRListDiscussions(rn, mr.IID)
230+
if err != nil {
231+
log.Fatal(err)
232+
}
233+
234+
totalresolved := 0
235+
totalresolvable := 0
236+
for _, discussion := range discussions {
237+
resolved := 0
238+
resolvable := 0
239+
for _, note := range discussion.Notes {
240+
if note.Resolved {
241+
resolved++
242+
}
243+
if note.Resolvable {
244+
resolvable++
245+
}
246+
}
247+
if resolved != 0 {
248+
totalresolved++
249+
}
250+
if resolvable != 0 {
251+
totalresolvable++
252+
}
253+
}
254+
if totalresolvable != 0 && totalresolvable != totalresolved {
255+
detailedMergeStatus = fmt.Sprintf("unresolved threads(%d/%d)", totalresolved, totalresolvable)
256+
}
257+
}
258+
259+
// Custom Status: If the MR Status is 'not approved' also output
260+
// the number of remaining approvals necessary.
261+
if detailedMergeStatus == "not approved" {
262+
approvals, err := lab.GetMRApprovalsConfiguration(rn, mr.IID)
263+
if err != nil {
264+
log.Fatal(err)
265+
}
266+
detailedMergeStatus = fmt.Sprintf("%s(%d/%d)", detailedMergeStatus, len(approvals.ApprovedBy), approvals.ApprovalsRequired)
267+
}
268+
output = append(output,
269+
[]string{mr.WebURL, // weburl (used to convert MRID to URL)
270+
strconv.Itoa(mr.IID), // MRID
271+
mr.Title, // Title
272+
CIStatus, // CI Status
273+
detailedMergeStatus}) // MR Status
78274
}
275+
printColumns(output)
79276
},
80277
}
81278

@@ -252,6 +449,8 @@ func init() {
252449
listCmd.Flags().BoolVarP(&mrExactMatch, "exact-match", "x", false, "match on the exact (case-insensitive) search terms")
253450
listCmd.Flags().StringVar(
254451
&mrReviewer, "reviewer", "", "list only MRs with reviewer set to $username/any/none")
452+
listCmd.Flags().BoolP("show-status", "", false, "show CI and MR status (slow on projects with large number of MRs)")
453+
255454

256455
mrCmd.AddCommand(listCmd)
257456
carapace.Gen(listCmd).FlagCompletion(carapace.ActionMap{

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616
github.com/pkg/errors v0.9.1
1717
github.com/rivo/tview v0.0.0-20240101144852-b3bd1aa5e9f2
1818
github.com/rsteube/carapace v0.48.4
19+
github.com/savioxavier/termlink v1.4.2
1920
github.com/spf13/afero v1.11.0
2021
github.com/spf13/cobra v1.8.0
2122
github.com/spf13/pflag v1.0.5
@@ -73,7 +74,7 @@ require (
7374
golang.org/x/net v0.20.0 // indirect
7475
golang.org/x/oauth2 v0.25.0 // indirect
7576
golang.org/x/sys v0.20.0 // indirect
76-
golang.org/x/term v0.16.0 // indirect
77+
golang.org/x/term v0.16.0
7778
golang.org/x/text v0.14.0 // indirect
7879
golang.org/x/time v0.10.0 // indirect
7980
gopkg.in/ini.v1 v1.67.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
128128
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
129129
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
130130
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
131+
github.com/savioxavier/termlink v1.4.2 h1:PRlvcStluuSKA87KCIqzODknYeQ3XEcgJP6DvAAVl1c=
132+
github.com/savioxavier/termlink v1.4.2/go.mod h1:5T5ePUlWbxCHIwyF8/Ez1qufOoGM89RCg9NvG+3G3gc=
131133
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
132134
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
133135
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=

0 commit comments

Comments
 (0)