github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/repo/view/view.go (about)

     1  package view
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/http"
     7  	"net/url"
     8  	"strings"
     9  	"syscall"
    10  	"text/template"
    11  
    12  	"github.com/MakeNowJust/heredoc"
    13  	"github.com/cli/cli/api"
    14  	"github.com/cli/cli/internal/ghinstance"
    15  	"github.com/cli/cli/internal/ghrepo"
    16  	"github.com/cli/cli/pkg/cmdutil"
    17  	"github.com/cli/cli/pkg/iostreams"
    18  	"github.com/cli/cli/pkg/markdown"
    19  	"github.com/cli/cli/utils"
    20  	"github.com/spf13/cobra"
    21  )
    22  
    23  type browser interface {
    24  	Browse(string) error
    25  }
    26  
    27  type ViewOptions struct {
    28  	HttpClient func() (*http.Client, error)
    29  	IO         *iostreams.IOStreams
    30  	BaseRepo   func() (ghrepo.Interface, error)
    31  	Browser    browser
    32  	Exporter   cmdutil.Exporter
    33  
    34  	RepoArg string
    35  	Web     bool
    36  	Branch  string
    37  }
    38  
    39  func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command {
    40  	opts := ViewOptions{
    41  		IO:         f.IOStreams,
    42  		HttpClient: f.HttpClient,
    43  		BaseRepo:   f.BaseRepo,
    44  		Browser:    f.Browser,
    45  	}
    46  
    47  	cmd := &cobra.Command{
    48  		Use:   "view [<repository>]",
    49  		Short: "View a repository",
    50  		Long: `Display the description and the README of a GitHub repository.
    51  
    52  With no argument, the repository for the current directory is displayed.
    53  
    54  With '--web', open the repository in a web browser instead.
    55  
    56  With '--branch', view a specific branch of the repository.`,
    57  		Args: cobra.MaximumNArgs(1),
    58  		RunE: func(c *cobra.Command, args []string) error {
    59  			if len(args) > 0 {
    60  				opts.RepoArg = args[0]
    61  			}
    62  			if runF != nil {
    63  				return runF(&opts)
    64  			}
    65  			return viewRun(&opts)
    66  		},
    67  	}
    68  
    69  	cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open a repository in the browser")
    70  	cmd.Flags().StringVarP(&opts.Branch, "branch", "b", "", "View a specific branch of the repository")
    71  	cmdutil.AddJSONFlags(cmd, &opts.Exporter, api.RepositoryFields)
    72  
    73  	return cmd
    74  }
    75  
    76  var defaultFields = []string{"name", "owner", "description"}
    77  
    78  func viewRun(opts *ViewOptions) error {
    79  	httpClient, err := opts.HttpClient()
    80  	if err != nil {
    81  		return err
    82  	}
    83  
    84  	var toView ghrepo.Interface
    85  	apiClient := api.NewClientFromHTTP(httpClient)
    86  	if opts.RepoArg == "" {
    87  		var err error
    88  		toView, err = opts.BaseRepo()
    89  		if err != nil {
    90  			return err
    91  		}
    92  	} else {
    93  		var err error
    94  		viewURL := opts.RepoArg
    95  		if !strings.Contains(viewURL, "/") {
    96  			currentUser, err := api.CurrentLoginName(apiClient, ghinstance.Default())
    97  			if err != nil {
    98  				return err
    99  			}
   100  			viewURL = currentUser + "/" + viewURL
   101  		}
   102  		toView, err = ghrepo.FromFullName(viewURL)
   103  		if err != nil {
   104  			return fmt.Errorf("argument error: %w", err)
   105  		}
   106  	}
   107  
   108  	var readme *RepoReadme
   109  	fields := defaultFields
   110  	if opts.Exporter != nil {
   111  		fields = opts.Exporter.Fields()
   112  	}
   113  
   114  	repo, err := fetchRepository(apiClient, toView, fields)
   115  	if err != nil {
   116  		return err
   117  	}
   118  
   119  	if !opts.Web && opts.Exporter == nil {
   120  		readme, err = RepositoryReadme(httpClient, toView, opts.Branch)
   121  		if err != nil && !errors.Is(err, NotFoundError) {
   122  			return err
   123  		}
   124  	}
   125  
   126  	openURL := generateBranchURL(toView, opts.Branch)
   127  	if opts.Web {
   128  		if opts.IO.IsStdoutTTY() {
   129  			fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
   130  		}
   131  		return opts.Browser.Browse(openURL)
   132  	}
   133  
   134  	opts.IO.DetectTerminalTheme()
   135  	if err := opts.IO.StartPager(); err != nil {
   136  		return err
   137  	}
   138  	defer opts.IO.StopPager()
   139  
   140  	if opts.Exporter != nil {
   141  		return opts.Exporter.Write(opts.IO, repo)
   142  	}
   143  
   144  	fullName := ghrepo.FullName(toView)
   145  	stdout := opts.IO.Out
   146  
   147  	if !opts.IO.IsStdoutTTY() {
   148  		fmt.Fprintf(stdout, "name:\t%s\n", fullName)
   149  		fmt.Fprintf(stdout, "description:\t%s\n", repo.Description)
   150  		if readme != nil {
   151  			fmt.Fprintln(stdout, "--")
   152  			fmt.Fprintf(stdout, readme.Content)
   153  			fmt.Fprintln(stdout)
   154  		}
   155  
   156  		return nil
   157  	}
   158  
   159  	repoTmpl := heredoc.Doc(`
   160  		{{.FullName}}
   161  		{{.Description}}
   162  
   163  		{{.Readme}}
   164  
   165  		{{.View}}
   166  	`)
   167  
   168  	tmpl, err := template.New("repo").Parse(repoTmpl)
   169  	if err != nil {
   170  		return err
   171  	}
   172  
   173  	cs := opts.IO.ColorScheme()
   174  
   175  	var readmeContent string
   176  	if readme == nil {
   177  		readmeContent = cs.Gray("This repository does not have a README")
   178  	} else if isMarkdownFile(readme.Filename) {
   179  		var err error
   180  		style := markdown.GetStyle(opts.IO.TerminalTheme())
   181  		readmeContent, err = markdown.RenderWithBaseURL(readme.Content, style, readme.BaseURL)
   182  		if err != nil {
   183  			return fmt.Errorf("error rendering markdown: %w", err)
   184  		}
   185  	} else {
   186  		readmeContent = readme.Content
   187  	}
   188  
   189  	description := repo.Description
   190  	if description == "" {
   191  		description = cs.Gray("No description provided")
   192  	}
   193  
   194  	repoData := struct {
   195  		FullName    string
   196  		Description string
   197  		Readme      string
   198  		View        string
   199  	}{
   200  		FullName:    cs.Bold(fullName),
   201  		Description: description,
   202  		Readme:      readmeContent,
   203  		View:        cs.Gray(fmt.Sprintf("View this repository on GitHub: %s", openURL)),
   204  	}
   205  
   206  	err = tmpl.Execute(stdout, repoData)
   207  	if err != nil && !errors.Is(err, syscall.EPIPE) {
   208  		return err
   209  	}
   210  
   211  	return nil
   212  }
   213  
   214  func isMarkdownFile(filename string) bool {
   215  	// kind of gross, but i'm assuming that 90% of the time the suffix will just be .md. it didn't
   216  	// seem worth executing a regex for this given that assumption.
   217  	return strings.HasSuffix(filename, ".md") ||
   218  		strings.HasSuffix(filename, ".markdown") ||
   219  		strings.HasSuffix(filename, ".mdown") ||
   220  		strings.HasSuffix(filename, ".mkdown")
   221  }
   222  
   223  func generateBranchURL(r ghrepo.Interface, branch string) string {
   224  	if branch == "" {
   225  		return ghrepo.GenerateRepoURL(r, "")
   226  	}
   227  
   228  	return ghrepo.GenerateRepoURL(r, "tree/%s", url.QueryEscape(branch))
   229  }