cli/pkg/cmd/release/view/view.go
Mislav Marohnić 36ffbe18de
Improve looking up draft releases by tag name
This changes the FetchRelease implementation to look up draft releases directly using by its pending tag name, as opposed to resorting to the Releases list API which is backed by Elastic Search and thus suffers replication lag after the creation of a draft release.

Bonus: all release lookup functions now accept a context for cancellation.
2022-12-14 21:24:08 +01:00

221 lines
5.4 KiB
Go

package view
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/release/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/markdown"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
type browser interface {
Browse(string) error
}
type ViewOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
BaseRepo func() (ghrepo.Interface, error)
Browser browser
Exporter cmdutil.Exporter
TagName string
WebMode bool
}
func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command {
opts := &ViewOptions{
IO: f.IOStreams,
HttpClient: f.HttpClient,
Browser: f.Browser,
}
cmd := &cobra.Command{
Use: "view [<tag>]",
Short: "View information about a release",
Long: heredoc.Doc(`
View information about a GitHub Release.
Without an explicit tag name argument, the latest release in the project
is shown.
`),
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
// support `-R, --repo` override
opts.BaseRepo = f.BaseRepo
if len(args) > 0 {
opts.TagName = args[0]
}
if runF != nil {
return runF(opts)
}
return viewRun(opts)
},
}
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the release in the browser")
cmdutil.AddJSONFlags(cmd, &opts.Exporter, shared.ReleaseFields)
return cmd
}
func viewRun(opts *ViewOptions) error {
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
baseRepo, err := opts.BaseRepo()
if err != nil {
return err
}
ctx := context.Background()
var release *shared.Release
if opts.TagName == "" {
release, err = shared.FetchLatestRelease(ctx, httpClient, baseRepo)
if err != nil {
return err
}
} else {
release, err = shared.FetchRelease(ctx, httpClient, baseRepo, opts.TagName)
if err != nil {
return err
}
}
if opts.WebMode {
if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(release.URL))
}
return opts.Browser.Browse(release.URL)
}
opts.IO.DetectTerminalTheme()
if err := opts.IO.StartPager(); err != nil {
fmt.Fprintf(opts.IO.ErrOut, "error starting pager: %v\n", err)
}
defer opts.IO.StopPager()
if opts.Exporter != nil {
return opts.Exporter.Write(opts.IO, release)
}
if opts.IO.IsStdoutTTY() {
if err := renderReleaseTTY(opts.IO, release); err != nil {
return err
}
} else {
if err := renderReleasePlain(opts.IO.Out, release); err != nil {
return err
}
}
return nil
}
func renderReleaseTTY(io *iostreams.IOStreams, release *shared.Release) error {
iofmt := io.ColorScheme()
w := io.Out
fmt.Fprintf(w, "%s\n", iofmt.Bold(release.TagName))
if release.IsDraft {
fmt.Fprintf(w, "%s • ", iofmt.Red("Draft"))
} else if release.IsPrerelease {
fmt.Fprintf(w, "%s • ", iofmt.Yellow("Pre-release"))
}
if release.IsDraft {
fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("%s created this %s", release.Author.Login, text.FuzzyAgo(time.Now(), release.CreatedAt))))
} else {
fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("%s released this %s", release.Author.Login, text.FuzzyAgo(time.Now(), *release.PublishedAt))))
}
renderedDescription, err := markdown.Render(release.Body,
markdown.WithTheme(io.TerminalTheme()),
markdown.WithWrap(io.TerminalWidth()))
if err != nil {
return err
}
fmt.Fprintln(w, renderedDescription)
if len(release.Assets) > 0 {
fmt.Fprintf(w, "%s\n", iofmt.Bold("Assets"))
//nolint:staticcheck // SA1019: utils.NewTablePrinter is deprecated: use internal/tableprinter
table := utils.NewTablePrinter(io)
for _, a := range release.Assets {
table.AddField(a.Name, nil, nil)
table.AddField(humanFileSize(a.Size), nil, nil)
table.EndRow()
}
err := table.Render()
if err != nil {
return err
}
fmt.Fprint(w, "\n")
}
fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("View on GitHub: %s", release.URL)))
return nil
}
func renderReleasePlain(w io.Writer, release *shared.Release) error {
fmt.Fprintf(w, "title:\t%s\n", release.Name)
fmt.Fprintf(w, "tag:\t%s\n", release.TagName)
fmt.Fprintf(w, "draft:\t%v\n", release.IsDraft)
fmt.Fprintf(w, "prerelease:\t%v\n", release.IsPrerelease)
fmt.Fprintf(w, "author:\t%s\n", release.Author.Login)
fmt.Fprintf(w, "created:\t%s\n", release.CreatedAt.Format(time.RFC3339))
if !release.IsDraft {
fmt.Fprintf(w, "published:\t%s\n", release.PublishedAt.Format(time.RFC3339))
}
fmt.Fprintf(w, "url:\t%s\n", release.URL)
for _, a := range release.Assets {
fmt.Fprintf(w, "asset:\t%s\n", a.Name)
}
fmt.Fprint(w, "--\n")
fmt.Fprint(w, release.Body)
if !strings.HasSuffix(release.Body, "\n") {
fmt.Fprintf(w, "\n")
}
return nil
}
func humanFileSize(s int64) string {
if s < 1024 {
return fmt.Sprintf("%d B", s)
}
kb := float64(s) / 1024
if kb < 1024 {
return fmt.Sprintf("%s KiB", floatToString(kb, 2))
}
mb := kb / 1024
if mb < 1024 {
return fmt.Sprintf("%s MiB", floatToString(mb, 2))
}
gb := mb / 1024
return fmt.Sprintf("%s GiB", floatToString(gb, 2))
}
// render float to fixed precision using truncation instead of rounding
func floatToString(f float64, p uint8) string {
fs := fmt.Sprintf("%#f%0*s", f, p, "")
idx := strings.IndexRune(fs, '.')
return fs[:idx+int(p)+1]
}