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()`.
This commit is contained in:
Mislav Marohnić 2019-11-27 20:51:51 +01:00
parent 4148cf76b9
commit b6fa88337d
5 changed files with 69 additions and 43 deletions

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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

View file

@ -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,
}
}