From 33def95b73aa7d663c66f37001278b44a13eae84 Mon Sep 17 00:00:00 2001 From: Greg Soltis Date: Thu, 3 Mar 2022 14:13:50 -0800 Subject: [PATCH 1/2] Start work on plan --- cli/cmd/turbo/main.go | 4 + cli/internal/plan/plan.go | 162 ++++++++++++++++++++++++++++++++++++++ cli/internal/ui/ui.go | 26 ++++++ 3 files changed, 192 insertions(+) create mode 100644 cli/internal/plan/plan.go diff --git a/cli/cmd/turbo/main.go b/cli/cmd/turbo/main.go index 69c994c677f8c..cf774c89f0eed 100644 --- a/cli/cmd/turbo/main.go +++ b/cli/cmd/turbo/main.go @@ -10,6 +10,7 @@ import ( "github.com/vercel/turborepo/cli/internal/config" "github.com/vercel/turborepo/cli/internal/info" "github.com/vercel/turborepo/cli/internal/login" + "github.com/vercel/turborepo/cli/internal/plan" "github.com/vercel/turborepo/cli/internal/process" prune "github.com/vercel/turborepo/cli/internal/prune" "github.com/vercel/turborepo/cli/internal/run" @@ -74,6 +75,9 @@ func main() { return &run.RunCommand{Config: cf, Ui: ui, Processes: processes}, nil }, + "plan": func() (cli.Command, error) { + return plan.NewCmd(cf, ui), nil + }, "prune": func() (cli.Command, error) { return &prune.PruneCommand{Config: cf, Ui: ui}, nil }, diff --git a/cli/internal/plan/plan.go b/cli/internal/plan/plan.go new file mode 100644 index 0000000000000..8e6ae43e58cfe --- /dev/null +++ b/cli/internal/plan/plan.go @@ -0,0 +1,162 @@ +package plan + +import ( + "encoding/json" + "fmt" + "os" + "sort" + + "github.com/fatih/color" + "github.com/mitchellh/cli" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/vercel/turborepo/cli/internal/config" + "github.com/vercel/turborepo/cli/internal/context" + "github.com/vercel/turborepo/cli/internal/scm" + "github.com/vercel/turborepo/cli/internal/scope" + "github.com/vercel/turborepo/cli/internal/ui" +) + +type PlanCommand struct { + config *config.Config + ui cli.Ui +} + +func NewCmd(config *config.Config, UI cli.Ui) *PlanCommand { + return &PlanCommand{ + config: config, + ui: UI, + } +} + +func (pc *PlanCommand) Help() string { + cmd := pc.getCmd() + return cmd.UsageString() +} + +func (pc *PlanCommand) Run(args []string) int { + cmd := pc.getCmd() + cmd.SetArgs(args) + err := cmd.Execute() + if err != nil { + pc.config.Logger.Error("error", err) + pc.ui.Error(fmt.Sprintf("%s%s", ui.ERROR_PREFIX, color.RedString(" %v", err))) + return 1 + } + return 0 +} + +func (pc *PlanCommand) Synopsis() string { + cmd := pc.getCmd() + return cmd.Short +} + +type planOpts struct { + Scope []string + GlobalDeps []string + Since string + Ignore []string + Cwd string + IncludeDepdendencies bool + NoDependents bool + OutputJSON bool +} + +func (po *planOpts) ScopeOpts() *scope.Opts { + + // includeDependencies := false + // if po.IncludeDepdendencies != nil { + // includeDependencies = po.IncludeDepdendencies + // } else if po.Since != "" && len(po.Scope) != 0 { + // includeDependencies = true + // } + return &scope.Opts{ + IncludeDependencies: po.IncludeDepdendencies, + IncludeDependents: !po.NoDependents, + Patterns: po.Scope, + Since: po.Since, + Cwd: po.Cwd, + IgnorePatterns: po.Ignore, + GlobalDepPatterns: po.GlobalDeps, + } +} + +func (pc *PlanCommand) getCmd() *cobra.Command { + opts := &planOpts{} + cmd := &cobra.Command{ + Use: "turbo plan", + Short: "Display packages affected by 'turbo run'", + Long: "Display the packages that would be affected by 'turbo run' with similar options", + RunE: func(cmd *cobra.Command, args []string) error { + // match the logic from run opts. If --since and --scope are specified, + // assume --include-dependencies. If the user has specified --include-dependencies, + // use the user's value. + depsWasSet := cmd.Flags().Changed("include-dependencies") + if !depsWasSet && opts.Since != "" && len(opts.Scope) != 0 { + opts.IncludeDepdendencies = true + } + return plan(pc.config, pc.ui, opts) + }, + } + flags := cmd.Flags() + flags.StringSliceVar(&opts.Scope, "scope", nil, "Specify package(s) to act as entry points for task\nexecution. Supports globs.") + flags.StringSliceVar(&opts.GlobalDeps, "global-deps", nil, "Specify glob of global filesystem dependencies to\nbe hashed. Useful for .env and files in the root\ndirectory. Can be specified multiple times.") + flags.StringVar(&opts.Since, "since", "", "Limit/Set scope to changed packages since a\nmergebase. This uses the git diff ${target_branch}...\nmechanism to identify which packages have changed.") + flags.StringSliceVar(&opts.Ignore, "ignore", nil, "Files to ignore when calculating changed files\n(i.e. --since). Supports globs.") + flags.BoolVar(&opts.IncludeDepdendencies, "include-dependencies", false, "Include the dependencies of tasks in execution.\n(default false)") + flags.BoolVar(&opts.NoDependents, "no-deps", false, "Exclude dependent task consumers from execution.\n(default false)") + flags.BoolVar(&opts.OutputJSON, "json", false, "If set, output the list of affected packages as a json array") + // TODO(gsoltis): this should probably be a permanent flag + flags.StringVar(&opts.Cwd, "cwd", "", "Which directory to run turbo in") + flags.MarkHidden("cwd") + return cmd +} + +func plan(config *config.Config, tui cli.Ui, opts *planOpts) error { + if opts.Cwd == "" { + cwd, err := os.Getwd() + if err != nil { + return errors.Wrap(err, "failed to get cwd") + } + opts.Cwd = cwd + } + scopeOpts := opts.ScopeOpts() + ctx, err := context.New(context.WithGraph(opts.Cwd, config)) + if err != nil { + return err + } + + scmInstance, err := scm.FromInRepo(scopeOpts.Cwd) + if err != nil { + if errors.Is(err, scm.ErrFallback) { + config.Logger.Warn(err.Error()) + } else { + return err + } + } + var resolveUI cli.Ui + if opts.OutputJSON { + resolveUI = ui.NullUI + } else { + resolveUI = tui + } + pkgs, err := scope.ResolvePackages(scopeOpts, scmInstance, ctx, resolveUI, config.Logger) + if err != nil { + return err + } + pkgList := pkgs.UnsafeListOfStrings() + sort.Strings(pkgList) + if opts.OutputJSON { + bytes, err := json.MarshalIndent(pkgList, "", " ") + if err != nil { + return errors.Wrap(err, "failed to render to JSON") + } + tui.Output(string(bytes)) + } else { + tui.Output("Packages in scope:") + for _, pkg := range pkgList { + tui.Output(fmt.Sprintf(" %v", pkg)) + } + } + return nil +} diff --git a/cli/internal/ui/ui.go b/cli/internal/ui/ui.go index c4c9852ee0579..727247103268d 100644 --- a/cli/internal/ui/ui.go +++ b/cli/internal/ui/ui.go @@ -77,3 +77,29 @@ func Default() *cli.ColoredUi { ErrorColor: cli.UiColorRed, } } + +type nullUI struct{} + +// Ask implements cli.Ui +func (*nullUI) Ask(string) (string, error) { + panic("unimplemented") +} + +// AskSecret implements cli.Ui +func (*nullUI) AskSecret(string) (string, error) { + panic("unimplemented") +} + +// Error implements cli.Ui +func (*nullUI) Error(string) {} + +// Info implements cli.Ui +func (*nullUI) Info(string) {} + +// Output implements cli.Ui +func (*nullUI) Output(string) {} + +// Warn implements cli.Ui +func (*nullUI) Warn(string) {} + +var NullUI cli.Ui = &nullUI{} From 60f0685e453082c68e76ab47b6db7c85aa004574 Mon Sep 17 00:00:00 2001 From: Greg Soltis Date: Wed, 9 Mar 2022 12:33:11 -0800 Subject: [PATCH 2/2] Fix typos, remove commented-out code --- cli/internal/plan/plan.go | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/cli/internal/plan/plan.go b/cli/internal/plan/plan.go index 8e6ae43e58cfe..46b97cf92aff0 100644 --- a/cli/internal/plan/plan.go +++ b/cli/internal/plan/plan.go @@ -52,26 +52,19 @@ func (pc *PlanCommand) Synopsis() string { } type planOpts struct { - Scope []string - GlobalDeps []string - Since string - Ignore []string - Cwd string - IncludeDepdendencies bool - NoDependents bool - OutputJSON bool + Scope []string + GlobalDeps []string + Since string + Ignore []string + Cwd string + IncludeDependencies bool + NoDependents bool + OutputJSON bool } func (po *planOpts) ScopeOpts() *scope.Opts { - - // includeDependencies := false - // if po.IncludeDepdendencies != nil { - // includeDependencies = po.IncludeDepdendencies - // } else if po.Since != "" && len(po.Scope) != 0 { - // includeDependencies = true - // } return &scope.Opts{ - IncludeDependencies: po.IncludeDepdendencies, + IncludeDependencies: po.IncludeDependencies, IncludeDependents: !po.NoDependents, Patterns: po.Scope, Since: po.Since, @@ -93,7 +86,7 @@ func (pc *PlanCommand) getCmd() *cobra.Command { // use the user's value. depsWasSet := cmd.Flags().Changed("include-dependencies") if !depsWasSet && opts.Since != "" && len(opts.Scope) != 0 { - opts.IncludeDepdendencies = true + opts.IncludeDependencies = true } return plan(pc.config, pc.ui, opts) }, @@ -103,7 +96,7 @@ func (pc *PlanCommand) getCmd() *cobra.Command { flags.StringSliceVar(&opts.GlobalDeps, "global-deps", nil, "Specify glob of global filesystem dependencies to\nbe hashed. Useful for .env and files in the root\ndirectory. Can be specified multiple times.") flags.StringVar(&opts.Since, "since", "", "Limit/Set scope to changed packages since a\nmergebase. This uses the git diff ${target_branch}...\nmechanism to identify which packages have changed.") flags.StringSliceVar(&opts.Ignore, "ignore", nil, "Files to ignore when calculating changed files\n(i.e. --since). Supports globs.") - flags.BoolVar(&opts.IncludeDepdendencies, "include-dependencies", false, "Include the dependencies of tasks in execution.\n(default false)") + flags.BoolVar(&opts.IncludeDependencies, "include-dependencies", false, "Include the dependencies of tasks in execution.\n(default false)") flags.BoolVar(&opts.NoDependents, "no-deps", false, "Exclude dependent task consumers from execution.\n(default false)") flags.BoolVar(&opts.OutputJSON, "json", false, "If set, output the list of affected packages as a json array") // TODO(gsoltis): this should probably be a permanent flag