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