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 }