mirror of
https://github.com/avivsinai/bitbucket-cli.git
synced 2026-09-19 07:13:00 +08:00
feat: infer repo defaults from git remotes
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
package bktcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/avivsinai/bitbucket-cli/internal/build"
|
||||
"github.com/avivsinai/bitbucket-cli/pkg/cmd/factory"
|
||||
@@ -13,6 +16,9 @@ import (
|
||||
|
||||
// Main initialises CLI dependencies and executes the root command.
|
||||
func Main() int {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
f, err := factory.New(build.Version)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to initialise factory: %v\n", err)
|
||||
@@ -30,8 +36,9 @@ func Main() int {
|
||||
_, _ = fmt.Fprintf(ios.ErrOut, "failed to create root command: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
rootCmd.SetContext(ctx)
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
if err := rootCmd.ExecuteContext(ctx); err != nil {
|
||||
var exitErr *cmdutil.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
if exitErr.Msg != "" {
|
||||
|
||||
+253
-4
@@ -1,9 +1,18 @@
|
||||
package remote
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrNotImplemented signals that remote detection is not yet implemented.
|
||||
var ErrNotImplemented = errors.New("remote detection not implemented")
|
||||
// ErrNoGitRemote indicates that the repository does not contain a Bitbucket
|
||||
// remote we can infer defaults from.
|
||||
var ErrNoGitRemote = errors.New("no Bitbucket git remote found")
|
||||
|
||||
// Locator represents a repository identifier derived from a git remote.
|
||||
type Locator struct {
|
||||
@@ -16,5 +25,245 @@ type Locator struct {
|
||||
|
||||
// Detect attempts to infer the locator from git remotes.
|
||||
func Detect(repoPath string) (Locator, error) {
|
||||
return Locator{}, ErrNotImplemented
|
||||
repoPath = strings.TrimSpace(repoPath)
|
||||
if repoPath == "" {
|
||||
repoPath = "."
|
||||
}
|
||||
|
||||
remotes, err := listGitRemotes(repoPath)
|
||||
if err != nil {
|
||||
return Locator{}, err
|
||||
}
|
||||
if len(remotes) == 0 {
|
||||
return Locator{}, ErrNoGitRemote
|
||||
}
|
||||
|
||||
var tried []string
|
||||
appendIfMissing := func(values []string, candidate string) []string {
|
||||
for _, v := range values {
|
||||
if v == candidate {
|
||||
return values
|
||||
}
|
||||
}
|
||||
return append(values, candidate)
|
||||
}
|
||||
|
||||
var urls []string
|
||||
for _, name := range []string{"origin", "upstream"} {
|
||||
if candidates, ok := remotes[name]; ok {
|
||||
for _, candidate := range candidates {
|
||||
urls = appendIfMissing(urls, candidate)
|
||||
}
|
||||
tried = append(tried, name)
|
||||
}
|
||||
}
|
||||
|
||||
for name, candidates := range remotes {
|
||||
skipped := false
|
||||
for _, t := range tried {
|
||||
if t == name {
|
||||
skipped = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if skipped {
|
||||
continue
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
urls = appendIfMissing(urls, candidate)
|
||||
}
|
||||
}
|
||||
|
||||
for _, raw := range urls {
|
||||
loc, err := parseLocator(raw)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if loc.RepoSlug == "" {
|
||||
continue
|
||||
}
|
||||
return loc, nil
|
||||
}
|
||||
|
||||
return Locator{}, ErrNoGitRemote
|
||||
}
|
||||
|
||||
func listGitRemotes(repoPath string) (map[string][]string, error) {
|
||||
args := []string{"remote", "-v"}
|
||||
if repoPath != "." && repoPath != "" {
|
||||
args = append([]string{"-C", repoPath}, args...)
|
||||
} else {
|
||||
args = append([]string{"-C", "."}, args...)
|
||||
}
|
||||
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
return nil, ErrNoGitRemote
|
||||
}
|
||||
if errors.Is(err, exec.ErrNotFound) {
|
||||
return nil, fmt.Errorf("git executable not found: %w", err)
|
||||
}
|
||||
return nil, fmt.Errorf("git remote -v: %w", err)
|
||||
}
|
||||
|
||||
lines := strings.Split(string(out), "\n")
|
||||
result := make(map[string][]string)
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
name := fields[0]
|
||||
u := fields[1]
|
||||
|
||||
existing := result[name]
|
||||
already := false
|
||||
for _, v := range existing {
|
||||
if v == u {
|
||||
already = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !already {
|
||||
result[name] = append(result[name], u)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseLocator(raw string) (Locator, error) {
|
||||
host, segments, err := dissectRemote(raw)
|
||||
if err != nil {
|
||||
return Locator{}, err
|
||||
}
|
||||
if len(segments) < 2 {
|
||||
return Locator{}, fmt.Errorf("remote %q missing repository segments", raw)
|
||||
}
|
||||
|
||||
loc := Locator{
|
||||
Host: host,
|
||||
}
|
||||
|
||||
if host == "bitbucket.org" {
|
||||
loc.Kind = "cloud"
|
||||
loc.Workspace = segments[0]
|
||||
loc.RepoSlug = segments[1]
|
||||
return loc, nil
|
||||
}
|
||||
|
||||
project, repo := extractDCProjectRepo(segments)
|
||||
if project == "" || repo == "" {
|
||||
return Locator{}, fmt.Errorf("unable to parse Bitbucket Data Center remote %q", raw)
|
||||
}
|
||||
|
||||
loc.Kind = "dc"
|
||||
loc.ProjectKey = strings.ToUpper(project)
|
||||
loc.RepoSlug = repo
|
||||
return loc, nil
|
||||
}
|
||||
|
||||
func dissectRemote(raw string) (string, []string, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return "", nil, fmt.Errorf("empty remote URL")
|
||||
}
|
||||
|
||||
if strings.Contains(raw, "://") {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("parse remote: %w", err)
|
||||
}
|
||||
host := hostWithoutPort(u.Host)
|
||||
path := cleanPath(u.Path)
|
||||
segments := splitSegments(path)
|
||||
return host, segments, nil
|
||||
}
|
||||
|
||||
colon := strings.Index(raw, ":")
|
||||
if colon == -1 {
|
||||
return "", nil, fmt.Errorf("invalid remote URL %q", raw)
|
||||
}
|
||||
hostPart := raw[:colon]
|
||||
pathPart := raw[colon+1:]
|
||||
|
||||
if at := strings.LastIndex(hostPart, "@"); at != -1 {
|
||||
hostPart = hostPart[at+1:]
|
||||
}
|
||||
|
||||
host := hostWithoutPort(hostPart)
|
||||
segments := splitSegments(pathPart)
|
||||
return host, segments, nil
|
||||
}
|
||||
|
||||
func cleanPath(path string) string {
|
||||
path = strings.TrimSpace(path)
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
if idx := strings.Index(path, "?"); idx != -1 {
|
||||
path = path[:idx]
|
||||
}
|
||||
if idx := strings.Index(path, "#"); idx != -1 {
|
||||
path = path[:idx]
|
||||
}
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
return path
|
||||
}
|
||||
|
||||
func splitSegments(path string) []string {
|
||||
rawSegments := strings.FieldsFunc(path, func(r rune) bool { return r == '/' })
|
||||
var segments []string
|
||||
for _, seg := range rawSegments {
|
||||
seg = strings.TrimSpace(seg)
|
||||
if seg == "" {
|
||||
continue
|
||||
}
|
||||
segments = append(segments, seg)
|
||||
}
|
||||
if len(segments) == 0 {
|
||||
return segments
|
||||
}
|
||||
|
||||
last := segments[len(segments)-1]
|
||||
last = strings.TrimSuffix(last, ".git")
|
||||
segments[len(segments)-1] = last
|
||||
return segments
|
||||
}
|
||||
|
||||
func extractDCProjectRepo(segments []string) (string, string) {
|
||||
if len(segments) >= 4 && strings.EqualFold(segments[0], "projects") && strings.EqualFold(segments[2], "repos") {
|
||||
return segments[1], segments[3]
|
||||
}
|
||||
if len(segments) >= 3 && strings.EqualFold(segments[0], "scm") {
|
||||
return segments[1], segments[2]
|
||||
}
|
||||
if len(segments) >= 2 {
|
||||
return segments[0], segments[1]
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func hostWithoutPort(host string) string {
|
||||
host = strings.TrimSpace(host)
|
||||
host = strings.Trim(host, "[]")
|
||||
if host == "" {
|
||||
return host
|
||||
}
|
||||
if strings.Contains(host, ":") {
|
||||
if parsed, _, err := net.SplitHostPort(host); err == nil {
|
||||
host = parsed
|
||||
}
|
||||
}
|
||||
return strings.ToLower(host)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package remote
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDetectCloudHTTPS(t *testing.T) {
|
||||
dir := initGitRepo(t, "https://bitbucket.org/workspace/repo.git")
|
||||
|
||||
loc, err := Detect(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Detect() error = %v", err)
|
||||
}
|
||||
if loc.Kind != "cloud" {
|
||||
t.Fatalf("kind = %q, want %q", loc.Kind, "cloud")
|
||||
}
|
||||
if loc.Workspace != "workspace" {
|
||||
t.Fatalf("workspace = %q, want %q", loc.Workspace, "workspace")
|
||||
}
|
||||
if loc.RepoSlug != "repo" {
|
||||
t.Fatalf("repo = %q, want %q", loc.RepoSlug, "repo")
|
||||
}
|
||||
if loc.Host != "bitbucket.org" {
|
||||
t.Fatalf("host = %q, want %q", loc.Host, "bitbucket.org")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectCloudSSH(t *testing.T) {
|
||||
dir := initGitRepo(t, "git@bitbucket.org:workspace/repo.git")
|
||||
|
||||
loc, err := Detect(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Detect() error = %v", err)
|
||||
}
|
||||
if loc.Kind != "cloud" || loc.Workspace != "workspace" || loc.RepoSlug != "repo" {
|
||||
t.Fatalf("locator = %+v", loc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectDataCenterScm(t *testing.T) {
|
||||
dir := initGitRepo(t, "https://bitbucket.example.com/scm/PROJ/service.git")
|
||||
|
||||
loc, err := Detect(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Detect() error = %v", err)
|
||||
}
|
||||
if loc.Kind != "dc" {
|
||||
t.Fatalf("kind = %q, want %q", loc.Kind, "dc")
|
||||
}
|
||||
if loc.ProjectKey != "PROJ" {
|
||||
t.Fatalf("project = %q, want %q", loc.ProjectKey, "PROJ")
|
||||
}
|
||||
if loc.RepoSlug != "service" {
|
||||
t.Fatalf("repo = %q, want %q", loc.RepoSlug, "service")
|
||||
}
|
||||
if loc.Host != "bitbucket.example.com" {
|
||||
t.Fatalf("host = %q, want %q", loc.Host, "bitbucket.example.com")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectDataCenterProjects(t *testing.T) {
|
||||
dir := initGitRepo(t, "ssh://git@bitbucket.example.com:7999/projects/PROJ/repos/service.git")
|
||||
|
||||
loc, err := Detect(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Detect() error = %v", err)
|
||||
}
|
||||
if loc.Kind != "dc" || loc.ProjectKey != "PROJ" || loc.RepoSlug != "service" {
|
||||
t.Fatalf("locator = %+v", loc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectNoRemote(t *testing.T) {
|
||||
dir := initGitRepo(t, "")
|
||||
|
||||
_, err := Detect(dir)
|
||||
if !errors.Is(err, ErrNoGitRemote) {
|
||||
t.Fatalf("Detect() error = %v, want %v", err, ErrNoGitRemote)
|
||||
}
|
||||
}
|
||||
|
||||
func initGitRepo(t *testing.T, remoteURL string) string {
|
||||
t.Helper()
|
||||
|
||||
dir := t.TempDir()
|
||||
runGit(t, dir, "init", ".")
|
||||
|
||||
if remoteURL != "" {
|
||||
runGit(t, dir, "remote", "add", "origin", remoteURL)
|
||||
}
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
func runGit(t *testing.T, dir string, args ...string) {
|
||||
t.Helper()
|
||||
|
||||
cmdArgs := append([]string{"-C", dir}, args...)
|
||||
cmd := exec.Command("git", cmdArgs...)
|
||||
cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("git %v failed: %v\n%s", args, err, output)
|
||||
}
|
||||
}
|
||||
+10
-4
@@ -768,16 +768,22 @@ func cloneLinksCloud(repo bbcloud.Repository) []string {
|
||||
}
|
||||
|
||||
func selectCloneURLDC(repo bbdc.Repository, useSSH bool) (string, error) {
|
||||
desired := "http"
|
||||
if useSSH {
|
||||
desired = "ssh"
|
||||
for _, link := range repo.Links.Clone {
|
||||
if strings.EqualFold(link.Name, "ssh") {
|
||||
return link.Href, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no ssh clone URL available")
|
||||
}
|
||||
|
||||
for _, link := range repo.Links.Clone {
|
||||
if strings.EqualFold(link.Name, desired) {
|
||||
name := strings.ToLower(strings.TrimSpace(link.Name))
|
||||
if name == "https" || name == "http" {
|
||||
return link.Href, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no %s clone URL available", desired)
|
||||
return "", fmt.Errorf("no https clone URL available")
|
||||
}
|
||||
|
||||
func selectCloneURLCloud(repo bbcloud.Repository, useSSH bool) (string, error) {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/avivsinai/bitbucket-cli/pkg/bbdc"
|
||||
)
|
||||
|
||||
func TestSelectCloneURLDCPrefersHTTPS(t *testing.T) {
|
||||
var r bbdc.Repository
|
||||
r.Links.Clone = []struct {
|
||||
Href string `json:"href"`
|
||||
Name string `json:"name"`
|
||||
}{
|
||||
{Href: "ssh://git@bitbucket.example.com:7999/PROJ/repo.git", Name: "ssh"},
|
||||
{Href: "https://bitbucket.example.com/scm/PROJ/repo.git", Name: "https"},
|
||||
}
|
||||
|
||||
got, err := selectCloneURLDC(r, false)
|
||||
if err != nil {
|
||||
t.Fatalf("selectCloneURLDC returned error: %v", err)
|
||||
}
|
||||
if got != "https://bitbucket.example.com/scm/PROJ/repo.git" {
|
||||
t.Fatalf("got %q, want https link", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectCloneURLDCHttpAlias(t *testing.T) {
|
||||
var r bbdc.Repository
|
||||
r.Links.Clone = []struct {
|
||||
Href string `json:"href"`
|
||||
Name string `json:"name"`
|
||||
}{
|
||||
{Href: "http://bitbucket.example.com/scm/PROJ/repo.git", Name: "http"},
|
||||
}
|
||||
|
||||
got, err := selectCloneURLDC(r, false)
|
||||
if err != nil {
|
||||
t.Fatalf("selectCloneURLDC returned error: %v", err)
|
||||
}
|
||||
if got != "http://bitbucket.example.com/scm/PROJ/repo.git" {
|
||||
t.Fatalf("got %q, want http link", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectCloneURLDCSsh(t *testing.T) {
|
||||
var r bbdc.Repository
|
||||
r.Links.Clone = []struct {
|
||||
Href string `json:"href"`
|
||||
Name string `json:"name"`
|
||||
}{
|
||||
{Href: "ssh://git@bitbucket.example.com:7999/PROJ/repo.git", Name: "ssh"},
|
||||
{Href: "https://bitbucket.example.com/scm/PROJ/repo.git", Name: "https"},
|
||||
}
|
||||
|
||||
got, err := selectCloneURLDC(r, true)
|
||||
if err != nil {
|
||||
t.Fatalf("selectCloneURLDC returned error: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(got, "ssh://") {
|
||||
t.Fatalf("got %q, want ssh link", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectCloneURLDCMissing(t *testing.T) {
|
||||
var r bbdc.Repository
|
||||
r.Links.Clone = []struct {
|
||||
Href string `json:"href"`
|
||||
Name string `json:"name"`
|
||||
}{
|
||||
{Href: "https://bitbucket.example.com/scm/PROJ/repo.git", Name: "https"},
|
||||
}
|
||||
|
||||
_, err := selectCloneURLDC(r, true)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error when ssh clone missing")
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package root
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/avivsinai/bitbucket-cli/pkg/cmd/admin"
|
||||
@@ -42,8 +40,6 @@ Common flows:
|
||||
},
|
||||
}
|
||||
|
||||
root.SetContext(context.Background())
|
||||
|
||||
root.PersistentFlags().StringP("context", "c", "", "Active Bitbucket context name")
|
||||
root.PersistentFlags().Bool("json", false, "Output in JSON format when supported")
|
||||
root.PersistentFlags().Bool("yaml", false, "Output in YAML format when supported")
|
||||
|
||||
@@ -2,10 +2,15 @@ package cmdutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/avivsinai/bitbucket-cli/internal/config"
|
||||
"github.com/avivsinai/bitbucket-cli/internal/remote"
|
||||
)
|
||||
|
||||
// ResolveContext fetches the context and host configuration given an optional
|
||||
@@ -40,6 +45,8 @@ func ResolveContext(f *Factory, cmd *cobra.Command, override string) (string, *c
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
applyRemoteDefaults(ctx, host)
|
||||
|
||||
return contextName, ctx, host, nil
|
||||
}
|
||||
|
||||
@@ -51,3 +58,81 @@ func FlagValue(cmd *cobra.Command, name string) string {
|
||||
}
|
||||
return flag.Value.String()
|
||||
}
|
||||
|
||||
func applyRemoteDefaults(ctx *config.Context, host *config.Host) {
|
||||
if ctx == nil || host == nil {
|
||||
return
|
||||
}
|
||||
|
||||
needsWorkspace := host.Kind == "cloud" && ctx.Workspace == ""
|
||||
needsProject := host.Kind == "dc" && ctx.ProjectKey == ""
|
||||
needsRepo := ctx.DefaultRepo == ""
|
||||
if !needsWorkspace && !needsProject && !needsRepo {
|
||||
return
|
||||
}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
loc, err := remote.Detect(wd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !locatorMatchesHost(host, loc) {
|
||||
return
|
||||
}
|
||||
|
||||
if needsRepo && loc.RepoSlug != "" {
|
||||
ctx.DefaultRepo = loc.RepoSlug
|
||||
}
|
||||
|
||||
if needsWorkspace && loc.Workspace != "" {
|
||||
ctx.Workspace = loc.Workspace
|
||||
}
|
||||
|
||||
if needsProject && loc.ProjectKey != "" {
|
||||
ctx.ProjectKey = loc.ProjectKey
|
||||
}
|
||||
}
|
||||
|
||||
func locatorMatchesHost(host *config.Host, loc remote.Locator) bool {
|
||||
if host == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch host.Kind {
|
||||
case "cloud":
|
||||
return loc.Kind == "cloud" && strings.EqualFold(loc.Host, "bitbucket.org")
|
||||
case "dc":
|
||||
if loc.Kind != "dc" {
|
||||
return false
|
||||
}
|
||||
baseHost := hostHostname(host.BaseURL)
|
||||
return baseHost != "" && strings.EqualFold(baseHost, loc.Host)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func hostHostname(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
u, err := url.Parse(raw)
|
||||
if err == nil && u.Host != "" {
|
||||
raw = u.Host
|
||||
}
|
||||
raw = strings.Trim(raw, "[]")
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.Contains(raw, ":") {
|
||||
if host, _, err := net.SplitHostPort(raw); err == nil {
|
||||
raw = host
|
||||
}
|
||||
}
|
||||
return strings.ToLower(raw)
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@ type noopSpinner struct {
|
||||
ios *iostreams.IOStreams
|
||||
}
|
||||
|
||||
// NewSpinner constructs a terminal spinner when stdout is a TTY. Otherwise a
|
||||
// NewSpinner constructs a terminal spinner when stderr is a TTY. Otherwise a
|
||||
// newline-based fallback is returned.
|
||||
func NewSpinner(ios *iostreams.IOStreams) Spinner {
|
||||
if ios != nil && ios.IsStdoutTTY() {
|
||||
if ios != nil && ios.IsStderrTTY() {
|
||||
return newTTYSpinner(ios)
|
||||
}
|
||||
return &noopSpinner{ios: ios}
|
||||
@@ -38,7 +38,7 @@ func (s *noopSpinner) write(msg string) {
|
||||
if s.ios == nil || msg == "" {
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(s.ios.Out, msg)
|
||||
fmt.Fprintln(s.ios.ErrOut, msg)
|
||||
}
|
||||
|
||||
type ttySpinner struct {
|
||||
@@ -71,7 +71,7 @@ func (s *ttySpinner) Start(msg string) {
|
||||
case <-stop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
fmt.Fprintf(s.ios.Out, "\r%c %s", frames[idx], msg)
|
||||
fmt.Fprintf(s.ios.ErrOut, "\r%c %s", frames[idx], msg)
|
||||
idx = (idx + 1) % len(frames)
|
||||
}
|
||||
}
|
||||
@@ -97,5 +97,5 @@ func (s *ttySpinner) endWithPrefix(prefix, msg string) {
|
||||
if msg == "" {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(s.ios.Out, "\r%s %s\n", prefix, msg)
|
||||
fmt.Fprintf(s.ios.ErrOut, "\r%s %s\n", prefix, msg)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user