Migrate to go-gh text package (#6236)

This commit is contained in:
Sam Coe 2022-09-14 09:23:55 +04:00 committed by GitHub
parent e16bf033ed
commit e7102f9d84
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 371 additions and 822 deletions

View file

@ -20,15 +20,16 @@ import (
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/run"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/internal/update"
"github.com/cli/cli/v2/pkg/cmd/alias/expand"
"github.com/cli/cli/v2/pkg/cmd/factory"
"github.com/cli/cli/v2/pkg/cmd/root"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/text"
"github.com/cli/cli/v2/utils"
"github.com/cli/safeexec"
"github.com/mattn/go-isatty"
"github.com/mgutz/ansi"
"github.com/spf13/cobra"
)
@ -336,7 +337,11 @@ func shouldCheckForUpdate() bool {
if os.Getenv("CODESPACES") != "" {
return false
}
return updaterEnabled != "" && !isCI() && utils.IsTerminal(os.Stdout) && utils.IsTerminal(os.Stderr)
return updaterEnabled != "" && !isCI() && isTerminal(os.Stdout) && isTerminal(os.Stderr)
}
func isTerminal(f *os.File) bool {
return isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd())
}
// based on https://github.com/watson/ci-info/blob/HEAD/index.js

4
go.mod
View file

@ -8,7 +8,7 @@ require (
github.com/briandowns/spinner v1.18.1
github.com/charmbracelet/glamour v0.5.0
github.com/charmbracelet/lipgloss v0.5.0
github.com/cli/go-gh v0.1.1-0.20220817122932-3630ab390fe7
github.com/cli/go-gh v0.1.1-0.20220913125123-04019861008e
github.com/cli/oauth v0.9.0
github.com/cli/safeexec v1.0.0
github.com/cpuguy83/go-md2man/v2 v2.0.2
@ -25,7 +25,6 @@ require (
github.com/mattn/go-colorable v0.1.13
github.com/mattn/go-isatty v0.0.16
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
github.com/muesli/reflow v0.3.0
github.com/muesli/termenv v0.12.0
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38
github.com/opentracing/opentracing-go v1.1.0
@ -59,6 +58,7 @@ require (
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/microcosm-cc/bluemonday v1.0.19 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect

4
go.sum
View file

@ -58,8 +58,8 @@ github.com/cli/browser v1.1.0 h1:xOZBfkfY9L9vMBgqb1YwRirGu6QFaQ5dP/vXt5ENSOY=
github.com/cli/browser v1.1.0/go.mod h1:HKMQAt9t12kov91Mn7RfZxyJQQgWgyS/3SZswlZ5iTI=
github.com/cli/crypto v0.0.0-20210929142629-6be313f59b03 h1:3f4uHLfWx4/WlnMPXGai03eoWAI+oGHJwr+5OXfxCr8=
github.com/cli/crypto v0.0.0-20210929142629-6be313f59b03/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
github.com/cli/go-gh v0.1.1-0.20220817122932-3630ab390fe7 h1:yLnT5/sLlUcnr5njBRmf/d7pEKr0Cu2TpeecdlQl8AY=
github.com/cli/go-gh v0.1.1-0.20220817122932-3630ab390fe7/go.mod h1:UKRuMl3ZaitTvO4LPWj5bVw7QwZHnLu0S0lI9WWbdpc=
github.com/cli/go-gh v0.1.1-0.20220913125123-04019861008e h1:zK2hqxSk5D/Jt4o+0NVH/qdEFh7fUhgGkhbukwPMzQU=
github.com/cli/go-gh v0.1.1-0.20220913125123-04019861008e/go.mod h1:UKRuMl3ZaitTvO4LPWj5bVw7QwZHnLu0S0lI9WWbdpc=
github.com/cli/oauth v0.9.0 h1:nxBC0Df4tUzMkqffAB+uZvisOwT3/N9FpkfdTDtafxc=
github.com/cli/oauth v0.9.0/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4=
github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=

View file

@ -11,8 +11,8 @@ import (
"time"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/liveshare"
"github.com/cli/cli/v2/pkg/text"
)
// PostCreateStateStatus is a string value representing the different statuses a state can have.

74
internal/text/text.go Normal file
View file

@ -0,0 +1,74 @@
package text
import (
"fmt"
"net/url"
"regexp"
"strings"
"time"
"github.com/cli/go-gh/pkg/text"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
var whitespaceRE = regexp.MustCompile(`\s+`)
func Indent(s, indent string) string {
return text.Indent(s, indent)
}
// Title returns a copy of the string s with all Unicode letters that begin words mapped to their Unicode title case.
func Title(s string) string {
c := cases.Title(language.English)
return c.String(s)
}
// RemoveExcessiveWhitespace returns a copy of the string s with excessive whitespace removed.
func RemoveExcessiveWhitespace(s string) string {
return whitespaceRE.ReplaceAllString(strings.TrimSpace(s), " ")
}
func DisplayWidth(s string) int {
return text.DisplayWidth(s)
}
func Truncate(maxWidth int, s string) string {
return text.Truncate(maxWidth, s)
}
func Pluralize(num int, thing string) string {
return text.Pluralize(num, thing)
}
func FuzzyAgo(a, b time.Time) string {
return text.RelativeTimeAgo(a, b)
}
// FuzzyAgoAbbr is an abbreviated version of FuzzyAgo. It returns a human readable string of the
// time duration between a and b that is estimated to the nearest unit of time.
func FuzzyAgoAbbr(a, b time.Time) string {
ago := a.Sub(b)
if ago < time.Hour {
return fmt.Sprintf("%d%s", int(ago.Minutes()), "m")
}
if ago < 24*time.Hour {
return fmt.Sprintf("%d%s", int(ago.Hours()), "h")
}
if ago < 30*24*time.Hour {
return fmt.Sprintf("%d%s", int(ago.Hours())/24, "d")
}
return b.Format("Jan _2, 2006")
}
// DisplayURL returns a copy of the string urlStr removing everything except the hostname and path.
// If there is an error parsing urlStr then urlStr is returned without modification.
func DisplayURL(urlStr string) string {
u, err := url.Parse(urlStr)
if err != nil {
return urlStr
}
return u.Hostname() + u.Path
}

View file

@ -0,0 +1,56 @@
package text
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestRemoveExcessiveWhitespace(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "nothing to remove",
input: "one two three",
want: "one two three",
},
{
name: "whitespace b-gone",
input: "\n one\n\t two three\r\n ",
want: "one two three",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := RemoveExcessiveWhitespace(tt.input)
assert.Equal(t, tt.want, got)
})
}
}
func TestFuzzyAgoAbbr(t *testing.T) {
const form = "2006-Jan-02 15:04:05"
now, _ := time.Parse(form, "2020-Nov-22 14:00:00")
cases := map[string]string{
"2020-Nov-22 14:00:00": "0m",
"2020-Nov-22 13:59:00": "1m",
"2020-Nov-22 13:30:00": "30m",
"2020-Nov-22 13:00:00": "1h",
"2020-Nov-22 02:00:00": "12h",
"2020-Nov-21 14:00:00": "1d",
"2020-Nov-07 14:00:00": "15d",
"2020-Oct-24 14:00:00": "29d",
"2020-Oct-23 14:00:00": "Oct 23, 2020",
"2019-Nov-22 14:00:00": "Nov 22, 2019",
}
for createdAt, expected := range cases {
d, err := time.Parse(form, createdAt)
assert.NoError(t, err)
fuzzy := FuzzyAgoAbbr(now, d)
assert.Equal(t, expected, fuzzy)
}
}

View file

@ -15,9 +15,9 @@ import (
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -148,7 +148,7 @@ func runBrowse(opts *BrowseOptions) error {
}
if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.Out, "Opening %s in your browser.\n", utils.DisplayURL(url))
fmt.Fprintf(opts.IO.Out, "Opening %s in your browser.\n", text.DisplayURL(url))
}
return opts.Browser.Browse(url)
}

