github.com/abdfnx/gh-api@v0.0.0-20210414084727-f5432eec23b8/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/abdfnx/gh-api/api"
    14  	"github.com/abdfnx/gh-api/internal/ghinstance"
    15  	"github.com/abdfnx/gh-api/internal/ghrepo"
    16  	"github.com/abdfnx/gh-api/pkg/cmdutil"
    17  	"github.com/abdfnx/gh-api/pkg/iostreams"
    18  	"github.com/abdfnx/gh-api/pkg/markdown"
    19  	"github.com/abdfnx/gh-api/utils"
    20  	"github.com/enescakir/emoji"
    21  	"github.com/spf13/cobra"
    22  )
    23  
    24  type browser interface {
    25  	Browse(string) error
    26  }
    27  
    28  type ViewOptions struct {
    29  	HttpClient func() (*http.Client, error)
    30  	IO         *iostreams.IOStreams
    31  	BaseRepo   func() (ghrepo.Interface, error)
    32  	Browser    browser
    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  
    72  	return cmd
    73  }
    74  
    75  func viewRun(opts *ViewOptions) error {
    76  	httpClient, err := opts.HttpClient()
    77  	if err != nil {
    78  		return err
    79  	}
    80  
    81  	var toView ghrepo.Interface
    82  	apiClient := api.NewClientFromHTTP(httpClient)
    83  	if opts.RepoArg == "" {
    84  		var err error
    85  		toView, err = opts.BaseRepo()
    86  		if err != nil {
    87  			return err
    88  		}
    89  	} else {
    90  		var err error
    91  		viewURL := opts.RepoArg
    92  		if !strings.Contains(viewURL, "/") {
    93  			currentUser, err := api.CurrentLoginName(apiClient, ghinstance.Default())
    94  			if err != nil {
    95  				return err
    96  			}
    97  			viewURL = currentUser + "/" + viewURL
    98  		}
    99  		toView, err = ghrepo.FromFullName(viewURL)
   100  		if err != nil {
   101  			return fmt.Errorf("argument error: %w", err)
   102  		}
   103  	}
   104  
   105  	repo, err := api.GitHubRepo(apiClient, toView)
   106  	if err != nil {
   107  		return err
   108  	}
   109  
   110  	openURL := generateBranchURL(toView, opts.Branch)
   111  	if opts.Web {
   112  		if opts.IO.IsStdoutTTY() {
   113  			fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
   114  		}
   115  		return opts.Browser.Browse(openURL)
   116  	}
   117  
   118  	fullName := ghrepo.FullName(toView)
   119  
   120  	readme, err := RepositoryReadme(httpClient, toView, opts.Branch)
   121  	if err != nil && err != NotFoundError {
   122  		return err
   123  	}
   124  
   125  	opts.IO.DetectTerminalTheme()
   126  
   127  	err = opts.IO.StartPager()
   128  	if err != nil {
   129  		return err
   130  	}
   131  	defer opts.IO.StopPager()
   132  
   133  	stdout := opts.IO.Out
   134  
   135  	if !opts.IO.IsStdoutTTY() {
   136  		fmt.Fprintf(stdout, "name:\t%s\n", fullName)
   137  		fmt.Fprintf(stdout, "description:\t%s\n", repo.Description)
   138  		if readme != nil {
   139  			fmt.Fprintln(stdout, "--")
   140  			fmt.Fprintf(stdout, readme.Content)
   141  			fmt.Fprintln(stdout)
   142  		}
   143  
   144  		return nil
   145  	}
   146  
   147  	repoTmpl := heredoc.Doc(`
   148  		{{.FullName}}
   149  		{{.Description}}
   150  		
   151  		{{.Readme}}
   152  		
   153  		{{.View}}
   154  	`)
   155  
   156  	tmpl, err := template.New("repo").Parse(repoTmpl)
   157  	if err != nil {
   158  		return err
   159  	}
   160  
   161  	cs := opts.IO.ColorScheme()
   162  
   163  	var readmeContent string
   164  	if readme == nil {
   165  		readmeContent = cs.Gray("This repository does not have a README")
   166  	} else if isMarkdownFile(readme.Filename) {
   167  		var err error
   168  		style := markdown.GetStyle(opts.IO.TerminalTheme())
   169  		readmeContent, err = markdown.RenderWithBaseURL(readme.Content, style, readme.BaseURL)
   170  		if err != nil {
   171  			return fmt.Errorf("error rendering markdown: %w", err)
   172  		}
   173  		readmeContent = emoji.Parse(readmeContent)
   174  	} else {
   175  		readmeContent = emoji.Parse(readme.Content)
   176  	}
   177  
   178  	description := repo.Description
   179  	if description == "" {
   180  		description = cs.Gray("No description provided")
   181  	}
   182  
   183  	repoData := struct {
   184  		FullName    string
   185  		Description string
   186  		Readme      string
   187  		View        string
   188  	}{
   189  		FullName:    cs.Bold(fullName),
   190  		Description: description,
   191  		Readme:      readmeContent,
   192  		View:        cs.Gray(fmt.Sprintf("View this repository on GitHub: %s", openURL)),
   193  	}
   194  
   195  	err = tmpl.Execute(stdout, repoData)
   196  	if err != nil && !errors.Is(err, syscall.EPIPE) {
   197  		return err
   198  	}
   199  
   200  	return nil
   201  }
   202  
   203  func isMarkdownFile(filename string) bool {
   204  	// kind of gross, but i'm assuming that 90% of the time the suffix will just be .md. it didn't
   205  	// seem worth executing a regex for this given that assumption.
   206  	return strings.HasSuffix(filename, ".md") ||
   207  		strings.HasSuffix(filename, ".markdown") ||
   208  		strings.HasSuffix(filename, ".mdown") ||
   209  		strings.HasSuffix(filename, ".mkdown")
   210  }
   211  
   212  func generateBranchURL(r ghrepo.Interface, branch string) string {
   213  	if branch == "" {
   214  		return ghrepo.GenerateRepoURL(r, "")
   215  	}
   216  
   217  	return ghrepo.GenerateRepoURL(r, "tree/%s", url.QueryEscape(branch))
   218  }