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