diff --git a/cmd/terramate/cli/cli.go b/cmd/terramate/cli/cli.go index 95e964585..f9c505b50 100644 --- a/cmd/terramate/cli/cli.go +++ b/cmd/terramate/cli/cli.go @@ -152,6 +152,7 @@ type cliSpec struct { Run struct { runCommandFlags `envprefix:"TM_ARG_RUN_"` runSafeguardsCliSpec + outputsSharingFlags } `cmd:"" help:"Run command in the stacks"` Generate struct { @@ -168,6 +169,7 @@ type cliSpec struct { Run struct { runScriptFlags `envprefix:"TM_ARG_RUN_"` runSafeguardsCliSpec + outputsSharingFlags } `cmd:"" help:"Run a Terramate Script in stacks."` } `cmd:"" help:"Use Terramate Scripts"` @@ -304,6 +306,11 @@ type changeDetectionFlags struct { DisableChangeDetection []string `help:"Disable specific change detection modes" enum:"git-untracked,git-uncommitted"` } +type outputsSharingFlags struct { + IncludeOutputDependencies bool `help:"Include stacks that are dependencies of the selected stacks. (requires outputs-sharing experiment enabled)"` + OnlyOutputDependencies bool `help:"Only include stacks that are dependencies of the selected stacks. (requires outputs-sharing experiment enabled)"` +} + type cloudTargetFlags struct { Target string `env:"TARGET" help:"Set the deployment target for stacks synchronized to Terramate Cloud."` FromTarget string `env:"FROM_TARGET" help:"Migrate stacks from given deployment target."` @@ -1151,7 +1158,7 @@ func (c *cli) triggerStackByFilter() { stackFilter := cloud.StatusFilters{ StackStatus: statusFilter, } - stacksReport, err := c.listStacks(false, "", stackFilter, false) + stacksReport, err := c.listStacks(false, cloudstack.AnyTarget, stackFilter, false) if err != nil { fatalWithDetailf(err, "unable to list stacks") } @@ -2189,19 +2196,19 @@ func generateDot( } func (c *cli) generateDebug() { - // TODO(KATCIPIS): When we introduce config defined on root context - // we need to know blocks that have root context, since they should - // not be filtered by stack selection. - stacks, err := c.computeSelectedStacks(false, cloudstack.AnyTarget, cloud.NoStatusFilters()) + report, err := c.listStacks(c.parsedArgs.Changed, cloudstack.AnyTarget, cloud.NoStatusFilters(), false) if err != nil { fatalWithDetailf(err, "generate debug: selecting stacks") } selectedStacks := map[prj.Path]struct{}{} - for _, stack := range stacks { - log.Debug().Msgf("selected stack: %s", stack.Dir()) + for _, entry := range report.Stacks { + stackdir := entry.Stack.HostDir(c.cfg()) + if stackdir == c.wd() || strings.HasPrefix(stackdir, c.wd()+string(filepath.Separator)) { + log.Debug().Msgf("selected stack: %s", entry.Stack.Dir) - selectedStacks[stack.Dir()] = struct{}{} + selectedStacks[entry.Stack.Dir] = struct{}{} + } } results, err := generate.Load(c.cfg(), c.vendorDir()) @@ -2584,7 +2591,7 @@ func (c *cli) friendlyFmtDir(dir string) (string, bool) { return prj.FriendlyFmtDir(c.rootdir(), c.wd(), dir) } -func (c *cli) computeSelectedStacks(ensureCleanRepo bool, target string, stackFilters cloud.StatusFilters) (config.List[*config.SortableStack], error) { +func (c *cli) computeSelectedStacks(ensureCleanRepo bool, outputFlags outputsSharingFlags, target string, stackFilters cloud.StatusFilters) (config.List[*config.SortableStack], error) { report, err := c.listStacks(c.parsedArgs.Changed, target, stackFilters, true) if err != nil { return nil, err @@ -2602,7 +2609,96 @@ func (c *cli) computeSelectedStacks(ensureCleanRepo bool, target string, stackFi if err != nil { return nil, errors.E(err, "adding wanted stacks") } - return stacks, nil + return c.addOutputDependencies(outputFlags, stacks), nil +} + +func (c *cli) addOutputDependencies(outputFlags outputsSharingFlags, stacks config.List[*config.SortableStack]) config.List[*config.SortableStack] { + logger := log.With(). + Str("action", "cli.addOutputDependencies()"). + Logger() + + if !outputFlags.IncludeOutputDependencies && !outputFlags.OnlyOutputDependencies { + logger.Debug().Msg("output dependencies not requested") + return stacks + } + + if outputFlags.IncludeOutputDependencies && outputFlags.OnlyOutputDependencies { + fatal(errors.E("--include-output-dependencies and --only-output-dependencies cannot be used together")) + } + if (outputFlags.IncludeOutputDependencies || outputFlags.OnlyOutputDependencies) && !c.cfg().HasExperiment(hcl.SharingIsCaringExperimentName) { + fatal(errors.E("--include-output-dependencies requires the '%s' experiment enabled", hcl.SharingIsCaringExperimentName)) + } + + stacksMap := map[string]*config.SortableStack{} + for _, stack := range stacks { + stacksMap[stack.Stack.Dir.String()] = stack + } + + rootcfg := c.cfg() + depIDs := map[string]struct{}{} + depOrigins := map[string][]string{} // id -> stack paths + for _, st := range stacks { + evalctx := c.setupEvalContext(st.Stack, map[string]string{}) + cfg, _ := rootcfg.Lookup(st.Stack.Dir) + for _, inputcfg := range cfg.Node.Inputs { + fromStackID, err := config.EvalInputFromStackID(evalctx, inputcfg) + if err != nil { + fatalWithDetailf(err, "evaluating `input.%s.from_stack_id`", inputcfg.Name) + } + depIDs[fromStackID] = struct{}{} + depOrigins[fromStackID] = append(depOrigins[fromStackID], st.Stack.Dir.String()) + + logger.Debug(). + Str("stack", st.Stack.Dir.String()). + Str("dependency", fromStackID). + Msg("stack has output dependency") + } + } + + mgr := c.stackManager() + outputsMap := map[string]*config.SortableStack{} + for depID := range depIDs { + st, found, err := mgr.StackByID(depID) + if err != nil { + fatalWithDetailf(err, "loading output dependencies of selected stacks") + } + if !found { + fatalWithDetailf(errors.E("dependency stack %s not found", depID), "loading output dependencies of selected stacks") + } + + var reason string + depsOf := depOrigins[depID] + if len(depsOf) == 1 { + reason = stdfmt.Sprintf("Output dependency of stack %s", depsOf[0]) + } else { + reason = stdfmt.Sprintf("Output dependency of stacks %s", strings.Join(depsOf, ", ")) + } + + logger.Debug(). + Str("stack", st.Dir.String()). + Str("reason", reason). + Msg("adding output dependency") + + outputsMap[st.Dir.String()] = &config.SortableStack{ + Stack: st, + } + } + + if outputFlags.IncludeOutputDependencies { + for _, dep := range outputsMap { + if _, found := stacksMap[dep.Stack.Dir.String()]; !found { + stacks = append(stacks, dep) + } + } + return stacks + } + + // only output dependencies + stacks = config.List[*config.SortableStack]{} + for _, dep := range outputsMap { + stacks = append(stacks, dep) + } + return stacks } func (c *cli) filterStacks(stacks []stack.Entry) []stack.Entry { diff --git a/cmd/terramate/cli/run.go b/cmd/terramate/cli/run.go index 700a3661c..705b0ee09 100644 --- a/cmd/terramate/cli/run.go +++ b/cmd/terramate/cli/run.go @@ -144,7 +144,7 @@ func (c *cli) runOnStacks() { stackFilter := parseStatusFilter(c.parsedArgs.Run.Status) deploymentFilter := parseDeploymentStatusFilter(c.parsedArgs.Run.DeploymentStatus) driftFilter := parseDriftStatusFilter(c.parsedArgs.Run.DriftStatus) - stacks, err = c.computeSelectedStacks(true, c.parsedArgs.Run.Target, cloud.StatusFilters{ + stacks, err = c.computeSelectedStacks(true, c.parsedArgs.Run.outputsSharingFlags, c.parsedArgs.Run.Target, cloud.StatusFilters{ StackStatus: stackFilter, DeploymentStatus: deploymentFilter, DriftStatus: driftFilter, diff --git a/cmd/terramate/cli/script_info.go b/cmd/terramate/cli/script_info.go index 12309ffe2..572148c09 100644 --- a/cmd/terramate/cli/script_info.go +++ b/cmd/terramate/cli/script_info.go @@ -25,7 +25,7 @@ import ( func (c *cli) printScriptInfo() { labels := c.parsedArgs.Script.Info.Cmds - stacks, err := c.computeSelectedStacks(false, cloudstack.AnyTarget, cloud.NoStatusFilters()) + stacks, err := c.computeSelectedStacks(false, outputsSharingFlags{}, cloudstack.AnyTarget, cloud.NoStatusFilters()) if err != nil { fatalWithDetailf(err, "computing selected stacks") } diff --git a/cmd/terramate/cli/script_run.go b/cmd/terramate/cli/script_run.go index 7e072669d..f159ba89c 100644 --- a/cmd/terramate/cli/script_run.go +++ b/cmd/terramate/cli/script_run.go @@ -58,7 +58,7 @@ func (c *cli) runScript() { statusFilter := parseStatusFilter(c.parsedArgs.Script.Run.Status) deploymentFilter := parseDeploymentStatusFilter(c.parsedArgs.Script.Run.DeploymentStatus) driftFilter := parseDriftStatusFilter(c.parsedArgs.Script.Run.DriftStatus) - stacks, err = c.computeSelectedStacks(true, c.parsedArgs.Script.Run.Target, cloud.StatusFilters{ + stacks, err = c.computeSelectedStacks(true, c.parsedArgs.Script.Run.outputsSharingFlags, c.parsedArgs.Script.Run.Target, cloud.StatusFilters{ StackStatus: statusFilter, DeploymentStatus: deploymentFilter, DriftStatus: driftFilter, diff --git a/config/config.go b/config/config.go index 4a3e1beee..4e32e203d 100644 --- a/config/config.go +++ b/config/config.go @@ -220,6 +220,24 @@ func (root *Root) StacksByTagsFilters(filters []string) (project.Paths, error) { }).Paths(), nil } +// StackByID returns the stack with the given ID. +func (root *Root) StackByID(id string) (*Stack, bool, error) { + stacks := root.tree.stacks(func(tree *Tree) bool { + return tree.IsStack() && tree.Node.Stack.ID == id + }) + if len(stacks) == 0 { + return nil, false, nil + } + if len(stacks) > 1 { + printer.Stderr.Warnf("multiple stacks with the same ID %q found.", id) + } + stack, err := stacks[0].Stack() + if err != nil { + return nil, true, err + } + return stack, true, nil +} + // LoadSubTree loads a subtree located at cfgdir into the current tree. func (root *Root) LoadSubTree(cfgdir project.Path) error { var parent project.Path diff --git a/config/sharing_backend.go b/config/sharing_backend.go index b4346971f..b3f16b87d 100644 --- a/config/sharing_backend.go +++ b/config/sharing_backend.go @@ -41,6 +41,11 @@ type ( Outputs []Output ) +// EvalInputFromStackID the `from_stack_id` field of an input block using the provided evaluation context. +func EvalInputFromStackID(evalctx *eval.Context, input hcl.Input) (string, error) { + return evalString(evalctx, input.FromStackID, "input.from_stack_id") +} + // EvalInput evaluates an input block using the provided evaluation context. func EvalInput(evalctx *eval.Context, input hcl.Input) (Input, error) { evaluatedInput := Input{ diff --git a/e2etests/core/run_sharing_test.go b/e2etests/core/run_sharing_test.go index 5b554f017..c346aefed 100644 --- a/e2etests/core/run_sharing_test.go +++ b/e2etests/core/run_sharing_test.go @@ -665,3 +665,224 @@ func TestRunSharingParallel(t *testing.T) { }, ) } + +func TestRunOutputDependencies(t *testing.T) { + t.Parallel() + + t.Run("--*-output-dependencies must show an error if experiment is disabled", func(t *testing.T) { + t.Parallel() + s := sandbox.New(t) + // NOTE: outputs-sharing experiment is still not enabled by default. + + s.BuildTree([]string{ + `f:terramate.tm:` + Terramate( + Config( + Experiments("scripts"), + ), + ).String(), + `f:script.tm:` + Script( + Labels("test"), + Block("job", + Command(HelperPathAsHCL, "stack-abs-path", fmt.Sprintf(`${tm_chomp(<<-EOF + %s + EOF + )}`, s.RootDir())), + ), + ).String(), + }) + + expected := RunExpected{ + Status: 1, + StderrRegex: regexp.QuoteMeta("--include-output-dependencies requires the 'outputs-sharing' experiment enabled"), + } + tmcli := NewCLI(t, s.RootDir()) + AssertRunResult(t, tmcli.Run("run", "-X", "--include-output-dependencies", "--", HelperPath, "stack-abs-path", s.RootDir()), expected) + AssertRunResult(t, tmcli.Run("script", "run", "-X", "--include-output-dependencies", "test"), expected) + AssertRunResult(t, tmcli.Run("run", "-X", "--only-output-dependencies", "--", HelperPath, "stack-abs-path", s.RootDir()), expected) + AssertRunResult(t, tmcli.Run("script", "run", "-X", "--only-output-dependencies", "test"), expected) + }) + + type fixture struct { + sandbox *sandbox.S + dependencyPath string + dependentPath string + } + + setupSandbox := func(t *testing.T) fixture { + s := sandbox.New(t) + const stackDependencyName = "stack-dependency" + const stackDependentName = "stack-dependent" + s.BuildTree([]string{ + `f:terramate.tm:` + Terramate( + Config( + Experiments(hcl.SharingIsCaringExperimentName, "scripts"), + ), + ).String(), + `f:script.tm:` + Script( + Labels("test"), + Block("job", + Expr("command", fmt.Sprintf( + `["%s", "stack-abs-path", env.TM_TEST_BASEDIR]`, HelperPathAsHCL), + ), + ), + ).String(), + `f:sharing.tm:` + Block("sharing_backend", + Labels("default"), + Expr("type", "terraform"), + Command("terraform", "output", "-json"), + Str("filename", "_sharing.tf"), + ).String(), + "s:" + stackDependencyName + `:id=` + stackDependencyName, + `s:` + stackDependentName + `:after=["/stack-dependency"];tags=["dependent"]`, + }) + + s.RootEntry().CreateFile(stackDependencyName+"/outputs.tm", Block("output", + Labels("output1"), + Str("backend", "default"), + Expr("value", "some.value"), + ).String()) + + s.RootEntry().CreateFile(stackDependentName+"/inputs.tm", Block("input", + Labels("input1"), + Str("backend", "default"), + Expr("value", "outputs.output1.value"), + Str("from_stack_id", stackDependencyName), + ).String()) + + s.Generate() + s.Git().CommitAll("initial commit") + s.Git().Push("main") + return fixture{ + sandbox: &s, + dependencyPath: "/" + stackDependencyName, + dependentPath: "/" + stackDependentName, + } + } + + t.Run("must not show the output dependencies by default", func(t *testing.T) { + t.Parallel() + f := setupSandbox(t) + + // without any filters should just execute the stacks in scope + + { + expected := RunExpected{Stdout: nljoin(f.dependencyPath, f.dependentPath)} + cli := NewCLI(t, f.sandbox.RootDir()) + cli.AppendEnv = []string{"TM_TEST_BASEDIR=" + f.sandbox.RootDir()} + AssertRunResult(t, cli.Run("run", "--quiet", "--", HelperPath, "stack-abs-path", f.sandbox.RootDir()), expected) + AssertRunResult(t, cli.Run("script", "run", "--quiet", "test"), expected) + } + + { + for _, stack := range []string{f.dependencyPath, f.dependentPath} { + expected := RunExpected{Stdout: nljoin(stack)} + cli := NewCLI(t, filepath.Join(f.sandbox.RootDir(), stack)) + cli.AppendEnv = []string{"TM_TEST_BASEDIR=" + f.sandbox.RootDir()} + AssertRunResult(t, cli.Run("run", "--quiet", "--", HelperPath, "stack-abs-path", f.sandbox.RootDir()), expected) + AssertRunResult(t, cli.Run("script", "run", "--quiet", "test"), expected) + } + } + + { + // when filtering, no output dependencies should be shown (by default) + + git := f.sandbox.Git() + git.CheckoutNew("change-stack-dependent") + f.sandbox.DirEntry(f.dependentPath).CreateFile("main.tf", "# add file") + + expected := RunExpected{Stdout: nljoin(f.dependentPath)} + + cli := NewCLI(t, f.sandbox.RootDir()) + cli.AppendEnv = []string{"TM_TEST_BASEDIR=" + f.sandbox.RootDir()} + AssertRunResult(t, cli.Run("run", "-X", "--quiet", "--changed", "--", HelperPath, "stack-abs-path", f.sandbox.RootDir()), expected) + AssertRunResult(t, cli.Run("run", "-X", "--quiet", "--tags=dependent", "--", HelperPath, "stack-abs-path", f.sandbox.RootDir()), expected) + AssertRunResult(t, cli.Run("script", "run", "-X", "--changed", "--quiet", "test"), expected) + AssertRunResult(t, cli.Run("script", "run", "-X", "--tags=dependent", "--quiet", "test"), expected) + } + }) + + t.Run("--*-output-dependencies pull dependencies", func(t *testing.T) { + t.Parallel() + f := setupSandbox(t) + + // if dependency also changed, they must return once (no duplicates). + + f.sandbox.Git().CheckoutNew("change-both-stacks") + + cli := NewCLI(t, f.sandbox.RootDir()) + f.sandbox.DirEntry(f.dependencyPath).CreateFile("main.tf", "# add file") + f.sandbox.DirEntry(f.dependentPath).CreateFile("main.tf", "# add file") + + AssertRunResult(t, cli.Run("run", "--quiet", "--changed", "-X", "--", HelperPath, "stack-abs-path", f.sandbox.RootDir()), RunExpected{ + Stdout: nljoin(f.dependencyPath, f.dependentPath), + }) + + f.sandbox.Git().CommitAll("change both") + f.sandbox.Git().Checkout("main") + f.sandbox.Git().Merge("change-both-stacks") + f.sandbox.Git().Push("main") + + f.sandbox.Git().CheckoutNew("change-dependent") + + { + // --*-output-dependencies must pull dependencies if they are out of scope + // scope=changed + + cli := NewCLI(t, f.sandbox.RootDir()) + cli.AppendEnv = []string{"TM_TEST_BASEDIR=" + f.sandbox.RootDir()} + + normalExpected := RunExpected{Stdout: nljoin(f.dependentPath)} + f.sandbox.DirEntry(f.dependentPath).CreateFile("main.tf", "# changed file") + AssertRunResult(t, cli.Run("run", "-X", "--quiet", "--changed", "--", HelperPath, "stack-abs-path", f.sandbox.RootDir()), normalExpected) // sanity check + + inclExpected := RunExpected{Stdout: nljoin(f.dependencyPath, f.dependentPath)} + AssertRunResult(t, cli.Run("run", "--quiet", "-X", "--quiet", "--changed", "--include-output-dependencies", "--", HelperPath, "stack-abs-path", f.sandbox.RootDir()), inclExpected) + AssertRunResult(t, cli.Run("script", "run", "--quiet", "-X", "--quiet", "--include-output-dependencies", "test"), inclExpected) + onlyExpected := RunExpected{Stdout: nljoin(f.dependencyPath)} + AssertRunResult(t, cli.Run("run", "--quiet", "-X", "--quiet", "--changed", "--only-output-dependencies", "--", HelperPath, "stack-abs-path", f.sandbox.RootDir()), onlyExpected) + AssertRunResult(t, cli.Run("script", "run", "--quiet", "-X", "--quiet", "--only-output-dependencies", "test"), onlyExpected) + } + + { + // --*-output-dependencies must pull dependencies if they are out of scope + // scope=path + + cwd := filepath.Join(f.sandbox.RootDir(), f.dependentPath) + cli := NewCLI(t, cwd) + cli.AppendEnv = []string{"TM_TEST_BASEDIR=" + f.sandbox.RootDir()} + + normalExpected := RunExpected{Stdout: nljoin(f.dependentPath)} + f.sandbox.DirEntry(f.dependentPath).CreateFile("main.tf", "# changed file") + AssertRunResult(t, cli.Run("run", "--quiet", "-X", "--", HelperPath, "stack-abs-path", f.sandbox.RootDir()), normalExpected) // sanity check + + inclExpected := RunExpected{Stdout: nljoin(f.dependencyPath, f.dependentPath)} + AssertRunResult(t, cli.Run("run", "--quiet", "-X", "--include-output-dependencies", "--", HelperPath, "stack-abs-path", f.sandbox.RootDir()), inclExpected) + AssertRunResult(t, cli.Run("script", "run", "--quiet", "-X", "--include-output-dependencies", "test"), inclExpected) + + onlyExpected := RunExpected{Stdout: nljoin(f.dependencyPath)} + AssertRunResult(t, cli.Run("run", "--quiet", "-X", "--only-output-dependencies", "--", HelperPath, "stack-abs-path", f.sandbox.RootDir()), onlyExpected) + AssertRunResult(t, cli.Run("script", "run", "--quiet", "-X", "--only-output-dependencies", "test"), onlyExpected) + } + + { + // --*-output-dependencies must pull dependencies if they are out of scope + // scope=tags + + cli := NewCLI(t, f.sandbox.RootDir()) + cli.AppendEnv = []string{"TM_TEST_BASEDIR=" + f.sandbox.RootDir()} + + normalExpected := RunExpected{Stdout: nljoin(f.dependentPath)} + f.sandbox.DirEntry(f.dependentPath).CreateFile("main.tf", "# changed file") + AssertRunResult(t, cli.Run("run", "--tags=dependent", "--quiet", "-X", "--", HelperPath, "stack-abs-path", f.sandbox.RootDir()), normalExpected) // sanity check + + inclExpected := RunExpected{Stdout: nljoin(f.dependencyPath, f.dependentPath)} + AssertRunResult(t, cli.Run("run", "--tags=dependent", "--quiet", "-X", "--include-output-dependencies", "--", HelperPath, "stack-abs-path", f.sandbox.RootDir()), inclExpected) + AssertRunResult(t, cli.Run("script", "run", "--tags=dependent", "--quiet", "-X", "--include-output-dependencies", "test"), inclExpected) + + onlyExpected := RunExpected{Stdout: nljoin(f.dependencyPath)} + AssertRunResult(t, cli.Run("run", "--tags=dependent", "--quiet", "-X", "--only-output-dependencies", "--", HelperPath, "stack-abs-path", f.sandbox.RootDir()), onlyExpected) + AssertRunResult(t, cli.Run("script", "run", "--tags=dependent", "--quiet", "-X", "--only-output-dependencies", "test"), onlyExpected) + + } + }) +} diff --git a/stack/manager.go b/stack/manager.go index 8c7bca393..eab72845b 100644 --- a/stack/manager.go +++ b/stack/manager.go @@ -33,6 +33,7 @@ type ( cache struct { stacks []Entry + stacksMap map[string]Entry changedFiles map[string]project.Paths // gitBaseRef -> changed files } } @@ -46,7 +47,7 @@ type ( // Report is the report of project's stacks and the result of its default checks. Report struct { - Stacks []Entry + Stacks config.List[Entry] // Checks contains the result info of default checks. Checks RepoChecks @@ -71,6 +72,8 @@ const ( ErrListChanged errors.Kind = "listing changed stacks error" ) +var _ config.List[Entry] + // NewManager creates a new stack manager. func NewManager(root *config.Root) *Manager { m := &Manager{ @@ -392,12 +395,12 @@ rangeStacks: delete(stackSet, ignored) } - changedStacks := make([]Entry, 0, len(stackSet)) + changedStacks := make(config.List[Entry], 0, len(stackSet)) for _, stack := range stackSet { changedStacks = append(changedStacks, stack) } - sort.Sort(EntrySlice(changedStacks)) + sort.Sort(changedStacks) return &Report{ Checks: checks, @@ -416,22 +419,30 @@ func (m *Manager) allStacks() ([]Entry, error) { return nil, errors.E(ErrListChanged, "searching for stacks", err) } m.cache.stacks = allstacks + m.cache.stacksMap = make(map[string]Entry) + for _, stack := range allstacks { + // at this point, the stack.ID is unique (if set) + if stack.Stack.ID != "" { + m.cache.stacksMap[stack.Stack.ID] = stack + } + } } return allstacks, nil } // StackByID returns the stack with the given id. func (m *Manager) StackByID(id string) (*config.Stack, bool, error) { - report, err := m.List(false) - if err != nil { - return nil, false, err - } - for _, entry := range report.Stacks { - if entry.Stack.ID == id { - return entry.Stack, true, nil + if m.cache.stacksMap == nil { + _, err := m.allStacks() + if err != nil { + return nil, false, err } } - return nil, false, nil + stack, ok := m.cache.stacksMap[id] + if !ok { + return nil, false, nil + } + return stack.Stack, true, nil } // AddWantedOf returns all wanted stacks from the given stacks. @@ -800,9 +811,7 @@ func checkRepoIsClean(g *git.Git) (RepoChecks, error) { }, nil } -// EntrySlice implements the Sort interface. -type EntrySlice []Entry - -func (x EntrySlice) Len() int { return len(x) } -func (x EntrySlice) Less(i, j int) bool { return x[i].Stack.Dir.String() < x[j].Stack.Dir.String() } -func (x EntrySlice) Swap(i, j int) { x[i], x[j] = x[j], x[i] } +// Dir returns the directory of the entry +func (e Entry) Dir() project.Path { + return e.Stack.Dir +} diff --git a/test/sandbox/sandbox.go b/test/sandbox/sandbox.go index d08c61633..31e116b42 100644 --- a/test/sandbox/sandbox.go +++ b/test/sandbox/sandbox.go @@ -391,7 +391,7 @@ func (s S) DirEntry(relpath string) DirEntry { } if path.IsAbs(relpath) { - t.Fatalf("DirEntry() needs a relative path but given %q", relpath) + relpath = relpath[1:] } abspath := filepath.Join(s.rootdir, filepath.FromSlash(relpath))