diff options
Diffstat (limited to 'cli/internal/scope')
| -rw-r--r-- | cli/internal/scope/filter/filter.go | 421 | ||||
| -rw-r--r-- | cli/internal/scope/filter/filter_test.go | 614 | ||||
| -rw-r--r-- | cli/internal/scope/filter/matcher.go | 32 | ||||
| -rw-r--r-- | cli/internal/scope/filter/matcher_test.go | 65 | ||||
| -rw-r--r-- | cli/internal/scope/filter/parse_target_selector.go | 165 | ||||
| -rw-r--r-- | cli/internal/scope/filter/parse_target_selector_test.go | 311 | ||||
| -rw-r--r-- | cli/internal/scope/scope.go | 380 | ||||
| -rw-r--r-- | cli/internal/scope/scope_test.go | 550 |
8 files changed, 0 insertions, 2538 deletions
diff --git a/cli/internal/scope/filter/filter.go b/cli/internal/scope/filter/filter.go deleted file mode 100644 index 60aaf1d..0000000 --- a/cli/internal/scope/filter/filter.go +++ /dev/null @@ -1,421 +0,0 @@ -package filter - -import ( - "fmt" - "strings" - - "github.com/pkg/errors" - "github.com/pyr-sh/dag" - "github.com/vercel/turbo/cli/internal/doublestar" - "github.com/vercel/turbo/cli/internal/turbopath" - "github.com/vercel/turbo/cli/internal/util" - "github.com/vercel/turbo/cli/internal/workspace" -) - -type SelectedPackages struct { - pkgs util.Set - unusedFilters []*TargetSelector -} - -// PackagesChangedInRange is the signature of a function to provide the set of -// packages that have changed in a particular range of git refs. -type PackagesChangedInRange = func(fromRef string, toRef string) (util.Set, error) - -// PackageInference holds the information we have inferred from the working-directory -// (really --infer-filter-root flag) about which packages are of interest. -type PackageInference struct { - // PackageName, if set, means that we have determined that filters without a package-specifier - // should get this package name - PackageName string - // DirectoryRoot is used to infer a "parentDir" for the filter in the event that we haven't - // identified a specific package. If the filter already contains a parentDir, this acts as - // a prefix. If the filter does not contain a parentDir, we consider this to be a glob for - // all subdirectories - DirectoryRoot turbopath.RelativeSystemPath -} - -type Resolver struct { - Graph *dag.AcyclicGraph - WorkspaceInfos workspace.Catalog - Cwd turbopath.AbsoluteSystemPath - Inference *PackageInference - PackagesChangedInRange PackagesChangedInRange -} - -// GetPackagesFromPatterns compiles filter patterns and applies them, returning -// the selected packages -func (r *Resolver) GetPackagesFromPatterns(patterns []string) (util.Set, error) { - selectors := []*TargetSelector{} - for _, pattern := range patterns { - selector, err := ParseTargetSelector(pattern) - if err != nil { - return nil, err - } - selectors = append(selectors, selector) - } - selected, err := r.getFilteredPackages(selectors) - if err != nil { - return nil, err - } - return selected.pkgs, nil -} - -func (pi *PackageInference) apply(selector *TargetSelector) error { - if selector.namePattern != "" { - // The selector references a package name, don't apply inference - return nil - } - if pi.PackageName != "" { - selector.namePattern = pi.PackageName - } - if selector.parentDir != "" { - parentDir := pi.DirectoryRoot.Join(selector.parentDir) - selector.parentDir = parentDir - } else if pi.PackageName == "" { - // The user didn't set a parent directory and we didn't find a single package, - // so use the directory we inferred and select all subdirectories - selector.parentDir = pi.DirectoryRoot.Join("**") - } - return nil -} - -func (r *Resolver) applyInference(selectors []*TargetSelector) ([]*TargetSelector, error) { - if r.Inference == nil { - return selectors, nil - } - // If there are existing patterns, use inference on those. If there are no - // patterns, but there is a directory supplied, synthesize a selector - if len(selectors) == 0 { - selectors = append(selectors, &TargetSelector{}) - } - for _, selector := range selectors { - if err := r.Inference.apply(selector); err != nil { - return nil, err - } - } - return selectors, nil -} - -func (r *Resolver) getFilteredPackages(selectors []*TargetSelector) (*SelectedPackages, error) { - selectors, err := r.applyInference(selectors) - if err != nil { - return nil, err - } - prodPackageSelectors := []*TargetSelector{} - allPackageSelectors := []*TargetSelector{} - for _, selector := range selectors { - if selector.followProdDepsOnly { - prodPackageSelectors = append(prodPackageSelectors, selector) - } else { - allPackageSelectors = append(allPackageSelectors, selector) - } - } - if len(allPackageSelectors) > 0 || len(prodPackageSelectors) > 0 { - if len(allPackageSelectors) > 0 { - selected, err := r.filterGraph(allPackageSelectors) - if err != nil { - return nil, err - } - return selected, nil - } - } - return &SelectedPackages{ - pkgs: make(util.Set), - }, nil -} - -func (r *Resolver) filterGraph(selectors []*TargetSelector) (*SelectedPackages, error) { - includeSelectors := []*TargetSelector{} - excludeSelectors := []*TargetSelector{} - for _, selector := range selectors { - if selector.exclude { - excludeSelectors = append(excludeSelectors, selector) - } else { - includeSelectors = append(includeSelectors, selector) - } - } - var include *SelectedPackages - if len(includeSelectors) > 0 { - found, err := r.filterGraphWithSelectors(includeSelectors) - if err != nil { - return nil, err - } - include = found - } else { - vertexSet := make(util.Set) - for _, v := range r.Graph.Vertices() { - vertexSet.Add(v) - } - include = &SelectedPackages{ - pkgs: vertexSet, - } - } - exclude, err := r.filterGraphWithSelectors(excludeSelectors) - if err != nil { - return nil, err - } - return &SelectedPackages{ - pkgs: include.pkgs.Difference(exclude.pkgs), - unusedFilters: append(include.unusedFilters, exclude.unusedFilters...), - }, nil -} - -func (r *Resolver) filterGraphWithSelectors(selectors []*TargetSelector) (*SelectedPackages, error) { - unmatchedSelectors := []*TargetSelector{} - - cherryPickedPackages := make(dag.Set) - walkedDependencies := make(dag.Set) - walkedDependents := make(dag.Set) - walkedDependentsDependencies := make(dag.Set) - - for _, selector := range selectors { - // TODO(gsoltis): this should be a list? - entryPackages, err := r.filterGraphWithSelector(selector) - if err != nil { - return nil, err - } - if entryPackages.Len() == 0 { - unmatchedSelectors = append(unmatchedSelectors, selector) - } - for _, pkg := range entryPackages { - if selector.includeDependencies { - dependencies, err := r.Graph.Ancestors(pkg) - if err != nil { - return nil, errors.Wrapf(err, "failed to get dependencies of package %v", pkg) - } - for dep := range dependencies { - walkedDependencies.Add(dep) - } - if !selector.excludeSelf { - walkedDependencies.Add(pkg) - } - } - if selector.includeDependents { - dependents, err := r.Graph.Descendents(pkg) - if err != nil { - return nil, errors.Wrapf(err, "failed to get dependents of package %v", pkg) - } - for dep := range dependents { - walkedDependents.Add(dep) - if selector.includeDependencies { - dependentDeps, err := r.Graph.Ancestors(dep) - if err != nil { - return nil, errors.Wrapf(err, "failed to get dependencies of dependent %v", dep) - } - for dependentDep := range dependentDeps { - walkedDependentsDependencies.Add(dependentDep) - } - } - } - if !selector.excludeSelf { - walkedDependents.Add(pkg) - } - } - if !selector.includeDependencies && !selector.includeDependents { - cherryPickedPackages.Add(pkg) - } - } - } - allPkgs := make(util.Set) - for pkg := range cherryPickedPackages { - allPkgs.Add(pkg) - } - for pkg := range walkedDependencies { - allPkgs.Add(pkg) - } - for pkg := range walkedDependents { - allPkgs.Add(pkg) - } - for pkg := range walkedDependentsDependencies { - allPkgs.Add(pkg) - } - return &SelectedPackages{ - pkgs: allPkgs, - unusedFilters: unmatchedSelectors, - }, nil -} - -func (r *Resolver) filterGraphWithSelector(selector *TargetSelector) (util.Set, error) { - if selector.matchDependencies { - return r.filterSubtreesWithSelector(selector) - } - return r.filterNodesWithSelector(selector) -} - -// filterNodesWithSelector returns the set of nodes that match a given selector -func (r *Resolver) filterNodesWithSelector(selector *TargetSelector) (util.Set, error) { - entryPackages := make(util.Set) - selectorWasUsed := false - if selector.fromRef != "" { - // get changed packaged - selectorWasUsed = true - changedPkgs, err := r.PackagesChangedInRange(selector.fromRef, selector.getToRef()) - if err != nil { - return nil, err - } - parentDir := selector.parentDir - for pkgName := range changedPkgs { - if parentDir != "" { - // Type assert/coerce to string here because we want to use - // this value in a map that has string keys. - // TODO(mehulkar) `changedPkgs` is a util.Set, we could make a `util.PackageNamesSet`` - // or something similar that is all strings. - pkgNameStr := pkgName.(string) - if pkgName == util.RootPkgName { - // The root package changed, only add it if - // the parentDir is equivalent to the root - if matches, err := doublestar.PathMatch(r.Cwd.Join(parentDir).ToString(), r.Cwd.ToString()); err != nil { - return nil, fmt.Errorf("failed to resolve directory relationship %v contains %v: %v", parentDir, r.Cwd, err) - } else if matches { - entryPackages.Add(pkgName) - } - } else if pkg, ok := r.WorkspaceInfos.PackageJSONs[pkgNameStr]; !ok { - return nil, fmt.Errorf("missing info for package %v", pkgName) - } else if matches, err := doublestar.PathMatch(r.Cwd.Join(parentDir).ToString(), pkg.Dir.RestoreAnchor(r.Cwd).ToString()); err != nil { - return nil, fmt.Errorf("failed to resolve directory relationship %v contains %v: %v", selector.parentDir, pkg.Dir, err) - } else if matches { - entryPackages.Add(pkgName) - } - } else { - entryPackages.Add(pkgName) - } - } - } else if selector.parentDir != "" { - // get packages by path - selectorWasUsed = true - parentDir := selector.parentDir - if parentDir == "." { - entryPackages.Add(util.RootPkgName) - } else { - for name, pkg := range r.WorkspaceInfos.PackageJSONs { - if matches, err := doublestar.PathMatch(r.Cwd.Join(parentDir).ToString(), pkg.Dir.RestoreAnchor(r.Cwd).ToString()); err != nil { - return nil, fmt.Errorf("failed to resolve directory relationship %v contains %v: %v", selector.parentDir, pkg.Dir, err) - } else if matches { - entryPackages.Add(name) - } - } - } - } - if selector.namePattern != "" { - // find packages that match name - if !selectorWasUsed { - matched, err := matchPackageNamesToVertices(selector.namePattern, r.Graph.Vertices()) - if err != nil { - return nil, err - } - entryPackages = matched - selectorWasUsed = true - } else { - matched, err := matchPackageNames(selector.namePattern, entryPackages) - if err != nil { - return nil, err - } - entryPackages = matched - } - } - // TODO(gsoltis): we can do this earlier - // Check if the selector specified anything - if !selectorWasUsed { - return nil, fmt.Errorf("invalid selector: %v", selector.raw) - } - return entryPackages, nil -} - -// filterSubtreesWithSelector returns the set of nodes where the node or any of its dependencies -// match a selector -func (r *Resolver) filterSubtreesWithSelector(selector *TargetSelector) (util.Set, error) { - // foreach package that matches parentDir && namePattern, check if any dependency is in changed packages - changedPkgs, err := r.PackagesChangedInRange(selector.fromRef, selector.getToRef()) - if err != nil { - return nil, err - } - - parentDir := selector.parentDir - entryPackages := make(util.Set) - for name, pkg := range r.WorkspaceInfos.PackageJSONs { - if parentDir == "" { - entryPackages.Add(name) - } else if matches, err := doublestar.PathMatch(parentDir.ToString(), pkg.Dir.RestoreAnchor(r.Cwd).ToString()); err != nil { - return nil, fmt.Errorf("failed to resolve directory relationship %v contains %v: %v", selector.parentDir, pkg.Dir, err) - } else if matches { - entryPackages.Add(name) - } - } - if selector.namePattern != "" { - matched, err := matchPackageNames(selector.namePattern, entryPackages) - if err != nil { - return nil, err - } - entryPackages = matched - } - roots := make(util.Set) - matched := make(util.Set) - for pkg := range entryPackages { - if matched.Includes(pkg) { - roots.Add(pkg) - continue - } - deps, err := r.Graph.Ancestors(pkg) - if err != nil { - return nil, err - } - for changedPkg := range changedPkgs { - if !selector.excludeSelf && pkg == changedPkg { - roots.Add(pkg) - break - } - if deps.Include(changedPkg) { - roots.Add(pkg) - matched.Add(changedPkg) - break - } - } - } - return roots, nil -} - -func matchPackageNamesToVertices(pattern string, vertices []dag.Vertex) (util.Set, error) { - packages := make(util.Set) - for _, v := range vertices { - packages.Add(v) - } - packages.Add(util.RootPkgName) - return matchPackageNames(pattern, packages) -} - -func matchPackageNames(pattern string, packages util.Set) (util.Set, error) { - matcher, err := matcherFromPattern(pattern) - if err != nil { - return nil, err - } - matched := make(util.Set) - for _, pkg := range packages { - pkg := pkg.(string) - if matcher(pkg) { - matched.Add(pkg) - } - } - if matched.Len() == 0 && !strings.HasPrefix(pattern, "@") && !strings.Contains(pattern, "/") { - // we got no matches and the pattern isn't a scoped package. - // Check if we have exactly one scoped package that does match - scopedPattern := fmt.Sprintf("@*/%v", pattern) - matcher, err = matcherFromPattern(scopedPattern) - if err != nil { - return nil, err - } - foundScopedPkg := false - for _, pkg := range packages { - pkg := pkg.(string) - if matcher(pkg) { - if foundScopedPkg { - // we found a second scoped package. Return the empty set, we can't - // disambiguate - return make(util.Set), nil - } - foundScopedPkg = true - matched.Add(pkg) - } - } - } - return matched, nil -} diff --git a/cli/internal/scope/filter/filter_test.go b/cli/internal/scope/filter/filter_test.go deleted file mode 100644 index a23ae1d..0000000 --- a/cli/internal/scope/filter/filter_test.go +++ /dev/null @@ -1,614 +0,0 @@ -package filter - -import ( - "fmt" - "os" - "strings" - "testing" - - "github.com/pyr-sh/dag" - "github.com/vercel/turbo/cli/internal/fs" - "github.com/vercel/turbo/cli/internal/turbopath" - "github.com/vercel/turbo/cli/internal/util" - "github.com/vercel/turbo/cli/internal/workspace" -) - -func setMatches(t *testing.T, name string, s util.Set, expected []string) { - expectedSet := make(util.Set) - for _, item := range expected { - expectedSet.Add(item) - } - missing := s.Difference(expectedSet) - if missing.Len() > 0 { - t.Errorf("%v set has extra elements: %v", name, strings.Join(missing.UnsafeListOfStrings(), ", ")) - } - extra := expectedSet.Difference(s) - if extra.Len() > 0 { - t.Errorf("%v set missing elements: %v", name, strings.Join(extra.UnsafeListOfStrings(), ", ")) - } -} - -func Test_filter(t *testing.T) { - rawCwd, err := os.Getwd() - if err != nil { - t.Fatalf("failed to get working directory: %v", err) - } - root, err := fs.GetCwd(rawCwd) - if err != nil { - t.Fatalf("failed to get working directory: %v", err) - } - workspaceInfos := workspace.Catalog{ - PackageJSONs: make(map[string]*fs.PackageJSON), - } - packageJSONs := workspaceInfos.PackageJSONs - graph := &dag.AcyclicGraph{} - graph.Add("project-0") - packageJSONs["project-0"] = &fs.PackageJSON{ - Name: "project-0", - Dir: turbopath.AnchoredUnixPath("packages/project-0").ToSystemPath(), - } - graph.Add("project-1") - packageJSONs["project-1"] = &fs.PackageJSON{ - Name: "project-1", - Dir: turbopath.AnchoredUnixPath("packages/project-1").ToSystemPath(), - } - graph.Add("project-2") - packageJSONs["project-2"] = &fs.PackageJSON{ - Name: "project-2", - Dir: "project-2", - } - graph.Add("project-3") - packageJSONs["project-3"] = &fs.PackageJSON{ - Name: "project-3", - Dir: "project-3", - } - graph.Add("project-4") - packageJSONs["project-4"] = &fs.PackageJSON{ - Name: "project-4", - Dir: "project-4", - } - graph.Add("project-5") - packageJSONs["project-5"] = &fs.PackageJSON{ - Name: "project-5", - Dir: "project-5", - } - // Note: inside project-5 - graph.Add("project-6") - packageJSONs["project-6"] = &fs.PackageJSON{ - Name: "project-6", - Dir: turbopath.AnchoredUnixPath("project-5/packages/project-6").ToSystemPath(), - } - // Add dependencies - graph.Connect(dag.BasicEdge("project-0", "project-1")) - graph.Connect(dag.BasicEdge("project-0", "project-5")) - graph.Connect(dag.BasicEdge("project-1", "project-2")) - graph.Connect(dag.BasicEdge("project-1", "project-4")) - - testCases := []struct { - Name string - Selectors []*TargetSelector - PackageInference *PackageInference - Expected []string - }{ - { - "select root package", - []*TargetSelector{ - { - namePattern: util.RootPkgName, - }, - }, - nil, - []string{util.RootPkgName}, - }, - { - "select only package dependencies (excluding the package itself)", - []*TargetSelector{ - { - excludeSelf: true, - includeDependencies: true, - namePattern: "project-1", - }, - }, - nil, - []string{"project-2", "project-4"}, - }, - { - "select package with dependencies", - []*TargetSelector{ - { - excludeSelf: false, - includeDependencies: true, - namePattern: "project-1", - }, - }, - nil, - []string{"project-1", "project-2", "project-4"}, - }, - { - "select package with dependencies and dependents, including dependent dependencies", - []*TargetSelector{ - { - excludeSelf: true, - includeDependencies: true, - includeDependents: true, - namePattern: "project-1", - }, - }, - nil, - []string{"project-0", "project-1", "project-2", "project-4", "project-5"}, - }, - { - "select package with dependents", - []*TargetSelector{ - { - includeDependents: true, - namePattern: "project-2", - }, - }, - nil, - []string{"project-1", "project-2", "project-0"}, - }, - { - "select dependents excluding package itself", - []*TargetSelector{ - { - excludeSelf: true, - includeDependents: true, - namePattern: "project-2", - }, - }, - nil, - []string{"project-0", "project-1"}, - }, - { - "filter using two selectors: one selects dependencies another selects dependents", - []*TargetSelector{ - { - excludeSelf: true, - includeDependents: true, - namePattern: "project-2", - }, - { - excludeSelf: true, - includeDependencies: true, - namePattern: "project-1", - }, - }, - nil, - []string{"project-0", "project-1", "project-2", "project-4"}, - }, - { - "select just a package by name", - []*TargetSelector{ - { - namePattern: "project-2", - }, - }, - nil, - []string{"project-2"}, - }, - // Note: we don't support the option to switch path prefix mode - // { - // "select by parentDir", - // []*TargetSelector{ - // { - // parentDir: "/packages", - // }, - // }, - // []string{"project-0", "project-1"}, - // }, - { - "select by parentDir using glob", - []*TargetSelector{ - { - parentDir: turbopath.MakeRelativeSystemPath("packages", "*"), - }, - }, - nil, - []string{"project-0", "project-1"}, - }, - { - "select by parentDir using globstar", - []*TargetSelector{ - { - parentDir: turbopath.MakeRelativeSystemPath("project-5", "**"), - }, - }, - nil, - []string{"project-5", "project-6"}, - }, - { - "select by parentDir with no glob", - []*TargetSelector{ - { - parentDir: turbopath.MakeRelativeSystemPath("project-5"), - }, - }, - nil, - []string{"project-5"}, - }, - { - "select all packages except one", - []*TargetSelector{ - { - exclude: true, - namePattern: "project-1", - }, - }, - nil, - []string{"project-0", "project-2", "project-3", "project-4", "project-5", "project-6"}, - }, - { - "select by parentDir and exclude one package by pattern", - []*TargetSelector{ - { - parentDir: turbopath.MakeRelativeSystemPath("packages", "*"), - }, - { - exclude: true, - namePattern: "*-1", - }, - }, - nil, - []string{"project-0"}, - }, - { - "select root package by directory", - []*TargetSelector{ - { - parentDir: turbopath.MakeRelativeSystemPath("."), // input . gets cleaned to "" - }, - }, - nil, - []string{util.RootPkgName}, - }, - { - "select packages directory", - []*TargetSelector{}, - &PackageInference{ - DirectoryRoot: turbopath.MakeRelativeSystemPath("packages"), - }, - []string{"project-0", "project-1"}, - }, - { - "infer single package", - []*TargetSelector{}, - &PackageInference{ - DirectoryRoot: turbopath.MakeRelativeSystemPath("packages", "project-0"), - PackageName: "project-0", - }, - []string{"project-0"}, - }, - { - "infer single package from subdirectory", - []*TargetSelector{}, - &PackageInference{ - DirectoryRoot: turbopath.MakeRelativeSystemPath("packages", "project-0", "src"), - PackageName: "project-0", - }, - []string{"project-0"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - r := &Resolver{ - Graph: graph, - WorkspaceInfos: workspaceInfos, - Cwd: root, - Inference: tc.PackageInference, - } - pkgs, err := r.getFilteredPackages(tc.Selectors) - if err != nil { - t.Fatalf("%v failed to filter packages: %v", tc.Name, err) - } - setMatches(t, tc.Name, pkgs.pkgs, tc.Expected) - }) - } - - t.Run("report unmatched filters", func(t *testing.T) { - r := &Resolver{ - Graph: graph, - WorkspaceInfos: workspaceInfos, - Cwd: root, - } - pkgs, err := r.getFilteredPackages([]*TargetSelector{ - { - excludeSelf: true, - includeDependencies: true, - namePattern: "project-7", - }, - }) - if err != nil { - t.Fatalf("unmatched filter failed to filter packages: %v", err) - } - if pkgs.pkgs.Len() != 0 { - t.Errorf("unmatched filter expected no packages, got %v", strings.Join(pkgs.pkgs.UnsafeListOfStrings(), ", ")) - } - if len(pkgs.unusedFilters) != 1 { - t.Errorf("unmatched filter expected to report one unused filter, got %v", len(pkgs.unusedFilters)) - } - }) -} - -func Test_matchScopedPackage(t *testing.T) { - rawCwd, err := os.Getwd() - if err != nil { - t.Fatalf("failed to get working directory: %v", err) - } - root, err := fs.GetCwd(rawCwd) - if err != nil { - t.Fatalf("failed to get working directory: %v", err) - } - - workspaceInfos := workspace.Catalog{ - PackageJSONs: make(map[string]*fs.PackageJSON), - } - packageJSONs := workspaceInfos.PackageJSONs - graph := &dag.AcyclicGraph{} - graph.Add("@foo/bar") - packageJSONs["@foo/bar"] = &fs.PackageJSON{ - Name: "@foo/bar", - Dir: turbopath.AnchoredUnixPath("packages/bar").ToSystemPath(), - } - r := &Resolver{ - Graph: graph, - WorkspaceInfos: workspaceInfos, - Cwd: root, - } - pkgs, err := r.getFilteredPackages([]*TargetSelector{ - { - namePattern: "bar", - }, - }) - if err != nil { - t.Fatalf("failed to filter packages: %v", err) - } - setMatches(t, "match scoped package", pkgs.pkgs, []string{"@foo/bar"}) -} - -func Test_matchExactPackages(t *testing.T) { - rawCwd, err := os.Getwd() - if err != nil { - t.Fatalf("failed to get working directory: %v", err) - } - root, err := fs.GetCwd(rawCwd) - if err != nil { - t.Fatalf("failed to get working directory: %v", err) - } - - workspaceInfos := workspace.Catalog{ - PackageJSONs: make(map[string]*fs.PackageJSON), - } - packageJSONs := workspaceInfos.PackageJSONs - graph := &dag.AcyclicGraph{} - graph.Add("@foo/bar") - packageJSONs["@foo/bar"] = &fs.PackageJSON{ - Name: "@foo/bar", - Dir: turbopath.AnchoredUnixPath("packages/@foo/bar").ToSystemPath(), - } - graph.Add("bar") - packageJSONs["bar"] = &fs.PackageJSON{ - Name: "bar", - Dir: turbopath.AnchoredUnixPath("packages/bar").ToSystemPath(), - } - r := &Resolver{ - Graph: graph, - WorkspaceInfos: workspaceInfos, - Cwd: root, - } - pkgs, err := r.getFilteredPackages([]*TargetSelector{ - { - namePattern: "bar", - }, - }) - if err != nil { - t.Fatalf("failed to filter packages: %v", err) - } - setMatches(t, "match exact package", pkgs.pkgs, []string{"bar"}) -} - -func Test_matchMultipleScopedPackages(t *testing.T) { - rawCwd, err := os.Getwd() - if err != nil { - t.Fatalf("failed to get working directory: %v", err) - } - root, err := fs.GetCwd(rawCwd) - if err != nil { - t.Fatalf("failed to get working directory: %v", err) - } - - workspaceInfos := workspace.Catalog{ - PackageJSONs: make(map[string]*fs.PackageJSON), - } - packageJSONs := workspaceInfos.PackageJSONs - graph := &dag.AcyclicGraph{} - graph.Add("@foo/bar") - packageJSONs["@foo/bar"] = &fs.PackageJSON{ - Name: "@foo/bar", - Dir: turbopath.AnchoredUnixPath("packages/@foo/bar").ToSystemPath(), - } - graph.Add("@types/bar") - packageJSONs["@types/bar"] = &fs.PackageJSON{ - Name: "@types/bar", - Dir: turbopath.AnchoredUnixPath("packages/@types/bar").ToSystemPath(), - } - r := &Resolver{ - Graph: graph, - WorkspaceInfos: workspaceInfos, - Cwd: root, - } - pkgs, err := r.getFilteredPackages([]*TargetSelector{ - { - namePattern: "bar", - }, - }) - if err != nil { - t.Fatalf("failed to filter packages: %v", err) - } - setMatches(t, "match nothing with multiple scoped packages", pkgs.pkgs, []string{}) -} - -func Test_SCM(t *testing.T) { - rawCwd, err := os.Getwd() - if err != nil { - t.Fatalf("failed to get working directory: %v", err) - } - root, err := fs.GetCwd(rawCwd) - if err != nil { - t.Fatalf("failed to get working directory: %v", err) - } - head1Changed := make(util.Set) - head1Changed.Add("package-1") - head1Changed.Add("package-2") - head1Changed.Add(util.RootPkgName) - head2Changed := make(util.Set) - head2Changed.Add("package-3") - workspaceInfos := workspace.Catalog{ - PackageJSONs: make(map[string]*fs.PackageJSON), - } - packageJSONs := workspaceInfos.PackageJSONs - graph := &dag.AcyclicGraph{} - graph.Add("package-1") - packageJSONs["package-1"] = &fs.PackageJSON{ - Name: "package-1", - Dir: "package-1", - } - graph.Add("package-2") - packageJSONs["package-2"] = &fs.PackageJSON{ - Name: "package-2", - Dir: "package-2", - } - graph.Add("package-3") - packageJSONs["package-3"] = &fs.PackageJSON{ - Name: "package-3", - Dir: "package-3", - } - graph.Add("package-20") - packageJSONs["package-20"] = &fs.PackageJSON{ - Name: "package-20", - Dir: "package-20", - } - - graph.Connect(dag.BasicEdge("package-3", "package-20")) - - r := &Resolver{ - Graph: graph, - WorkspaceInfos: workspaceInfos, - Cwd: root, - PackagesChangedInRange: func(fromRef string, toRef string) (util.Set, error) { - if fromRef == "HEAD~1" && toRef == "HEAD" { - return head1Changed, nil - } else if fromRef == "HEAD~2" && toRef == "HEAD" { - union := head1Changed.Copy() - for val := range head2Changed { - union.Add(val) - } - return union, nil - } else if fromRef == "HEAD~2" && toRef == "HEAD~1" { - return head2Changed, nil - } - panic(fmt.Sprintf("unsupported commit range %v...%v", fromRef, toRef)) - }, - } - - testCases := []struct { - Name string - Selectors []*TargetSelector - Expected []string - }{ - { - "all changed packages", - []*TargetSelector{ - { - fromRef: "HEAD~1", - }, - }, - []string{"package-1", "package-2", util.RootPkgName}, - }, - { - "all changed packages with parent dir exact match", - []*TargetSelector{ - { - fromRef: "HEAD~1", - parentDir: ".", - }, - }, - []string{util.RootPkgName}, - }, - { - "changed packages in directory", - []*TargetSelector{ - { - fromRef: "HEAD~1", - parentDir: "package-2", - }, - }, - []string{"package-2"}, - }, - { - "changed packages matching pattern", - []*TargetSelector{ - { - fromRef: "HEAD~1", - namePattern: "package-2*", - }, - }, - []string{"package-2"}, - }, - { - "changed packages matching pattern", - []*TargetSelector{ - { - fromRef: "HEAD~1", - namePattern: "package-2*", - }, - }, - []string{"package-2"}, - }, - // Note: missing test here that takes advantage of automatically exempting - // test-only changes from pulling in dependents - // - // turbo-specific tests below here - { - "changed package was requested scope, and we're matching dependencies", - []*TargetSelector{ - { - fromRef: "HEAD~1", - namePattern: "package-1", - matchDependencies: true, - }, - }, - []string{"package-1"}, - }, - { - "older commit", - []*TargetSelector{ - { - fromRef: "HEAD~2", - }, - }, - []string{"package-1", "package-2", "package-3", util.RootPkgName}, - }, - { - "commit range", - []*TargetSelector{ - { - fromRef: "HEAD~2", - toRefOverride: "HEAD~1", - }, - }, - []string{"package-3"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - pkgs, err := r.getFilteredPackages(tc.Selectors) - if err != nil { - t.Fatalf("%v failed to filter packages: %v", tc.Name, err) - } - setMatches(t, tc.Name, pkgs.pkgs, tc.Expected) - }) - } -} diff --git a/cli/internal/scope/filter/matcher.go b/cli/internal/scope/filter/matcher.go deleted file mode 100644 index 2460326..0000000 --- a/cli/internal/scope/filter/matcher.go +++ /dev/null @@ -1,32 +0,0 @@ -package filter - -import ( - "regexp" - "strings" - - "github.com/pkg/errors" -) - -type Matcher = func(pkgName string) bool - -func matchAll(pkgName string) bool { - return true -} - -func matcherFromPattern(pattern string) (Matcher, error) { - if pattern == "*" { - return matchAll, nil - } - - escaped := regexp.QuoteMeta(pattern) - // replace escaped '*' with regex '.*' - normalized := strings.ReplaceAll(escaped, "\\*", ".*") - if normalized == pattern { - return func(pkgName string) bool { return pkgName == pattern }, nil - } - regex, err := regexp.Compile("^" + normalized + "$") - if err != nil { - return nil, errors.Wrapf(err, "failed to compile filter pattern to regex: %v", pattern) - } - return func(pkgName string) bool { return regex.Match([]byte(pkgName)) }, nil -} diff --git a/cli/internal/scope/filter/matcher_test.go b/cli/internal/scope/filter/matcher_test.go deleted file mode 100644 index 966be2b..0000000 --- a/cli/internal/scope/filter/matcher_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package filter - -import "testing" - -func TestMatcher(t *testing.T) { - testCases := map[string][]struct { - test string - want bool - }{ - "*": { - { - test: "@eslint/plugin-foo", - want: true, - }, - { - test: "express", - want: true, - }, - }, - "eslint-*": { - { - test: "eslint-plugin-foo", - want: true, - }, - { - test: "express", - want: false, - }, - }, - "*plugin*": { - { - test: "@eslint/plugin-foo", - want: true, - }, - { - test: "express", - want: false, - }, - }, - "a*c": { - { - test: "abc", - want: true, - }, - }, - "*-positive": { - { - test: "is-positive", - want: true, - }, - }, - } - for pattern, tests := range testCases { - matcher, err := matcherFromPattern(pattern) - if err != nil { - t.Fatalf("failed to compile match pattern %v, %v", pattern, err) - } - for _, testCase := range tests { - got := matcher(testCase.test) - if got != testCase.want { - t.Errorf("%v.match(%v) got %v, want %v", pattern, testCase.test, got, testCase.want) - } - } - } -} diff --git a/cli/internal/scope/filter/parse_target_selector.go b/cli/internal/scope/filter/parse_target_selector.go deleted file mode 100644 index 4f5c90f..0000000 --- a/cli/internal/scope/filter/parse_target_selector.go +++ /dev/null @@ -1,165 +0,0 @@ -package filter - -import ( - "regexp" - "strings" - - "github.com/pkg/errors" - "github.com/vercel/turbo/cli/internal/turbopath" -) - -type TargetSelector struct { - includeDependencies bool - matchDependencies bool - includeDependents bool - exclude bool - excludeSelf bool - followProdDepsOnly bool - parentDir turbopath.RelativeSystemPath - namePattern string - fromRef string - toRefOverride string - raw string -} - -func (ts *TargetSelector) IsValid() bool { - return ts.fromRef != "" || ts.parentDir != "" || ts.namePattern != "" -} - -// getToRef returns the git ref to use for upper bound of the comparison when finding changed -// packages. -func (ts *TargetSelector) getToRef() string { - if ts.toRefOverride == "" { - return "HEAD" - } - return ts.toRefOverride -} - -var errCantMatchDependencies = errors.New("cannot use match dependencies without specifying either a directory or package") - -var targetSelectorRegex = regexp.MustCompile(`^(?P<name>[^.](?:[^{}[\]]*[^{}[\].])?)?(?P<directory>\{[^}]*\})?(?P<commits>(?:\.{3})?\[[^\]]+\])?$`) - -// ParseTargetSelector is a function that returns pnpm compatible --filter command line flags -func ParseTargetSelector(rawSelector string) (*TargetSelector, error) { - exclude := false - firstChar := rawSelector[0] - selector := rawSelector - if firstChar == '!' { - selector = selector[1:] - exclude = true - } - excludeSelf := false - includeDependencies := strings.HasSuffix(selector, "...") - if includeDependencies { - selector = selector[:len(selector)-3] - if strings.HasSuffix(selector, "^") { - excludeSelf = true - selector = selector[:len(selector)-1] - } - } - includeDependents := strings.HasPrefix(selector, "...") - if includeDependents { - selector = selector[3:] - if strings.HasPrefix(selector, "^") { - excludeSelf = true - selector = selector[1:] - } - } - - matches := targetSelectorRegex.FindAllStringSubmatch(selector, -1) - - if len(matches) == 0 { - if relativePath, ok := isSelectorByLocation(selector); ok { - return &TargetSelector{ - exclude: exclude, - includeDependencies: includeDependencies, - includeDependents: includeDependents, - parentDir: relativePath, - raw: rawSelector, - }, nil - } - return &TargetSelector{ - exclude: exclude, - excludeSelf: excludeSelf, - includeDependencies: includeDependencies, - includeDependents: includeDependents, - namePattern: selector, - raw: rawSelector, - }, nil - } - - fromRef := "" - toRefOverride := "" - var parentDir turbopath.RelativeSystemPath - namePattern := "" - preAddDepdencies := false - if len(matches) > 0 && len(matches[0]) > 0 { - match := matches[0] - namePattern = match[targetSelectorRegex.SubexpIndex("name")] - rawParentDir := match[targetSelectorRegex.SubexpIndex("directory")] - if len(rawParentDir) > 0 { - // trim {} - rawParentDir = rawParentDir[1 : len(rawParentDir)-1] - if rawParentDir == "" { - return nil, errors.New("empty path specification") - } else if relPath, err := turbopath.CheckedToRelativeSystemPath(rawParentDir); err == nil { - parentDir = relPath - } else { - return nil, errors.Wrapf(err, "invalid path specification: %v", rawParentDir) - } - } - rawCommits := match[targetSelectorRegex.SubexpIndex("commits")] - if len(rawCommits) > 0 { - fromRef = rawCommits - if strings.HasPrefix(fromRef, "...") { - if parentDir == "" && namePattern == "" { - return &TargetSelector{}, errCantMatchDependencies - } - preAddDepdencies = true - fromRef = fromRef[3:] - } - // strip [] - fromRef = fromRef[1 : len(fromRef)-1] - refs := strings.Split(fromRef, "...") - if len(refs) == 2 { - fromRef = refs[0] - toRefOverride = refs[1] - } - } - } - - return &TargetSelector{ - fromRef: fromRef, - toRefOverride: toRefOverride, - exclude: exclude, - excludeSelf: excludeSelf, - includeDependencies: includeDependencies, - matchDependencies: preAddDepdencies, - includeDependents: includeDependents, - namePattern: namePattern, - parentDir: parentDir, - raw: rawSelector, - }, nil -} - -// isSelectorByLocation returns true if the selector is by filesystem location -func isSelectorByLocation(rawSelector string) (turbopath.RelativeSystemPath, bool) { - if rawSelector[0:1] != "." { - return "", false - } - - // . or ./ or .\ - if len(rawSelector) == 1 || rawSelector[1:2] == "/" || rawSelector[1:2] == "\\" { - return turbopath.MakeRelativeSystemPath(rawSelector), true - } - - if rawSelector[1:2] != "." { - return "", false - } - - // .. or ../ or ..\ - if len(rawSelector) == 2 || rawSelector[2:3] == "/" || rawSelector[2:3] == "\\" { - return turbopath.MakeRelativeSystemPath(rawSelector), true - } - return "", false -} diff --git a/cli/internal/scope/filter/parse_target_selector_test.go b/cli/internal/scope/filter/parse_target_selector_test.go deleted file mode 100644 index 2973a61..0000000 --- a/cli/internal/scope/filter/parse_target_selector_test.go +++ /dev/null @@ -1,311 +0,0 @@ -package filter - -import ( - "reflect" - "testing" - - "github.com/vercel/turbo/cli/internal/turbopath" -) - -func TestParseTargetSelector(t *testing.T) { - tests := []struct { - rawSelector string - want *TargetSelector - wantErr bool - }{ - { - "{}", - &TargetSelector{}, - true, - }, - { - "foo", - &TargetSelector{ - fromRef: "", - exclude: false, - excludeSelf: false, - includeDependencies: false, - includeDependents: false, - namePattern: "foo", - parentDir: "", - }, - false, - }, - { - "foo...", - &TargetSelector{ - fromRef: "", - exclude: false, - excludeSelf: false, - includeDependencies: true, - includeDependents: false, - namePattern: "foo", - parentDir: "", - }, - false, - }, - { - "...foo", - &TargetSelector{ - fromRef: "", - exclude: false, - excludeSelf: false, - includeDependencies: false, - includeDependents: true, - namePattern: "foo", - parentDir: "", - }, - false, - }, - { - "...foo...", - &TargetSelector{ - fromRef: "", - exclude: false, - excludeSelf: false, - includeDependencies: true, - includeDependents: true, - namePattern: "foo", - parentDir: "", - }, - false, - }, - { - "foo^...", - &TargetSelector{ - fromRef: "", - exclude: false, - excludeSelf: true, - includeDependencies: true, - includeDependents: false, - namePattern: "foo", - parentDir: "", - }, - false, - }, - { - "...^foo", - &TargetSelector{ - fromRef: "", - exclude: false, - excludeSelf: true, - includeDependencies: false, - includeDependents: true, - namePattern: "foo", - parentDir: "", - }, - false, - }, - { - "./foo", - &TargetSelector{ - fromRef: "", - exclude: false, - excludeSelf: false, - includeDependencies: false, - includeDependents: false, - namePattern: "", - parentDir: "foo", - }, - false, - }, - { - "../foo", - &TargetSelector{ - fromRef: "", - exclude: false, - excludeSelf: false, - includeDependencies: false, - includeDependents: false, - namePattern: "", - parentDir: turbopath.MakeRelativeSystemPath("..", "foo"), - }, - false, - }, - { - "...{./foo}", - &TargetSelector{ - fromRef: "", - exclude: false, - excludeSelf: false, - includeDependencies: false, - includeDependents: true, - namePattern: "", - parentDir: "foo", - }, - false, - }, - { - ".", - &TargetSelector{ - fromRef: "", - exclude: false, - excludeSelf: false, - includeDependencies: false, - includeDependents: false, - namePattern: "", - parentDir: ".", - }, - false, - }, - { - "..", - &TargetSelector{ - fromRef: "", - exclude: false, - excludeSelf: false, - includeDependencies: false, - includeDependents: false, - namePattern: "", - parentDir: "..", - }, - false, - }, - { - "[master]", - &TargetSelector{ - fromRef: "master", - exclude: false, - excludeSelf: false, - includeDependencies: false, - includeDependents: false, - namePattern: "", - parentDir: "", - }, - false, - }, - { - "[from...to]", - &TargetSelector{ - fromRef: "from", - toRefOverride: "to", - }, - false, - }, - { - "{foo}[master]", - &TargetSelector{ - fromRef: "master", - exclude: false, - excludeSelf: false, - includeDependencies: false, - includeDependents: false, - namePattern: "", - parentDir: "foo", - }, - false, - }, - { - "pattern{foo}[master]", - &TargetSelector{ - fromRef: "master", - exclude: false, - excludeSelf: false, - includeDependencies: false, - includeDependents: false, - namePattern: "pattern", - parentDir: "foo", - }, - false, - }, - { - "[master]...", - &TargetSelector{ - fromRef: "master", - exclude: false, - excludeSelf: false, - includeDependencies: true, - includeDependents: false, - namePattern: "", - parentDir: "", - }, - false, - }, - { - "...[master]", - &TargetSelector{ - fromRef: "master", - exclude: false, - excludeSelf: false, - includeDependencies: false, - includeDependents: true, - namePattern: "", - parentDir: "", - }, - false, - }, - { - "...[master]...", - &TargetSelector{ - fromRef: "master", - exclude: false, - excludeSelf: false, - includeDependencies: true, - includeDependents: true, - namePattern: "", - parentDir: "", - }, - false, - }, - { - "...[from...to]...", - &TargetSelector{ - fromRef: "from", - toRefOverride: "to", - includeDependencies: true, - includeDependents: true, - }, - false, - }, - { - "foo...[master]", - &TargetSelector{ - fromRef: "master", - namePattern: "foo", - matchDependencies: true, - }, - false, - }, - { - "foo...[master]...", - &TargetSelector{ - fromRef: "master", - namePattern: "foo", - matchDependencies: true, - includeDependencies: true, - }, - false, - }, - { - "{foo}...[master]", - &TargetSelector{ - fromRef: "master", - parentDir: "foo", - matchDependencies: true, - }, - false, - }, - { - "......[master]", - &TargetSelector{}, - true, - }, - } - for _, tt := range tests { - t.Run(tt.rawSelector, func(t *testing.T) { - got, err := ParseTargetSelector(tt.rawSelector) - if tt.wantErr { - if err == nil { - t.Errorf("ParseTargetSelector() error = %#v, wantErr %#v", err, tt.wantErr) - } - } else { - // copy the raw selector from the args into what we want. This value is used - // for reporting errors in the case of a malformed selector - tt.want.raw = tt.rawSelector - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ParseTargetSelector() = %#v, want %#v", got, tt.want) - } - } - }) - } -} diff --git a/cli/internal/scope/scope.go b/cli/internal/scope/scope.go deleted file mode 100644 index b5ed4e7..0000000 --- a/cli/internal/scope/scope.go +++ /dev/null @@ -1,380 +0,0 @@ -package scope - -import ( - "fmt" - "os" - "path/filepath" - "sort" - "strings" - - "github.com/hashicorp/go-hclog" - "github.com/mitchellh/cli" - "github.com/pkg/errors" - "github.com/vercel/turbo/cli/internal/context" - "github.com/vercel/turbo/cli/internal/lockfile" - "github.com/vercel/turbo/cli/internal/scm" - scope_filter "github.com/vercel/turbo/cli/internal/scope/filter" - "github.com/vercel/turbo/cli/internal/turbopath" - "github.com/vercel/turbo/cli/internal/turbostate" - "github.com/vercel/turbo/cli/internal/util" - "github.com/vercel/turbo/cli/internal/util/filter" - "github.com/vercel/turbo/cli/internal/workspace" -) - -// LegacyFilter holds the options in use before the filter syntax. They have their own rules -// for how they are compiled into filter expressions. -type LegacyFilter struct { - // IncludeDependencies is whether to include pkg.dependencies in execution (defaults to false) - IncludeDependencies bool - // SkipDependents is whether to skip dependent impacted consumers in execution (defaults to false) - SkipDependents bool - // Entrypoints is a list of package entrypoints - Entrypoints []string - // Since is the git ref used to calculate changed packages - Since string -} - -var _sinceHelp = `Limit/Set scope to changed packages since a -mergebase. This uses the git diff ${target_branch}... -mechanism to identify which packages have changed.` - -func addLegacyFlagsFromArgs(opts *LegacyFilter, args *turbostate.ParsedArgsFromRust) { - opts.IncludeDependencies = args.Command.Run.IncludeDependencies - opts.SkipDependents = args.Command.Run.NoDeps - opts.Entrypoints = args.Command.Run.Scope - opts.Since = args.Command.Run.Since -} - -// Opts holds the options for how to select the entrypoint packages for a turbo run -type Opts struct { - LegacyFilter LegacyFilter - // IgnorePatterns is the list of globs of file paths to ignore from execution scope calculation - IgnorePatterns []string - // GlobalDepPatterns is a list of globs to global files whose contents will be included in the global hash calculation - GlobalDepPatterns []string - // Patterns are the filter patterns supplied to --filter on the commandline - FilterPatterns []string - - PackageInferenceRoot turbopath.RelativeSystemPath -} - -var ( - _filterHelp = `Use the given selector to specify package(s) to act as -entry points. The syntax mirrors pnpm's syntax, and -additional documentation and examples can be found in -turbo's documentation https://turbo.build/repo/docs/reference/command-line-reference#--filter ---filter can be specified multiple times. Packages that -match any filter will be included.` - _ignoreHelp = `Files to ignore when calculating changed files (i.e. --since). Supports globs.` - _globalDepHelp = `Specify glob of global filesystem dependencies to be hashed. Useful for .env and files -in the root directory. Includes turbo.json, root package.json, and the root lockfile by default.` -) - -// normalize package inference path. We compare against "" in several places, so maintain -// that behavior. In a post-rust-port world, this should more properly be an Option -func resolvePackageInferencePath(raw string) (turbopath.RelativeSystemPath, error) { - pkgInferenceRoot, err := turbopath.CheckedToRelativeSystemPath(raw) - if err != nil { - return "", errors.Wrapf(err, "invalid package inference root %v", raw) - } - if pkgInferenceRoot == "." { - return "", nil - } - return pkgInferenceRoot, nil -} - -// OptsFromArgs adds the settings relevant to this package to the given Opts -func OptsFromArgs(opts *Opts, args *turbostate.ParsedArgsFromRust) error { - opts.FilterPatterns = args.Command.Run.Filter - opts.IgnorePatterns = args.Command.Run.Ignore - opts.GlobalDepPatterns = args.Command.Run.GlobalDeps - pkgInferenceRoot, err := resolvePackageInferencePath(args.Command.Run.PkgInferenceRoot) - if err != nil { - return err - } - opts.PackageInferenceRoot = pkgInferenceRoot - addLegacyFlagsFromArgs(&opts.LegacyFilter, args) - return nil -} - -// AsFilterPatterns normalizes legacy selectors to filter syntax -func (l *LegacyFilter) AsFilterPatterns() []string { - var patterns []string - prefix := "" - if !l.SkipDependents { - prefix = "..." - } - suffix := "" - if l.IncludeDependencies { - suffix = "..." - } - since := "" - if l.Since != "" { - since = fmt.Sprintf("[%v]", l.Since) - } - if len(l.Entrypoints) > 0 { - // --scope implies our tweaked syntax to see if any dependency matches - if since != "" { - since = "..." + since - } - for _, pattern := range l.Entrypoints { - if strings.HasPrefix(pattern, "!") { - patterns = append(patterns, pattern) - } else { - filterPattern := fmt.Sprintf("%v%v%v%v", prefix, pattern, since, suffix) - patterns = append(patterns, filterPattern) - } - } - } else if since != "" { - // no scopes specified, but --since was provided - filterPattern := fmt.Sprintf("%v%v%v", prefix, since, suffix) - patterns = append(patterns, filterPattern) - } - return patterns -} - -// ResolvePackages translates specified flags to a set of entry point packages for -// the selected tasks. Returns the selected packages and whether or not the selected -// packages represents a default "all packages". -func ResolvePackages(opts *Opts, repoRoot turbopath.AbsoluteSystemPath, scm scm.SCM, ctx *context.Context, tui cli.Ui, logger hclog.Logger) (util.Set, bool, error) { - inferenceBase, err := calculateInference(repoRoot, opts.PackageInferenceRoot, ctx.WorkspaceInfos, logger) - if err != nil { - return nil, false, err - } - filterResolver := &scope_filter.Resolver{ - Graph: &ctx.WorkspaceGraph, - WorkspaceInfos: ctx.WorkspaceInfos, - Cwd: repoRoot, - Inference: inferenceBase, - PackagesChangedInRange: opts.getPackageChangeFunc(scm, repoRoot, ctx), - } - filterPatterns := opts.FilterPatterns - legacyFilterPatterns := opts.LegacyFilter.AsFilterPatterns() - filterPatterns = append(filterPatterns, legacyFilterPatterns...) - isAllPackages := len(filterPatterns) == 0 && opts.PackageInferenceRoot == "" - filteredPkgs, err := filterResolver.GetPackagesFromPatterns(filterPatterns) - if err != nil { - return nil, false, err - } - - if isAllPackages { - // no filters specified, run every package - for _, f := range ctx.WorkspaceNames { - filteredPkgs.Add(f) - } - } - filteredPkgs.Delete(ctx.RootNode) - return filteredPkgs, isAllPackages, nil -} - -func calculateInference(repoRoot turbopath.AbsoluteSystemPath, pkgInferencePath turbopath.RelativeSystemPath, packageInfos workspace.Catalog, logger hclog.Logger) (*scope_filter.PackageInference, error) { - if pkgInferencePath == "" { - // No inference specified, no need to calculate anything - return nil, nil - } - logger.Debug(fmt.Sprintf("Using %v as a basis for selecting packages", pkgInferencePath)) - fullInferencePath := repoRoot.Join(pkgInferencePath) - for _, pkgInfo := range packageInfos.PackageJSONs { - pkgPath := pkgInfo.Dir.RestoreAnchor(repoRoot) - inferredPathIsBelow, err := pkgPath.ContainsPath(fullInferencePath) - if err != nil { - return nil, err - } - // We skip over the root package as the inferred path will always be below it - if inferredPathIsBelow && pkgPath != repoRoot { - // set both. The user might have set a parent directory filter, - // in which case we *should* fail to find any packages, but we should - // do so in a consistent manner - return &scope_filter.PackageInference{ - PackageName: pkgInfo.Name, - DirectoryRoot: pkgInferencePath, - }, nil - } - inferredPathIsBetweenRootAndPkg, err := fullInferencePath.ContainsPath(pkgPath) - if err != nil { - return nil, err - } - if inferredPathIsBetweenRootAndPkg { - // we've found *some* package below our inference directory. We can stop now and conclude - // that we're looking for all packages in a subdirectory - break - } - } - return &scope_filter.PackageInference{ - DirectoryRoot: pkgInferencePath, - }, nil -} - -func (o *Opts) getPackageChangeFunc(scm scm.SCM, cwd turbopath.AbsoluteSystemPath, ctx *context.Context) scope_filter.PackagesChangedInRange { - return func(fromRef string, toRef string) (util.Set, error) { - // We could filter changed files at the git level, since it's possible - // that the changes we're interested in are scoped, but we need to handle - // global dependencies changing as well. A future optimization might be to - // scope changed files more deeply if we know there are no global dependencies. - var changedFiles []string - if fromRef != "" { - scmChangedFiles, err := scm.ChangedFiles(fromRef, toRef, cwd.ToStringDuringMigration()) - if err != nil { - return nil, err - } - sort.Strings(scmChangedFiles) - changedFiles = scmChangedFiles - } - makeAllPkgs := func() util.Set { - allPkgs := make(util.Set) - for pkg := range ctx.WorkspaceInfos.PackageJSONs { - allPkgs.Add(pkg) - } - return allPkgs - } - if hasRepoGlobalFileChanged, err := repoGlobalFileHasChanged(o, getDefaultGlobalDeps(), changedFiles); err != nil { - return nil, err - } else if hasRepoGlobalFileChanged { - return makeAllPkgs(), nil - } - - filteredChangedFiles, err := filterIgnoredFiles(o, changedFiles) - if err != nil { - return nil, err - } - changedPkgs := getChangedPackages(filteredChangedFiles, ctx.WorkspaceInfos) - - if lockfileChanges, fullChanges := getChangesFromLockfile(scm, ctx, changedFiles, fromRef); !fullChanges { - for _, pkg := range lockfileChanges { - changedPkgs.Add(pkg) - } - } else { - return makeAllPkgs(), nil - } - - return changedPkgs, nil - } -} - -func getChangesFromLockfile(scm scm.SCM, ctx *context.Context, changedFiles []string, fromRef string) ([]string, bool) { - lockfileFilter, err := filter.Compile([]string{ctx.PackageManager.Lockfile}) - if err != nil { - panic(fmt.Sprintf("Lockfile is invalid glob: %v", err)) - } - match := false - for _, file := range changedFiles { - if lockfileFilter.Match(file) { - match = true - break - } - } - if !match { - return nil, false - } - - if lockfile.IsNil(ctx.Lockfile) { - return nil, true - } - - prevContents, err := scm.PreviousContent(fromRef, ctx.PackageManager.Lockfile) - if err != nil { - // unable to reconstruct old lockfile, assume everything changed - return nil, true - } - prevLockfile, err := ctx.PackageManager.UnmarshalLockfile(ctx.WorkspaceInfos.PackageJSONs[util.RootPkgName], prevContents) - if err != nil { - // unable to parse old lockfile, assume everything changed - return nil, true - } - additionalPkgs, err := ctx.ChangedPackages(prevLockfile) - if err != nil { - // missing at least one lockfile, assume everything changed - return nil, true - } - - return additionalPkgs, false -} - -func getDefaultGlobalDeps() []string { - // include turbo.json and root package.json as implicit global dependencies - defaultGlobalDeps := []string{ - "turbo.json", - "package.json", - } - return defaultGlobalDeps -} - -func repoGlobalFileHasChanged(opts *Opts, defaultGlobalDeps []string, changedFiles []string) (bool, error) { - globalDepsGlob, err := filter.Compile(append(opts.GlobalDepPatterns, defaultGlobalDeps...)) - if err != nil { - return false, errors.Wrap(err, "invalid global deps glob") - } - - if globalDepsGlob != nil { - for _, file := range changedFiles { - if globalDepsGlob.Match(filepath.ToSlash(file)) { - return true, nil - } - } - } - return false, nil -} - -func filterIgnoredFiles(opts *Opts, changedFiles []string) ([]string, error) { - // changedFiles is an array of repo-relative system paths. - // opts.IgnorePatterns is an array of unix-separator glob paths. - ignoreGlob, err := filter.Compile(opts.IgnorePatterns) - if err != nil { - return nil, errors.Wrap(err, "invalid ignore globs") - } - filteredChanges := []string{} - for _, file := range changedFiles { - // If we don't have anything to ignore, or if this file doesn't match the ignore pattern, - // keep it as a changed file. - if ignoreGlob == nil || !ignoreGlob.Match(filepath.ToSlash(file)) { - filteredChanges = append(filteredChanges, file) - } - } - return filteredChanges, nil -} - -func fileInPackage(changedFile string, packagePath string) bool { - // This whole method is basically this regex: /^.*\/?$/ - // The regex is more-expensive, so we don't do it. - - // If it has the prefix, it might be in the package. - if strings.HasPrefix(changedFile, packagePath) { - // Now we need to see if the prefix stopped at a reasonable boundary. - prefixLen := len(packagePath) - changedFileLen := len(changedFile) - - // Same path. - if prefixLen == changedFileLen { - return true - } - - // We know changedFile is longer than packagePath. - // We can safely directly index into it. - // Look ahead one byte and see if it's the separator. - if changedFile[prefixLen] == os.PathSeparator { - return true - } - } - - // If it does not have the prefix, it's definitely not in the package. - return false -} - -func getChangedPackages(changedFiles []string, packageInfos workspace.Catalog) util.Set { - changedPackages := make(util.Set) - for _, changedFile := range changedFiles { - found := false - for pkgName, pkgInfo := range packageInfos.PackageJSONs { - if pkgName != util.RootPkgName && fileInPackage(changedFile, pkgInfo.Dir.ToStringDuringMigration()) { - changedPackages.Add(pkgName) - found = true - break - } - } - if !found { - // Consider the root package to have changed - changedPackages.Add(util.RootPkgName) - } - } - return changedPackages -} diff --git a/cli/internal/scope/scope_test.go b/cli/internal/scope/scope_test.go deleted file mode 100644 index 216984d..0000000 --- a/cli/internal/scope/scope_test.go +++ /dev/null @@ -1,550 +0,0 @@ -package scope - -import ( - "fmt" - "io" - "os" - "path/filepath" - "reflect" - "testing" - - "github.com/hashicorp/go-hclog" - "github.com/pyr-sh/dag" - "github.com/vercel/turbo/cli/internal/context" - "github.com/vercel/turbo/cli/internal/fs" - "github.com/vercel/turbo/cli/internal/lockfile" - "github.com/vercel/turbo/cli/internal/packagemanager" - "github.com/vercel/turbo/cli/internal/turbopath" - "github.com/vercel/turbo/cli/internal/ui" - "github.com/vercel/turbo/cli/internal/util" - "github.com/vercel/turbo/cli/internal/workspace" -) - -type mockSCM struct { - changed []string - contents map[string][]byte -} - -func (m *mockSCM) ChangedFiles(_fromCommit string, _toCommit string, _relativeTo string) ([]string, error) { - return m.changed, nil -} - -func (m *mockSCM) PreviousContent(fromCommit string, filePath string) ([]byte, error) { - contents, ok := m.contents[filePath] - if !ok { - return nil, fmt.Errorf("No contents found") - } - return contents, nil -} - -type mockLockfile struct { - globalChange bool - versions map[string]string - allDeps map[string]map[string]string -} - -func (m *mockLockfile) ResolvePackage(workspacePath turbopath.AnchoredUnixPath, name string, version string) (lockfile.Package, error) { - resolvedVersion, ok := m.versions[name] - if ok { - key := fmt.Sprintf("%s%s", name, version) - return lockfile.Package{Key: key, Version: resolvedVersion, Found: true}, nil - } - return lockfile.Package{Found: false}, nil -} - -func (m *mockLockfile) AllDependencies(key string) (map[string]string, bool) { - deps, ok := m.allDeps[key] - return deps, ok -} - -func (m *mockLockfile) Encode(w io.Writer) error { - return nil -} - -func (m *mockLockfile) GlobalChange(other lockfile.Lockfile) bool { - return m.globalChange || (other != nil && other.(*mockLockfile).globalChange) -} - -func (m *mockLockfile) Patches() []turbopath.AnchoredUnixPath { - return nil -} - -func (m *mockLockfile) Subgraph(workspaces []turbopath.AnchoredSystemPath, packages []string) (lockfile.Lockfile, error) { - return nil, nil -} - -var _ (lockfile.Lockfile) = (*mockLockfile)(nil) - -func TestResolvePackages(t *testing.T) { - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("cwd: %v", err) - } - root, err := fs.GetCwd(cwd) - if err != nil { - t.Fatalf("cwd: %v", err) - } - tui := ui.Default() - logger := hclog.Default() - // Dependency graph: - // - // app0 - - // \ - // app1 -> libA - // \ - // > libB -> libD - // / - // app2 < - // \ - // > libC - // / - // app2-a < - // - // Filesystem layout: - // - // app/ - // app0 - // app1 - // app2 - // app2-a - // libs/ - // libA - // libB - // libC - // libD - graph := dag.AcyclicGraph{} - graph.Add("app0") - graph.Add("app1") - graph.Add("app2") - graph.Add("app2-a") - graph.Add("libA") - graph.Add("libB") - graph.Add("libC") - graph.Add("libD") - graph.Connect(dag.BasicEdge("libA", "libB")) - graph.Connect(dag.BasicEdge("libB", "libD")) - graph.Connect(dag.BasicEdge("app0", "libA")) - graph.Connect(dag.BasicEdge("app1", "libA")) - graph.Connect(dag.BasicEdge("app2", "libB")) - graph.Connect(dag.BasicEdge("app2", "libC")) - graph.Connect(dag.BasicEdge("app2-a", "libC")) - workspaceInfos := workspace.Catalog{ - PackageJSONs: map[string]*fs.PackageJSON{ - "//": { - Dir: turbopath.AnchoredSystemPath("").ToSystemPath(), - UnresolvedExternalDeps: map[string]string{"global": "2"}, - TransitiveDeps: []lockfile.Package{{Key: "global2", Version: "2", Found: true}}, - }, - "app0": { - Dir: turbopath.AnchoredUnixPath("app/app0").ToSystemPath(), - Name: "app0", - UnresolvedExternalDeps: map[string]string{"app0-dep": "2"}, - TransitiveDeps: []lockfile.Package{ - {Key: "app0-dep2", Version: "2", Found: true}, - {Key: "app0-util2", Version: "2", Found: true}, - }, - }, - "app1": { - Dir: turbopath.AnchoredUnixPath("app/app1").ToSystemPath(), - Name: "app1", - }, - "app2": { - Dir: turbopath.AnchoredUnixPath("app/app2").ToSystemPath(), - Name: "app2", - }, - "app2-a": { - Dir: turbopath.AnchoredUnixPath("app/app2-a").ToSystemPath(), - Name: "app2-a", - }, - "libA": { - Dir: turbopath.AnchoredUnixPath("libs/libA").ToSystemPath(), - Name: "libA", - }, - "libB": { - Dir: turbopath.AnchoredUnixPath("libs/libB").ToSystemPath(), - Name: "libB", - UnresolvedExternalDeps: map[string]string{"external": "1"}, - TransitiveDeps: []lockfile.Package{ - {Key: "external-dep-a1", Version: "1", Found: true}, - {Key: "external-dep-b1", Version: "1", Found: true}, - {Key: "external1", Version: "1", Found: true}, - }, - }, - "libC": { - Dir: turbopath.AnchoredUnixPath("libs/libC").ToSystemPath(), - Name: "libC", - }, - "libD": { - Dir: turbopath.AnchoredUnixPath("libs/libD").ToSystemPath(), - Name: "libD", - }, - }, - } - packageNames := []string{} - for name := range workspaceInfos.PackageJSONs { - packageNames = append(packageNames, name) - } - - // global -> globalDep - // app0-dep -> app0-dep :) - - makeLockfile := func(f func(*mockLockfile)) *mockLockfile { - l := mockLockfile{ - globalChange: false, - versions: map[string]string{ - "global": "2", - "app0-dep": "2", - "app0-util": "2", - "external": "1", - "external-dep-a": "1", - "external-dep-b": "1", - }, - allDeps: map[string]map[string]string{ - "global2": map[string]string{}, - "app0-dep2": map[string]string{ - "app0-util": "2", - }, - "app0-util2": map[string]string{}, - "external1": map[string]string{ - "external-dep-a": "1", - "external-dep-b": "1", - }, - "external-dep-a1": map[string]string{}, - "external-dep-b1": map[string]string{}, - }, - } - if f != nil { - f(&l) - } - return &l - } - - testCases := []struct { - name string - changed []string - expected []string - expectAllPackages bool - scope []string - since string - ignore string - globalDeps []string - includeDependencies bool - includeDependents bool - lockfile string - currLockfile *mockLockfile - prevLockfile *mockLockfile - inferPkgPath string - }{ - { - name: "Just scope and dependencies", - changed: []string{}, - includeDependencies: true, - scope: []string{"app2"}, - expected: []string{"app2", "libB", "libC", "libD"}, - }, - { - name: "Only turbo.json changed", - changed: []string{"turbo.json"}, - expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, - since: "dummy", - includeDependencies: true, - }, - { - name: "Only root package.json changed", - changed: []string{"package.json"}, - expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, - since: "dummy", - includeDependencies: true, - }, - { - name: "Only package-lock.json changed", - changed: []string{"package-lock.json"}, - expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, - since: "dummy", - includeDependencies: true, - lockfile: "package-lock.json", - }, - { - name: "Only yarn.lock changed", - changed: []string{"yarn.lock"}, - expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, - since: "dummy", - includeDependencies: true, - lockfile: "yarn.lock", - }, - { - name: "Only pnpm-lock.yaml changed", - changed: []string{"pnpm-lock.yaml"}, - expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, - since: "dummy", - includeDependencies: true, - lockfile: "pnpm-lock.yaml", - }, - { - name: "One package changed", - changed: []string{"libs/libB/src/index.ts"}, - expected: []string{"libB"}, - since: "dummy", - }, - { - name: "One package manifest changed", - changed: []string{"libs/libB/package.json"}, - expected: []string{"libB"}, - since: "dummy", - }, - { - name: "An ignored package changed", - changed: []string{"libs/libB/src/index.ts"}, - expected: []string{}, - since: "dummy", - ignore: "libs/libB/**/*.ts", - }, - { - // nothing in scope depends on the change - name: "unrelated library changed", - changed: []string{"libs/libC/src/index.ts"}, - expected: []string{}, - since: "dummy", - scope: []string{"app1"}, - includeDependencies: true, // scope implies include-dependencies - }, - { - // a dependent lib changed, scope implies include-dependencies, - // so all deps of app1 get built - name: "dependency of scope changed", - changed: []string{"libs/libA/src/index.ts"}, - expected: []string{"libA", "libB", "libD", "app1"}, - since: "dummy", - scope: []string{"app1"}, - includeDependencies: true, // scope implies include-dependencies - }, - { - // a dependent lib changed, user explicitly asked to not build dependencies. - // Since the package matching the scope had a changed dependency, we run it. - // We don't include its dependencies because the user asked for no dependencies. - // note: this is not yet supported by the CLI, as you cannot specify --include-dependencies=false - name: "dependency of scope changed, user asked to not include depedencies", - changed: []string{"libs/libA/src/index.ts"}, - expected: []string{"app1"}, - since: "dummy", - scope: []string{"app1"}, - includeDependencies: false, - }, - { - // a nested dependent lib changed, user explicitly asked to not build dependencies - // note: this is not yet supported by the CLI, as you cannot specify --include-dependencies=false - name: "nested dependency of scope changed, user asked to not include dependencies", - changed: []string{"libs/libB/src/index.ts"}, - expected: []string{"app1"}, - since: "dummy", - scope: []string{"app1"}, - includeDependencies: false, - }, - { - name: "global dependency changed, even though it was ignored, forcing a build of everything", - changed: []string{"libs/libB/src/index.ts"}, - expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, - since: "dummy", - ignore: "libs/libB/**/*.ts", - globalDeps: []string{"libs/**/*.ts"}, - }, - { - name: "an app changed, user asked for dependencies to build", - changed: []string{"app/app2/src/index.ts"}, - since: "dummy", - includeDependencies: true, - expected: []string{"app2", "libB", "libC", "libD"}, - }, - { - name: "a library changed, user asked for dependents to be built", - changed: []string{"libs/libB"}, - since: "dummy", - includeDependents: true, - expected: []string{"app0", "app1", "app2", "libA", "libB"}, - }, - { - // no changes, no base to compare against, defaults to everything - name: "no changes or scope specified, build everything", - since: "", - expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, - expectAllPackages: true, - }, - { - // a dependent library changed, no deps beyond the scope are build - // "libB" is still built because it is a dependent within the scope, but libB's dependents - // are skipped - name: "a dependent library changed, build up to scope", - changed: []string{"libs/libD/src/index.ts"}, - since: "dummy", - scope: []string{"libB"}, - expected: []string{"libB", "libD"}, - includeDependencies: true, // scope implies include-dependencies - }, - { - name: "library change, no scope", - changed: []string{"libs/libA/src/index.ts"}, - expected: []string{"libA", "app0", "app1"}, - includeDependents: true, - since: "dummy", - }, - { - // make sure multiple apps with the same prefix are handled separately. - // prevents this issue: https://github.com/vercel/turbo/issues/1528 - name: "Two apps with an overlapping prefix changed", - changed: []string{"app/app2/src/index.js", "app/app2-a/src/index.js"}, - expected: []string{"app2", "app2-a"}, - since: "dummy", - }, - { - name: "Global lockfile change invalidates all packages", - changed: []string{"dummy.lock"}, - expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, - lockfile: "dummy.lock", - currLockfile: makeLockfile(nil), - prevLockfile: makeLockfile(func(ml *mockLockfile) { - ml.globalChange = true - }), - since: "dummy", - }, - { - name: "Dependency of workspace root change invalidates all packages", - changed: []string{"dummy.lock"}, - expected: []string{"//", "app0", "app1", "app2", "app2-a", "libA", "libB", "libC", "libD"}, - lockfile: "dummy.lock", - currLockfile: makeLockfile(nil), - prevLockfile: makeLockfile(func(ml *mockLockfile) { - ml.versions["global"] = "3" - ml.allDeps["global3"] = map[string]string{} - }), - since: "dummy", - }, - { - name: "Version change invalidates package", - changed: []string{"dummy.lock"}, - expected: []string{"//", "app0"}, - lockfile: "dummy.lock", - currLockfile: makeLockfile(nil), - prevLockfile: makeLockfile(func(ml *mockLockfile) { - ml.versions["app0-util"] = "3" - ml.allDeps["app0-dep2"] = map[string]string{"app0-util": "3"} - ml.allDeps["app0-util3"] = map[string]string{} - }), - since: "dummy", - }, - { - name: "Transitive dep invalidates package", - changed: []string{"dummy.lock"}, - expected: []string{"//", "libB"}, - lockfile: "dummy.lock", - currLockfile: makeLockfile(nil), - prevLockfile: makeLockfile(func(ml *mockLockfile) { - ml.versions["external-dep-a"] = "2" - ml.allDeps["external1"] = map[string]string{"external-dep-a": "2", "external-dep-b": "1"} - ml.allDeps["external-dep-a2"] = map[string]string{} - }), - since: "dummy", - }, - { - name: "Transitive dep invalidates package and dependents", - changed: []string{"dummy.lock"}, - expected: []string{"//", "app0", "app1", "app2", "libA", "libB"}, - lockfile: "dummy.lock", - includeDependents: true, - currLockfile: makeLockfile(nil), - prevLockfile: makeLockfile(func(ml *mockLockfile) { - ml.versions["external-dep-a"] = "2" - ml.allDeps["external1"] = map[string]string{"external-dep-a": "2", "external-dep-b": "1"} - ml.allDeps["external-dep-a2"] = map[string]string{} - }), - since: "dummy", - }, - { - name: "Infer app2 from directory", - inferPkgPath: "app/app2", - expected: []string{"app2"}, - }, - { - name: "Infer app2 from a subdirectory", - inferPkgPath: "app/app2/src", - expected: []string{"app2"}, - }, - { - name: "Infer from a directory with no packages", - inferPkgPath: "wrong", - expected: []string{}, - }, - { - name: "Infer from a parent directory", - inferPkgPath: "app", - expected: []string{"app0", "app1", "app2", "app2-a"}, - }, - { - name: "library change, no scope, inferred libs", - changed: []string{"libs/libA/src/index.ts"}, - expected: []string{"libA"}, - since: "dummy", - inferPkgPath: "libs", - }, - { - name: "library change, no scope, inferred app", - changed: []string{"libs/libA/src/index.ts"}, - expected: []string{}, - since: "dummy", - inferPkgPath: "app", - }, - } - for i, tc := range testCases { - t.Run(fmt.Sprintf("test #%v %v", i, tc.name), func(t *testing.T) { - // Convert test data to system separators. - systemSeparatorChanged := make([]string, len(tc.changed)) - for index, path := range tc.changed { - systemSeparatorChanged[index] = filepath.FromSlash(path) - } - scm := &mockSCM{ - changed: systemSeparatorChanged, - contents: make(map[string][]byte, len(systemSeparatorChanged)), - } - for _, path := range systemSeparatorChanged { - scm.contents[path] = nil - } - readLockfile := func(_rootPackageJSON *fs.PackageJSON, content []byte) (lockfile.Lockfile, error) { - return tc.prevLockfile, nil - } - pkgInferenceRoot, err := resolvePackageInferencePath(tc.inferPkgPath) - if err != nil { - t.Errorf("bad inference path (%v): %v", tc.inferPkgPath, err) - } - pkgs, isAllPackages, err := ResolvePackages(&Opts{ - LegacyFilter: LegacyFilter{ - Entrypoints: tc.scope, - Since: tc.since, - IncludeDependencies: tc.includeDependencies, - SkipDependents: !tc.includeDependents, - }, - IgnorePatterns: []string{tc.ignore}, - GlobalDepPatterns: tc.globalDeps, - PackageInferenceRoot: pkgInferenceRoot, - }, root, scm, &context.Context{ - WorkspaceInfos: workspaceInfos, - WorkspaceNames: packageNames, - PackageManager: &packagemanager.PackageManager{Lockfile: tc.lockfile, UnmarshalLockfile: readLockfile}, - WorkspaceGraph: graph, - RootNode: "root", - Lockfile: tc.currLockfile, - }, tui, logger) - if err != nil { - t.Errorf("expected no error, got %v", err) - } - expected := make(util.Set) - for _, pkg := range tc.expected { - expected.Add(pkg) - } - if !reflect.DeepEqual(pkgs, expected) { - t.Errorf("ResolvePackages got %v, want %v", pkgs, expected) - } - if isAllPackages != tc.expectAllPackages { - t.Errorf("isAllPackages got %v, want %v", isAllPackages, tc.expectAllPackages) - } - }) - } -} |
