From d1eac7b211b7087bae0abf76529b7f7ceb26268c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 15 Nov 2019 12:35:44 +0100 Subject: [PATCH 01/53] `pr status`: avoid printing a lonely "1" when there is only one Check In a repository that only has a single Check configured (e.g. this repo), we would print "checks: 1" for PRs where the CI is passing. This looks akward when repeated for each PR and provides little useful information. This avoids ever printing "1" and instead prints "failing", "pending", or "success", respectively. We now only show numbers for repositories that have more than one Check runs. --- command/pr.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/command/pr.go b/command/pr.go index 0445b81d6..1ae542323 100644 --- a/command/pr.go +++ b/command/pr.go @@ -258,7 +258,7 @@ func printPrs(prs ...api.PullRequest) { fmt.Printf("\n ") } - if checks.Total > 0 { + if checks.Total > 1 { var ratio string if checks.Failing > 0 { ratio = fmt.Sprintf("%d/%d", checks.Passing, checks.Total) @@ -271,6 +271,16 @@ func printPrs(prs ...api.PullRequest) { ratio = utils.Green(ratio) } fmt.Printf(" - checks: %s", ratio) + } else if checks.Total == 1 { + var state string + if checks.Failing > 0 { + state = utils.Red("failing") + } else if checks.Pending > 0 { + state = utils.Yellow("pending") + } else if checks.Passing == checks.Total { + state = utils.Green("success") + } + fmt.Printf(" - checks: %s", state) } if reviews.ChangesRequested { From f30e973b9db937b6888b3d51408935e2648cfccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 15 Nov 2019 19:19:41 +0100 Subject: [PATCH 02/53] Extract generic row printer that adjusts itself for receiving terminal This makes the approach from `pr list` reusable across other commands that may benefit from table-based output, e.g. `issue list` or `pr status` The idea is: instantiate a printer, connect it to stdout, feed it some data, and it does the rest: colored, truncated column output that fits into a terminal, or tab-delimited output (no color, no truncation) for scripts. --- command/pr.go | 61 +++++++---------------- utils/table_printer.go | 108 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 43 deletions(-) create mode 100644 utils/table_printer.go diff --git a/command/pr.go b/command/pr.go index 0445b81d6..b7cca14a1 100644 --- a/command/pr.go +++ b/command/pr.go @@ -2,13 +2,11 @@ package command import ( "fmt" - "os" "strconv" "github.com/github/gh-cli/api" "github.com/github/gh-cli/utils" "github.com/spf13/cobra" - "golang.org/x/crypto/ssh/terminal" ) func init() { @@ -158,53 +156,30 @@ func prList(cmd *cobra.Command, args []string) error { return err } - tty := false - ttyWidth := 80 - out := cmd.OutOrStdout() - if outFile, isFile := out.(*os.File); isFile { - fd := int(outFile.Fd()) - tty = terminal.IsTerminal(fd) - if w, _, err := terminal.GetSize(fd); err == nil { - ttyWidth = w - } - } - - numWidth := 0 - maxTitleWidth := 0 + table := utils.NewTablePrinter(cmd.OutOrStdout()) for _, pr := range prs { - numLen := len(strconv.Itoa(pr.Number)) + 1 - if numLen > numWidth { - numWidth = numLen - } - if len(pr.Title) > maxTitleWidth { - maxTitleWidth = len(pr.Title) - } + table.SetContentWidth(0, len(strconv.Itoa(pr.Number))+1) + table.SetContentWidth(1, len(pr.Title)) + table.SetContentWidth(2, len(pr.HeadLabel())) } - branchWidth := 40 - titleWidth := ttyWidth - branchWidth - 2 - numWidth - 2 - - if maxTitleWidth < titleWidth { - branchWidth += titleWidth - maxTitleWidth - titleWidth = maxTitleWidth - } + table.FitColumns() + table.SetColorFunc(2, utils.Cyan) for _, pr := range prs { - if tty { - prNum := fmt.Sprintf("% *s", numWidth, fmt.Sprintf("#%d", pr.Number)) - switch pr.State { - case "OPEN": - prNum = utils.Green(prNum) - case "CLOSED": - prNum = utils.Red(prNum) - case "MERGED": - prNum = utils.Magenta(prNum) - } - prBranch := utils.Cyan(truncate(branchWidth, pr.HeadLabel())) - fmt.Fprintf(out, "%s %-*s %s\n", prNum, titleWidth, truncate(titleWidth, pr.Title), prBranch) - } else { - fmt.Fprintf(out, "%d\t%s\t%s\n", pr.Number, pr.Title, pr.HeadLabel()) + prNum := strconv.Itoa(pr.Number) + if table.IsTTY { + prNum = "#" + prNum } + switch pr.State { + case "OPEN": + table.SetColorFunc(0, utils.Green) + case "CLOSED": + table.SetColorFunc(0, utils.Red) + case "MERGED": + table.SetColorFunc(0, utils.Magenta) + } + table.WriteRow(prNum, pr.Title, pr.HeadLabel()) } return nil } diff --git a/utils/table_printer.go b/utils/table_printer.go new file mode 100644 index 000000000..fd38a928e --- /dev/null +++ b/utils/table_printer.go @@ -0,0 +1,108 @@ +package utils + +import ( + "fmt" + "io" + "os" + + "golang.org/x/crypto/ssh/terminal" +) + +func NewTablePrinter(w io.Writer) *TTYTablePrinter { + tty := false + ttyWidth := 80 + if outFile, isFile := w.(*os.File); isFile { + fd := int(outFile.Fd()) + tty = terminal.IsTerminal(fd) + if w, _, err := terminal.GetSize(fd); err == nil { + ttyWidth = w + } + } + return &TTYTablePrinter{ + out: w, + IsTTY: tty, + maxWidth: ttyWidth, + colWidths: []int{}, + colFuncs: make(map[int]func(string) string), + } +} + +type TTYTablePrinter struct { + out io.Writer + IsTTY bool + maxWidth int + colWidths []int + colFuncs map[int]func(string) string +} + +func (t *TTYTablePrinter) SetContentWidth(col, width int) { + if col == len(t.colWidths) { + t.colWidths = append(t.colWidths, 0) + } + if width > t.colWidths[col] { + t.colWidths[col] = width + } +} + +func (t *TTYTablePrinter) SetColorFunc(col int, colorize func(string) string) { + t.colFuncs[col] = colorize +} + +// FitColumns caps all but first column to fit available terminal width. +func (t *TTYTablePrinter) FitColumns() { + numCols := len(t.colWidths) + delimWidth := 2 + availWidth := t.maxWidth - t.colWidths[0] - ((numCols - 1) * delimWidth) + // TODO: avoid widening columns that already fit + // TODO: support weighted instead of even redistribution + for col := 1; col < len(t.colWidths); col++ { + t.colWidths[col] = availWidth / (numCols - 1) + } +} + +func (t *TTYTablePrinter) WriteRow(fields ...string) error { + lastCol := len(fields) - 1 + delim := "\t" + if t.IsTTY { + delim = " " + } + + for col, val := range fields { + if col > 0 { + _, err := fmt.Fprint(t.out, delim) + if err != nil { + return err + } + } + if t.IsTTY { + truncVal := truncate(t.colWidths[col], val) + if col != lastCol { + truncVal = fmt.Sprintf("%-*s", t.colWidths[col], truncVal) + } + if t.colFuncs[col] != nil { + truncVal = t.colFuncs[col](truncVal) + } + _, err := fmt.Fprint(t.out, truncVal) + if err != nil { + return err + } + } else { + _, err := fmt.Fprint(t.out, val) + if err != nil { + return err + } + } + } + _, err := fmt.Fprint(t.out, "\n") + if err != nil { + return err + } + return nil +} + +func truncate(maxLength int, title string) string { + if len(title) > maxLength { + return title[0:maxLength-3] + "..." + } + return title +} From d372467f5d2e57c9d551fde7b7c3d8a83c85ecb7 Mon Sep 17 00:00:00 2001 From: Tiernan L Date: Mon, 18 Nov 2019 14:31:00 -1000 Subject: [PATCH 03/53] Create bug_report.md --- .github/ISSUE_TEMPLATE/bug_report.md | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..d129a19a3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,39 @@ +--- +name: "\U0001F41B Bug report" +about: Report a problem encountered while using GitHub CLI + +--- + +### Describe the bug + +A clear and concise description of what the bug is. + +### Version & OS + +Type `gh --version` to see the CLI version. Also include what operating system you are using. + +### Steps to reproduce the behavior + +1. Type this '...' +2. View the output '....' +3. See error + +### Expected behavior + +A clear and concise description of what you expected to happen. + +### Actual behavior + +A clear and concise description of what actually happened. + +### Screenshots + +Add screenshots to help explain the bug, if you feel comfortable with sharing it. + +### Logs + +Paste the activity from your command line, if you feel comfortable with sharing it. + +### Additional context + +Add any other context about the bug here. From 30f36d6dd0c743276dba987d43516d397906efbf Mon Sep 17 00:00:00 2001 From: Tiernan L Date: Mon, 18 Nov 2019 14:33:05 -1000 Subject: [PATCH 04/53] Create problem-to-rasie.md --- .github/ISSUE_TEMPLATE/problem-to-rasie.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/problem-to-rasie.md diff --git a/.github/ISSUE_TEMPLATE/problem-to-rasie.md b/.github/ISSUE_TEMPLATE/problem-to-rasie.md new file mode 100644 index 000000000..46b344358 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/problem-to-rasie.md @@ -0,0 +1,17 @@ +--- +name: "\U00002B50 Submit a request or solve a problem" +about: Surface a problem that you think should be solved + +--- + +### Describe the feature or problem you’d like to solve + +A clear and concise description of what the feature or problem is. + +### Proposed solution + +How will it benefit CLI and its users? + +### Additional context + +Add any other context like screenshots or mockups are helpful, if applicable. From 4a5ed8157734460b75cdb279e7bebffcb0f666b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Tue, 19 Nov 2019 09:01:18 +0100 Subject: [PATCH 05/53] Fix injecting version information into build from git This was a typo. Note that Makefile is only used for building a development version after cloning from git; the tagged release process uses `.goreleaser.yml` and skips the Makefile. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 120d44cad..0aa490445 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ BUILD_FILES = $(shell go list -f '{{range .GoFiles}}{{$$.Dir}}/{{.}}\ {{end}}' ./...) -GH_VERSION = $(shell go describe --tags 2>/dev/null || git rev-parse --short HEAD) +GH_VERSION = $(shell git describe --tags 2>/dev/null || git rev-parse --short HEAD) LDFLAGS := -X github.com/github/gh-cli/command.Version=$(GH_VERSION) $(LDFLAGS) LDFLAGS := -X github.com/github/gh-cli/command.BuildDate=$(shell date +%Y-%m-%d) $(LDFLAGS) ifdef GH_OAUTH_CLIENT_SECRET From 39f535f0a179d0dcf1de0078e97acb07cf8acc97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Tue, 19 Nov 2019 12:01:46 +0100 Subject: [PATCH 06/53] Only show ratio of PR checks when some are failing Now the possible outputs are: - "checks: pending" (yellow) - "checks: success" (green) - "checks: failing" (red) - 1 out of 1 check failed - "checks: 3/5 failing" (red) - 3 out of 5 checks failed --- command/pr.go | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/command/pr.go b/command/pr.go index 924ce1666..775d10d6c 100644 --- a/command/pr.go +++ b/command/pr.go @@ -361,29 +361,20 @@ func printPrs(prs ...api.PullRequest) { fmt.Printf("\n ") } - if checks.Total > 1 { - var ratio string + if checks.Total > 0 { + var summary string if checks.Failing > 0 { - ratio = fmt.Sprintf("%d/%d", checks.Passing, checks.Total) - ratio = utils.Red(ratio) + if checks.Total > 1 { + summary = utils.Red(fmt.Sprintf("%d/%d failing", checks.Failing, checks.Total)) + } else { + summary = utils.Red("failing") + } } else if checks.Pending > 0 { - ratio = fmt.Sprintf("%d/%d", checks.Passing, checks.Total) - ratio = utils.Yellow(ratio) + summary = utils.Yellow("pending") } else if checks.Passing == checks.Total { - ratio = fmt.Sprintf("%d", checks.Total) - ratio = utils.Green(ratio) + summary = utils.Green("success") } - fmt.Printf(" - checks: %s", ratio) - } else if checks.Total == 1 { - var state string - if checks.Failing > 0 { - state = utils.Red("failing") - } else if checks.Pending > 0 { - state = utils.Yellow("pending") - } else if checks.Passing == checks.Total { - state = utils.Green("success") - } - fmt.Printf(" - checks: %s", state) + fmt.Printf(" - checks: %s", summary) } if reviews.ChangesRequested { From 26c1e4a170a3e185247d781bb089ea0490466eb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 20 Nov 2019 12:00:24 +0100 Subject: [PATCH 07/53] Align checks wording with dotcom --- command/pr.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/command/pr.go b/command/pr.go index 775d10d6c..203995554 100644 --- a/command/pr.go +++ b/command/pr.go @@ -364,17 +364,17 @@ func printPrs(prs ...api.PullRequest) { if checks.Total > 0 { var summary string if checks.Failing > 0 { - if checks.Total > 1 { - summary = utils.Red(fmt.Sprintf("%d/%d failing", checks.Failing, checks.Total)) + if checks.Failing == checks.Total { + summary = utils.Red("All checks failing") } else { - summary = utils.Red("failing") + summary = utils.Red(fmt.Sprintf("%d/%d checks failing", checks.Failing, checks.Total)) } } else if checks.Pending > 0 { - summary = utils.Yellow("pending") + summary = utils.Yellow("Checks pending") } else if checks.Passing == checks.Total { - summary = utils.Green("success") + summary = utils.Green("Checks passing") } - fmt.Printf(" - checks: %s", summary) + fmt.Printf(" - %s", summary) } if reviews.ChangesRequested { From 9fc80a1f8abcdb43366945f597d0b191738efb2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 20 Nov 2019 12:18:50 +0100 Subject: [PATCH 08/53] Fix crash with empty table --- utils/table_printer.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/utils/table_printer.go b/utils/table_printer.go index fd38a928e..fa66b5ee4 100644 --- a/utils/table_printer.go +++ b/utils/table_printer.go @@ -22,7 +22,7 @@ func NewTablePrinter(w io.Writer) *TTYTablePrinter { out: w, IsTTY: tty, maxWidth: ttyWidth, - colWidths: []int{}, + colWidths: make(map[int]int), colFuncs: make(map[int]func(string) string), } } @@ -31,14 +31,11 @@ type TTYTablePrinter struct { out io.Writer IsTTY bool maxWidth int - colWidths []int + colWidths map[int]int colFuncs map[int]func(string) string } func (t *TTYTablePrinter) SetContentWidth(col, width int) { - if col == len(t.colWidths) { - t.colWidths = append(t.colWidths, 0) - } if width > t.colWidths[col] { t.colWidths[col] = width } From 2022f8e74b83b09b3440ba395fa979535c92afbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 20 Nov 2019 12:30:24 +0100 Subject: [PATCH 09/53] Avoid widening table columns that already fit --- utils/table_printer.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/utils/table_printer.go b/utils/table_printer.go index fa66b5ee4..858c736ff 100644 --- a/utils/table_printer.go +++ b/utils/table_printer.go @@ -50,10 +50,19 @@ func (t *TTYTablePrinter) FitColumns() { numCols := len(t.colWidths) delimWidth := 2 availWidth := t.maxWidth - t.colWidths[0] - ((numCols - 1) * delimWidth) - // TODO: avoid widening columns that already fit + // add extra space from columns that are already narrower than threshold + for col := 1; col < len(t.colWidths); col++ { + availColWidth := availWidth / (numCols - 1) + if extra := availColWidth - t.colWidths[col]; extra > 0 { + availWidth += extra + } + } // TODO: support weighted instead of even redistribution for col := 1; col < len(t.colWidths); col++ { - t.colWidths[col] = availWidth / (numCols - 1) + availColWidth := availWidth / (numCols - 1) + if t.colWidths[col] > availColWidth { + t.colWidths[col] = availColWidth + } } } From 97a6dc494baefdca91011062ed6c1c5a41a903e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 20 Nov 2019 13:29:27 +0100 Subject: [PATCH 10/53] Redesign TablePrinter to avoid SetContentWidth / FitColumns steps The API is now: - AddField; - EndRow; - Render. --- command/pr.go | 42 +++++----- utils/table_printer.go | 172 +++++++++++++++++++++++++++-------------- 2 files changed, 137 insertions(+), 77 deletions(-) diff --git a/command/pr.go b/command/pr.go index 7135bbc55..7660e7740 100644 --- a/command/pr.go +++ b/command/pr.go @@ -167,33 +167,37 @@ func prList(cmd *cobra.Command, args []string) error { } table := utils.NewTablePrinter(cmd.OutOrStdout()) - for _, pr := range prs { - table.SetContentWidth(0, len(strconv.Itoa(pr.Number))+1) - table.SetContentWidth(1, len(pr.Title)) - table.SetContentWidth(2, len(pr.HeadLabel())) - } - - table.FitColumns() - table.SetColorFunc(2, utils.Cyan) - for _, pr := range prs { prNum := strconv.Itoa(pr.Number) - if table.IsTTY { + if table.IsTTY() { prNum = "#" + prNum } - switch pr.State { - case "OPEN": - table.SetColorFunc(0, utils.Green) - case "CLOSED": - table.SetColorFunc(0, utils.Red) - case "MERGED": - table.SetColorFunc(0, utils.Magenta) - } - table.WriteRow(prNum, pr.Title, pr.HeadLabel()) + table.AddField(prNum, nil, colorFuncForState(pr.State)) + table.AddField(pr.Title, nil, nil) + table.AddField(pr.HeadLabel(), nil, utils.Cyan) + table.EndRow() } + err = table.Render() + if err != nil { + return err + } + return nil } +func colorFuncForState(state string) func(string) string { + switch state { + case "OPEN": + return utils.Green + case "CLOSED": + return utils.Red + case "MERGED": + return utils.Magenta + default: + return nil + } +} + func prView(cmd *cobra.Command, args []string) error { ctx := contextForCommand(cmd) baseRepo, err := ctx.BaseRepo() diff --git a/utils/table_printer.go b/utils/table_printer.go index 858c736ff..1d763b8ce 100644 --- a/utils/table_printer.go +++ b/utils/table_printer.go @@ -8,101 +8,157 @@ import ( "golang.org/x/crypto/ssh/terminal" ) -func NewTablePrinter(w io.Writer) *TTYTablePrinter { - tty := false - ttyWidth := 80 +type TablePrinter interface { + IsTTY() bool + AddField(string, func(int, string) string, func(string) string) + EndRow() + Render() error +} + +func NewTablePrinter(w io.Writer) TablePrinter { if outFile, isFile := w.(*os.File); isFile { fd := int(outFile.Fd()) - tty = terminal.IsTerminal(fd) - if w, _, err := terminal.GetSize(fd); err == nil { - ttyWidth = w + if terminal.IsTerminal(fd) { + ttyWidth := 80 + if w, _, err := terminal.GetSize(fd); err == nil { + ttyWidth = w + } + return &ttyTablePrinter{ + out: w, + maxWidth: ttyWidth, + } } } - return &TTYTablePrinter{ - out: w, - IsTTY: tty, - maxWidth: ttyWidth, - colWidths: make(map[int]int), - colFuncs: make(map[int]func(string) string), + return &tsvTablePrinter{ + out: w, } } -type TTYTablePrinter struct { - out io.Writer - IsTTY bool - maxWidth int - colWidths map[int]int - colFuncs map[int]func(string) string +type tableField struct { + Text string + TruncateFunc func(int, string) string + ColorFunc func(string) string } -func (t *TTYTablePrinter) SetContentWidth(col, width int) { - if width > t.colWidths[col] { - t.colWidths[col] = width +type ttyTablePrinter struct { + out io.Writer + maxWidth int + rows [][]tableField +} + +func (t ttyTablePrinter) IsTTY() bool { + return true +} + +func (t *ttyTablePrinter) AddField(text string, truncateFunc func(int, string) string, colorFunc func(string) string) { + if truncateFunc == nil { + truncateFunc = truncate } + if t.rows == nil { + t.rows = [][]tableField{[]tableField{}} + } + rowI := len(t.rows) - 1 + field := tableField{ + Text: text, + TruncateFunc: truncateFunc, + ColorFunc: colorFunc, + } + t.rows[rowI] = append(t.rows[rowI], field) } -func (t *TTYTablePrinter) SetColorFunc(col int, colorize func(string) string) { - t.colFuncs[col] = colorize +func (t *ttyTablePrinter) EndRow() { + t.rows = append(t.rows, []tableField{}) } -// FitColumns caps all but first column to fit available terminal width. -func (t *TTYTablePrinter) FitColumns() { - numCols := len(t.colWidths) - delimWidth := 2 - availWidth := t.maxWidth - t.colWidths[0] - ((numCols - 1) * delimWidth) +func (t *ttyTablePrinter) Render() error { + if len(t.rows) == 0 { + return nil + } + + numCols := len(t.rows[0]) + colWidths := make([]int, numCols) + // measure maximum content width per column + for _, row := range t.rows { + for col, field := range row { + textLen := len(field.Text) + if textLen > colWidths[col] { + colWidths[col] = textLen + } + } + } + + delim := " " + availWidth := t.maxWidth - colWidths[0] - ((numCols - 1) * len(delim)) // add extra space from columns that are already narrower than threshold - for col := 1; col < len(t.colWidths); col++ { + for col := 1; col < numCols; col++ { availColWidth := availWidth / (numCols - 1) - if extra := availColWidth - t.colWidths[col]; extra > 0 { + if extra := availColWidth - colWidths[col]; extra > 0 { availWidth += extra } } + // cap all but first column to fit available terminal width // TODO: support weighted instead of even redistribution - for col := 1; col < len(t.colWidths); col++ { + for col := 1; col < numCols; col++ { availColWidth := availWidth / (numCols - 1) - if t.colWidths[col] > availColWidth { - t.colWidths[col] = availColWidth + if colWidths[col] > availColWidth { + colWidths[col] = availColWidth } } -} -func (t *TTYTablePrinter) WriteRow(fields ...string) error { - lastCol := len(fields) - 1 - delim := "\t" - if t.IsTTY { - delim = " " - } - - for col, val := range fields { - if col > 0 { - _, err := fmt.Fprint(t.out, delim) - if err != nil { - return err + for _, row := range t.rows { + for col, field := range row { + if col > 0 { + _, err := fmt.Fprint(t.out, delim) + if err != nil { + return err + } } - } - if t.IsTTY { - truncVal := truncate(t.colWidths[col], val) - if col != lastCol { - truncVal = fmt.Sprintf("%-*s", t.colWidths[col], truncVal) + truncVal := field.TruncateFunc(colWidths[col], field.Text) + if col < numCols-1 { + // pad value with spaces on the right + truncVal = fmt.Sprintf("%-*s", colWidths[col], truncVal) } - if t.colFuncs[col] != nil { - truncVal = t.colFuncs[col](truncVal) + if field.ColorFunc != nil { + truncVal = field.ColorFunc(truncVal) } _, err := fmt.Fprint(t.out, truncVal) if err != nil { return err } - } else { - _, err := fmt.Fprint(t.out, val) + } + if len(row) > 0 { + _, err := fmt.Fprint(t.out, "\n") if err != nil { return err } } } - _, err := fmt.Fprint(t.out, "\n") - if err != nil { - return err + return nil +} + +type tsvTablePrinter struct { + out io.Writer + currentCol int +} + +func (t tsvTablePrinter) IsTTY() bool { + return false +} + +func (t *tsvTablePrinter) AddField(text string, _ func(int, string) string, _ func(string) string) { + if t.currentCol > 0 { + fmt.Fprint(t.out, "\t") } + fmt.Fprint(t.out, text) + t.currentCol++ +} + +func (t *tsvTablePrinter) EndRow() { + fmt.Fprint(t.out, "\n") + t.currentCol = 0 +} + +func (t *tsvTablePrinter) Render() error { return nil } From 633c8c070b4e09eeb719600b4e1df51d3ec79772 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 20 Nov 2019 11:39:42 -0600 Subject: [PATCH 11/53] factor out title body prompting --- command/pr_create.go | 88 +++++------------------------- command/title_body_survey.go | 102 +++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 75 deletions(-) create mode 100644 command/title_body_survey.go diff --git a/command/pr_create.go b/command/pr_create.go index c8d86f53a..8f635e306 100644 --- a/command/pr_create.go +++ b/command/pr_create.go @@ -5,7 +5,6 @@ import ( "os" "runtime" - "github.com/AlecAivazis/survey/v2" "github.com/github/gh-cli/api" "github.com/github/gh-cli/context" "github.com/github/gh-cli/git" @@ -55,86 +54,25 @@ func prCreate(cmd *cobra.Command, _ []string) error { interactive := title == "" || body == "" - inProgress := struct { - Body string - Title string - }{} - if interactive { - confirmed := false - editor := determineEditor() + tb, err := titleBodySurvey(cmd, title, body) + if err != nil { + return errors.Wrap(err, "could not collect title and/or body") + } - for !confirmed { - titleQuestion := &survey.Question{ - Name: "title", - Prompt: &survey.Input{ - Message: "PR Title", - Default: inProgress.Title, - }, - } - bodyQuestion := &survey.Question{ - Name: "body", - Prompt: &survey.Editor{ - Message: fmt.Sprintf("PR Body (%s)", editor), - FileName: "*.md", - Default: inProgress.Body, - AppendDefault: true, - Editor: editor, - }, - } + if tb == nil { + // editing was canceled, we can just leave + return nil + } - qs := []*survey.Question{} - if title == "" { - qs = append(qs, titleQuestion) - } - if body == "" { - qs = append(qs, bodyQuestion) - } - - err := survey.Ask(qs, &inProgress) - if err != nil { - return errors.Wrap(err, "could not prompt") - } - confirmAnswers := struct { - Confirmation string - }{} - confirmQs := []*survey.Question{ - { - Name: "confirmation", - Prompt: &survey.Select{ - Message: "Submit?", - Options: []string{ - "Yes", - "Edit", - "Cancel", - }, - }, - }, - } - - err = survey.Ask(confirmQs, &confirmAnswers) - if err != nil { - return errors.Wrap(err, "could not prompt") - } - - switch confirmAnswers.Confirmation { - case "Yes": - confirmed = true - case "Edit": - continue - case "Cancel": - cmd.Println("Discarding pull request") - return nil - } + if title == "" { + title = tb.Title + } + if body == "" { + body = tb.Body } } - if title == "" { - title = inProgress.Title - } - if body == "" { - body = inProgress.Body - } base, err := cmd.Flags().GetString("base") if err != nil { return errors.Wrap(err, "could not parse base") diff --git a/command/title_body_survey.go b/command/title_body_survey.go new file mode 100644 index 000000000..c1d62ed4a --- /dev/null +++ b/command/title_body_survey.go @@ -0,0 +1,102 @@ +package command + +import ( + "fmt" + "github.com/AlecAivazis/survey/v2" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +type titleBody struct { + Body string + Title string +} + +const _confirmed = 0 +const _unconfirmed = 1 +const _cancel = 2 + +func confirm() (int, error) { + confirmAnswers := struct { + Confirmation int + }{} + confirmQs := []*survey.Question{ + { + Name: "confirmation", + Prompt: &survey.Select{ + Message: "Submit?", + Options: []string{ + "Yes", + "Edit", + "Cancel", + }, + }, + }, + } + + err := survey.Ask(confirmQs, &confirmAnswers) + if err != nil { + return -1, errors.Wrap(err, "could not prompt") + } + + return confirmAnswers.Confirmation, nil +} + +func titleBodySurvey(cmd *cobra.Command, providedTitle string, providedBody string) (*titleBody, error) { + inProgress := titleBody{} + + confirmed := false + editor := determineEditor() + + for !confirmed { + titleQuestion := &survey.Question{ + Name: "title", + Prompt: &survey.Input{ + Message: "Title", + Default: inProgress.Title, + }, + } + bodyQuestion := &survey.Question{ + Name: "body", + Prompt: &survey.Editor{ + Message: fmt.Sprintf("Body (%s)", editor), + FileName: "*.md", + Default: inProgress.Body, + AppendDefault: true, + Editor: editor, + }, + } + + qs := []*survey.Question{} + if providedTitle == "" { + qs = append(qs, titleQuestion) + } + if providedBody == "" { + qs = append(qs, bodyQuestion) + } + + err := survey.Ask(qs, &inProgress) + if err != nil { + return nil, errors.Wrap(err, "could not prompt") + } + + confirmA, err := confirm() + if err != nil { + return nil, errors.Wrap(err, "unable to confirm") + } + switch confirmA { + case _confirmed: + confirmed = true + case _unconfirmed: + continue + case _cancel: + cmd.Println("Discarding.") + return nil, nil + default: + panic("reached unreachable case") + } + + } + + return &inProgress, nil +} From 88446276e834ff44fa089dbe8a523690bf8dc689 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 20 Nov 2019 11:54:42 -0600 Subject: [PATCH 12/53] use survey when creating issues --- command/issue.go | 65 +++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/command/issue.go b/command/issue.go index ace7ac2e3..5e84d604c 100644 --- a/command/issue.go +++ b/command/issue.go @@ -2,15 +2,14 @@ package command import ( "fmt" - "io/ioutil" "os" "strconv" "strings" "github.com/github/gh-cli/api" "github.com/github/gh-cli/utils" + "github.com/pkg/errors" "github.com/spf13/cobra" - "golang.org/x/crypto/ssh/terminal" ) func init() { @@ -29,7 +28,10 @@ func init() { }, ) issueCmd.AddCommand(issueCreateCmd) - issueCreateCmd.Flags().StringArrayP("message", "m", nil, "set title and body") + issueCreateCmd.Flags().StringP("title", "t", "", + "Supply a title. Will prompt for one otherwise.") + issueCreateCmd.Flags().StringP("body", "b", "", + "Supply a body. Will prompt for one otherwise.") issueCreateCmd.Flags().BoolP("web", "w", false, "open the web browser to create an issue") issueListCmd := &cobra.Command{ @@ -189,44 +191,39 @@ func issueCreate(cmd *cobra.Command, args []string) error { return utils.OpenInBrowser(openURL) } - var title string - var body string - - message, err := cmd.Flags().GetStringArray("message") - if err != nil { - return err - } - apiClient, err := apiClientForContext(ctx) if err != nil { return err } - if len(message) > 0 { - title = message[0] - body = strings.Join(message[1:], "\n\n") - } else { - // TODO: open the text editor for issue title & body - input := os.Stdin - if terminal.IsTerminal(int(input.Fd())) { - cmd.Println("Enter the issue title and body; press Enter + Ctrl-D when done:") - } - inputBytes, err := ioutil.ReadAll(input) - if err != nil { - return err - } - - parts := strings.SplitN(string(inputBytes), "\n\n", 2) - if len(parts) > 0 { - title = parts[0] - } - if len(parts) > 1 { - body = parts[1] - } + title, err := cmd.Flags().GetString("title") + if err != nil { + return errors.Wrap(err, "could not parse title") + } + body, err := cmd.Flags().GetString("body") + if err != nil { + return errors.Wrap(err, "could not parse body") } - if title == "" { - return fmt.Errorf("aborting due to empty title") + interactive := title == "" || body == "" + + if interactive { + tb, err := titleBodySurvey(cmd, title, body) + if err != nil { + return errors.Wrap(err, "could not collect title and/or body") + } + + if tb == nil { + // editing was canceled, we can just leave + return nil + } + + if title == "" { + title = tb.Title + } + if body == "" { + body = tb.Body + } } params := map[string]interface{}{ "title": title, From 84d393d54381aec04c879aaa0f4c170a529e71b0 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 20 Nov 2019 11:57:17 -0600 Subject: [PATCH 13/53] fix issue create test --- command/issue_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command/issue_test.go b/command/issue_test.go index 09a55609f..4c4b7e2f6 100644 --- a/command/issue_test.go +++ b/command/issue_test.go @@ -144,7 +144,7 @@ func TestIssueCreate(t *testing.T) { out := bytes.Buffer{} issueCreateCmd.SetOut(&out) - RootCmd.SetArgs([]string{"issue", "create", "-m", "hello", "-m", "ab", "-m", "cd"}) + RootCmd.SetArgs([]string{"issue", "create", "-t", "hello", "-b", "cash rules everything around me"}) _, err := RootCmd.ExecuteC() if err != nil { t.Errorf("error running command `issue create`: %v", err) @@ -164,7 +164,7 @@ func TestIssueCreate(t *testing.T) { eq(t, reqBody.Variables.Input.RepositoryID, "REPOID") eq(t, reqBody.Variables.Input.Title, "hello") - eq(t, reqBody.Variables.Input.Body, "ab\n\ncd") + eq(t, reqBody.Variables.Input.Body, "cash rules everything around me") eq(t, out.String(), "https://github.com/OWNER/REPO/issues/12\n") } From 98062b7c0e0f880815c91d99343b2383b47a1767 Mon Sep 17 00:00:00 2001 From: evelyn masso Date: Wed, 20 Nov 2019 11:38:27 -0800 Subject: [PATCH 14/53] slightly more specific command summaries --- command/issue.go | 2 +- command/pr.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/command/issue.go b/command/issue.go index 6c4b0597e..84f1c07fd 100644 --- a/command/issue.go +++ b/command/issue.go @@ -46,7 +46,7 @@ func init() { var issueCmd = &cobra.Command{ Use: "issue", - Short: "Work with issues", + Short: "Create and view issues", Long: `Work with GitHub issues`, } var issueCreateCmd = &cobra.Command{ diff --git a/command/pr.go b/command/pr.go index ab1ad4ad5..988a9ae60 100644 --- a/command/pr.go +++ b/command/pr.go @@ -29,7 +29,7 @@ func init() { var prCmd = &cobra.Command{ Use: "pr", - Short: "Work with pull requests", + Short: "Create, view, and checkout pull requests", Long: `Work with GitHub pull requests.`, } var prCheckoutCmd = &cobra.Command{ From 11d569f340cba06c8ac7b17eb8e01ad9b033b2fc Mon Sep 17 00:00:00 2001 From: evelyn masso Date: Wed, 20 Nov 2019 15:10:14 -0800 Subject: [PATCH 15/53] expand root help summary --- command/root.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/command/root.go b/command/root.go index f2a73430d..7191d454c 100644 --- a/command/root.go +++ b/command/root.go @@ -37,7 +37,10 @@ type FlagError struct { var RootCmd = &cobra.Command{ Use: "gh", Short: "GitHub CLI", - Long: `Work with GitHub from your terminal`, + Long: `Integrate GitHub into your command-line workflow, starting + with issues and pull requests. + + (Note: gh is in a very alpha phase.)`, SilenceErrors: true, SilenceUsage: true, From b64db1213fc9fb8cdca0096fa7bb78a593678b43 Mon Sep 17 00:00:00 2001 From: evelyn masso Date: Wed, 20 Nov 2019 15:11:25 -0800 Subject: [PATCH 16/53] formatting :sweat_smile: --- command/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/root.go b/command/root.go index 7191d454c..e7ed45b63 100644 --- a/command/root.go +++ b/command/root.go @@ -37,7 +37,7 @@ type FlagError struct { var RootCmd = &cobra.Command{ Use: "gh", Short: "GitHub CLI", - Long: `Integrate GitHub into your command-line workflow, starting + Long: `Integrate GitHub into your command-line workflow, starting with issues and pull requests. (Note: gh is in a very alpha phase.)`, From 2a0771c1717e5e538ff48c470a7088b669a8a4d8 Mon Sep 17 00:00:00 2001 From: Tiernan L Date: Wed, 20 Nov 2019 14:27:53 -1000 Subject: [PATCH 17/53] minor updates --- .../{problem-to-rasie.md => submit-a-request.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename .github/ISSUE_TEMPLATE/{problem-to-rasie.md => submit-a-request.md} (72%) diff --git a/.github/ISSUE_TEMPLATE/problem-to-rasie.md b/.github/ISSUE_TEMPLATE/submit-a-request.md similarity index 72% rename from .github/ISSUE_TEMPLATE/problem-to-rasie.md rename to .github/ISSUE_TEMPLATE/submit-a-request.md index 46b344358..585afa21e 100644 --- a/.github/ISSUE_TEMPLATE/problem-to-rasie.md +++ b/.github/ISSUE_TEMPLATE/submit-a-request.md @@ -1,6 +1,6 @@ --- -name: "\U00002B50 Submit a request or solve a problem" -about: Surface a problem that you think should be solved +name: "\U00002B50 Submit a request" +about: Surface a feature or problem that you think should be solved --- From 4f7fbe29b44876f1f46779b0b17a8752a00ed51a Mon Sep 17 00:00:00 2001 From: Tiernan L Date: Wed, 20 Nov 2019 14:31:55 -1000 Subject: [PATCH 18/53] shortened template --- .github/ISSUE_TEMPLATE/bug_report.md | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d129a19a3..232436886 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,16 +1,12 @@ --- name: "\U0001F41B Bug report" -about: Report a problem encountered while using GitHub CLI +about: Report a bug or unexpected behavior while using GitHub CLI --- ### Describe the bug -A clear and concise description of what the bug is. - -### Version & OS - -Type `gh --version` to see the CLI version. Also include what operating system you are using. +A clear and concise description of what the bug is. Include version by typing `gh --version`. ### Steps to reproduce the behavior @@ -18,22 +14,10 @@ Type `gh --version` to see the CLI version. Also include what operating system y 2. View the output '....' 3. See error -### Expected behavior +### Expected vs actual behavior -A clear and concise description of what you expected to happen. - -### Actual behavior - -A clear and concise description of what actually happened. - -### Screenshots - -Add screenshots to help explain the bug, if you feel comfortable with sharing it. +A clear and concise description of what you expected to happen and what acutally happened. ### Logs -Paste the activity from your command line, if you feel comfortable with sharing it. - -### Additional context - -Add any other context about the bug here. +Paste the activity from your command line. Redact if needed. From 08687f70e0f2a88d7b4cbbfff11002d8586971bb Mon Sep 17 00:00:00 2001 From: Tiernan L Date: Wed, 20 Nov 2019 14:33:16 -1000 Subject: [PATCH 19/53] spelling --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 232436886..19e727fdf 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -16,7 +16,7 @@ A clear and concise description of what the bug is. Include version by typing `g ### Expected vs actual behavior -A clear and concise description of what you expected to happen and what acutally happened. +A clear and concise description of what you expected to happen and what actually happened. ### Logs From b30054c55a2d7af3697f6817ac8ea6733097f47c Mon Sep 17 00:00:00 2001 From: evelyn masso Date: Thu, 21 Nov 2019 10:20:54 -0800 Subject: [PATCH 20/53] use the offish tag line from @ampinsk --- command/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/root.go b/command/root.go index e7ed45b63..9b1040be6 100644 --- a/command/root.go +++ b/command/root.go @@ -37,7 +37,7 @@ type FlagError struct { var RootCmd = &cobra.Command{ Use: "gh", Short: "GitHub CLI", - Long: `Integrate GitHub into your command-line workflow, starting + Long: `Work seamlessly with GitHub from your command-line, starting with issues and pull requests. (Note: gh is in a very alpha phase.)`, From 7f3bc25e3ae3833518cbc0efa6ff61aba709e8a4 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 21 Nov 2019 13:22:23 -0600 Subject: [PATCH 21/53] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..b6a58a957 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 GitHub Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 7b9e29795ad1bbb1a020d7b3ac0fc7c36f6fa9a2 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Thu, 21 Nov 2019 13:34:25 -0600 Subject: [PATCH 22/53] add nfpms section to go-releaser config --- .goreleaser.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.goreleaser.yml b/.goreleaser.yml index eef85417e..d84bd18bd 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -24,6 +24,14 @@ archives: format_overrides: - goos: windows format: zip + +nfpms: + - license: MIT + maintainer: GitHub + homepage: https://github.com/github/gh-cli + formats: + - deb + changelog: sort: asc filters: From f2064bb0f96043cefdb8678f6ec9ff307819e75c Mon Sep 17 00:00:00 2001 From: vilmibm Date: Thu, 21 Nov 2019 16:17:36 -0600 Subject: [PATCH 23/53] fix path issue and set git as dependency --- .goreleaser.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.goreleaser.yml b/.goreleaser.yml index d84bd18bd..a0cfc2e10 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -29,6 +29,9 @@ nfpms: - license: MIT maintainer: GitHub homepage: https://github.com/github/gh-cli + bindir: /usr/local + dependencies: + - git formats: - deb From a650cbe0020681ea8ddd2c79dc9b7393ea26fb30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 22 Nov 2019 17:04:32 +0100 Subject: [PATCH 24/53] Simplify parsing API issues --- api/queries_issue.go | 98 +++++++++++----------------------- command/issue.go | 29 +++++++--- command/issue_test.go | 6 +-- test/fixtures/issueList.json | 20 ++----- test/fixtures/issueStatus.json | 14 ++--- 5 files changed, 61 insertions(+), 106 deletions(-) diff --git a/api/queries_issue.go b/api/queries_issue.go index 31793e608..69c3dd988 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -12,30 +12,23 @@ type IssuesPayload struct { } type Issue struct { - Number int - Title string - URL string - Labels []string - TotalLabelCount int + Number int + Title string + URL string + + Labels struct { + Nodes []IssueLabel + TotalCount int + } +} + +type IssueLabel struct { + Name string } type apiIssues struct { Issues struct { - Edges []struct { - Node struct { - Number int - Title string - URL string - Labels struct { - Edges []struct { - Node struct { - Name string - } - } - TotalCount int - } - } - } + Nodes []Issue } } @@ -43,11 +36,10 @@ const fragments = ` fragment issue on Issue { number title + url labels(first: 3) { - edges { - node { - name - } + nodes { + name } totalCount } @@ -104,28 +96,22 @@ func IssueStatus(client *Client, ghRepo Repo, currentUsername string) (*IssuesPa query($owner: String!, $repo: String!, $since: DateTime!, $viewer: String!, $per_page: Int = 10) { assigned: repository(owner: $owner, name: $repo) { issues(filterBy: {assignee: $viewer, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) { - edges { - node { - ...issue - } + nodes { + ...issue } } } mentioned: repository(owner: $owner, name: $repo) { issues(filterBy: {mentioned: $viewer, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) { - edges { - node { - ...issue - } + nodes { + ...issue } } } recent: repository(owner: $owner, name: $repo) { issues(filterBy: {since: $since, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) { - edges { - node { - ...issue - } + nodes { + ...issue } } } @@ -148,14 +134,10 @@ func IssueStatus(client *Client, ghRepo Repo, currentUsername string) (*IssuesPa return nil, err } - assigned := convertAPIToIssues(resp.Assigned) - mentioned := convertAPIToIssues(resp.Mentioned) - recent := convertAPIToIssues(resp.Recent) - payload := IssuesPayload{ - assigned, - mentioned, - recent, + Assigned: resp.Assigned.Issues.Nodes, + Mentioned: resp.Mentioned.Issues.Nodes, + Recent: resp.Recent.Issues.Nodes, } return &payload, nil @@ -191,10 +173,8 @@ func IssueList(client *Client, ghRepo Repo, state string, labels []string, assig query($owner: String!, $repo: String!, $limit: Int, $states: [IssueState!] = OPEN, $labels: [String!], $assignee: String) { repository(owner: $owner, name: $repo) { issues(first: $limit, orderBy: {field: CREATED_AT, direction: DESC}, states: $states, labels: $labels, filterBy: {assignee: $assignee}) { - edges { - node { - ...issue - } + nodes { + ...issue } } } @@ -221,27 +201,9 @@ func IssueList(client *Client, ghRepo Repo, state string, labels []string, assig return nil, err } - issues := convertAPIToIssues(resp.Repository) - return issues, nil -} - -func convertAPIToIssues(i apiIssues) []Issue { - var issues []Issue - for _, edge := range i.Issues.Edges { - var labels []string - for _, labelEdge := range edge.Node.Labels.Edges { - labels = append(labels, labelEdge.Node.Name) - } - - issue := Issue{ - Number: edge.Node.Number, - Title: edge.Node.Title, - URL: edge.Node.URL, - Labels: labels, - TotalLabelCount: edge.Node.Labels.TotalCount, - } + issues := []Issue{} + for _, issue := range resp.Repository.Issues.Nodes { issues = append(issues, issue) } - - return issues + return issues, nil } diff --git a/command/issue.go b/command/issue.go index 6c4b0597e..ad1db6066 100644 --- a/command/issue.go +++ b/command/issue.go @@ -245,14 +245,27 @@ func issueCreate(cmd *cobra.Command, args []string) error { func printIssues(prefix string, issues ...api.Issue) { for _, issue := range issues { number := utils.Green("#" + strconv.Itoa(issue.Number)) - var coloredLabels string - if len(issue.Labels) > 0 { - var ellipse string - if issue.TotalLabelCount > len(issue.Labels) { - ellipse = "…" - } - coloredLabels = utils.Gray(fmt.Sprintf(" (%s%s)", strings.Join(issue.Labels, ", "), ellipse)) + coloredLabels := labelList(issue) + if coloredLabels != "" { + coloredLabels = utils.Gray(fmt.Sprintf(" (%s)", coloredLabels)) } - fmt.Printf("%s%s %s %s\n", prefix, number, truncate(70, issue.Title), coloredLabels) + fmt.Printf("%s%s %s%s\n", prefix, number, truncate(70, issue.Title), coloredLabels) } } + +func labelList(issue api.Issue) string { + if len(issue.Labels.Nodes) == 0 { + return "" + } + + labelNames := []string{} + for _, label := range issue.Labels.Nodes { + labelNames = append(labelNames, label.Name) + } + + list := strings.Join(labelNames, ", ") + if issue.Labels.TotalCount > len(issue.Labels.Nodes) { + list += ", …" + } + return list +} diff --git a/command/issue_test.go b/command/issue_test.go index 09a55609f..7c76859f7 100644 --- a/command/issue_test.go +++ b/command/issue_test.go @@ -55,9 +55,9 @@ func TestIssueList(t *testing.T) { } expectedIssues := []*regexp.Regexp{ - regexp.MustCompile(`#1.*won`), - regexp.MustCompile(`#2.*too`), - regexp.MustCompile(`#4.*fore`), + regexp.MustCompile(`(?m)^1\t.*won`), + regexp.MustCompile(`(?m)^2\t.*too`), + regexp.MustCompile(`(?m)^4\t.*fore`), } for _, r := range expectedIssues { diff --git a/test/fixtures/issueList.json b/test/fixtures/issueList.json index 17e92c743..b22899cee 100644 --- a/test/fixtures/issueList.json +++ b/test/fixtures/issueList.json @@ -2,57 +2,45 @@ "data": { "repository": { "issues": { - "edges": [ + "nodes": [ { - "node": { "number": 1, "title": "number won", "url": "https://wow.com", "labels": { - "edges": [ + "nodes": [ { - "node": { "name": "label" - } } ], "totalCount": 1 } - } }, { - "node": { "number": 2, "title": "number too", "url": "https://wow.com", "labels": { - "edges": [ + "nodes": [ { - "node": { "name": "label" - } } ], "totalCount": 1 } - } }, { - "node": { "number": 4, "title": "number fore", "url": "https://wow.com", "labels": { - "edges": [ + "nodes": [ { - "node": { "name": "label" - } } ], "totalCount": 1 } - } } ] } diff --git a/test/fixtures/issueStatus.json b/test/fixtures/issueStatus.json index 784c0cc8f..37d0818da 100644 --- a/test/fixtures/issueStatus.json +++ b/test/fixtures/issueStatus.json @@ -2,43 +2,35 @@ "data": { "assigned": { "issues": { - "edges": [ + "nodes": [ { - "node": { "number": 9, "title": "corey thinks squash tastes bad" - } }, { - "node": { "number": 10, "title": "broccoli is a superfood" - } } ] } }, "mentioned": { "issues": { - "edges": [ + "nodes": [ { - "node": { "number": 8, "title": "rabbits eat carrots" - } }, { - "node": { "number": 11, "title": "swiss chard is neutral" - } } ] } }, "recent": { "issues": { - "edges": [] + "nodes": [] } }, From fdcf028cce59bef18c03e7e927d47855d01e3df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 22 Nov 2019 17:15:11 +0100 Subject: [PATCH 25/53] Align `issue list` output with that of `pr list` --- api/queries_issue.go | 2 ++ command/issue.go | 26 +++++++++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/api/queries_issue.go b/api/queries_issue.go index 69c3dd988..da98e8442 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -15,6 +15,7 @@ type Issue struct { Number int Title string URL string + State string Labels struct { Nodes []IssueLabel @@ -37,6 +38,7 @@ const fragments = ` number title url + state labels(first: 3) { nodes { name diff --git a/command/issue.go b/command/issue.go index ad1db6066..f0aa5be78 100644 --- a/command/issue.go +++ b/command/issue.go @@ -92,12 +92,28 @@ func issueList(cmd *cobra.Command, args []string) error { return err } - if len(issues) > 0 { - printIssues("", issues...) - } else { - message := fmt.Sprintf("There are no open issues") - printMessage(message) + if len(issues) == 0 { + printMessage("There are no open issues") + return nil } + + table := utils.NewTablePrinter(cmd.OutOrStdout()) + for _, issue := range issues { + issueNum := strconv.Itoa(issue.Number) + if table.IsTTY() { + issueNum = "#" + issueNum + } + labels := labelList(issue) + if labels != "" && table.IsTTY() { + labels = fmt.Sprintf("(%s)", labels) + } + table.AddField(issueNum, nil, colorFuncForState(issue.State)) + table.AddField(issue.Title, nil, nil) + table.AddField(labels, nil, utils.Gray) + table.EndRow() + } + table.Render() + return nil } From 977d47b263180e3f7bedd2270652959eef41d2e9 Mon Sep 17 00:00:00 2001 From: Amanda Pinsker Date: Fri, 22 Nov 2019 11:07:59 -0800 Subject: [PATCH 26/53] Shorten top summary --- command/root.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/command/root.go b/command/root.go index 9b1040be6..489cd7f30 100644 --- a/command/root.go +++ b/command/root.go @@ -37,11 +37,7 @@ type FlagError struct { var RootCmd = &cobra.Command{ Use: "gh", Short: "GitHub CLI", - Long: `Work seamlessly with GitHub from your command-line, starting - with issues and pull requests. - - (Note: gh is in a very alpha phase.)`, - + Long: `Work seamlessly with GitHub from the command line`, SilenceErrors: true, SilenceUsage: true, } From 9c459659e7503b6784dff1abc9ee59ae96536a3f Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 22 Nov 2019 14:51:25 -0600 Subject: [PATCH 27/53] try out rpm builds --- .goreleaser.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.goreleaser.yml b/.goreleaser.yml index a0cfc2e10..dbddbb888 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -34,6 +34,7 @@ nfpms: - git formats: - deb + - rpm changelog: sort: asc From 23c7eeb6b0a5f6cd851847d70d2d32c611460657 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 22 Nov 2019 14:51:52 -0600 Subject: [PATCH 28/53] add linux instructions and test release instructions to readme --- README.md | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 47f80bd8b..73470d65c 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,27 @@ This tool is an endeavor separate from [github/hub](https://github.com/github/hu _warning, gh is in a very alpha phase_ +## OSX + `brew install github/gh/gh` -That's it. You are now ready to use `gh` on the command line. 🥳 +## Debian/Ubuntu Linux + +1. Download the latest `.deb` file from the [releases page](https://github.com/github/gh-cli/releases) +2. Install it with `sudo dpkg -i gh_0.2.2_linux_amd64.deb`, changing version number accordingly + +_(Uninstall with `sudo apt remove gh`)_ + +## Fedora/Centos Linux + +1. Download the latest `.rpm` file from the [releases page](https://github.com/github/gh-cli/releases) +2. Install it with `sudo rpm -ivh gh_0.2.2_linux_amd64.rpm`, changing version number accordingly + +## Other Linux + +1. Download the latest `_linux_amd64.tar.gz` file from the [releases page](https://github.com/github/gh-cli/releases) +2. `tar -xvf gh_0.2.2_linux_amd64.tar.gz`, changing version number accordingly +3. Copy the uncompressed `gh` somewhere on your `$PATH` (e.g. `sudo cp gh /usr/local/bin/`) # Process @@ -25,4 +43,13 @@ This can all be done from your local terminal. 1. `git tag 'vVERSION_NUMBER' # example git tag 'v0.0.1'` 2. `git push origin vVERSION_NUMBER` 3. Wait a few minutes for the build to run and CI to pass. Look at the [actions tab](https://github.com/github/gh-cli/actions) to check the progress. -4. Go to https://github.com/github/homebrew-gh/releases and look at the release +4. Go to and look at the release + +# Test a release + +A local release can be created for testing without creating anything official on the release page. + +1. `git tag 'v6.6.6' # some throwaway version number` +2. `env GH_OAUTH_CLIENT_SECRET=foobar GH_OAUTH_CLIENT_ID=1234 goreleaser --skip-publish --rm-dist` +3. Check and test files in `dist/` +4. `git tag -d v6.6.6 # delete the throwaway tag` From 234702717450f7a688e6f86da3547a522df94abf Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 22 Nov 2019 15:17:38 -0600 Subject: [PATCH 29/53] improve rpm instructions --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 73470d65c..aad7b2da8 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,9 @@ _(Uninstall with `sudo apt remove gh`)_ ## Fedora/Centos Linux 1. Download the latest `.rpm` file from the [releases page](https://github.com/github/gh-cli/releases) -2. Install it with `sudo rpm -ivh gh_0.2.2_linux_amd64.rpm`, changing version number accordingly +2. Install it with `sudo yum localinstall gh_0.2.2_linux_amd64.rpm`, changing version number accordingly + +_(Uninstall with `sudo yum remove gh`)_ ## Other Linux @@ -30,6 +32,8 @@ _(Uninstall with `sudo apt remove gh`)_ 2. `tar -xvf gh_0.2.2_linux_amd64.tar.gz`, changing version number accordingly 3. Copy the uncompressed `gh` somewhere on your `$PATH` (e.g. `sudo cp gh /usr/local/bin/`) +_(Uninstall with `rm`)_ + # Process - [Demo planning doc](https://docs.google.com/document/d/18ym-_xjFTSXe0-xzgaBn13Su7MEhWfLE5qSNPJV4M0A/edit) From 619c42fc8701464537cb9e24d272542d1e7d8acc Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 25 Nov 2019 11:33:59 -0600 Subject: [PATCH 30/53] use iota --- command/title_body_survey.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/command/title_body_survey.go b/command/title_body_survey.go index c1d62ed4a..af4dc34e1 100644 --- a/command/title_body_survey.go +++ b/command/title_body_survey.go @@ -12,9 +12,11 @@ type titleBody struct { Title string } -const _confirmed = 0 -const _unconfirmed = 1 -const _cancel = 2 +const ( + _confirmed = iota + _unconfirmed = iota + _cancel = iota +) func confirm() (int, error) { confirmAnswers := struct { From 7f4833d84eb2bcd1337f583787dfaa9775d50262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Tue, 26 Nov 2019 14:14:31 +0100 Subject: [PATCH 31/53] :fire: unused code --- ui/ui.go | 98 -------------------------------------------------- utils/utils.go | 73 ------------------------------------- 2 files changed, 171 deletions(-) delete mode 100644 ui/ui.go diff --git a/ui/ui.go b/ui/ui.go deleted file mode 100644 index a824ce963..000000000 --- a/ui/ui.go +++ /dev/null @@ -1,98 +0,0 @@ -package ui - -import ( - "fmt" - "io" - "os" - - "github.com/mattn/go-colorable" - "github.com/mattn/go-isatty" -) - -type UI interface { - Print(a ...interface{}) (n int, err error) - Printf(format string, a ...interface{}) (n int, err error) - Println(a ...interface{}) (n int, err error) - Errorf(format string, a ...interface{}) (n int, err error) - Errorln(a ...interface{}) (n int, err error) -} - -var ( - Stdout = colorable.NewColorableStdout() - Stderr = colorable.NewColorableStderr() - Default UI = Console{Stdout: Stdout, Stderr: Stderr} -) - -func Print(a ...interface{}) (n int) { - n, err := Default.Print(a...) - if err != nil { - // If something as basic as printing to stdout fails, just panic and exit - os.Exit(1) - } - return -} - -func Printf(format string, a ...interface{}) (n int) { - n, err := Default.Printf(format, a...) - if err != nil { - // If something as basic as printing to stdout fails, just panic and exit - os.Exit(1) - } - return -} - -func Println(a ...interface{}) (n int) { - n, err := Default.Println(a...) - if err != nil { - // If something as basic as printing to stdout fails, just panic and exit - os.Exit(1) - } - return -} - -func Errorf(format string, a ...interface{}) (n int) { - n, err := Default.Errorf(format, a...) - if err != nil { - // If something as basic as printing to stderr fails, just panic and exit - os.Exit(1) - } - return -} - -func Errorln(a ...interface{}) (n int) { - n, err := Default.Errorln(a...) - if err != nil { - // If something as basic as printing to stderr fails, just panic and exit - os.Exit(1) - } - return -} - -func IsTerminal(f *os.File) bool { - return isatty.IsTerminal(f.Fd()) -} - -type Console struct { - Stdout io.Writer - Stderr io.Writer -} - -func (c Console) Print(a ...interface{}) (n int, err error) { - return fmt.Fprint(c.Stdout, a...) -} - -func (c Console) Printf(format string, a ...interface{}) (n int, err error) { - return fmt.Fprintf(c.Stdout, format, a...) -} - -func (c Console) Println(a ...interface{}) (n int, err error) { - return fmt.Fprintln(c.Stdout, a...) -} - -func (c Console) Errorf(format string, a ...interface{}) (n int, err error) { - return fmt.Fprintf(c.Stderr, format, a...) -} - -func (c Console) Errorln(a ...interface{}) (n int, err error) { - return fmt.Fprintln(c.Stderr, a...) -} diff --git a/utils/utils.go b/utils/utils.go index b034a18d9..6a1730cb8 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -2,31 +2,13 @@ package utils import ( "errors" - "fmt" "os" "os/exec" - "path/filepath" "runtime" - "strings" - "time" - "github.com/github/gh-cli/ui" "github.com/kballard/go-shellquote" ) -var timeNow = time.Now - -func Check(err error) { - if err != nil { - ui.Errorln(err) - os.Exit(1) - } -} - -func ConcatPaths(paths ...string) string { - return strings.Join(paths, "/") -} - func OpenInBrowser(url string) error { browser := os.Getenv("BROWSER") if browser == "" { @@ -69,58 +51,3 @@ func searchBrowserLauncher(goos string) (browser string) { return browser } - -func CommandPath(cmd string) (string, error) { - if runtime.GOOS == "windows" { - cmd = cmd + ".exe" - } - - path, err := exec.LookPath(cmd) - if err != nil { - return "", err - } - - path, err = filepath.Abs(path) - if err != nil { - return "", err - } - - return filepath.EvalSymlinks(path) -} - -func TimeAgo(t time.Time) string { - duration := timeNow().Sub(t) - minutes := duration.Minutes() - hours := duration.Hours() - days := hours / 24 - months := days / 30 - years := months / 12 - - var val int - var unit string - - if minutes < 1 { - return "now" - } else if hours < 1 { - val = int(minutes) - unit = "minute" - } else if days < 1 { - val = int(hours) - unit = "hour" - } else if months < 1 { - val = int(days) - unit = "day" - } else if years < 1 { - val = int(months) - unit = "month" - } else { - val = int(years) - unit = "year" - } - - var plural string - if val > 1 { - plural = "s" - } - return fmt.Sprintf("%d %s%s ago", val, unit, plural) -} From d99698f048b7c5486416b863aa9a73a671c54de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Tue, 26 Nov 2019 16:18:20 +0100 Subject: [PATCH 32/53] Fix minor code issues discovered by staticcheck `honnef.co/go/tools/cmd/staticcheck` --- api/queries_issue.go | 6 +----- command/pr_test.go | 4 ++-- context/config_setup.go | 3 +++ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/api/queries_issue.go b/api/queries_issue.go index da98e8442..54aab484f 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -203,9 +203,5 @@ func IssueList(client *Client, ghRepo Repo, state string, labels []string, assig return nil, err } - issues := []Issue{} - for _, issue := range resp.Repository.Issues.Nodes { - issues = append(issues, issue) - } - return issues, nil + return resp.Repository.Issues.Nodes, nil } diff --git a/command/pr_test.go b/command/pr_test.go index 47851a5e8..701874f13 100644 --- a/command/pr_test.go +++ b/command/pr_test.go @@ -147,7 +147,7 @@ func TestPRView_NoActiveBranch(t *testing.T) { }) defer restoreCmd() - output, err := test.RunCommand(RootCmd, "pr view") + _, err := test.RunCommand(RootCmd, "pr view") if err == nil || err.Error() != "the 'master' branch has no open pull requests" { t.Errorf("error running command `pr view`: %v", err) } @@ -157,7 +157,7 @@ func TestPRView_NoActiveBranch(t *testing.T) { } // Now run again but provide a PR number - output, err = test.RunCommand(RootCmd, "pr view 23") + output, err := test.RunCommand(RootCmd, "pr view 23") if err != nil { t.Errorf("error running command `pr view`: %v", err) } diff --git a/context/config_setup.go b/context/config_setup.go index bb7addbce..48a037a24 100644 --- a/context/config_setup.go +++ b/context/config_setup.go @@ -66,6 +66,9 @@ func setupConfigFile(filename string) (*configEntry, error) { defer config.Close() yamlData, err := yaml.Marshal(data) + if err != nil { + return nil, err + } n, err := config.Write(yamlData) if err == nil && n < len(yamlData) { err = io.ErrShortWrite From 62bbcb266e3891b6a2082644f83472b333633317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Tue, 26 Nov 2019 16:23:03 +0100 Subject: [PATCH 33/53] :fire: more unused git functions --- git/git.go | 101 ----------------------------------------------------- 1 file changed, 101 deletions(-) diff --git a/git/git.go b/git/git.go index 8824872a4..7732751bd 100644 --- a/git/git.go +++ b/git/git.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "io/ioutil" - "os" "os/exec" "path/filepath" "strings" @@ -31,16 +30,6 @@ func Dir() (string, error) { return gitDir, nil } -func WorkdirName() (string, error) { - toplevelCmd := exec.Command("git", "rev-parse", "--show-toplevel") - output, err := utils.PrepareCmd(toplevelCmd).Output() - dir := firstLine(output) - if dir == "" { - return "", fmt.Errorf("unable to determine git working directory") - } - return dir, err -} - func VerifyRef(ref string) bool { showRef := exec.Command("git", "show-ref", "--verify", "--quiet", ref) err := utils.PrepareCmd(showRef).Run() @@ -73,72 +62,10 @@ func BranchAtRef(paths ...string) (name string, err error) { return } -func Editor() (string, error) { - varCmd := exec.Command("git", "var", "GIT_EDITOR") - output, err := utils.PrepareCmd(varCmd).Output() - if err != nil { - return "", fmt.Errorf("Can't load git var: GIT_EDITOR") - } - - return os.ExpandEnv(firstLine(output)), nil -} - func Head() (string, error) { return BranchAtRef("HEAD") } -func SymbolicFullName(name string) (string, error) { - parseCmd := exec.Command("git", "rev-parse", "--symbolic-full-name", name) - output, err := utils.PrepareCmd(parseCmd).Output() - if err != nil { - return "", fmt.Errorf("Unknown revision or path not in the working tree: %s", name) - } - - return firstLine(output), nil -} - -func CommentChar(text string) (string, error) { - char, err := Config("core.commentchar") - if err != nil { - return "#", nil - } else if char == "auto" { - lines := strings.Split(text, "\n") - commentCharCandidates := strings.Split("#;@!$%^&|:", "") - candidateLoop: - for _, candidate := range commentCharCandidates { - for _, line := range lines { - if strings.HasPrefix(line, candidate) { - continue candidateLoop - } - } - return candidate, nil - } - return "", fmt.Errorf("unable to select a comment character that is not used in the current message") - } else { - return char, nil - } -} - -func Show(sha string) (string, error) { - cmd := exec.Command("git", "-c", "log.showSignature=false", "show", "-s", "--format=%s%n%+b", sha) - output, err := utils.PrepareCmd(cmd).Output() - return strings.TrimSpace(string(output)), err -} - -func Log(sha1, sha2 string) (string, error) { - shaRange := fmt.Sprintf("%s...%s", sha1, sha2) - cmd := exec.Command( - "-c", "log.showSignature=false", "log", "--no-color", - "--format=%h (%aN, %ar)%n%w(78,3,3)%s%n%+b", - "--cherry", shaRange) - outputs, err := utils.PrepareCmd(cmd).Output() - if err != nil { - return "", fmt.Errorf("Can't load git log %s..%s", sha1, sha2) - } - - return string(outputs), nil -} - func listRemotes() ([]string, error) { remoteCmd := exec.Command("git", "remote", "-v") output, err := utils.PrepareCmd(remoteCmd).Output() @@ -156,34 +83,6 @@ func Config(name string) (string, error) { } -func ConfigAll(name string) ([]string, error) { - mode := "--get-all" - if strings.Contains(name, "*") { - mode = "--get-regexp" - } - - configCmd := exec.Command("git", "config", mode, name) - output, err := utils.PrepareCmd(configCmd).Output() - if err != nil { - return nil, fmt.Errorf("Unknown config %s", name) - } - return outputLines(output), nil -} - -func LocalBranches() ([]string, error) { - branchesCmd := exec.Command("git", "branch", "--list") - output, err := utils.PrepareCmd(branchesCmd).Output() - if err != nil { - return nil, err - } - - branches := []string{} - for _, branch := range outputLines(output) { - branches = append(branches, branch[2:]) - } - return branches, nil -} - var GitCommand = func(args ...string) *exec.Cmd { return exec.Command("git", args...) } From 52a1575fc1c03f2bcce76803bf570053b7852bae Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 26 Nov 2019 10:22:13 -0600 Subject: [PATCH 34/53] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aad7b2da8..d8e9a6edc 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This tool is an endeavor separate from [github/hub](https://github.com/github/hu _warning, gh is in a very alpha phase_ -## OSX +## macOS `brew install github/gh/gh` From d9d745447b8a6d1219e5f0c2cab661d8522bf74d Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 26 Nov 2019 11:52:13 -0600 Subject: [PATCH 35/53] Update README.md --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d8e9a6edc..9388f0f4d 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,14 @@ _warning, gh is in a very alpha phase_ ## Debian/Ubuntu Linux -1. Download the latest `.deb` file from the [releases page](https://github.com/github/gh-cli/releases) +1. Download the `.deb` file from the [releases page](https://github.com/github/gh-cli/releases/latest) 2. Install it with `sudo dpkg -i gh_0.2.2_linux_amd64.deb`, changing version number accordingly _(Uninstall with `sudo apt remove gh`)_ ## Fedora/Centos Linux -1. Download the latest `.rpm` file from the [releases page](https://github.com/github/gh-cli/releases) +1. Download the `.rpm` file from the [releases page](https://github.com/github/gh-cli/releases/latest) 2. Install it with `sudo yum localinstall gh_0.2.2_linux_amd64.rpm`, changing version number accordingly _(Uninstall with `sudo yum remove gh`)_ @@ -29,8 +29,9 @@ _(Uninstall with `sudo yum remove gh`)_ ## Other Linux 1. Download the latest `_linux_amd64.tar.gz` file from the [releases page](https://github.com/github/gh-cli/releases) -2. `tar -xvf gh_0.2.2_linux_amd64.tar.gz`, changing version number accordingly -3. Copy the uncompressed `gh` somewhere on your `$PATH` (e.g. `sudo cp gh /usr/local/bin/`) +2. `tar -xf gh_0.2.2_linux_amd64.tar.gz`, changing version number accordingly +3. `chmod +x gh` ensure the binary is executable +4. Copy the uncompressed `gh` somewhere on your `$PATH` (e.g. `sudo cp gh /usr/local/bin/`) _(Uninstall with `rm`)_ From 295c5d122b973de8dffdb096ae93191ac9b52702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 27 Nov 2019 14:08:42 +0100 Subject: [PATCH 36/53] Fix opening OAuth URL in browser --- auth/oauth.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/auth/oauth.go b/auth/oauth.go index dba975f41..8325519fd 100644 --- a/auth/oauth.go +++ b/auth/oauth.go @@ -11,6 +11,7 @@ import ( "os" "os/exec" "runtime" + "strings" ) func randomString(length int) (string, error) { @@ -101,11 +102,14 @@ func openInBrowser(url string) error { args = []string{"open"} case "windows": args = []string{"cmd", "/c", "start"} + r := strings.NewReplacer("&", "^&") + url = r.Replace(url) default: args = []string{"xdg-open"} } args = append(args, url) cmd := exec.Command(args[0], args[1:]...) + cmd.Stderr = os.Stderr return cmd.Run() } From 7d904fdef7bae09d9373850346daccfcb7a24447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 27 Nov 2019 15:31:37 +0100 Subject: [PATCH 37/53] Fix detecting terminal under Git Bash on Windows --- utils/table_printer.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/utils/table_printer.go b/utils/table_printer.go index 1d763b8ce..177663096 100644 --- a/utils/table_printer.go +++ b/utils/table_printer.go @@ -4,7 +4,11 @@ import ( "fmt" "io" "os" + "os/exec" + "strconv" + "strings" + "github.com/mattn/go-isatty" "golang.org/x/crypto/ssh/terminal" ) @@ -17,11 +21,19 @@ type TablePrinter interface { func NewTablePrinter(w io.Writer) TablePrinter { if outFile, isFile := w.(*os.File); isFile { - fd := int(outFile.Fd()) - if terminal.IsTerminal(fd) { + isCygwin := isatty.IsCygwinTerminal(outFile.Fd()) + if isatty.IsTerminal(outFile.Fd()) || isCygwin { ttyWidth := 80 - if w, _, err := terminal.GetSize(fd); err == nil { + if w, _, err := terminal.GetSize(int(outFile.Fd())); err == nil { ttyWidth = w + } else if isCygwin { + tputCmd := exec.Command("tput", "cols") + tputCmd.Stdin = os.Stdin + if out, err := tputCmd.Output(); err == nil { + if w, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil { + ttyWidth = w + } + } } return &ttyTablePrinter{ out: w, From 4be04aded367e14539e8b628859a35c852eff59a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 27 Nov 2019 15:43:28 +0100 Subject: [PATCH 38/53] Fix ANSI color output on Windows --- utils/table_printer.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils/table_printer.go b/utils/table_printer.go index 1d763b8ce..451b546e7 100644 --- a/utils/table_printer.go +++ b/utils/table_printer.go @@ -5,6 +5,7 @@ import ( "io" "os" + "github.com/mattn/go-colorable" "golang.org/x/crypto/ssh/terminal" ) @@ -24,7 +25,7 @@ func NewTablePrinter(w io.Writer) TablePrinter { ttyWidth = w } return &ttyTablePrinter{ - out: w, + out: colorable.NewColorable(outFile), maxWidth: ttyWidth, } } From 4ec31ffd57cb3a60bc7a5d70eba89129b5c17b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 27 Nov 2019 16:42:13 +0100 Subject: [PATCH 39/53] Strip the "v" prefix when displaying gh version string --- command/root.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/command/root.go b/command/root.go index 489cd7f30..4cbe2ddd1 100644 --- a/command/root.go +++ b/command/root.go @@ -3,6 +3,7 @@ package command import ( "fmt" "os" + "strings" "github.com/github/gh-cli/api" "github.com/github/gh-cli/context" @@ -17,7 +18,7 @@ var Version = "DEV" var BuildDate = "YYYY-MM-DD" func init() { - RootCmd.Version = fmt.Sprintf("%s (%s)", Version, BuildDate) + RootCmd.Version = fmt.Sprintf("%s (%s)", strings.TrimPrefix(Version, "v"), BuildDate) RootCmd.PersistentFlags().StringP("repo", "R", "", "current GitHub repository") RootCmd.PersistentFlags().StringP("current-branch", "B", "", "current git branch") // TODO: From b8251650db2f2d39c3773e08b6bac3c54b89186c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 27 Nov 2019 16:42:38 +0100 Subject: [PATCH 40/53] Have `gh version` be an alias for `gh --version` --- command/root.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/command/root.go b/command/root.go index 4cbe2ddd1..e8764c44b 100644 --- a/command/root.go +++ b/command/root.go @@ -19,6 +19,8 @@ var BuildDate = "YYYY-MM-DD" func init() { RootCmd.Version = fmt.Sprintf("%s (%s)", strings.TrimPrefix(Version, "v"), BuildDate) + RootCmd.AddCommand(versionCmd) + RootCmd.PersistentFlags().StringP("repo", "R", "", "current GitHub repository") RootCmd.PersistentFlags().StringP("current-branch", "B", "", "current git branch") // TODO: @@ -38,11 +40,20 @@ type FlagError struct { var RootCmd = &cobra.Command{ Use: "gh", Short: "GitHub CLI", - Long: `Work seamlessly with GitHub from the command line`, + Long: `Work seamlessly with GitHub from the command line`, + SilenceErrors: true, SilenceUsage: true, } +var versionCmd = &cobra.Command{ + Use: "version", + Hidden: true, + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("gh version %s\n", RootCmd.Version) + }, +} + // overriden in tests var initContext = func() context.Context { ctx := context.New() From bfdef59377cdcbee135d4002e895cbe9400b6520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 27 Nov 2019 16:49:24 +0100 Subject: [PATCH 41/53] :nail_care: consistent sub-command initialization style --- command/issue.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/command/issue.go b/command/issue.go index 0c022939f..7a5a1b086 100644 --- a/command/issue.go +++ b/command/issue.go @@ -14,20 +14,9 @@ import ( func init() { RootCmd.AddCommand(issueCmd) - issueCmd.AddCommand( - &cobra.Command{ - Use: "status", - Short: "Show status of relevant issues", - RunE: issueStatus, - }, - &cobra.Command{ - Use: "view ", - Args: cobra.MinimumNArgs(1), - Short: "View an issue in the browser", - RunE: issueView, - }, - ) issueCmd.AddCommand(issueCreateCmd) + issueCmd.AddCommand(issueStatusCmd) + issueCmd.AddCommand(issueViewCmd) issueCreateCmd.Flags().StringP("title", "t", "", "Supply a title. Will prompt for one otherwise.") issueCreateCmd.Flags().StringP("body", "b", "", @@ -56,6 +45,17 @@ var issueCreateCmd = &cobra.Command{ Short: "Create a new issue", RunE: issueCreate, } +var issueStatusCmd = &cobra.Command{ + Use: "status", + Short: "Show status of relevant issues", + RunE: issueStatus, +} +var issueViewCmd = &cobra.Command{ + Use: "view ", + Args: cobra.MinimumNArgs(1), + Short: "View an issue in the browser", + RunE: issueView, +} func issueList(cmd *cobra.Command, args []string) error { ctx := contextForCommand(cmd) From 617957087351f686abc4eb82a41a7b9fe030e47e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 27 Nov 2019 16:56:53 +0100 Subject: [PATCH 42/53] Replace "Recent issues" with "Issues opened by you" --- api/queries_issue.go | 15 ++++++--------- command/issue.go | 8 ++++---- test/fixtures/issueStatus.json | 2 +- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/api/queries_issue.go b/api/queries_issue.go index 54aab484f..401920e1b 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -2,13 +2,12 @@ package api import ( "fmt" - "time" ) type IssuesPayload struct { Assigned []Issue Mentioned []Issue - Recent []Issue + Authored []Issue } type Issue struct { @@ -91,11 +90,11 @@ func IssueStatus(client *Client, ghRepo Repo, currentUsername string) (*IssuesPa type response struct { Assigned apiIssues Mentioned apiIssues - Recent apiIssues + Authored apiIssues } query := fragments + ` - query($owner: String!, $repo: String!, $since: DateTime!, $viewer: String!, $per_page: Int = 10) { + query($owner: String!, $repo: String!, $viewer: String!, $per_page: Int = 10) { assigned: repository(owner: $owner, name: $repo) { issues(filterBy: {assignee: $viewer, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { @@ -110,8 +109,8 @@ func IssueStatus(client *Client, ghRepo Repo, currentUsername string) (*IssuesPa } } } - recent: repository(owner: $owner, name: $repo) { - issues(filterBy: {since: $since, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) { + authored: repository(owner: $owner, name: $repo) { + issues(filterBy: {createdBy: $viewer, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { ...issue } @@ -122,12 +121,10 @@ func IssueStatus(client *Client, ghRepo Repo, currentUsername string) (*IssuesPa owner := ghRepo.RepoOwner() repo := ghRepo.RepoName() - since := time.Now().UTC().Add(time.Hour * -24).Format("2006-01-02T15:04:05-0700") variables := map[string]interface{}{ "owner": owner, "repo": repo, "viewer": currentUsername, - "since": since, } var resp response @@ -139,7 +136,7 @@ func IssueStatus(client *Client, ghRepo Repo, currentUsername string) (*IssuesPa payload := IssuesPayload{ Assigned: resp.Assigned.Issues.Nodes, Mentioned: resp.Mentioned.Issues.Nodes, - Recent: resp.Recent.Issues.Nodes, + Authored: resp.Authored.Issues.Nodes, } return &payload, nil diff --git a/command/issue.go b/command/issue.go index 7a5a1b086..4fc937a1e 100644 --- a/command/issue.go +++ b/command/issue.go @@ -158,11 +158,11 @@ func issueStatus(cmd *cobra.Command, args []string) error { } fmt.Println() - printHeader("Recent issues") - if len(issuePayload.Recent) > 0 { - printIssues(" ", issuePayload.Recent...) + printHeader("Issues opened by you") + if len(issuePayload.Authored) > 0 { + printIssues(" ", issuePayload.Authored...) } else { - printMessage(" There are no recent issues") + printMessage(" There are no issues opened by you") } fmt.Println() diff --git a/test/fixtures/issueStatus.json b/test/fixtures/issueStatus.json index 37d0818da..f9e831c7a 100644 --- a/test/fixtures/issueStatus.json +++ b/test/fixtures/issueStatus.json @@ -28,7 +28,7 @@ ] } }, - "recent": { + "authored": { "issues": { "nodes": [] } From 002aac351945aad74c9b767d2ffb9dfe2d0ddb03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 27 Nov 2019 17:05:49 +0100 Subject: [PATCH 43/53] Remove global `-B, --current-branch` flag Now `pr list --base` has shorthand `-B` --- command/pr.go | 2 +- command/root.go | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/command/pr.go b/command/pr.go index 5b3a4a16a..cabd27949 100644 --- a/command/pr.go +++ b/command/pr.go @@ -22,7 +22,7 @@ func init() { prListCmd.Flags().IntP("limit", "L", 30, "maximum number of items to fetch") prListCmd.Flags().StringP("state", "s", "open", "filter by state") - prListCmd.Flags().StringP("base", "b", "", "filter by base branch") + prListCmd.Flags().StringP("base", "B", "", "filter by base branch") prListCmd.Flags().StringArrayP("label", "l", nil, "filter by label") } diff --git a/command/root.go b/command/root.go index 489cd7f30..96feb1605 100644 --- a/command/root.go +++ b/command/root.go @@ -19,7 +19,6 @@ var BuildDate = "YYYY-MM-DD" func init() { RootCmd.Version = fmt.Sprintf("%s (%s)", Version, BuildDate) RootCmd.PersistentFlags().StringP("repo", "R", "", "current GitHub repository") - RootCmd.PersistentFlags().StringP("current-branch", "B", "", "current git branch") // TODO: // RootCmd.PersistentFlags().BoolP("verbose", "V", false, "enable verbose output") @@ -55,9 +54,7 @@ func contextForCommand(cmd *cobra.Command) context.Context { ctx := initContext() if repo, err := cmd.Flags().GetString("repo"); err == nil && repo != "" { ctx.SetBaseRepo(repo) - } - if branch, err := cmd.Flags().GetString("current-branch"); err == nil && branch != "" { - ctx.SetBranch(branch) + ctx.SetBranch("master") } return ctx } From b8a0754a0329767491df9710314af0fb938dbd87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 27 Nov 2019 17:08:28 +0100 Subject: [PATCH 44/53] :nail_care: Sentence case for CLI flags --- command/issue.go | 10 +++++----- command/pr.go | 8 ++++---- command/root.go | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/command/issue.go b/command/issue.go index 0c022939f..f8fa7f059 100644 --- a/command/issue.go +++ b/command/issue.go @@ -32,17 +32,17 @@ func init() { "Supply a title. Will prompt for one otherwise.") issueCreateCmd.Flags().StringP("body", "b", "", "Supply a body. Will prompt for one otherwise.") - issueCreateCmd.Flags().BoolP("web", "w", false, "open the web browser to create an issue") + issueCreateCmd.Flags().BoolP("web", "w", false, "Open the web browser to create an issue") issueListCmd := &cobra.Command{ Use: "list", Short: "List and filter issues in this repository", RunE: issueList, } - issueListCmd.Flags().StringP("assignee", "a", "", "filter by assignee") - issueListCmd.Flags().StringSliceP("label", "l", nil, "filter by label") - issueListCmd.Flags().StringP("state", "s", "", "filter by state (open|closed|all)") - issueListCmd.Flags().IntP("limit", "L", 30, "maximum number of issues to fetch") + issueListCmd.Flags().StringP("assignee", "a", "", "Filter by assignee") + issueListCmd.Flags().StringSliceP("label", "l", nil, "Filter by label") + issueListCmd.Flags().StringP("state", "s", "", "Filter by state (open|closed|all)") + issueListCmd.Flags().IntP("limit", "L", 30, "Maximum number of issues to fetch") issueCmd.AddCommand((issueListCmd)) } diff --git a/command/pr.go b/command/pr.go index cabd27949..5d45de54b 100644 --- a/command/pr.go +++ b/command/pr.go @@ -20,10 +20,10 @@ func init() { prCmd.AddCommand(prStatusCmd) prCmd.AddCommand(prViewCmd) - prListCmd.Flags().IntP("limit", "L", 30, "maximum number of items to fetch") - prListCmd.Flags().StringP("state", "s", "open", "filter by state") - prListCmd.Flags().StringP("base", "B", "", "filter by base branch") - prListCmd.Flags().StringArrayP("label", "l", nil, "filter by label") + prListCmd.Flags().IntP("limit", "L", 30, "Maximum number of items to fetch") + prListCmd.Flags().StringP("state", "s", "open", "Filter by state") + prListCmd.Flags().StringP("base", "B", "", "Filter by base branch") + prListCmd.Flags().StringArrayP("label", "l", nil, "Filter by label") } var prCmd = &cobra.Command{ diff --git a/command/root.go b/command/root.go index 96feb1605..5ae77fe1d 100644 --- a/command/root.go +++ b/command/root.go @@ -18,7 +18,7 @@ var BuildDate = "YYYY-MM-DD" func init() { RootCmd.Version = fmt.Sprintf("%s (%s)", Version, BuildDate) - RootCmd.PersistentFlags().StringP("repo", "R", "", "current GitHub repository") + RootCmd.PersistentFlags().StringP("repo", "R", "", "Current GitHub repository") // TODO: // RootCmd.PersistentFlags().BoolP("verbose", "V", false, "enable verbose output") From 854a4b3fdffddad53cfe9d129cae41a5560b39b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 27 Nov 2019 17:26:27 +0100 Subject: [PATCH 45/53] :nail_care: Sentence-case for `--help` and `--version` flags --- command/root.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/command/root.go b/command/root.go index 5ae77fe1d..0e24de8b5 100644 --- a/command/root.go +++ b/command/root.go @@ -19,6 +19,8 @@ var BuildDate = "YYYY-MM-DD" func init() { RootCmd.Version = fmt.Sprintf("%s (%s)", Version, BuildDate) RootCmd.PersistentFlags().StringP("repo", "R", "", "Current GitHub repository") + RootCmd.PersistentFlags().Bool("help", false, "Show help for command") + RootCmd.Flags().Bool("version", false, "Print gh version") // TODO: // RootCmd.PersistentFlags().BoolP("verbose", "V", false, "enable verbose output") From cce72cc7a42a0647fe0556792091761ddc219cd2 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 27 Nov 2019 10:35:19 -0600 Subject: [PATCH 46/53] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 9388f0f4d..baa586cdb 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,7 @@ _(Uninstall with `sudo yum remove gh`)_ 1. Download the latest `_linux_amd64.tar.gz` file from the [releases page](https://github.com/github/gh-cli/releases) 2. `tar -xf gh_0.2.2_linux_amd64.tar.gz`, changing version number accordingly -3. `chmod +x gh` ensure the binary is executable -4. Copy the uncompressed `gh` somewhere on your `$PATH` (e.g. `sudo cp gh /usr/local/bin/`) +3. Copy the uncompressed `gh` somewhere on your `$PATH` (e.g. `sudo cp gh /usr/local/bin/`) _(Uninstall with `rm`)_ From c5eba17429eca6da68dd3ce8adc7aa035a2ad8e7 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 27 Nov 2019 10:35:41 -0600 Subject: [PATCH 47/53] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Mislav Marohnić --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index baa586cdb..5803408c2 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ _(Uninstall with `sudo yum remove gh`)_ ## Other Linux 1. Download the latest `_linux_amd64.tar.gz` file from the [releases page](https://github.com/github/gh-cli/releases) -2. `tar -xf gh_0.2.2_linux_amd64.tar.gz`, changing version number accordingly +2. `tar -xf gh_*_linux_amd64.tar.gz` 3. Copy the uncompressed `gh` somewhere on your `$PATH` (e.g. `sudo cp gh /usr/local/bin/`) _(Uninstall with `rm`)_ From 478d5de9a1f40e86c60bb46b19c013fe61987222 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 27 Nov 2019 10:45:05 -0600 Subject: [PATCH 48/53] Update README.md --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5803408c2..3deb491a7 100644 --- a/README.md +++ b/README.md @@ -14,21 +14,22 @@ _warning, gh is in a very alpha phase_ ## Debian/Ubuntu Linux -1. Download the `.deb` file from the [releases page](https://github.com/github/gh-cli/releases/latest) -2. Install it with `sudo dpkg -i gh_0.2.2_linux_amd64.deb`, changing version number accordingly +1. `sudo apt install git` if you don't already have git +2. Download the `.deb` file from the [releases page](https://github.com/github/gh-cli/releases/latest) +3. `sudo dpkg -i gh_*_linux_amd64.deb` install the downloaded file _(Uninstall with `sudo apt remove gh`)_ ## Fedora/Centos Linux 1. Download the `.rpm` file from the [releases page](https://github.com/github/gh-cli/releases/latest) -2. Install it with `sudo yum localinstall gh_0.2.2_linux_amd64.rpm`, changing version number accordingly +2. `sudo yum localinstall gh_*_linux_amd64.rpm` install the downloaded file _(Uninstall with `sudo yum remove gh`)_ ## Other Linux -1. Download the latest `_linux_amd64.tar.gz` file from the [releases page](https://github.com/github/gh-cli/releases) +1. Download the `_linux_amd64.tar.gz` file from the [releases page](https://github.com/github/gh-cli/releases/latest) 2. `tar -xf gh_*_linux_amd64.tar.gz` 3. Copy the uncompressed `gh` somewhere on your `$PATH` (e.g. `sudo cp gh /usr/local/bin/`) From 6777b4f16c6a4d45cfabae0f3eb410ac88645477 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 27 Nov 2019 10:47:35 -0600 Subject: [PATCH 49/53] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3deb491a7..036c37b90 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ _(Uninstall with `sudo yum remove gh`)_ 1. Download the `_linux_amd64.tar.gz` file from the [releases page](https://github.com/github/gh-cli/releases/latest) 2. `tar -xf gh_*_linux_amd64.tar.gz` -3. Copy the uncompressed `gh` somewhere on your `$PATH` (e.g. `sudo cp gh /usr/local/bin/`) +3. Copy the uncompressed `gh` somewhere on your `$PATH` (e.g. `sudo cp gh_*_linux_amd64/bin/gh /usr/local/bin/`) _(Uninstall with `rm`)_ From 004ab1e9db2979ce6813d1e2c27175cb48331b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 27 Nov 2019 18:58:23 +0100 Subject: [PATCH 50/53] Fix color output to Git Bash --- utils/color.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/utils/color.go b/utils/color.go index fb8479734..0a7e346a0 100644 --- a/utils/color.go +++ b/utils/color.go @@ -1,15 +1,28 @@ package utils import ( + "os" + "github.com/mattn/go-isatty" "github.com/mgutz/ansi" - "os" ) +var _isStdoutTerminal = false +var checkedTerminal = false + +func isStdoutTerminal() bool { + if !checkedTerminal { + fd := os.Stdout.Fd() + _isStdoutTerminal = isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd) + checkedTerminal = true + } + return _isStdoutTerminal +} + func makeColorFunc(color string) func(string) string { return func(arg string) string { output := arg - if isatty.IsTerminal(os.Stdout.Fd()) { + if isStdoutTerminal() { output = ansi.Color(color+arg+ansi.Reset, "") } @@ -29,7 +42,7 @@ var Gray = makeColorFunc(ansi.LightBlack) func Bold(arg string) string { output := arg - if isatty.IsTerminal(os.Stdout.Fd()) { + if isStdoutTerminal() { // This is really annoying. If you just define Bold as ColorFunc("+b") it will properly bold but // will not use the default color, resulting in black and probably unreadable text. This forces // the default color before bolding. From 24b04b5fca6fa167f709a3f69c3ed77b487417a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 27 Nov 2019 19:16:53 +0100 Subject: [PATCH 51/53] Make use of ansi.ColorFunc Speed up repeated calls to color functions by using ansi.ColorFunc to create a closure per each color. https://godoc.org/github.com/mgutz/ansi#ColorFunc --- utils/color.go | 38 +++++++++++++------------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/utils/color.go b/utils/color.go index fb8479734..fd53d80e7 100644 --- a/utils/color.go +++ b/utils/color.go @@ -1,39 +1,27 @@ package utils import ( + "os" + "github.com/mattn/go-isatty" "github.com/mgutz/ansi" - "os" ) func makeColorFunc(color string) func(string) string { + cf := ansi.ColorFunc(color) return func(arg string) string { - output := arg if isatty.IsTerminal(os.Stdout.Fd()) { - output = ansi.Color(color+arg+ansi.Reset, "") + return cf(arg) } - - return output + return arg } } -var Black = makeColorFunc(ansi.Black) -var White = makeColorFunc(ansi.White) -var Magenta = makeColorFunc(ansi.Magenta) -var Cyan = makeColorFunc(ansi.Cyan) -var Red = makeColorFunc(ansi.Red) -var Yellow = makeColorFunc(ansi.Yellow) -var Blue = makeColorFunc(ansi.Blue) -var Green = makeColorFunc(ansi.Green) -var Gray = makeColorFunc(ansi.LightBlack) - -func Bold(arg string) string { - output := arg - if isatty.IsTerminal(os.Stdout.Fd()) { - // This is really annoying. If you just define Bold as ColorFunc("+b") it will properly bold but - // will not use the default color, resulting in black and probably unreadable text. This forces - // the default color before bolding. - output = ansi.Color(ansi.DefaultFG+arg+ansi.Reset, "+b") - } - return output -} +var Magenta = makeColorFunc("magenta") +var Cyan = makeColorFunc("cyan") +var Red = makeColorFunc("red") +var Yellow = makeColorFunc("yellow") +var Blue = makeColorFunc("blue") +var Green = makeColorFunc("green") +var Gray = makeColorFunc("black+h") +var Bold = makeColorFunc("default+b") From b6fa88337d6330f246292a89bc7ff26a04845666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 27 Nov 2019 20:51:51 +0100 Subject: [PATCH 52/53] Ensure that commands print to a colorable output If a command does `fmt.Print(...)` for output that contains ANSI color codes, this not safe on Windows. We have to ensure that we always use the `fmt.Fprint*` family of functions with a writer that was transformed using `utils.NewColorable()`. --- command/issue.go | 38 ++++++++++++++++++------------- command/pr.go | 51 ++++++++++++++++++++++-------------------- command/root.go | 10 +++++++++ utils/color.go | 10 ++++++++- utils/table_printer.go | 3 +-- 5 files changed, 69 insertions(+), 43 deletions(-) diff --git a/command/issue.go b/command/issue.go index 4fc937a1e..53db25ad3 100644 --- a/command/issue.go +++ b/command/issue.go @@ -2,6 +2,7 @@ package command import ( "fmt" + "io" "os" "strconv" "strings" @@ -94,12 +95,15 @@ func issueList(cmd *cobra.Command, args []string) error { return err } + out := cmd.OutOrStdout() + colorOut := colorableOut(cmd) + if len(issues) == 0 { - printMessage("There are no open issues") + printMessage(colorOut, "There are no open issues") return nil } - table := utils.NewTablePrinter(cmd.OutOrStdout()) + table := utils.NewTablePrinter(out) for _, issue := range issues { issueNum := strconv.Itoa(issue.Number) if table.IsTTY() { @@ -141,30 +145,32 @@ func issueStatus(cmd *cobra.Command, args []string) error { return err } - printHeader("Issues assigned to you") + out := colorableOut(cmd) + + printHeader(out, "Issues assigned to you") if issuePayload.Assigned != nil { - printIssues(" ", issuePayload.Assigned...) + printIssues(out, " ", issuePayload.Assigned...) } else { message := fmt.Sprintf(" There are no issues assgined to you") - printMessage(message) + printMessage(out, message) } - fmt.Println() + fmt.Fprintln(out) - printHeader("Issues mentioning you") + printHeader(out, "Issues mentioning you") if len(issuePayload.Mentioned) > 0 { - printIssues(" ", issuePayload.Mentioned...) + printIssues(out, " ", issuePayload.Mentioned...) } else { - printMessage(" There are no issues mentioning you") + printMessage(out, " There are no issues mentioning you") } - fmt.Println() + fmt.Fprintln(out) - printHeader("Issues opened by you") + printHeader(out, "Issues opened by you") if len(issuePayload.Authored) > 0 { - printIssues(" ", issuePayload.Authored...) + printIssues(out, " ", issuePayload.Authored...) } else { - printMessage(" There are no issues opened by you") + printMessage(out, " There are no issues opened by you") } - fmt.Println() + fmt.Fprintln(out) return nil } @@ -255,14 +261,14 @@ func issueCreate(cmd *cobra.Command, args []string) error { return nil } -func printIssues(prefix string, issues ...api.Issue) { +func printIssues(w io.Writer, prefix string, issues ...api.Issue) { for _, issue := range issues { number := utils.Green("#" + strconv.Itoa(issue.Number)) coloredLabels := labelList(issue) if coloredLabels != "" { coloredLabels = utils.Gray(fmt.Sprintf(" (%s)", coloredLabels)) } - fmt.Printf("%s%s %s%s\n", prefix, number, truncate(70, issue.Title), coloredLabels) + fmt.Fprintf(w, "%s%s %s%s\n", prefix, number, truncate(70, issue.Title), coloredLabels) } } diff --git a/command/pr.go b/command/pr.go index 5b3a4a16a..3fdf42964 100644 --- a/command/pr.go +++ b/command/pr.go @@ -2,6 +2,7 @@ package command import ( "fmt" + "io" "os" "os/exec" "strconv" @@ -78,30 +79,32 @@ func prStatus(cmd *cobra.Command, args []string) error { return err } - printHeader("Current branch") + out := colorableOut(cmd) + + printHeader(out, "Current branch") if prPayload.CurrentPR != nil { - printPrs(*prPayload.CurrentPR) + printPrs(out, *prPayload.CurrentPR) } else { message := fmt.Sprintf(" There is no pull request associated with %s", utils.Cyan("["+currentBranch+"]")) - printMessage(message) + printMessage(out, message) } - fmt.Println() + fmt.Fprintln(out) - printHeader("Created by you") + printHeader(out, "Created by you") if len(prPayload.ViewerCreated) > 0 { - printPrs(prPayload.ViewerCreated...) + printPrs(out, prPayload.ViewerCreated...) } else { - printMessage(" You have no open pull requests") + printMessage(out, " You have no open pull requests") } - fmt.Println() + fmt.Fprintln(out) - printHeader("Requesting a code review from you") + printHeader(out, "Requesting a code review from you") if len(prPayload.ReviewRequested) > 0 { - printPrs(prPayload.ReviewRequested...) + printPrs(out, prPayload.ReviewRequested...) } else { - printMessage(" You have no pull requests to review") + printMessage(out, " You have no pull requests to review") } - fmt.Println() + fmt.Fprintln(out) return nil } @@ -330,15 +333,15 @@ func prCheckout(cmd *cobra.Command, args []string) error { return nil } -func printPrs(prs ...api.PullRequest) { +func printPrs(w io.Writer, prs ...api.PullRequest) { for _, pr := range prs { prNumber := fmt.Sprintf("#%d", pr.Number) - fmt.Printf(" %s %s %s", utils.Yellow(prNumber), truncate(50, pr.Title), utils.Cyan("["+pr.HeadLabel()+"]")) + fmt.Fprintf(w, " %s %s %s", utils.Yellow(prNumber), truncate(50, pr.Title), utils.Cyan("["+pr.HeadLabel()+"]")) checks := pr.ChecksStatus() reviews := pr.ReviewStatus() if checks.Total > 0 || reviews.ChangesRequested || reviews.Approved { - fmt.Printf("\n ") + fmt.Fprintf(w, "\n ") } if checks.Total > 0 { @@ -354,27 +357,27 @@ func printPrs(prs ...api.PullRequest) { } else if checks.Passing == checks.Total { summary = utils.Green("Checks passing") } - fmt.Printf(" - %s", summary) + fmt.Fprintf(w, " - %s", summary) } if reviews.ChangesRequested { - fmt.Printf(" - %s", utils.Red("changes requested")) + fmt.Fprintf(w, " - %s", utils.Red("changes requested")) } else if reviews.ReviewRequired { - fmt.Printf(" - %s", utils.Yellow("review required")) + fmt.Fprintf(w, " - %s", utils.Yellow("review required")) } else if reviews.Approved { - fmt.Printf(" - %s", utils.Green("approved")) + fmt.Fprintf(w, " - %s", utils.Green("approved")) } - fmt.Printf("\n") + fmt.Fprint(w, "\n") } } -func printHeader(s string) { - fmt.Println(utils.Bold(s)) +func printHeader(w io.Writer, s string) { + fmt.Fprintln(w, utils.Bold(s)) } -func printMessage(s string) { - fmt.Println(utils.Gray(s)) +func printMessage(w io.Writer, s string) { + fmt.Fprintln(w, utils.Gray(s)) } func truncate(maxLength int, title string) string { diff --git a/command/root.go b/command/root.go index e8764c44b..da730dd2b 100644 --- a/command/root.go +++ b/command/root.go @@ -2,11 +2,13 @@ package command import ( "fmt" + "io" "os" "strings" "github.com/github/gh-cli/api" "github.com/github/gh-cli/context" + "github.com/github/gh-cli/utils" "github.com/spf13/cobra" ) @@ -93,3 +95,11 @@ var apiClientForContext = func(ctx context.Context) (*api.Client, error) { } return api.NewClient(opts...), nil } + +func colorableOut(cmd *cobra.Command) io.Writer { + out := cmd.OutOrStdout() + if outFile, isFile := out.(*os.File); isFile { + return utils.NewColorable(outFile) + } + return out +} diff --git a/utils/color.go b/utils/color.go index fb8479734..b955c4755 100644 --- a/utils/color.go +++ b/utils/color.go @@ -1,11 +1,19 @@ package utils import ( + "io" + "os" + + "github.com/mattn/go-colorable" "github.com/mattn/go-isatty" "github.com/mgutz/ansi" - "os" ) +// NewColorable returns an output stream that handles ANSI color sequences on Windows +func NewColorable(f *os.File) io.Writer { + return colorable.NewColorable(f) +} + func makeColorFunc(color string) func(string) string { return func(arg string) string { output := arg diff --git a/utils/table_printer.go b/utils/table_printer.go index 89b1aebce..649e246e1 100644 --- a/utils/table_printer.go +++ b/utils/table_printer.go @@ -8,7 +8,6 @@ import ( "strconv" "strings" - "github.com/mattn/go-colorable" "github.com/mattn/go-isatty" "golang.org/x/crypto/ssh/terminal" ) @@ -37,7 +36,7 @@ func NewTablePrinter(w io.Writer) TablePrinter { } } return &ttyTablePrinter{ - out: colorable.NewColorable(outFile), + out: NewColorable(outFile), maxWidth: ttyWidth, } } From a0458956c02e5b6dbc2689eb0fa078ea870a75d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 28 Nov 2019 11:55:14 +0100 Subject: [PATCH 53/53] Add docs to color funcs --- utils/color.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/utils/color.go b/utils/color.go index 097bad9cf..ed22bc3e9 100644 --- a/utils/color.go +++ b/utils/color.go @@ -36,11 +36,26 @@ func makeColorFunc(color string) func(string) string { } } +// Magenta outputs ANSI color if stdout is a tty var Magenta = makeColorFunc("magenta") + +// Cyan outputs ANSI color if stdout is a tty var Cyan = makeColorFunc("cyan") + +// Red outputs ANSI color if stdout is a tty var Red = makeColorFunc("red") + +// Yellow outputs ANSI color if stdout is a tty var Yellow = makeColorFunc("yellow") + +// Blue outputs ANSI color if stdout is a tty var Blue = makeColorFunc("blue") + +// Green outputs ANSI color if stdout is a tty var Green = makeColorFunc("green") + +// Gray outputs ANSI color if stdout is a tty var Gray = makeColorFunc("black+h") + +// Bold outputs ANSI color if stdout is a tty var Bold = makeColorFunc("default+b")