这是indexloc提供的服务,不要输入任何密码
Skip to content

Housekeeping on "link" before Remote-Caching-Enabled work #1080

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 25, 2022
10 changes: 10 additions & 0 deletions cli/internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ func NewClient(baseURL string, logger hclog.Logger, turboVersion string, teamID
return client
}

// IsLoggedIn returns true if this ApiClient has a credential (token)
func (c *ApiClient) IsLoggedIn() bool {
return c.Token != ""
}

// SetTeamID sets the team parameter used on all requests by this client
func (c *ApiClient) SetTeamID(teamID string) {
c.teamID = teamID
}

func (c *ApiClient) retryCachePolicy(resp *http.Response, err error) (bool, error) {
if err != nil {
if errors.As(err, &x509.UnknownAuthorityError{}) {
Expand Down
247 changes: 156 additions & 91 deletions cli/internal/login/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
"path/filepath"
"strings"

"github.com/hashicorp/go-hclog"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/vercel/turborepo/cli/internal/client"
"github.com/vercel/turborepo/cli/internal/config"
"github.com/vercel/turborepo/cli/internal/fs"
Expand All @@ -24,111 +27,138 @@ type LinkCommand struct {
Ui *cli.ColoredUi
}

type link struct {
ui cli.Ui
logger hclog.Logger
modifyGitIgnore bool
apiURL string
apiClient linkAPIClient
promptSetup func(location string) (bool, error)
promptTeam func(teams []string) (string, error)
}

type linkAPIClient interface {
IsLoggedIn() bool
GetTeams() (*client.TeamsResponse, error)
GetUser() (*client.UserResponse, error)
SetTeamID(teamID string)
}

func getCmd(config *config.Config, ui cli.Ui) *cobra.Command {
var dontModifyGitIgnore bool
cmd := &cobra.Command{
Use: "turbo link",
Short: "Link your local directory to a Vercel organization and enable remote caching.",
RunE: func(cmd *cobra.Command, args []string) error {
link := &link{
ui: ui,
logger: config.Logger,
modifyGitIgnore: !dontModifyGitIgnore,
apiURL: config.ApiUrl,
apiClient: config.ApiClient,
promptSetup: promptSetup,
promptTeam: promptTeam,
}
err := link.run()
if err != nil {
if errors.Is(err, errUserCanceled) {
ui.Info("Canceled. Turborepo not set up.")
} else {
link.logError(err)
}
return err
}
return nil
},
}
cmd.Flags().BoolVar(&dontModifyGitIgnore, "no-gitignore", false, "Do not create or modify .gitignore (default false)")
return cmd
}

// Synopsis of link command
func (c *LinkCommand) Synopsis() string {
return "Link your local directory to a Vercel organization and enable remote caching."
cmd := getCmd(c.Config, c.Ui)
return cmd.Short
}

// Help returns information about the `link` command
func (c *LinkCommand) Help() string {
helpText := `
Usage: turbo link

Link your local directory to a Vercel organization and enable remote caching.

Options:
--help Show this screen.
--no-gitignore Do not create or modify .gitignore
(default false)
`
return strings.TrimSpace(helpText)
cmd := getCmd(c.Config, c.Ui)
return util.HelpForCobraCmd(cmd)
}

// Run links a local directory to a Vercel organization and enables remote caching
func (c *LinkCommand) Run(args []string) int {
var dontModifyGitIgnore bool
shouldSetup := true
dir, homeDirErr := homedir.Dir()
if homeDirErr != nil {
c.logError(fmt.Errorf("could not find home directory.\n%w", homeDirErr))
return 1
}
c.Ui.Info(">>> Remote Caching (beta)")
c.Ui.Info("")
c.Ui.Info(" Remote Caching shares your cached Turborepo task outputs and logs across")
c.Ui.Info(" all your team’s Vercel projects. It also can share outputs")
c.Ui.Info(" with other services that enable Remote Caching, like CI/CD systems.")
c.Ui.Info(" This results in faster build times and deployments for your team.")
c.Ui.Info(util.Sprintf(" For more info, see ${UNDERLINE}https://turborepo.org/docs/features/remote-caching${RESET}"))
c.Ui.Info("")
currentDir, fpErr := filepath.Abs(".")
if fpErr != nil {
c.logError(fmt.Errorf("could figure out file path.\n%w", fpErr))
cmd := getCmd(c.Config, c.Ui)
cmd.SetArgs(args)
err := cmd.Execute()
if err != nil {
return 1
}
return 0
}

survey.AskOne(
&survey.Confirm{
Default: true,
Message: util.Sprintf("Would you like to enable Remote Caching for ${CYAN}${BOLD}\"%s\"${RESET}?", strings.Replace(currentDir, dir, "~", 1)),
},
&shouldSetup, survey.WithValidator(survey.Required),
survey.WithIcons(func(icons *survey.IconSet) {
// for more information on formatting the icons, see here: https://github.com/mgutz/ansi#style-format
icons.Question.Format = "gray+hb"
}))
var errUserCanceled = errors.New("Canceled")

func (l *link) run() error {
dir, err := homedir.Dir()
if err != nil {
return fmt.Errorf("could not find home directory.\n%w", err)
}
l.ui.Info(">>> Remote Caching (beta)")
l.ui.Info("")
l.ui.Info(" Remote Caching shares your cached Turborepo task outputs and logs across")
l.ui.Info(" all your team’s Vercel projects. It also can share outputs")
l.ui.Info(" with other services that enable Remote Caching, like CI/CD systems.")
l.ui.Info(" This results in faster build times and deployments for your team.")
l.ui.Info(util.Sprintf(" For more info, see ${UNDERLINE}https://turborepo.org/docs/features/remote-caching${RESET}"))
l.ui.Info("")
currentDir, err := filepath.Abs(".")
if err != nil {
return fmt.Errorf("could figure out file path.\n%w", err)
}
repoLocation := strings.Replace(currentDir, dir, "~", 1)
shouldSetup, err := l.promptSetup(repoLocation)
if err != nil {
return err
}
if !shouldSetup {
c.Ui.Info("> Canceled.")
return 1
return errUserCanceled
}

if c.Config.Token == "" {
c.logError(fmt.Errorf(util.Sprintf("User not found. Please login to Turborepo first by running ${BOLD}`npx turbo login`${RESET}.")))
return 1
if !l.apiClient.IsLoggedIn() {
return fmt.Errorf(util.Sprintf("User not found. Please login to Turborepo first by running ${BOLD}`npx turbo login`${RESET}."))
}

teamsResponse, err := c.Config.ApiClient.GetTeams()
teamsResponse, err := l.apiClient.GetTeams()
if err != nil {
c.logError(fmt.Errorf("could not get team information.\n%w", err))
return 1
return fmt.Errorf("could not get team information.\n%w", err)
}
userResponse, err := c.Config.ApiClient.GetUser()
userResponse, err := l.apiClient.GetUser()
if err != nil {
c.logError(fmt.Errorf("could not get user information.\n%w", err))
return 1
return fmt.Errorf("could not get user information.\n%w", err)
}

var chosenTeam client.Team

teamOptions := make([]string, len(teamsResponse.Teams))

// Gather team options
for i, team := range teamsResponse.Teams {
teamOptions[i] = team.Name
}

var chosenTeamName string
teamOptions := make([]string, len(teamsResponse.Teams)+1)
nameWithFallback := userResponse.User.Name
if nameWithFallback == "" {
nameWithFallback = userResponse.User.Username
}
survey.AskOne(
&survey.Select{
Message: "Which Vercel scope (and Remote Cache) do you want associate with this Turborepo? ",
Options: append([]string{nameWithFallback}, teamOptions...),
},
&chosenTeamName,
survey.WithValidator(survey.Required),
survey.WithIcons(func(icons *survey.IconSet) {
// for more information on formatting the icons, see here: https://github.com/mgutz/ansi#style-format
icons.Question.Format = "gray+hb"
}))
teamOptions[0] = nameWithFallback
for i, team := range teamsResponse.Teams {
teamOptions[i+1] = team.Name
}

chosenTeamName, err := l.promptTeam(teamOptions)
if err != nil {
return err
}
if chosenTeamName == "" {
c.Ui.Info("Canceled. Turborepo not set up.")
return 1
} else if (chosenTeamName == userResponse.User.Name) || (chosenTeamName == userResponse.User.Username) {
return errUserCanceled
}
var chosenTeam client.Team
if (chosenTeamName == userResponse.User.Name) || (chosenTeamName == userResponse.User.Username) {
chosenTeam = client.Team{
ID: userResponse.User.ID,
Name: userResponse.User.Name,
Expand All @@ -143,34 +173,69 @@ func (c *LinkCommand) Run(args []string) int {
}
}
fs.EnsureDir(filepath.Join(".turbo", "config.json"))
fsErr := config.WriteRepoConfigFile(&config.TurborepoConfig{
err = config.WriteRepoConfigFile(&config.TurborepoConfig{
TeamId: chosenTeam.ID,
ApiUrl: c.Config.ApiUrl,
ApiUrl: l.apiURL,
})
if fsErr != nil {
c.logError(fmt.Errorf("could not link current directory to team/user.\n%w", fsErr))
return 1
if err != nil {
return fmt.Errorf("could not link current directory to team/user.\n%w", err)
}

if !dontModifyGitIgnore {
if l.modifyGitIgnore {
fs.EnsureDir(".gitignore")
_, gitIgnoreErr := exec.Command("sh", "-c", "grep -qxF '.turbo' .gitignore || echo '.turbo' >> .gitignore").CombinedOutput()
if err != nil {
c.logError(fmt.Errorf("could find or update .gitignore.\n%w", gitIgnoreErr))
return 1
return fmt.Errorf("could find or update .gitignore.\n%w", gitIgnoreErr)
}
}

c.Ui.Info("")
c.Ui.Info(util.Sprintf("%s${RESET} Turborepo CLI authorized for ${BOLD}%s${RESET}", ui.Rainbow(">>> Success!"), chosenTeamName))
c.Ui.Info("")
c.Ui.Info(util.Sprintf("${GREY}To disable Remote Caching, run `npx turbo unlink`${RESET}"))
c.Ui.Info("")
return 0
l.ui.Info("")
l.ui.Info(util.Sprintf("%s${RESET} Turborepo CLI authorized for ${BOLD}%s${RESET}", ui.Rainbow(">>> Success!"), chosenTeamName))
l.ui.Info("")
l.ui.Info(util.Sprintf("${GREY}To disable Remote Caching, run `npx turbo unlink`${RESET}"))
l.ui.Info("")
return nil
}

// logError logs an error and outputs it to the UI.
func (c *LinkCommand) logError(err error) {
c.Config.Logger.Error("error", err)
c.Ui.Error(fmt.Sprintf("%s%s", ui.ERROR_PREFIX, color.RedString(" %v", err)))
func (l *link) logError(err error) {
l.logger.Error("error", err)
l.ui.Error(fmt.Sprintf("%s%s", ui.ERROR_PREFIX, color.RedString(" %v", err)))
}

func promptSetup(location string) (bool, error) {
shouldSetup := true
err := survey.AskOne(
&survey.Confirm{
Default: true,
Message: util.Sprintf("Would you like to enable Remote Caching for ${CYAN}${BOLD}\"%s\"${RESET}?", location),
},
&shouldSetup, survey.WithValidator(survey.Required),
survey.WithIcons(func(icons *survey.IconSet) {
// for more information on formatting the icons, see here: https://github.com/mgutz/ansi#style-format
icons.Question.Format = "gray+hb"
}))
if err != nil {
return false, err
}
return shouldSetup, nil
}

func promptTeam(teams []string) (string, error) {
chosenTeamName := ""
err := survey.AskOne(
&survey.Select{
Message: "Which Vercel scope (and Remote Cache) do you want associate with this Turborepo? ",
Options: teams,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

survey supports pagination, ApiClient supports pagination, we should remember to go back and add the ability to request more.

},
&chosenTeamName,
survey.WithValidator(survey.Required),
survey.WithIcons(func(icons *survey.IconSet) {
// for more information on formatting the icons, see here: https://github.com/mgutz/ansi#style-format
icons.Question.Format = "gray+hb"
}))
if err != nil {
return "", err
}
return chosenTeamName, nil
}
17 changes: 17 additions & 0 deletions cli/internal/util/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package util

import (
"bytes"

"github.com/spf13/cobra"
)

// HelpForCobraCmd returns the help string for a given command
// Note that this overwrites the output for the command
func HelpForCobraCmd(cmd *cobra.Command) string {
f := cmd.HelpFunc()
buf := bytes.NewBufferString("")
cmd.SetOut(buf)
f(cmd, []string{})
return buf.String()
}