View file

@ -10,8 +10,8 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/cli/cli/v2/internal/codespaces"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -192,12 +192,12 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
if len(devcontainers) > 0 {
// if there is only one devcontainer.json file and it is one of the default paths we can auto-select it
if len(devcontainers) == 1 && utils.StringInSlice(devcontainers[0].Path, DEFAULT_DEVCONTAINER_DEFINITIONS) {
if len(devcontainers) == 1 && stringInSlice(devcontainers[0].Path, DEFAULT_DEVCONTAINER_DEFINITIONS) {
devContainerPath = devcontainers[0].Path
} else {
promptOptions := []string{}
if !utils.StringInSlice(devcontainers[0].Path, DEFAULT_DEVCONTAINER_DEFINITIONS) {
if !stringInSlice(devcontainers[0].Path, DEFAULT_DEVCONTAINER_DEFINITIONS) {
promptOptions = []string{DEVCONTAINER_PROMPT_DEFAULT}
}
@ -284,7 +284,7 @@ func (a *App) handleAdditionalPermissions(ctx context.Context, createParams *api
var (
isInteractive = a.io.CanPrompt()
cs = a.io.ColorScheme()
displayURL = utils.DisplayURL(allowPermissionsURL)
displayURL = text.DisplayURL(allowPermissionsURL)
)
fmt.Fprintf(a.io.ErrOut, "You must authorize or deny additional permissions requested by this codespace before continuing.\n")
@ -498,3 +498,12 @@ func buildDisplayName(displayName string, prebuildAvailability string) string {
return displayName
}
}
func stringInSlice(a string, slice []string) bool {
for _, b := range slice {
if b == a {
return true
}
}
return false
}

View file

@ -7,6 +7,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
@ -144,7 +145,7 @@ func (a *App) List(ctx context.Context, opts *listOptions, exporter cmdutil.Expo
if err != nil {
return fmt.Errorf("error parsing date %q: %w", c.CreatedAt, err)
}
tp.AddField(utils.FuzzyAgoAbbr(time.Now(), ct), nil, cs.Gray)
tp.AddField(text.FuzzyAgoAbbr(time.Now(), ct), nil, cs.Gray)
} else {
tp.AddField(c.CreatedAt, nil, nil)
}

View file

@ -18,10 +18,10 @@ import (
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -164,7 +164,7 @@ func createRun(opts *CreateOptions) error {
fmt.Fprintf(errOut, "%s %s\n", cs.SuccessIconWithColor(cs.Green), completionMessage)
if opts.WebMode {
fmt.Fprintf(opts.IO.Out, "Opening %s in your browser.\n", utils.DisplayURL(gist.HTMLURL))
fmt.Fprintf(opts.IO.Out, "Opening %s in your browser.\n", text.DisplayURL(gist.HTMLURL))
return opts.Browser.Browse(gist.HTMLURL)
}

View file

@ -7,10 +7,10 @@ import (
"time"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/text"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -119,12 +119,12 @@ func listRun(opts *ListOptions) error {
gistTime := gist.UpdatedAt.Format(time.RFC3339)
if tp.IsTTY() {
gistTime = utils.FuzzyAgo(time.Since(gist.UpdatedAt))
gistTime = text.FuzzyAgo(time.Now(), gist.UpdatedAt)
}
tp.AddField(gist.ID, nil, nil)
tp.AddField(text.ReplaceExcessiveWhitespace(description), nil, cs.Bold)
tp.AddField(utils.Pluralize(fileCount, "file"), nil, nil)
tp.AddField(text.RemoveExcessiveWhitespace(description), nil, cs.Bold)
tp.AddField(text.Pluralize(fileCount, "file"), nil, nil)
tp.AddField(visibility, nil, visColor)
tp.AddField(gistTime, nil, cs.Gray)
tp.EndRow()

View file

@ -10,13 +10,12 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/gist/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/pkg/prompt"
"github.com/cli/cli/v2/pkg/text"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -107,7 +106,7 @@ func viewRun(opts *ViewOptions) error {
gistURL = ghinstance.GistPrefix(hostname) + gistID
}
if opts.IO.IsStderrTTY() {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(gistURL))
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(gistURL))
}
return opts.Browser.Browse(gistURL)
}
@ -236,9 +235,9 @@ func promptGists(client *http.Client, host string, cs *iostreams.ColorScheme) (g
sort.Strings(filenames)
gistName = filenames[0]
gistTime := utils.FuzzyAgo(time.Since(gist.UpdatedAt))
gistTime := text.FuzzyAgo(time.Now(), gist.UpdatedAt)
// TODO: support dynamic maxWidth
description = text.Truncate(100, text.ReplaceExcessiveWhitespace(description))
description = text.Truncate(100, text.RemoveExcessiveWhitespace(description))
opt := fmt.Sprintf("%s %s %s", cs.Bold(gistName), description, cs.Gray(gistTime))
opts = append(opts, opt)
}

View file

@ -7,6 +7,7 @@ import (
"time"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/utils"
@ -81,7 +82,7 @@ func listRun(opts *ListOptions) error {
createdAt := gpgKey.CreatedAt.Format(time.RFC3339)
if t.IsTTY() {
createdAt = "Created " + utils.FuzzyAgoAbbr(now, gpgKey.CreatedAt)
createdAt = "Created " + text.FuzzyAgoAbbr(now, gpgKey.CreatedAt)
}
t.AddField(createdAt, nil, cs.Gray)

View file

@ -9,11 +9,11 @@ import (
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -161,7 +161,7 @@ func createRun(opts *CreateOptions) (err error) {
if err != nil {
return
}
if !utils.ValidURL(openURL) {
if !prShared.ValidURL(openURL) {
err = fmt.Errorf("cannot open in browser: maximum URL length exceeded")
return
}
@ -171,7 +171,7 @@ func createRun(opts *CreateOptions) (err error) {
openURL = ghrepo.GenerateRepoURL(baseRepo, "issues/new")
}
if isTerminal {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL))
}
return opts.Browser.Browse(openURL)
}
@ -238,7 +238,7 @@ func createRun(opts *CreateOptions) (err error) {
return
}
allowPreview := !tb.HasMetadata() && utils.ValidURL(openURL)
allowPreview := !tb.HasMetadata() && prShared.ValidURL(openURL)
action, err = prShared.ConfirmIssueSubmission(allowPreview, repo.ViewerCanTriage())
if err != nil {
err = fmt.Errorf("unable to confirm: %w", err)
@ -277,7 +277,7 @@ func createRun(opts *CreateOptions) (err error) {
if action == prShared.PreviewAction {
if isTerminal {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL))
}
return opts.Browser.Browse(openURL)
} else if action == prShared.SubmitAction {

View file

@ -12,12 +12,12 @@ import (
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
issueShared "github.com/cli/cli/v2/pkg/cmd/issue/shared"
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/utils"
"github.com/shurcooL/githubv4"
"github.com/spf13/cobra"
)
@ -158,7 +158,7 @@ func listRun(opts *ListOptions) error {
}
if isTerminal {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL))
}
return opts.Browser.Browse(openURL)
}

View file

@ -7,9 +7,9 @@ import (
"time"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/text"
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/text"
"github.com/cli/cli/v2/utils"
)
@ -22,15 +22,14 @@ func PrintIssues(io *iostreams.IOStreams, now time.Time, prefix string, totalCou
issueNum = "#" + issueNum
}
issueNum = prefix + issueNum
ago := now.Sub(issue.UpdatedAt)
table.AddField(issueNum, nil, cs.ColorFromString(prShared.ColorForIssueState(issue)))
if !table.IsTTY() {
table.AddField(issue.State, nil, nil)
}
table.AddField(text.ReplaceExcessiveWhitespace(issue.Title), nil, nil)
table.AddField(text.RemoveExcessiveWhitespace(issue.Title), nil, nil)
table.AddField(issueLabelList(&issue, cs, table.IsTTY()), nil, nil)
if table.IsTTY() {
table.AddField(utils.FuzzyAgo(ago), nil, cs.Gray)
table.AddField(text.FuzzyAgo(now, issue.UpdatedAt), nil, cs.Gray)
} else {
table.AddField(issue.UpdatedAt.String(), nil, nil)
}

View file

@ -12,13 +12,13 @@ import (
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
issueShared "github.com/cli/cli/v2/pkg/cmd/issue/shared"
prShared "github.com/cli/cli/v2/pkg/cmd/pr/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/pkg/set"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -114,7 +114,7 @@ func viewRun(opts *ViewOptions) error {
if opts.WebMode {
openURL := issue.URL
if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL))
}
return opts.Browser.Browse(openURL)
}
@ -184,8 +184,6 @@ func printRawIssuePreview(out io.Writer, issue *api.Issue) error {
func printHumanIssuePreview(opts *ViewOptions, issue *api.Issue) error {
out := opts.IO.Out
now := opts.Now()
ago := now.Sub(issue.CreatedAt)
cs := opts.IO.ColorScheme()
// Header (Title and State)
@ -194,8 +192,8 @@ func printHumanIssuePreview(opts *ViewOptions, issue *api.Issue) error {
"%s • %s opened %s • %s\n",
issueStateTitleWithColor(cs, issue),
issue.Author.Login,
utils.FuzzyAgo(ago),
utils.Pluralize(issue.Comments.TotalCount, "comment"),
text.FuzzyAgo(opts.Now(), issue.CreatedAt),
text.Pluralize(issue.Comments.TotalCount, "comment"),
)
// Reactions

View file

@ -9,9 +9,9 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)
@ -94,7 +94,7 @@ func cloneRun(opts *cloneOptions) error {
if opts.IO.IsStdoutTTY() {
cs := opts.IO.ColorScheme()
pluralize := func(num int) string {
return utils.Pluralize(num, "label")
return text.Pluralize(num, "label")
}
successCount := int(successCount)

View file

@ -7,9 +7,9 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/text"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -96,7 +96,7 @@ func listRun(opts *listOptions) error {
labelListURL := ghrepo.GenerateRepoURL(baseRepo, "labels")
if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(labelListURL))
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(labelListURL))
}
return opts.Browser.Browse(labelListURL)
@ -148,5 +148,5 @@ func printLabels(io *iostreams.IOStreams, labels []label) error {
}
func listHeader(repoName string, count int, totalCount int) string {
return fmt.Sprintf("Showing %d of %s in %s", count, utils.Pluralize(totalCount, "label"), repoName)
return fmt.Sprintf("Showing %d of %s in %s", count, text.Pluralize(totalCount, "label"), repoName)
}

View file

@ -9,10 +9,10 @@ import (
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -105,7 +105,7 @@ func checksRunWebMode(opts *ChecksOptions) error {
openURL := ghrepo.GenerateRepoURL(baseRepo, "pull/%d/checks", pr.Number)
if isTerminal {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL))
}
return opts.Browser.Browse(openURL)

View file

@ -17,11 +17,11 @@ import (
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -206,7 +206,7 @@ func createRun(opts *CreateOptions) (err error) {
if err != nil {
return
}
if !utils.ValidURL(openURL) {
if !shared.ValidURL(openURL) {
err = fmt.Errorf("cannot open in browser: maximum URL length exceeded")
return
}
@ -307,7 +307,7 @@ func createRun(opts *CreateOptions) (err error) {
return
}
allowPreview := !state.HasMetadata() && utils.ValidURL(openURL)
allowPreview := !state.HasMetadata() && shared.ValidURL(openURL)
allowMetadata := ctx.BaseRepo.ViewerCanTriage()
action, err := shared.ConfirmPRSubmission(allowPreview, allowMetadata, state.Draft)
if err != nil {
@ -377,7 +377,7 @@ func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState) e
}
state.Body = body
} else {
state.Title = utils.Humanize(headRef)
state.Title = humanize(headRef)
var body strings.Builder
for i := len(commits) - 1; i >= 0; i-- {
@ -510,7 +510,7 @@ func NewCreateContext(opts *CreateOptions) (*CreateContext, error) {
}
if ucc, err := git.UncommittedChangeCount(); err == nil && ucc > 0 {
fmt.Fprintf(opts.IO.ErrOut, "Warning: %s\n", utils.Pluralize(ucc, "uncommitted change"))
fmt.Fprintf(opts.IO.ErrOut, "Warning: %s\n", text.Pluralize(ucc, "uncommitted change"))
}
var headRepo ghrepo.Interface
@ -661,7 +661,7 @@ func submitPR(opts CreateOptions, ctx CreateContext, state shared.IssueMetadataS
func previewPR(opts CreateOptions, openURL string) error {
if opts.IO.IsStdinTTY() && opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL))
}
return opts.Browser.Browse(openURL)
@ -732,7 +732,7 @@ func handlePush(opts CreateOptions, ctx CreateContext) error {
pushTries++
// first wait 2 seconds after forking, then 4s, then 6s
waitSeconds := 2 * pushTries
fmt.Fprintf(opts.IO.ErrOut, "waiting %s before retrying...\n", utils.Pluralize(waitSeconds, "second"))
fmt.Fprintf(opts.IO.ErrOut, "waiting %s before retrying...\n", text.Pluralize(waitSeconds, "second"))
time.Sleep(time.Duration(waitSeconds) * time.Second)
continue
}
@ -764,4 +764,16 @@ func generateCompareURL(ctx CreateContext, state shared.IssueMetadataState) (str
return url, nil
}
// Humanize returns a copy of the string s that replaces all instance of '-' and '_' with spaces.
func humanize(s string) string {
replace := "_-"
h := func(r rune) rune {
if strings.ContainsRune(replace, r) {
return ' '
}
return r
}
return strings.Map(h, s)
}
var gitPushRegexp = regexp.MustCompile("^remote: (Create a pull request.*by visiting|[[:space:]]*https://.*/pull/new/).*\n?$")

View file

@ -11,10 +11,10 @@ import (
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/text"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -166,7 +166,7 @@ func listRun(opts *ListOptions) error {
}
if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL))
}
return opts.Browser.Browse(openURL)
}
@ -204,17 +204,15 @@ func listRun(opts *ListOptions) error {
if table.IsTTY() {
prNum = "#" + prNum
}
now := opts.Now()
ago := now.Sub(pr.CreatedAt)
table.AddField(prNum, nil, cs.ColorFromString(shared.ColorForPRState(pr)))
table.AddField(text.ReplaceExcessiveWhitespace(pr.Title), nil, nil)
table.AddField(text.RemoveExcessiveWhitespace(pr.Title), nil, nil)
table.AddField(pr.HeadLabel(), nil, cs.Cyan)
if !table.IsTTY() {
table.AddField(prStateWithDraft(&pr), nil, nil)
}
if table.IsTTY() {
table.AddField(utils.FuzzyAgo(ago), nil, cs.Gray)
table.AddField(text.FuzzyAgo(opts.Now(), pr.CreatedAt), nil, cs.Gray)
} else {
table.AddField(pr.CreatedAt.String(), nil, nil)
}

View file

@ -11,10 +11,10 @@ import (
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/surveyext"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -86,7 +86,7 @@ func CommentableRun(opts *CommentableOptions) error {
case InputTypeWeb:
openURL := commentable.Link() + "#issuecomment-new"
if opts.IO.IsStdoutTTY() && !opts.Quiet {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL))
}
return opts.OpenInBrowser(openURL)
case InputTypeEditor:

View file

@ -7,10 +7,9 @@ import (
"time"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/markdown"
"github.com/cli/cli/v2/pkg/text"
"github.com/cli/cli/v2/utils"
)
type Comment interface {
@ -62,7 +61,7 @@ func CommentList(io *iostreams.IOStreams, comments api.Comments, reviews api.Pul
hiddenCount := totalCount - retrievedCount
if preview && hiddenCount > 0 {
fmt.Fprint(&b, cs.Gray(fmt.Sprintf("———————— Not showing %s ————————", utils.Pluralize(hiddenCount, "comment"))))
fmt.Fprint(&b, cs.Gray(fmt.Sprintf("———————— Not showing %s ————————", text.Pluralize(hiddenCount, "comment"))))
fmt.Fprintf(&b, "\n\n\n")
}
@ -102,7 +101,7 @@ func formatComment(io *iostreams.IOStreams, comment Comment, newest bool) (strin
if comment.Association() != "NONE" {
fmt.Fprint(&b, cs.Boldf(" (%s)", text.Title(comment.Association())))
}
fmt.Fprint(&b, cs.Boldf(" • %s", utils.FuzzyAgoAbbr(time.Now(), comment.Created())))
fmt.Fprint(&b, cs.Boldf(" • %s", text.FuzzyAgoAbbr(time.Now(), comment.Created())))
if comment.IsEdited() {
fmt.Fprint(&b, cs.Bold(" • Edited"))
}

View file

@ -4,10 +4,9 @@ import (
"fmt"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/text"
"github.com/cli/cli/v2/utils"
)
func StateTitleWithColor(cs *iostreams.ColorScheme, pr api.PullRequest) string {
@ -66,8 +65,8 @@ func ListHeader(repoName string, itemName string, matchCount int, totalMatchCoun
if totalMatchCount == 1 {
matchVerb = "matches"
}
return fmt.Sprintf("Showing %d of %s in %s that %s your search", matchCount, utils.Pluralize(totalMatchCount, itemName), repoName, matchVerb)
return fmt.Sprintf("Showing %d of %s in %s that %s your search", matchCount, text.Pluralize(totalMatchCount, itemName), repoName, matchVerb)
}
return fmt.Sprintf("Showing %d of %s in %s", matchCount, utils.Pluralize(totalMatchCount, fmt.Sprintf("open %s", itemName)), repoName)
return fmt.Sprintf("Showing %d of %s in %s", matchCount, text.Pluralize(totalMatchCount, fmt.Sprintf("open %s", itemName)), repoName)
}

View file

@ -44,6 +44,11 @@ func WithPrAndIssueQueryParams(client *api.Client, baseRepo ghrepo.Interface, ba
return u.String(), nil
}
// Maximum length of a URL: 8192 bytes
func ValidURL(urlStr string) bool {
return len(urlStr) < 8192
}
// Ensure that tb.MetadataResult object exists and contains enough pre-fetched API data to be able
// to resolve all object listed in tb to GraphQL IDs.
func fillMetadata(client *api.Client, baseRepo ghrepo.Interface, tb *IssueMetadataState) error {

View file

@ -13,10 +13,10 @@ import (
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/text"
"github.com/spf13/cobra"
)
@ -217,7 +217,7 @@ func printPrs(io *iostreams.IOStreams, totalCount int, prs ...api.PullRequest) {
prStateColorFunc := cs.ColorFromString(shared.ColorForPRState(pr))
fmt.Fprintf(w, " %s %s %s", prStateColorFunc(prNumber), text.Truncate(50, text.ReplaceExcessiveWhitespace(pr.Title)), cs.Cyan("["+pr.HeadLabel()+"]"))
fmt.Fprintf(w, " %s %s %s", prStateColorFunc(prNumber), text.Truncate(50, text.RemoveExcessiveWhitespace(pr.Title)), cs.Cyan("["+pr.HeadLabel()+"]"))
checks := pr.ChecksStatus()
reviews := pr.ReviewStatus()

View file

@ -10,12 +10,11 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/pr/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/pkg/text"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -105,7 +104,7 @@ func viewRun(opts *ViewOptions) error {
if opts.BrowserMode {
openURL := pr.URL
if connectedToTerminal {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL))
}
return opts.Browser.Browse(openURL)
}
@ -168,8 +167,6 @@ func printRawPrPreview(io *iostreams.IOStreams, pr *api.PullRequest) error {
func printHumanPrPreview(opts *ViewOptions, pr *api.PullRequest) error {
out := opts.IO.Out
cs := opts.IO.ColorScheme()
now := opts.Now()
ago := now.Sub(pr.CreatedAt)
// Header (Title and State)
fmt.Fprintf(out, "%s #%d\n", cs.Bold(pr.Title), pr.Number)
@ -177,10 +174,10 @@ func printHumanPrPreview(opts *ViewOptions, pr *api.PullRequest) error {
"%s • %s wants to merge %s into %s from %s • %s • %s %s \n",
shared.StateTitleWithColor(cs, *pr),
pr.Author.Login,
utils.Pluralize(pr.Commits.TotalCount, "commit"),
text.Pluralize(pr.Commits.TotalCount, "commit"),
pr.BaseRefName,
pr.HeadRefName,
utils.FuzzyAgo(ago),
text.FuzzyAgo(opts.Now(), pr.CreatedAt),
cs.Green("+"+strconv.Itoa(pr.Additions)),
cs.Red("-"+strconv.Itoa(pr.Deletions)),
)

View file

@ -14,12 +14,12 @@ import (
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/run"
"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/prompt"
"github.com/cli/cli/v2/pkg/surveyext"
"github.com/cli/cli/v2/pkg/text"
"github.com/spf13/cobra"
)

View file

@ -6,9 +6,9 @@ import (
"time"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/text"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -76,11 +76,10 @@ func listRun(opts *ListOptions) error {
fmt.Fprintf(opts.IO.ErrOut, "failed to start pager: %v\n", err)
}
now := time.Now()
table := utils.NewTablePrinter(opts.IO)
iofmt := opts.IO.ColorScheme()
for _, rel := range releases {
title := text.ReplaceExcessiveWhitespace(rel.Name)
title := text.RemoveExcessiveWhitespace(rel.Name)
if title == "" {
title = rel.TagName
}
@ -112,7 +111,7 @@ func listRun(opts *ListOptions) error {
}
publishedAt := pubDate.Format(time.RFC3339)
if table.IsTTY() {
publishedAt = utils.FuzzyAgo(now.Sub(pubDate))
publishedAt = text.FuzzyAgo(time.Now(), pubDate)
}
table.AddField(publishedAt, nil, iofmt.Gray)
table.EndRow()

View file

@ -7,10 +7,10 @@ import (
"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/utils"
"github.com/spf13/cobra"
)
@ -115,7 +115,7 @@ func uploadRun(opts *UploadOptions) error {
if opts.IO.IsStdoutTTY() {
iofmt := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.Out, "Successfully uploaded %s to %s\n",
utils.Pluralize(len(opts.Assets), "asset"),
text.Pluralize(len(opts.Assets), "asset"),
iofmt.Bold(release.TagName))
}

View file

@ -9,6 +9,7 @@ import (
"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"
@ -97,7 +98,7 @@ func viewRun(opts *ViewOptions) error {
if opts.WebMode {
if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(release.URL))
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(release.URL))
}
return opts.Browser.Browse(release.URL)
}
@ -136,9 +137,9 @@ func renderReleaseTTY(io *iostreams.IOStreams, release *shared.Release) error {
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, utils.FuzzyAgo(time.Since(release.CreatedAt)))))
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, utils.FuzzyAgo(time.Since(*release.PublishedAt)))))
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,

View file

@ -7,6 +7,7 @@ import (
"time"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/utils"
@ -81,7 +82,7 @@ func listRun(opts *ListOptions) error {
createdAt := deployKey.CreatedAt.Format(time.RFC3339)
if t.IsTTY() {
createdAt = utils.FuzzyAgoAbbr(now, deployKey.CreatedAt)
createdAt = text.FuzzyAgoAbbr(now, deployKey.CreatedAt)
}
t.AddField(createdAt, nil, cs.Gray)
t.EndRow()

View file

@ -18,7 +18,6 @@ import (
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
@ -139,7 +138,7 @@ func forkRun(opts *ForkOptions) error {
} else {
repoArg := opts.Repository
if utils.IsURL(repoArg) {
if isURL(repoArg) {
parsedURL, err := url.Parse(repoArg)
if err != nil {
return fmt.Errorf("did not understand argument: %w", err)
@ -330,3 +329,7 @@ func forkRun(opts *ForkOptions) error {
return nil
}
func isURL(s string) bool {
return strings.HasPrefix(s, "http:/") || strings.HasPrefix(s, "https:/")
}

View file

@ -11,9 +11,9 @@ import (
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/config"
fd "github.com/cli/cli/v2/internal/featuredetection"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/text"
"github.com/cli/cli/v2/utils"
)
@ -165,7 +165,6 @@ func listRun(opts *ListOptions) error {
cs := opts.IO.ColorScheme()
tp := utils.NewTablePrinter(opts.IO)
now := opts.Now()
for _, repo := range listResult.Repositories {
info := repoInfo(repo)
@ -181,10 +180,10 @@ func listRun(opts *ListOptions) error {
}
tp.AddField(repo.NameWithOwner, nil, cs.Bold)
tp.AddField(text.ReplaceExcessiveWhitespace(repo.Description), nil, nil)
tp.AddField(text.RemoveExcessiveWhitespace(repo.Description), nil, nil)
tp.AddField(info, nil, infoColor)
if tp.IsTTY() {
tp.AddField(utils.FuzzyAgoAbbr(now, *t), nil, cs.Gray)
tp.AddField(text.FuzzyAgoAbbr(opts.Now(), *t), nil, cs.Gray)
} else {
tp.AddField(t.Format(time.RFC3339), nil, nil)
}

View file

@ -13,10 +13,10 @@ import (
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
"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"
)
@ -128,7 +128,7 @@ func viewRun(opts *ViewOptions) error {
openURL := generateBranchURL(toView, opts.Branch)
if opts.Web {
if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL))
}
return opts.Browser.Browse(openURL)
}

View file

@ -7,8 +7,8 @@ import (
"sort"
"strings"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/text"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

View file

@ -5,8 +5,8 @@ import (
"io"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/text"
"github.com/spf13/cobra"
)

View file

@ -7,6 +7,7 @@ import (
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/run/shared"
workflowShared "github.com/cli/cli/v2/pkg/cmd/workflow/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
@ -154,7 +155,7 @@ func listRun(opts *ListOptions) error {
tp.AddField(fmt.Sprintf("%d", run.ID), nil, cs.Cyan)
tp.AddField(run.Duration(opts.now).String(), nil, nil)
tp.AddField(utils.FuzzyAgoAbbr(time.Now(), run.StartedTime()), nil, nil)
tp.AddField(text.FuzzyAgoAbbr(time.Now(), run.StartedTime()), nil, nil)
tp.EndRow()
}

View file

@ -20,11 +20,11 @@ import (
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/run/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -243,7 +243,7 @@ func runView(opts *ViewOptions) error {
url = selectedJob.URL + "?check_suite_focus=true"
}
if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.Out, "Opening %s in your browser.\n", utils.DisplayURL(url))
fmt.Fprintf(opts.IO.Out, "Opening %s in your browser.\n", text.DisplayURL(url))
}
return opts.Browser.Browse(url)
@ -316,10 +316,8 @@ func runView(opts *ViewOptions) error {
out := opts.IO.Out
ago := opts.Now().Sub(run.StartedTime())
fmt.Fprintln(out)
fmt.Fprintln(out, shared.RenderRunHeader(cs, *run, utils.FuzzyAgo(ago), prNumber))
fmt.Fprintln(out, shared.RenderRunHeader(cs, *run, text.FuzzyAgo(opts.Now(), run.StartedTime()), prNumber))
fmt.Fprintln(out)
if len(jobs) == 0 && run.Conclusion == shared.Failure || run.Conclusion == shared.StartupFailure {

View file

@ -10,10 +10,10 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/run/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -206,8 +206,6 @@ func renderRun(out io.Writer, opts WatchOptions, client *api.Client, repo ghrepo
return nil, fmt.Errorf("failed to get run: %w", err)
}
ago := opts.Now().Sub(run.StartedTime())
jobs, err := shared.GetJobs(client, repo, *run)
if err != nil {
return nil, fmt.Errorf("failed to get jobs: %w", err)
@ -238,7 +236,7 @@ func renderRun(out io.Writer, opts WatchOptions, client *api.Client, repo ghrepo
return nil, fmt.Errorf("failed to get annotations: %w", annotationErr)
}
fmt.Fprintln(out, shared.RenderRunHeader(cs, *run, utils.FuzzyAgo(ago), prNumber))
fmt.Fprintln(out, shared.RenderRunHeader(cs, *run, text.FuzzyAgo(opts.Now(), run.StartedTime()), prNumber))
fmt.Fprintln(out)
if len(jobs) == 0 {

View file

@ -7,11 +7,11 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/search/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/search"
"github.com/cli/cli/v2/pkg/text"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -127,7 +127,7 @@ func reposRun(opts *ReposOptions) error {
if opts.WebMode {
url := opts.Searcher.URL(opts.Query)
if io.IsStdoutTTY() {
fmt.Fprintf(io.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(url))
fmt.Fprintf(io.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(url))
}
return opts.Browser.Browse(url)
}
@ -169,10 +169,10 @@ func displayResults(io *iostreams.IOStreams, results search.RepositoriesResult)
}
tp.AddField(repo.FullName, nil, cs.Bold)
description := repo.Description
tp.AddField(text.ReplaceExcessiveWhitespace(description), nil, nil)
tp.AddField(text.RemoveExcessiveWhitespace(description), nil, nil)
tp.AddField(info, nil, infoColor)
if tp.IsTTY() {
tp.AddField(utils.FuzzyAgoAbbr(time.Now(), repo.UpdatedAt), nil, cs.Gray)
tp.AddField(text.FuzzyAgoAbbr(time.Now(), repo.UpdatedAt), nil, cs.Gray)
} else {
tp.AddField(repo.UpdatedAt.Format(time.RFC3339), nil, nil)
}

View file

@ -7,10 +7,10 @@ import (
"time"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/search"
"github.com/cli/cli/v2/pkg/text"
"github.com/cli/cli/v2/utils"
)
@ -54,7 +54,7 @@ func SearchIssues(opts *IssuesOptions) error {
if opts.WebMode {
url := opts.Searcher.URL(opts.Query)
if io.IsStdoutTTY() {
fmt.Fprintf(io.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(url))
fmt.Fprintf(io.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(url))
}
return opts.Browser.Browse(url)
}
@ -117,12 +117,10 @@ func displayIssueResults(io *iostreams.IOStreams, et EntityType, results search.
if !tp.IsTTY() {
tp.AddField(issue.State, nil, nil)
}
tp.AddField(text.ReplaceExcessiveWhitespace(issue.Title), nil, nil)
tp.AddField(text.RemoveExcessiveWhitespace(issue.Title), nil, nil)
tp.AddField(listIssueLabels(&issue, cs, tp.IsTTY()), nil, nil)
now := time.Now()
ago := now.Sub(issue.UpdatedAt)
if tp.IsTTY() {
tp.AddField(utils.FuzzyAgo(ago), nil, cs.Gray)
tp.AddField(text.FuzzyAgo(time.Now(), issue.UpdatedAt), nil, cs.Gray)
} else {
tp.AddField(issue.UpdatedAt.String(), nil, nil)
}

View file

@ -5,7 +5,7 @@ import (
"fmt"
"strings"
"github.com/cli/cli/v2/pkg/text"
"github.com/cli/cli/v2/internal/text"
)
type Visibility string

View file

@ -5,6 +5,7 @@ import (
"time"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/utils"
@ -72,7 +73,7 @@ func listRun(opts *ListOptions) error {
createdAt := sshKey.CreatedAt.Format(time.RFC3339)
if t.IsTTY() {
createdAt = utils.FuzzyAgoAbbr(now, sshKey.CreatedAt)
createdAt = text.FuzzyAgoAbbr(now, sshKey.CreatedAt)
}
t.AddField(createdAt, nil, cs.Gray)
t.EndRow()

View file

@ -11,6 +11,7 @@ import (
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
runShared "github.com/cli/cli/v2/pkg/cmd/run/shared"
"github.com/cli/cli/v2/pkg/cmd/workflow/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
@ -123,7 +124,7 @@ func runView(opts *ViewOptions) error {
address = ghrepo.GenerateRepoURL(repo, "actions/workflows/%s", url.QueryEscape(workflow.Base()))
}
if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.Out, "Opening %s in your browser.\n", utils.DisplayURL(address))
fmt.Fprintf(opts.IO.Out, "Opening %s in your browser.\n", text.DisplayURL(address))
}
return opts.Browser.Browse(address)
}

View file

@ -5,8 +5,7 @@ import (
"reflect"
"sort"
"strings"
"github.com/cli/cli/v2/pkg/text"
"unicode"
)
const (
@ -84,7 +83,7 @@ func (q Qualifiers) Map() map[string][]string {
t := reflect.TypeOf(q)
for i := 0; i < v.NumField(); i++ {
fieldName := t.Field(i).Name
key := text.CamelToKebab(fieldName)
key := camelToKebab(fieldName)
typ := v.FieldByName(fieldName).Kind()
value := v.FieldByName(fieldName)
switch typ {
@ -140,3 +139,29 @@ func formatKeywords(ks []string) []string {
}
return ks
}
// CamelToKebab returns a copy of the string s that is converted from camel case form to '-' separated form.
func camelToKebab(s string) string {
var output []rune
var segment []rune
for _, r := range s {
if !unicode.IsLower(r) && string(r) != "-" && !unicode.IsNumber(r) {
output = addSegment(output, segment)
segment = nil
}
segment = append(segment, unicode.ToLower(r))
}
output = addSegment(output, segment)
return string(output)
}
func addSegment(inrune, segment []rune) []rune {
if len(segment) == 0 {
return inrune
}
if len(inrune) != 0 {
inrune = append(inrune, '-')
}
inrune = append(inrune, segment...)
return inrune
}

View file

@ -133,3 +133,57 @@ func TestQualifiersMap(t *testing.T) {
})
}
}
func TestCamelToKebab(t *testing.T) {
tests := []struct {
name string
in string
out string
}{
{
name: "single lowercase word",
in: "test",
out: "test",
},
{
name: "multiple mixed words",
in: "testTestTest",
out: "test-test-test",
},
{
name: "multiple uppercase words",
in: "TestTest",
out: "test-test",
},
{
name: "multiple lowercase words",
in: "testtest",
out: "testtest",
},
{
name: "multiple mixed words with number",
in: "test2Test",
out: "test2-test",
},
{
name: "multiple lowercase words with number",
in: "test2test",
out: "test2test",
},
{
name: "multiple lowercase words with dash",
in: "test-test",
out: "test-test",
},
{
name: "multiple uppercase words with dash",
in: "Test-Test",
out: "test--test",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.out, camelToKebab(tt.in))
})
}
}

View file

@ -1,39 +0,0 @@
package text
import (
"unicode"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// Copied from: https://github.com/asaskevich/govalidator
func CamelToKebab(str string) string {
var output []rune
var segment []rune
for _, r := range str {
if !unicode.IsLower(r) && string(r) != "-" && !unicode.IsNumber(r) {
output = addSegment(output, segment)
segment = nil
}
segment = append(segment, unicode.ToLower(r))
}
output = addSegment(output, segment)
return string(output)
}
func addSegment(inrune, segment []rune) []rune {
if len(segment) == 0 {
return inrune
}
if len(inrune) != 0 {
inrune = append(inrune, '-')
}
inrune = append(inrune, segment...)
return inrune
}
func Title(str string) string {
c := cases.Title(language.English)
return c.String(str)
}

View file

@ -1,61 +0,0 @@
package text
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCamelToKebab(t *testing.T) {
tests := []struct {
name string
in string
out string
}{
{
name: "single lowercase word",
in: "test",
out: "test",
},
{
name: "multiple mixed words",
in: "testTestTest",
out: "test-test-test",
},
{
name: "multiple uppercase words",
in: "TestTest",
out: "test-test",
},
{
name: "multiple lowercase words",
in: "testtest",
out: "testtest",
},
{
name: "multiple mixed words with number",
in: "test2Test",
out: "test2-test",
},
{
name: "multiple lowercase words with number",
in: "test2test",
out: "test2test",
},
{
name: "multiple lowercase words with dash",
in: "test-test",
out: "test-test",
},
{
name: "multiple uppercase words with dash",
in: "Test-Test",
out: "test--test",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.out, CamelToKebab(tt.in))
})
}
}

View file

@ -1,15 +0,0 @@
package text
import (
"regexp"
"strings"
)
var lineRE = regexp.MustCompile(`(?m)^`)
func Indent(s, indent string) string {
if len(strings.TrimSpace(s)) == 0 {
return s
}
return lineRE.ReplaceAllLiteralString(s, indent)
}

View file

@ -1,47 +0,0 @@
package text
import "testing"
func Test_Indent(t *testing.T) {
type args struct {
s string
indent string
}
tests := []struct {
name string
args args
want string
}{
{
name: "empty",
args: args{
s: "",
indent: "--",
},
want: "",
},
{
name: "blank",
args: args{
s: "\n",
indent: "--",
},
want: "\n",
},
{
name: "indent",
args: args{
s: "one\ntwo\nthree",
indent: "--",
},
want: "--one\n--two\n--three",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Indent(tt.args.s, tt.args.indent); got != tt.want {
t.Errorf("indent() = %q, want %q", got, tt.want)
}
})
}
}

View file

@ -1,12 +0,0 @@
package text
import (
"regexp"
"strings"
)
var ws = regexp.MustCompile(`\s+`)
func ReplaceExcessiveWhitespace(s string) string {
return ws.ReplaceAllString(strings.TrimSpace(s), " ")
}

View file

@ -1,29 +0,0 @@
package text
import "testing"
func TestReplaceExcessiveWhitespace(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "no replacements",
input: "one two three",
want: "one two three",
},
{
name: "whitespace b-gone",
input: "\n one\n\t two three\r\n ",
want: "one two three",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ReplaceExcessiveWhitespace(tt.input); got != tt.want {
t.Errorf("ReplaceExcessiveWhitespace() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -1,47 +0,0 @@
package text
import (
"strings"
"github.com/muesli/reflow/ansi"
"github.com/muesli/reflow/truncate"
)
const (
ellipsis = "..."
minWidthForEllipsis = len(ellipsis) + 2
)
// DisplayWidth calculates what the rendered width of a string may be
func DisplayWidth(s string) int {
return ansi.PrintableRuneWidth(s)
}
// Truncate shortens a string to fit the maximum display width
func Truncate(maxWidth int, s string) string {
w := DisplayWidth(s)
if w <= maxWidth {
return s
}
tail := ""
if maxWidth >= minWidthForEllipsis {
tail = ellipsis
}
r := truncate.StringWithTail(s, uint(maxWidth), tail)
if DisplayWidth(r) < maxWidth {
r += " "
}
return r
}
// TruncateColumn replaces the first new line character with an ellipsis
// and shortens a string to fit the maximum display width
func TruncateColumn(maxWidth int, s string) string {
if i := strings.IndexAny(s, "\r\n"); i >= 0 {
s = s[:i] + ellipsis
}
return Truncate(maxWidth, s)
}

View file

@ -1,265 +0,0 @@
package text
import (
"testing"
)
func TestTruncate(t *testing.T) {
type args struct {
max int
s string
}
tests := []struct {
name string
args args
want string
}{
{
name: "Short enough",
args: args{
max: 5,
s: "short",
},
want: "short",
},
{
name: "Too short",
args: args{
max: 4,
s: "short",
},
want: "shor",
},
{
name: "Japanese",
args: args{
max: 11,
s: "テストテストテストテスト",
},
want: "テストテ...",
},
{
name: "Japanese filled",
args: args{
max: 11,
s: "aテストテストテストテスト",
},
want: "aテスト... ",
},
{
name: "Chinese",
args: args{
max: 11,
s: "幫新舉報違章工廠新增編號",
},
want: "幫新舉報...",
},
{
name: "Chinese filled",
args: args{
max: 11,
s: "a幫新舉報違章工廠新增編號",
},
want: "a幫新舉... ",
},
{
name: "Korean",
args: args{
max: 11,
s: "프로젝트 내의",
},
want: "프로젝트...",
},
{
name: "Korean filled",
args: args{
max: 11,
s: "a프로젝트 내의",
},
want: "a프로젝... ",
},
{
name: "Emoji",
args: args{
max: 11,
s: "💡💡💡💡💡💡💡💡💡💡💡💡",
},
want: "💡💡💡💡...",
},
{
name: "Accented characters",
args: args{
max: 11,
s: "é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́",
},
want: "é́́é́́é́́é́́é́́é́́é́́é́́...",
},
{
name: "Red accented characters",
args: args{
max: 11,
s: "\x1b[0;31mé́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́\x1b[0m",
},
want: "\x1b[0;31mé́́é́́é́́é́́é́́é́́é́́é́́...\x1b[0m",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Truncate(tt.args.max, tt.args.s); got != tt.want {
t.Errorf("Truncate() = %q, want %q", got, tt.want)
}
})
}
}
func TestTruncateColumn(t *testing.T) {
type args struct {
max int
s string
}
tests := []struct {
name string
args args
want string
}{
{
name: "exactly minimum width",
args: args{
max: 5,
s: "short",
},
want: "short",
},
{
name: "exactly minimum width with new line",
args: args{
max: 5,
s: "short\n",
},
want: "sh...",
},
{
name: "less than minimum width",
args: args{
max: 4,
s: "short",
},
want: "shor",
},
{
name: "less than minimum width with new line",
args: args{
max: 4,
s: "short\n",
},
want: "shor",
},
{
name: "first line of multiple is short enough",
args: args{
max: 80,
s: "short\n\nthis is a new line",
},
want: "short...",
},
{
name: "using Windows line endings",
args: args{
max: 80,
s: "short\r\n\r\nthis is a new line",
},
want: "short...",
},
{
name: "using older MacOS line endings",
args: args{
max: 80,
s: "short\r\rthis is a new line",
},
want: "short...",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := TruncateColumn(tt.args.max, tt.args.s); got != tt.want {
t.Errorf("TruncateColumn() = %q, want %q", got, tt.want)
}
})
}
}
func TestDisplayWidth(t *testing.T) {
tests := []struct {
name string
text string
want int
}{
{
name: "check mark",
text: ``,
want: 1,
},
{
name: "bullet icon",
text: ``,
want: 1,
},
{
name: "middle dot",
text: `·`,
want: 1,
},
{
name: "ellipsis",
text: ``,
want: 1,
},
{
name: "right arrow",
text: ``,
want: 1,
},
{
name: "smart double quotes",
text: `“”`,
want: 2,
},
{
name: "smart single quotes",
text: ``,
want: 2,
},
{
name: "em dash",
text: ``,
want: 1,
},
{
name: "en dash",
text: ``,
want: 1,
},
{
name: "emoji",
text: `👍`,
want: 2,
},
{
name: "accent character",
text: `é́́`,
want: 1,
},
{
name: "color codes",
text: "\x1b[0;31mred\x1b[0m",
want: 3,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := DisplayWidth(tt.text); got != tt.want {
t.Errorf("DisplayWidth() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -6,8 +6,8 @@ import (
"sort"
"strings"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/text"
)
type TablePrinter interface {

View file

@ -1,25 +0,0 @@
package utils
import (
"fmt"
"os"
"github.com/mattn/go-isatty"
"golang.org/x/term"
)
var IsTerminal = func(f *os.File) bool {
return isatty.IsTerminal(f.Fd()) || IsCygwinTerminal(f)
}
func IsCygwinTerminal(f *os.File) bool {
return isatty.IsCygwinTerminal(f.Fd())
}
var TerminalSize = func(w interface{}) (int, int, error) {
if f, isFile := w.(*os.File); isFile {
return term.GetSize(int(f.Fd()))
}
return 0, 0, fmt.Errorf("%v is not a file", w)
}

View file

@ -2,98 +2,11 @@ package utils
import (
"fmt"
"net/url"
"os"
"strings"
"time"
"golang.org/x/term"
)
func Pluralize(num int, thing string) string {
if num == 1 {
return fmt.Sprintf("%d %s", num, thing)
}
return fmt.Sprintf("%d %ss", num, thing)
}
func fmtDuration(amount int, unit string) string {
return fmt.Sprintf("about %s ago", Pluralize(amount, unit))
}
func FuzzyAgo(ago time.Duration) string {
if ago < time.Minute {
return "less than a minute ago"
}
if ago < time.Hour {
return fmtDuration(int(ago.Minutes()), "minute")
}
if ago < 24*time.Hour {
return fmtDuration(int(ago.Hours()), "hour")
}
if ago < 30*24*time.Hour {
return fmtDuration(int(ago.Hours())/24, "day")
}
if ago < 365*24*time.Hour {
return fmtDuration(int(ago.Hours())/24/30, "month")
}
return fmtDuration(int(ago.Hours()/24/365), "year")
}
func FuzzyAgoAbbr(now time.Time, createdAt time.Time) string {
ago := now.Sub(createdAt)
if ago < time.Hour {
return fmt.Sprintf("%d%s", int(ago.Minutes()), "m")
}
if ago < 24*time.Hour {
return fmt.Sprintf("%d%s", int(ago.Hours()), "h")
}
if ago < 30*24*time.Hour {
return fmt.Sprintf("%d%s", int(ago.Hours())/24, "d")
}
return createdAt.Format("Jan _2, 2006")
}
func Humanize(s string) string {
// Replaces - and _ with spaces.
replace := "_-"
h := func(r rune) rune {
if strings.ContainsRune(replace, r) {
return ' '
}
return r
}
return strings.Map(h, s)
}
func IsURL(s string) bool {
return strings.HasPrefix(s, "http:/") || strings.HasPrefix(s, "https:/")
}
func DisplayURL(urlStr string) string {
u, err := url.Parse(urlStr)
if err != nil {
return urlStr
}
return u.Hostname() + u.Path
}
// Maximum length of a URL: 8192 bytes
func ValidURL(urlStr string) bool {
return len(urlStr) < 8192
}
func StringInSlice(a string, slice []string) bool {
for _, b := range slice {
if b == a {
return true
}
}
return false
}
func IsDebugEnabled() (bool, string) {
debugValue, isDebugSet := os.LookupEnv("GH_DEBUG")
legacyDebugValue := os.Getenv("DEBUG")
@ -114,3 +27,11 @@ func IsDebugEnabled() (bool, string) {
return true, debugValue
}
}
var TerminalSize = func(w interface{}) (int, int, error) {
if f, isFile := w.(*os.File); isFile {
return term.GetSize(int(f.Fd()))
}
return 0, 0, fmt.Errorf("%v is not a file", w)
}

View file

@ -1,63 +0,0 @@
package utils
import (
"testing"
"time"
)
func TestFuzzyAgo(t *testing.T) {
cases := map[string]string{
"1s": "less than a minute ago",
"30s": "less than a minute ago",
"1m08s": "about 1 minute ago",
"15m0s": "about 15 minutes ago",
"59m10s": "about 59 minutes ago",
"1h10m02s": "about 1 hour ago",
"15h0m01s": "about 15 hours ago",
"30h10m": "about 1 day ago",
"50h": "about 2 days ago",
"720h05m": "about 1 month ago",
"3000h10m": "about 4 months ago",
"8760h59m": "about 1 year ago",
"17601h59m": "about 2 years ago",
"262800h19m": "about 30 years ago",
}
for duration, expected := range cases {
d, e := time.ParseDuration(duration)
if e != nil {
t.Errorf("failed to create a duration: %s", e)
}
fuzzy := FuzzyAgo(d)
if fuzzy != expected {
t.Errorf("unexpected fuzzy duration value: %s for %s", fuzzy, duration)
}
}
}
func TestFuzzyAgoAbbr(t *testing.T) {
const form = "2006-Jan-02 15:04:05"
now, _ := time.Parse(form, "2020-Nov-22 14:00:00")
cases := map[string]string{
"2020-Nov-22 14:00:00": "0m",
"2020-Nov-22 13:59:00": "1m",
"2020-Nov-22 13:30:00": "30m",
"2020-Nov-22 13:00:00": "1h",
"2020-Nov-22 02:00:00": "12h",
"2020-Nov-21 14:00:00": "1d",
"2020-Nov-07 14:00:00": "15d",
"2020-Oct-24 14:00:00": "29d",
"2020-Oct-23 14:00:00": "Oct 23, 2020",
"2019-Nov-22 14:00:00": "Nov 22, 2019",
}
for createdAt, expected := range cases {
d, _ := time.Parse(form, createdAt)
fuzzy := FuzzyAgoAbbr(now, d)
if fuzzy != expected {
t.Errorf("unexpected fuzzy duration abbr value: %s for %s", fuzzy, createdAt)
}
}
}