diff --git a/cmd/gh/main.go b/cmd/gh/main.go index 9a5086c6388..750233ba7fa 100644 --- a/cmd/gh/main.go +++ b/cmd/gh/main.go @@ -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 diff --git a/go.mod b/go.mod index d2e73f1b628..63f6fcd2673 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index debaf0046b5..853bf9c5bf5 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/codespaces/states.go b/internal/codespaces/states.go index 887735caf6d..8b41f6fc66e 100644 --- a/internal/codespaces/states.go +++ b/internal/codespaces/states.go @@ -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. diff --git a/internal/text/text.go b/internal/text/text.go new file mode 100644 index 00000000000..0671e1e7480 --- /dev/null +++ b/internal/text/text.go @@ -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 +} diff --git a/internal/text/text_test.go b/internal/text/text_test.go new file mode 100644 index 00000000000..98f16da5d99 --- /dev/null +++ b/internal/text/text_test.go @@ -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) + } +} diff --git a/pkg/cmd/browse/browse.go b/pkg/cmd/browse/browse.go index 094a5be8240..ddff8bd9246 100644 --- a/pkg/cmd/browse/browse.go +++ b/pkg/cmd/browse/browse.go @@ -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) } diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go index 4138e27717a..a5da972e335 100644 --- a/pkg/cmd/codespace/create.go +++ b/pkg/cmd/codespace/create.go @@ -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 +} diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index b5aaf9f5c8f..310f1c65811 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -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) } diff --git a/pkg/cmd/gist/create/create.go b/pkg/cmd/gist/create/create.go index 1055da46880..8dfd19664b1 100644 --- a/pkg/cmd/gist/create/create.go +++ b/pkg/cmd/gist/create/create.go @@ -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) } diff --git a/pkg/cmd/gist/list/list.go b/pkg/cmd/gist/list/list.go index e1f45378138..bdf936d2367 100644 --- a/pkg/cmd/gist/list/list.go +++ b/pkg/cmd/gist/list/list.go @@ -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() diff --git a/pkg/cmd/gist/view/view.go b/pkg/cmd/gist/view/view.go index 073700a300d..489994f54fd 100644 --- a/pkg/cmd/gist/view/view.go +++ b/pkg/cmd/gist/view/view.go @@ -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) } diff --git a/pkg/cmd/gpg-key/list/list.go b/pkg/cmd/gpg-key/list/list.go index 137f61f0b59..e8ac1644e53 100644 --- a/pkg/cmd/gpg-key/list/list.go +++ b/pkg/cmd/gpg-key/list/list.go @@ -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) diff --git a/pkg/cmd/issue/create/create.go b/pkg/cmd/issue/create/create.go index f12b6006f74..c6f057388f3 100644 --- a/pkg/cmd/issue/create/create.go +++ b/pkg/cmd/issue/create/create.go @@ -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 { diff --git a/pkg/cmd/issue/list/list.go b/pkg/cmd/issue/list/list.go index 20ac2156cb6..cdfc985bad5 100644 --- a/pkg/cmd/issue/list/list.go +++ b/pkg/cmd/issue/list/list.go @@ -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) } diff --git a/pkg/cmd/issue/shared/display.go b/pkg/cmd/issue/shared/display.go index 3997b6f5bca..84b26633a01 100644 --- a/pkg/cmd/issue/shared/display.go +++ b/pkg/cmd/issue/shared/display.go @@ -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) } diff --git a/pkg/cmd/issue/view/view.go b/pkg/cmd/issue/view/view.go index 9d1680359fc..044cb6c3d23 100644 --- a/pkg/cmd/issue/view/view.go +++ b/pkg/cmd/issue/view/view.go @@ -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 diff --git a/pkg/cmd/label/clone.go b/pkg/cmd/label/clone.go index b6d469cecee..302de145afe 100644 --- a/pkg/cmd/label/clone.go +++ b/pkg/cmd/label/clone.go @@ -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) diff --git a/pkg/cmd/label/list.go b/pkg/cmd/label/list.go index 98256c048ac..4efec440f17 100644 --- a/pkg/cmd/label/list.go +++ b/pkg/cmd/label/list.go @@ -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) } diff --git a/pkg/cmd/pr/checks/checks.go b/pkg/cmd/pr/checks/checks.go index 7e8294dbb20..67c897a3033 100644 --- a/pkg/cmd/pr/checks/checks.go +++ b/pkg/cmd/pr/checks/checks.go @@ -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) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index d2149d5d8cb..92deb469d1f 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -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?$") diff --git a/pkg/cmd/pr/list/list.go b/pkg/cmd/pr/list/list.go index 258a70b514a..6c1be1bef80 100644 --- a/pkg/cmd/pr/list/list.go +++ b/pkg/cmd/pr/list/list.go @@ -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) } diff --git a/pkg/cmd/pr/shared/commentable.go b/pkg/cmd/pr/shared/commentable.go index 4d19e01b911..5d1037fb750 100644 --- a/pkg/cmd/pr/shared/commentable.go +++ b/pkg/cmd/pr/shared/commentable.go @@ -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: diff --git a/pkg/cmd/pr/shared/comments.go b/pkg/cmd/pr/shared/comments.go index bf834cb921d..e7d90a7e6a6 100644 --- a/pkg/cmd/pr/shared/comments.go +++ b/pkg/cmd/pr/shared/comments.go @@ -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")) } diff --git a/pkg/cmd/pr/shared/display.go b/pkg/cmd/pr/shared/display.go index f82d0943d42..f788b46e92e 100644 --- a/pkg/cmd/pr/shared/display.go +++ b/pkg/cmd/pr/shared/display.go @@ -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) } diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go index ed0813145f2..5b13cf6818c 100644 --- a/pkg/cmd/pr/shared/params.go +++ b/pkg/cmd/pr/shared/params.go @@ -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 { diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index 02f830bfda9..e3fc92f5e7f 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -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() diff --git a/pkg/cmd/pr/view/view.go b/pkg/cmd/pr/view/view.go index 78436caab08..a055922a855 100644 --- a/pkg/cmd/pr/view/view.go +++ b/pkg/cmd/pr/view/view.go @@ -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)), ) diff --git a/pkg/cmd/release/create/create.go b/pkg/cmd/release/create/create.go index c6a3294c3fe..73c74e18f14 100644 --- a/pkg/cmd/release/create/create.go +++ b/pkg/cmd/release/create/create.go @@ -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" ) diff --git a/pkg/cmd/release/list/list.go b/pkg/cmd/release/list/list.go index 1747d56b001..ef9f65f9421 100644 --- a/pkg/cmd/release/list/list.go +++ b/pkg/cmd/release/list/list.go @@ -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() diff --git a/pkg/cmd/release/upload/upload.go b/pkg/cmd/release/upload/upload.go index dac790923ac..8f573bd4ff8 100644 --- a/pkg/cmd/release/upload/upload.go +++ b/pkg/cmd/release/upload/upload.go @@ -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)) } diff --git a/pkg/cmd/release/view/view.go b/pkg/cmd/release/view/view.go index 4a53e0b2074..38cdc6523fc 100644 --- a/pkg/cmd/release/view/view.go +++ b/pkg/cmd/release/view/view.go @@ -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, diff --git a/pkg/cmd/repo/deploy-key/list/list.go b/pkg/cmd/repo/deploy-key/list/list.go index ee55e1987dc..f682582eec5 100644 --- a/pkg/cmd/repo/deploy-key/list/list.go +++ b/pkg/cmd/repo/deploy-key/list/list.go @@ -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() diff --git a/pkg/cmd/repo/fork/fork.go b/pkg/cmd/repo/fork/fork.go index 324f8fb1b64..e3fe80247bd 100644 --- a/pkg/cmd/repo/fork/fork.go +++ b/pkg/cmd/repo/fork/fork.go @@ -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:/") +} diff --git a/pkg/cmd/repo/list/list.go b/pkg/cmd/repo/list/list.go index 223c16fb6ca..5a85485b113 100644 --- a/pkg/cmd/repo/list/list.go +++ b/pkg/cmd/repo/list/list.go @@ -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) } diff --git a/pkg/cmd/repo/view/view.go b/pkg/cmd/repo/view/view.go index 46b6a1b9deb..5074d02ee10 100644 --- a/pkg/cmd/repo/view/view.go +++ b/pkg/cmd/repo/view/view.go @@ -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) } diff --git a/pkg/cmd/root/help.go b/pkg/cmd/root/help.go index d6be5abeabb..40fbec01d5f 100644 --- a/pkg/cmd/root/help.go +++ b/pkg/cmd/root/help.go @@ -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" ) diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index 361d7322352..7f87aaa33ec 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -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" ) diff --git a/pkg/cmd/run/list/list.go b/pkg/cmd/run/list/list.go index 5cbefb561d2..c1a06d0a1a6 100644 --- a/pkg/cmd/run/list/list.go +++ b/pkg/cmd/run/list/list.go @@ -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() } diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index 5f185ee0de3..7be8f6458cd 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -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 { diff --git a/pkg/cmd/run/watch/watch.go b/pkg/cmd/run/watch/watch.go index c3ed5b61ad4..4e3d40c8a54 100644 --- a/pkg/cmd/run/watch/watch.go +++ b/pkg/cmd/run/watch/watch.go @@ -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 { diff --git a/pkg/cmd/search/repos/repos.go b/pkg/cmd/search/repos/repos.go index c0f52802976..412b36cbbe8 100644 --- a/pkg/cmd/search/repos/repos.go +++ b/pkg/cmd/search/repos/repos.go @@ -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) } diff --git a/pkg/cmd/search/shared/shared.go b/pkg/cmd/search/shared/shared.go index f5fea736ebb..e67bc838160 100644 --- a/pkg/cmd/search/shared/shared.go +++ b/pkg/cmd/search/shared/shared.go @@ -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) } diff --git a/pkg/cmd/secret/shared/shared.go b/pkg/cmd/secret/shared/shared.go index 1e812ced305..14ecd293a12 100644 --- a/pkg/cmd/secret/shared/shared.go +++ b/pkg/cmd/secret/shared/shared.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/cli/cli/v2/pkg/text" + "github.com/cli/cli/v2/internal/text" ) type Visibility string diff --git a/pkg/cmd/ssh-key/list/list.go b/pkg/cmd/ssh-key/list/list.go index 943a3ddb05a..bc4fcb6261c 100644 --- a/pkg/cmd/ssh-key/list/list.go +++ b/pkg/cmd/ssh-key/list/list.go @@ -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() diff --git a/pkg/cmd/workflow/view/view.go b/pkg/cmd/workflow/view/view.go index 94ecce645db..a03592bbe3a 100644 --- a/pkg/cmd/workflow/view/view.go +++ b/pkg/cmd/workflow/view/view.go @@ -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) } diff --git a/pkg/search/query.go b/pkg/search/query.go index 8eb440af8fb..f93e1a46fd3 100644 --- a/pkg/search/query.go +++ b/pkg/search/query.go @@ -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 +} diff --git a/pkg/search/query_test.go b/pkg/search/query_test.go index 3e35fd7dbdd..27f20150812 100644 --- a/pkg/search/query_test.go +++ b/pkg/search/query_test.go @@ -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)) + }) + } +} diff --git a/pkg/text/convert.go b/pkg/text/convert.go deleted file mode 100644 index 8ec3db0dd0a..00000000000 --- a/pkg/text/convert.go +++ /dev/null @@ -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) -} diff --git a/pkg/text/convert_test.go b/pkg/text/convert_test.go deleted file mode 100644 index 5321fbf101a..00000000000 --- a/pkg/text/convert_test.go +++ /dev/null @@ -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)) - }) - } -} diff --git a/pkg/text/indent.go b/pkg/text/indent.go deleted file mode 100644 index f635397780f..00000000000 --- a/pkg/text/indent.go +++ /dev/null @@ -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) -} diff --git a/pkg/text/indent_test.go b/pkg/text/indent_test.go deleted file mode 100644 index c9331011405..00000000000 --- a/pkg/text/indent_test.go +++ /dev/null @@ -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) - } - }) - } -} diff --git a/pkg/text/sanitize.go b/pkg/text/sanitize.go deleted file mode 100644 index 16bb902dca3..00000000000 --- a/pkg/text/sanitize.go +++ /dev/null @@ -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), " ") -} diff --git a/pkg/text/sanitize_test.go b/pkg/text/sanitize_test.go deleted file mode 100644 index 1c03362d9e0..00000000000 --- a/pkg/text/sanitize_test.go +++ /dev/null @@ -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) - } - }) - } -} diff --git a/pkg/text/truncate.go b/pkg/text/truncate.go deleted file mode 100644 index 2de016126a1..00000000000 --- a/pkg/text/truncate.go +++ /dev/null @@ -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) -} diff --git a/pkg/text/truncate_test.go b/pkg/text/truncate_test.go deleted file mode 100644 index 5c82d019487..00000000000 --- a/pkg/text/truncate_test.go +++ /dev/null @@ -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) - } - }) - } -} diff --git a/utils/table_printer.go b/utils/table_printer.go index 80af79db1f9..2ccef943ce2 100644 --- a/utils/table_printer.go +++ b/utils/table_printer.go @@ -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 { diff --git a/utils/terminal.go b/utils/terminal.go deleted file mode 100644 index e97c8f12c05..00000000000 --- a/utils/terminal.go +++ /dev/null @@ -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) -} diff --git a/utils/utils.go b/utils/utils.go index 503fdf0af27..58894ba4fd2 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -2,97 +2,10 @@ package utils import ( "fmt" - "net/url" "os" - "strings" - "time" -) - -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 -} + "golang.org/x/term" +) func IsDebugEnabled() (bool, string) { debugValue, isDebugSet := os.LookupEnv("GH_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) +} diff --git a/utils/utils_test.go b/utils/utils_test.go deleted file mode 100644 index 5dc7b2478e8..00000000000 --- a/utils/utils_test.go +++ /dev/null @@ -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) - } - } -}