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  }