cli/pkg/cmd/repo/view/view.go
Mislav Marohnić 0a5e220231 Ignore EPIPE errors when writing to a closed pager
While a gh command is writing stdout to a pager, the user may choose to
close the pager program before the pager has read all the data on its
standard input. In that case, the parent gh process will receive an
EPIPE error, which would bubble up its error handling and cause it to
print something like:

    write |1: broken pipe

Since this was caused by an explicit user action of closing the pager,
and since the user probably doesn't want to see this uninformative
error, this informs our global error handling of this error and causes
it to be ignored.
2022-02-10 16:42:00 +01:00

233 lines
5.4 KiB
Go

package view
import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"text/template"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/markdown"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
type browser interface {
Browse(string) error
}
type ViewOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
BaseRepo func() (ghrepo.Interface, error)
Browser browser
Exporter cmdutil.Exporter
Config func() (config.Config, error)
RepoArg string
Web bool
Branch string
}
func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command {
opts := ViewOptions{
IO: f.IOStreams,
HttpClient: f.HttpClient,
BaseRepo: f.BaseRepo,
Browser: f.Browser,
Config: f.Config,
}
cmd := &cobra.Command{
Use: "view [<repository>]",
Short: "View a repository",
Long: `Display the description and the README of a GitHub repository.
With no argument, the repository for the current directory is displayed.
With '--web', open the repository in a web browser instead.
With '--branch', view a specific branch of the repository.`,
Args: cobra.MaximumNArgs(1),
RunE: func(c *cobra.Command, args []string) error {
if len(args) > 0 {
opts.RepoArg = args[0]
}
if runF != nil {
return runF(&opts)
}
return viewRun(&opts)
},
}
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open a repository in the browser")
cmd.Flags().StringVarP(&opts.Branch, "branch", "b", "", "View a specific branch of the repository")
cmdutil.AddJSONFlags(cmd, &opts.Exporter, api.RepositoryFields)
return cmd
}
var defaultFields = []string{"name", "owner", "description"}
func viewRun(opts *ViewOptions) error {
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
var toView ghrepo.Interface
apiClient := api.NewClientFromHTTP(httpClient)
if opts.RepoArg == "" {
var err error
toView, err = opts.BaseRepo()
if err != nil {
return err
}
} else {
viewURL := opts.RepoArg
if !strings.Contains(viewURL, "/") {
cfg, err := opts.Config()
if err != nil {
return err
}
hostname, err := cfg.DefaultHost()
if err != nil {
return err
}
currentUser, err := api.CurrentLoginName(apiClient, hostname)
if err != nil {
return err
}
viewURL = currentUser + "/" + viewURL
}
toView, err = ghrepo.FromFullName(viewURL)
if err != nil {
return fmt.Errorf("argument error: %w", err)
}
}
var readme *RepoReadme
fields := defaultFields
if opts.Exporter != nil {
fields = opts.Exporter.Fields()
}
repo, err := api.FetchRepository(apiClient, toView, fields)
if err != nil {
return err
}
if !opts.Web && opts.Exporter == nil {
readme, err = RepositoryReadme(httpClient, toView, opts.Branch)
if err != nil && !errors.Is(err, NotFoundError) {
return err
}
}
openURL := generateBranchURL(toView, opts.Branch)
if opts.Web {
if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
}
return opts.Browser.Browse(openURL)
}
opts.IO.DetectTerminalTheme()
if err := opts.IO.StartPager(); err == nil {
defer opts.IO.StopPager()
} else {
fmt.Fprintf(opts.IO.ErrOut, "failed to start pager: %v\n", err)
}
if opts.Exporter != nil {
return opts.Exporter.Write(opts.IO, repo)
}
fullName := ghrepo.FullName(toView)
stdout := opts.IO.Out
if !opts.IO.IsStdoutTTY() {
fmt.Fprintf(stdout, "name:\t%s\n", fullName)
fmt.Fprintf(stdout, "description:\t%s\n", repo.Description)
if readme != nil {
fmt.Fprintln(stdout, "--")
fmt.Fprintf(stdout, readme.Content)
fmt.Fprintln(stdout)
}
return nil
}
repoTmpl := heredoc.Doc(`
{{.FullName}}
{{.Description}}
{{.Readme}}
{{.View}}
`)
tmpl, err := template.New("repo").Parse(repoTmpl)
if err != nil {
return err
}
cs := opts.IO.ColorScheme()
var readmeContent string
if readme == nil {
readmeContent = cs.Gray("This repository does not have a README")
} else if isMarkdownFile(readme.Filename) {
var err error
readmeContent, err = markdown.Render(readme.Content, markdown.WithIO(opts.IO), markdown.WithBaseURL(readme.BaseURL))
if err != nil {
return fmt.Errorf("error rendering markdown: %w", err)
}
} else {
readmeContent = readme.Content
}
description := repo.Description
if description == "" {
description = cs.Gray("No description provided")
}
repoData := struct {
FullName string
Description string
Readme string
View string
}{
FullName: cs.Bold(fullName),
Description: description,
Readme: readmeContent,
View: cs.Gray(fmt.Sprintf("View this repository on GitHub: %s", openURL)),
}
return tmpl.Execute(stdout, repoData)
}
func isMarkdownFile(filename string) bool {
// kind of gross, but i'm assuming that 90% of the time the suffix will just be .md. it didn't
// seem worth executing a regex for this given that assumption.
return strings.HasSuffix(filename, ".md") ||
strings.HasSuffix(filename, ".markdown") ||
strings.HasSuffix(filename, ".mdown") ||
strings.HasSuffix(filename, ".mkdown")
}
func generateBranchURL(r ghrepo.Interface, branch string) string {
if branch == "" {
return ghrepo.GenerateRepoURL(r, "")
}
return ghrepo.GenerateRepoURL(r, "tree/%s", url.QueryEscape(branch))
}