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  }