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 }