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 }