github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/release/view/view.go (about) 1 package view 2 3 import ( 4 "fmt" 5 "io" 6 "net/http" 7 "strings" 8 "time" 9 10 "github.com/MakeNowJust/heredoc" 11 "github.com/cli/cli/internal/ghrepo" 12 "github.com/cli/cli/pkg/cmd/release/shared" 13 "github.com/cli/cli/pkg/cmdutil" 14 "github.com/cli/cli/pkg/iostreams" 15 "github.com/cli/cli/pkg/markdown" 16 "github.com/cli/cli/utils" 17 "github.com/spf13/cobra" 18 ) 19 20 type browser interface { 21 Browse(string) error 22 } 23 24 type ViewOptions struct { 25 HttpClient func() (*http.Client, error) 26 IO *iostreams.IOStreams 27 BaseRepo func() (ghrepo.Interface, error) 28 Browser browser 29 Exporter cmdutil.Exporter 30 31 TagName string 32 WebMode bool 33 } 34 35 func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command { 36 opts := &ViewOptions{ 37 IO: f.IOStreams, 38 HttpClient: f.HttpClient, 39 Browser: f.Browser, 40 } 41 42 cmd := &cobra.Command{ 43 Use: "view [<tag>]", 44 Short: "View information about a release", 45 Long: heredoc.Doc(` 46 View information about a GitHub Release. 47 48 Without an explicit tag name argument, the latest release in the project 49 is shown. 50 `), 51 Args: cobra.MaximumNArgs(1), 52 RunE: func(cmd *cobra.Command, args []string) error { 53 // support `-R, --repo` override 54 opts.BaseRepo = f.BaseRepo 55 56 if len(args) > 0 { 57 opts.TagName = args[0] 58 } 59 60 if runF != nil { 61 return runF(opts) 62 } 63 return viewRun(opts) 64 }, 65 } 66 67 cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the release in the browser") 68 cmdutil.AddJSONFlags(cmd, &opts.Exporter, shared.ReleaseFields) 69 70 return cmd 71 } 72 73 func viewRun(opts *ViewOptions) error { 74 httpClient, err := opts.HttpClient() 75 if err != nil { 76 return err 77 } 78 79 baseRepo, err := opts.BaseRepo() 80 if err != nil { 81 return err 82 } 83 84 var release *shared.Release 85 86 if opts.TagName == "" { 87 release, err = shared.FetchLatestRelease(httpClient, baseRepo) 88 if err != nil { 89 return err 90 } 91 } else { 92 release, err = shared.FetchRelease(httpClient, baseRepo, opts.TagName) 93 if err != nil { 94 return err 95 } 96 } 97 98 if opts.WebMode { 99 if opts.IO.IsStdoutTTY() { 100 fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(release.URL)) 101 } 102 return opts.Browser.Browse(release.URL) 103 } 104 105 opts.IO.DetectTerminalTheme() 106 if err := opts.IO.StartPager(); err != nil { 107 fmt.Fprintf(opts.IO.ErrOut, "error starting pager: %v\n", err) 108 } 109 defer opts.IO.StopPager() 110 111 if opts.Exporter != nil { 112 return opts.Exporter.Write(opts.IO, release) 113 } 114 115 if opts.IO.IsStdoutTTY() { 116 if err := renderReleaseTTY(opts.IO, release); err != nil { 117 return err 118 } 119 } else { 120 if err := renderReleasePlain(opts.IO.Out, release); err != nil { 121 return err 122 } 123 } 124 125 return nil 126 } 127 128 func renderReleaseTTY(io *iostreams.IOStreams, release *shared.Release) error { 129 iofmt := io.ColorScheme() 130 w := io.Out 131 132 fmt.Fprintf(w, "%s\n", iofmt.Bold(release.TagName)) 133 if release.IsDraft { 134 fmt.Fprintf(w, "%s • ", iofmt.Red("Draft")) 135 } else if release.IsPrerelease { 136 fmt.Fprintf(w, "%s • ", iofmt.Yellow("Pre-release")) 137 } 138 if release.IsDraft { 139 fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("%s created this %s", release.Author.Login, utils.FuzzyAgo(time.Since(release.CreatedAt))))) 140 } else { 141 fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("%s released this %s", release.Author.Login, utils.FuzzyAgo(time.Since(*release.PublishedAt))))) 142 } 143 144 style := markdown.GetStyle(io.TerminalTheme()) 145 renderedDescription, err := markdown.Render(release.Body, style) 146 if err != nil { 147 return err 148 } 149 fmt.Fprintln(w, renderedDescription) 150 151 if len(release.Assets) > 0 { 152 fmt.Fprintf(w, "%s\n", iofmt.Bold("Assets")) 153 table := utils.NewTablePrinter(io) 154 for _, a := range release.Assets { 155 table.AddField(a.Name, nil, nil) 156 table.AddField(humanFileSize(a.Size), nil, nil) 157 table.EndRow() 158 } 159 err := table.Render() 160 if err != nil { 161 return err 162 } 163 fmt.Fprint(w, "\n") 164 } 165 166 fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("View on GitHub: %s", release.URL))) 167 return nil 168 } 169 170 func renderReleasePlain(w io.Writer, release *shared.Release) error { 171 fmt.Fprintf(w, "title:\t%s\n", release.Name) 172 fmt.Fprintf(w, "tag:\t%s\n", release.TagName) 173 fmt.Fprintf(w, "draft:\t%v\n", release.IsDraft) 174 fmt.Fprintf(w, "prerelease:\t%v\n", release.IsPrerelease) 175 fmt.Fprintf(w, "author:\t%s\n", release.Author.Login) 176 fmt.Fprintf(w, "created:\t%s\n", release.CreatedAt.Format(time.RFC3339)) 177 if !release.IsDraft { 178 fmt.Fprintf(w, "published:\t%s\n", release.PublishedAt.Format(time.RFC3339)) 179 } 180 fmt.Fprintf(w, "url:\t%s\n", release.URL) 181 for _, a := range release.Assets { 182 fmt.Fprintf(w, "asset:\t%s\n", a.Name) 183 } 184 fmt.Fprint(w, "--\n") 185 fmt.Fprint(w, release.Body) 186 return nil 187 } 188 189 func humanFileSize(s int64) string { 190 if s < 1024 { 191 return fmt.Sprintf("%d B", s) 192 } 193 194 kb := float64(s) / 1024 195 if kb < 1024 { 196 return fmt.Sprintf("%s KiB", floatToString(kb, 2)) 197 } 198 199 mb := kb / 1024 200 if mb < 1024 { 201 return fmt.Sprintf("%s MiB", floatToString(mb, 2)) 202 } 203 204 gb := mb / 1024 205 return fmt.Sprintf("%s GiB", floatToString(gb, 2)) 206 } 207 208 // render float to fixed precision using truncation instead of rounding 209 func floatToString(f float64, p uint8) string { 210 fs := fmt.Sprintf("%#f%0*s", f, p, "") 211 idx := strings.IndexRune(fs, '.') 212 return fs[:idx+int(p)+1] 213 }