-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Use lockfiles when determining what packages changed across commits #3250
Changes from all commits
c570b84
becb80a
2940972
93dab87
3a46ff0
fadb510
28ca025
664038a
f1e646b
7eacb16
0f886ce
865ba7d
9fe2ae9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -230,10 +230,7 @@ func BuildPackageGraph(repoRoot turbopath.AbsoluteSystemPath, rootPackageJSON *f | |
} | ||
|
||
func (c *Context) resolveWorkspaceRootDeps(rootPackageJSON *fs.PackageJSON, warnings *Warnings) error { | ||
seen := mapset.NewSet() | ||
var lockfileEg errgroup.Group | ||
pkg := rootPackageJSON | ||
depSet := mapset.NewSet() | ||
pkg.UnresolvedExternalDeps = make(map[string]string) | ||
for dep, version := range pkg.DevDependencies { | ||
pkg.UnresolvedExternalDeps[dep] = version | ||
|
@@ -245,25 +242,25 @@ func (c *Context) resolveWorkspaceRootDeps(rootPackageJSON *fs.PackageJSON, warn | |
pkg.UnresolvedExternalDeps[dep] = version | ||
} | ||
if c.Lockfile != nil { | ||
pkg.TransitiveDeps = []string{} | ||
c.resolveDepGraph(&lockfileEg, pkg, pkg.UnresolvedExternalDeps, depSet, seen, pkg) | ||
if err := lockfileEg.Wait(); err != nil { | ||
depSet, err := TransitiveClosure(pkg, c.Lockfile) | ||
if err != nil { | ||
warnings.append(err) | ||
// Return early to skip using results of incomplete dep graph resolution | ||
return nil | ||
} | ||
pkg.ExternalDeps = make([]string, 0, depSet.Cardinality()) | ||
pkg.TransitiveDeps = make([]lockfile.Package, 0, depSet.Cardinality()) | ||
for _, v := range depSet.ToSlice() { | ||
pkg.ExternalDeps = append(pkg.ExternalDeps, fmt.Sprintf("%v", v)) | ||
dep := v.(lockfile.Package) | ||
pkg.TransitiveDeps = append(pkg.TransitiveDeps, dep) | ||
} | ||
sort.Strings(pkg.ExternalDeps) | ||
hashOfExternalDeps, err := fs.HashObject(pkg.ExternalDeps) | ||
sort.Sort(lockfile.ByKey(pkg.TransitiveDeps)) | ||
hashOfExternalDeps, err := fs.HashObject(pkg.TransitiveDeps) | ||
if err != nil { | ||
return err | ||
} | ||
pkg.ExternalDepsHash = hashOfExternalDeps | ||
} else { | ||
pkg.ExternalDeps = []string{} | ||
pkg.TransitiveDeps = []lockfile.Package{} | ||
pkg.ExternalDepsHash = "" | ||
} | ||
|
||
|
@@ -280,7 +277,6 @@ func (c *Context) populateWorkspaceGraphForPackageJSON(pkg *fs.PackageJSON, root | |
depMap := make(map[string]string) | ||
internalDepsSet := make(dag.Set) | ||
externalUnresolvedDepsSet := make(dag.Set) | ||
externalDepSet := mapset.NewSet() | ||
pkg.UnresolvedExternalDeps = make(map[string]string) | ||
|
||
for dep, version := range pkg.DevDependencies { | ||
|
@@ -320,31 +316,29 @@ func (c *Context) populateWorkspaceGraphForPackageJSON(pkg *fs.PackageJSON, root | |
} | ||
} | ||
|
||
pkg.TransitiveDeps = []string{} | ||
seen := mapset.NewSet() | ||
lockfileEg := &errgroup.Group{} | ||
c.resolveDepGraph(lockfileEg, pkg, pkg.UnresolvedExternalDeps, externalDepSet, seen, pkg) | ||
if err := lockfileEg.Wait(); err != nil { | ||
externalDeps, err := TransitiveClosure(pkg, c.Lockfile) | ||
if err != nil { | ||
warnings.append(err) | ||
// reset external deps to original state | ||
externalDepSet = mapset.NewSet() | ||
externalDeps = mapset.NewSet() | ||
} | ||
|
||
// when there are no internal dependencies, we need to still add these leafs to the graph | ||
if internalDepsSet.Len() == 0 { | ||
c.WorkspaceGraph.Connect(dag.BasicEdge(pkg.Name, core.ROOT_NODE_NAME)) | ||
} | ||
pkg.ExternalDeps = make([]string, 0, externalDepSet.Cardinality()) | ||
for _, v := range externalDepSet.ToSlice() { | ||
pkg.ExternalDeps = append(pkg.ExternalDeps, fmt.Sprintf("%v", v)) | ||
pkg.TransitiveDeps = make([]lockfile.Package, 0, externalDeps.Cardinality()) | ||
for _, dependency := range externalDeps.ToSlice() { | ||
dependency := dependency.(lockfile.Package) | ||
pkg.TransitiveDeps = append(pkg.TransitiveDeps, dependency) | ||
} | ||
pkg.InternalDeps = make([]string, 0, internalDepsSet.Len()) | ||
for _, v := range internalDepsSet.List() { | ||
pkg.InternalDeps = append(pkg.InternalDeps, fmt.Sprintf("%v", v)) | ||
} | ||
sort.Strings(pkg.InternalDeps) | ||
sort.Strings(pkg.ExternalDeps) | ||
hashOfExternalDeps, err := fs.HashObject(pkg.ExternalDeps) | ||
sort.Sort(lockfile.ByKey(pkg.TransitiveDeps)) | ||
hashOfExternalDeps, err := fs.HashObject(pkg.TransitiveDeps) | ||
if err != nil { | ||
return err | ||
} | ||
|
@@ -379,40 +373,50 @@ func (c *Context) parsePackageJSON(repoRoot turbopath.AbsoluteSystemPath, pkgJSO | |
return nil | ||
} | ||
|
||
func (c *Context) resolveDepGraph(wg *errgroup.Group, workspace *fs.PackageJSON, unresolvedDirectDeps map[string]string, resolvedDepsSet mapset.Set, seen mapset.Set, pkg *fs.PackageJSON) { | ||
if c.Lockfile == (lockfile.Lockfile)(nil) { | ||
return | ||
// TransitiveClosure the set of all lockfile keys that pkg depends on | ||
func TransitiveClosure(pkg *fs.PackageJSON, lockFile lockfile.Lockfile) (mapset.Set, error) { | ||
if lockfile.IsNil(lockFile) { | ||
return nil, fmt.Errorf("No lockfile available to do analysis on") | ||
} | ||
|
||
resolvedPkgs := mapset.NewSet() | ||
lockfileEg := &errgroup.Group{} | ||
|
||
transitiveClosureHelper(lockfileEg, pkg, lockFile, pkg.UnresolvedExternalDeps, resolvedPkgs) | ||
|
||
if err := lockfileEg.Wait(); err != nil { | ||
return nil, err | ||
} | ||
|
||
return resolvedPkgs, nil | ||
} | ||
|
||
func transitiveClosureHelper(wg *errgroup.Group, pkg *fs.PackageJSON, lockfile lockfile.Lockfile, unresolvedDirectDeps map[string]string, resolvedDeps mapset.Set) { | ||
for directDepName, unresolvedVersion := range unresolvedDirectDeps { | ||
directDepName := directDepName | ||
unresolvedVersion := unresolvedVersion | ||
wg.Go(func() error { | ||
|
||
lockfilePkg, err := c.Lockfile.ResolvePackage(workspace.Dir.ToUnixPath(), directDepName, unresolvedVersion) | ||
lockfilePkg, err := lockfile.ResolvePackage(pkg.Dir.ToUnixPath(), directDepName, unresolvedVersion) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
if !lockfilePkg.Found || seen.Contains(lockfilePkg.Key) { | ||
if !lockfilePkg.Found || resolvedDeps.Contains(lockfilePkg) { | ||
return nil | ||
} | ||
|
||
seen.Add(lockfilePkg.Key) | ||
|
||
pkg.Mu.Lock() | ||
pkg.TransitiveDeps = append(pkg.TransitiveDeps, lockfilePkg.Key) | ||
pkg.Mu.Unlock() | ||
resolvedDepsSet.Add(fmt.Sprintf("%s@%s", lockfilePkg.Key, lockfilePkg.Version)) | ||
resolvedDeps.Add(lockfilePkg) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How come we can get rid of the lock here? Is it because we're only mutating There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, we needed the lock because each goroutine that was spun up via the working group could try to append |
||
|
||
allDeps, ok := c.Lockfile.AllDependencies(lockfilePkg.Key) | ||
allDeps, ok := lockfile.AllDependencies(lockfilePkg.Key) | ||
|
||
if !ok { | ||
panic(fmt.Sprintf("Unable to find entry for %s", lockfilePkg.Key)) | ||
} | ||
|
||
if len(allDeps) > 0 { | ||
c.resolveDepGraph(wg, workspace, allDeps, resolvedDepsSet, seen, pkg) | ||
transitiveClosureHelper(wg, pkg, lockfile, allDeps, resolvedDeps) | ||
} | ||
|
||
return nil | ||
|
@@ -448,3 +452,62 @@ func (c *Context) InternalDependencies(start []string) ([]string, error) { | |
|
||
return targets, nil | ||
} | ||
|
||
// ChangedPackages returns a list of changed packages based on the contents of a previous lockfile | ||
// This assumes that none of the package.json in the workspace change, it is | ||
// the responsibility of the caller to verify this. | ||
func (c *Context) ChangedPackages(previousLockfile lockfile.Lockfile) ([]string, error) { | ||
if lockfile.IsNil(previousLockfile) || lockfile.IsNil(c.Lockfile) { | ||
return nil, fmt.Errorf("Cannot detect changed packages without previous and current lockfile") | ||
} | ||
|
||
didPackageChange := func(pkgName string, pkg *fs.PackageJSON) bool { | ||
previousDeps, err := TransitiveClosure(pkg, previousLockfile) | ||
if err != nil || previousDeps.Cardinality() != len(pkg.TransitiveDeps) { | ||
return true | ||
} | ||
|
||
prevExternalDeps := make([]lockfile.Package, 0, previousDeps.Cardinality()) | ||
for _, d := range previousDeps.ToSlice() { | ||
prevExternalDeps = append(prevExternalDeps, d.(lockfile.Package)) | ||
} | ||
sort.Sort(lockfile.ByKey(prevExternalDeps)) | ||
|
||
for i := range prevExternalDeps { | ||
if prevExternalDeps[i] != pkg.TransitiveDeps[i] { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
changedPkgs := make([]string, 0, len(c.WorkspaceInfos)) | ||
|
||
// check if prev and current have "global" changes e.g. lockfile bump | ||
globalChange := c.Lockfile.GlobalChange(previousLockfile) | ||
|
||
for pkgName, pkg := range c.WorkspaceInfos { | ||
if globalChange { | ||
break | ||
} | ||
if didPackageChange(pkgName, pkg) { | ||
if pkgName == util.RootPkgName { | ||
globalChange = true | ||
} else { | ||
changedPkgs = append(changedPkgs, pkgName) | ||
} | ||
} | ||
} | ||
|
||
if globalChange { | ||
changedPkgs = make([]string, 0, len(c.WorkspaceInfos)) | ||
for pkgName := range c.WorkspaceInfos { | ||
changedPkgs = append(changedPkgs, pkgName) | ||
} | ||
sort.Strings(changedPkgs) | ||
return changedPkgs, nil | ||
} | ||
|
||
sort.Strings(changedPkgs) | ||
return changedPkgs, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the purpose of these lines?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we don't copy the values here then it'll resolve as the same value in every iteration of the loop: https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable
I'm so excited for Rust