From dd84b9d64fb98746a230cd24233ff50a562c39c9 Mon Sep 17 00:00:00 2001 From: 简律纯 Date: Fri, 28 Apr 2023 01:36:44 +0800 Subject: --- cli/internal/globby/globby.go | 187 +++++++++ cli/internal/globby/globby_test.go | 832 +++++++++++++++++++++++++++++++++++++ 2 files changed, 1019 insertions(+) create mode 100644 cli/internal/globby/globby.go create mode 100644 cli/internal/globby/globby_test.go (limited to 'cli/internal/globby') diff --git a/cli/internal/globby/globby.go b/cli/internal/globby/globby.go new file mode 100644 index 0000000..14c40d9 --- /dev/null +++ b/cli/internal/globby/globby.go @@ -0,0 +1,187 @@ +package globby + +import ( + "fmt" + "path/filepath" + "sort" + "strings" + + iofs "io/fs" + + "github.com/vercel/turbo/cli/internal/fs" + + "github.com/vercel/turbo/cli/internal/doublestar" + "github.com/vercel/turbo/cli/internal/util" +) + +// GlobAll returns an array of files and folders that match the specified set of glob patterns. +// The returned files and folders are absolute paths, assuming that basePath is an absolute path. +func GlobAll(basePath string, includePatterns []string, excludePatterns []string) ([]string, error) { + fsys := fs.CreateDirFSAtRoot(basePath) + fsysRoot := fs.GetDirFSRootPath(fsys) + output, err := globAllFs(fsys, fsysRoot, basePath, includePatterns, excludePatterns) + + // Because this is coming out of a map output is in no way ordered. + // Sorting will put the files in a depth-first order. + sort.Strings(output) + return output, err +} + +// GlobFiles returns an array of files that match the specified set of glob patterns. +// The return files are absolute paths, assuming that basePath is an absolute path. +func GlobFiles(basePath string, includePatterns []string, excludePatterns []string) ([]string, error) { + fsys := fs.CreateDirFSAtRoot(basePath) + fsysRoot := fs.GetDirFSRootPath(fsys) + output, err := globFilesFs(fsys, fsysRoot, basePath, includePatterns, excludePatterns) + + // Because this is coming out of a map output is in no way ordered. + // Sorting will put the files in a depth-first order. + sort.Strings(output) + return output, err +} + +// checkRelativePath ensures that the the requested file path is a child of `from`. +func checkRelativePath(from string, to string) error { + relativePath, err := filepath.Rel(from, to) + + if err != nil { + return err + } + + if strings.HasPrefix(relativePath, "..") { + return fmt.Errorf("the path you are attempting to specify (%s) is outside of the root", to) + } + + return nil +} + +// globFilesFs searches the specified file system to enumerate all files to include. +func globFilesFs(fsys iofs.FS, fsysRoot string, basePath string, includePatterns []string, excludePatterns []string) ([]string, error) { + return globWalkFs(fsys, fsysRoot, basePath, includePatterns, excludePatterns, false) +} + +// globAllFs searches the specified file system to enumerate all files to include. +func globAllFs(fsys iofs.FS, fsysRoot string, basePath string, includePatterns []string, excludePatterns []string) ([]string, error) { + return globWalkFs(fsys, fsysRoot, basePath, includePatterns, excludePatterns, true) +} + +// globWalkFs searches the specified file system to enumerate all files and folders to include. +func globWalkFs(fsys iofs.FS, fsysRoot string, basePath string, includePatterns []string, excludePatterns []string, includeDirs bool) ([]string, error) { + var processedIncludes []string + var processedExcludes []string + result := make(util.Set) + + for _, includePattern := range includePatterns { + includePath := filepath.Join(basePath, includePattern) + err := checkRelativePath(basePath, includePath) + + if err != nil { + return nil, err + } + + // fs.FS paths may not include leading separators. Calculate the + // correct path for this relative to the filesystem root. + // This will not error as it follows the call to checkRelativePath. + iofsRelativePath, _ := fs.IofsRelativePath(fsysRoot, includePath) + + // Includes only operate on files. + processedIncludes = append(processedIncludes, iofsRelativePath) + } + + for _, excludePattern := range excludePatterns { + excludePath := filepath.Join(basePath, excludePattern) + err := checkRelativePath(basePath, excludePath) + + if err != nil { + return nil, err + } + + // fs.FS paths may not include leading separators. Calculate the + // correct path for this relative to the filesystem root. + // This will not error as it follows the call to checkRelativePath. + iofsRelativePath, _ := fs.IofsRelativePath(fsysRoot, excludePath) + + // In case this is a file pattern and not a directory, add the exact pattern. + // In the event that the user has already specified /**, + if !strings.HasSuffix(iofsRelativePath, string(filepath.Separator)+"**") { + processedExcludes = append(processedExcludes, iofsRelativePath) + } + // TODO: we need to either document or change this behavior + // Excludes operate on entire folders, so we also exclude everything under this in case it represents a directory + processedExcludes = append(processedExcludes, filepath.Join(iofsRelativePath, "**")) + } + + // We start from a naive includePattern + includePattern := "" + includeCount := len(processedIncludes) + + // Do not use alternation if unnecessary. + if includeCount == 1 { + includePattern = processedIncludes[0] + } else if includeCount > 1 { + // We use alternation from the very root of the path. This avoids fs.Stat of the basePath. + includePattern = "{" + strings.Join(processedIncludes, ",") + "}" + } + + // We start with an empty string excludePattern which we only use if excludeCount > 0. + excludePattern := "" + excludeCount := len(processedExcludes) + + // Do not use alternation if unnecessary. + if excludeCount == 1 { + excludePattern = processedExcludes[0] + } else if excludeCount > 1 { + // We use alternation from the very root of the path. This avoids fs.Stat of the basePath. + excludePattern = "{" + strings.Join(processedExcludes, ",") + "}" + } + + // GlobWalk expects that everything uses Unix path conventions. + includePattern = filepath.ToSlash(includePattern) + excludePattern = filepath.ToSlash(excludePattern) + + err := doublestar.GlobWalk(fsys, includePattern, func(path string, dirEntry iofs.DirEntry) error { + if !includeDirs && dirEntry.IsDir() { + return nil + } + + // All files that are returned by doublestar.GlobWalk are relative to + // the fsys root. Go, however, has decided that `fs.FS` filesystems do + // not address the root of the file system using `/` and instead use + // paths without leading separators. + // + // We need to track where the `fsys` root is so that when we hand paths back + // we hand them back as the path addressable in the actual OS filesystem. + // + // As a consequence, when processing, we need to *restore* the original + // root to the file path after returning. This works because when we create + // the `os.dirFS` filesystem we do so at the root of the current volume. + if excludeCount == 0 { + // Reconstruct via string concatenation since the root is already pre-composed. + result.Add(fsysRoot + path) + return nil + } + + isExcluded, err := doublestar.Match(excludePattern, filepath.ToSlash(path)) + if err != nil { + return err + } + + if !isExcluded { + // Reconstruct via string concatenation since the root is already pre-composed. + result.Add(fsysRoot + path) + } + + return nil + }) + + // GlobWalk threw an error. + if err != nil { + return nil, err + } + + // Never actually capture the root folder. + // This is a risk because of how we rework the globs. + result.Delete(strings.TrimSuffix(basePath, "/")) + + return result.UnsafeListOfStrings(), nil +} diff --git a/cli/internal/globby/globby_test.go b/cli/internal/globby/globby_test.go new file mode 100644 index 0000000..2fdd613 --- /dev/null +++ b/cli/internal/globby/globby_test.go @@ -0,0 +1,832 @@ +package globby + +import ( + "io/fs" + "path/filepath" + "reflect" + "sort" + "testing" + + "testing/fstest" +) + +// setup prepares the test file system contents and returns the file system. +func setup(fsysRoot string, files []string) fs.FS { + fsys := fstest.MapFS{} + for _, file := range files { + // We're populating a `fs.FS` filesytem which requires paths to have no + // leading slash. As a consequence we strip it during creation. + iofsRelativePath := file[1:] + + fsys[iofsRelativePath] = &fstest.MapFile{Mode: 0666} + } + + return fsys +} + +func TestGlobFilesFs(t *testing.T) { + type args struct { + basePath string + includePatterns []string + excludePatterns []string + } + tests := []struct { + name string + files []string + args args + wantAll []string + wantFiles []string + wantErr bool + }{ + { + name: "hello world", + files: []string{"/test.txt"}, + args: args{ + basePath: "/", + includePatterns: []string{"*.txt"}, + excludePatterns: []string{}, + }, + wantAll: []string{"/test.txt"}, + wantFiles: []string{"/test.txt"}, + }, + { + name: "bullet files", + files: []string{ + "/test.txt", + "/subdir/test.txt", + "/other/test.txt", + }, + args: args{ + basePath: "/", + includePatterns: []string{"subdir/test.txt", "test.txt"}, + excludePatterns: []string{}, + }, + wantAll: []string{ + "/subdir/test.txt", + "/test.txt", + }, + wantFiles: []string{ + "/subdir/test.txt", + "/test.txt", + }, + }, + { + name: "finding workspace package.json files", + files: []string{ + "/external/file.txt", + "/repos/some-app/apps/docs/package.json", + "/repos/some-app/apps/web/package.json", + "/repos/some-app/bower_components/readline/package.json", + "/repos/some-app/examples/package.json", + "/repos/some-app/node_modules/gulp/bower_components/readline/package.json", + "/repos/some-app/node_modules/react/package.json", + "/repos/some-app/package.json", + "/repos/some-app/packages/colors/package.json", + "/repos/some-app/packages/faker/package.json", + "/repos/some-app/packages/left-pad/package.json", + "/repos/some-app/test/mocks/kitchen-sink/package.json", + "/repos/some-app/tests/mocks/kitchen-sink/package.json", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"packages/*/package.json", "apps/*/package.json"}, + excludePatterns: []string{"**/node_modules/", "**/bower_components/", "**/test/", "**/tests/"}, + }, + wantAll: []string{ + "/repos/some-app/apps/docs/package.json", + "/repos/some-app/apps/web/package.json", + "/repos/some-app/packages/colors/package.json", + "/repos/some-app/packages/faker/package.json", + "/repos/some-app/packages/left-pad/package.json", + }, + wantFiles: []string{ + "/repos/some-app/apps/docs/package.json", + "/repos/some-app/apps/web/package.json", + "/repos/some-app/packages/colors/package.json", + "/repos/some-app/packages/faker/package.json", + "/repos/some-app/packages/left-pad/package.json", + }, + }, + { + name: "excludes unexpected workspace package.json files", + files: []string{ + "/external/file.txt", + "/repos/some-app/apps/docs/package.json", + "/repos/some-app/apps/web/package.json", + "/repos/some-app/bower_components/readline/package.json", + "/repos/some-app/examples/package.json", + "/repos/some-app/node_modules/gulp/bower_components/readline/package.json", + "/repos/some-app/node_modules/react/package.json", + "/repos/some-app/package.json", + "/repos/some-app/packages/colors/package.json", + "/repos/some-app/packages/faker/package.json", + "/repos/some-app/packages/left-pad/package.json", + "/repos/some-app/test/mocks/spanish-inquisition/package.json", + "/repos/some-app/tests/mocks/spanish-inquisition/package.json", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"**/package.json"}, + excludePatterns: []string{"**/node_modules/", "**/bower_components/", "**/test/", "**/tests/"}, + }, + wantAll: []string{ + "/repos/some-app/apps/docs/package.json", + "/repos/some-app/apps/web/package.json", + "/repos/some-app/examples/package.json", + "/repos/some-app/package.json", + "/repos/some-app/packages/colors/package.json", + "/repos/some-app/packages/faker/package.json", + "/repos/some-app/packages/left-pad/package.json", + }, + wantFiles: []string{ + "/repos/some-app/apps/docs/package.json", + "/repos/some-app/apps/web/package.json", + "/repos/some-app/examples/package.json", + "/repos/some-app/package.json", + "/repos/some-app/packages/colors/package.json", + "/repos/some-app/packages/faker/package.json", + "/repos/some-app/packages/left-pad/package.json", + }, + }, + { + name: "nested packages work", + files: []string{ + "/external/file.txt", + "/repos/some-app/apps/docs/package.json", + "/repos/some-app/apps/web/package.json", + "/repos/some-app/bower_components/readline/package.json", + "/repos/some-app/examples/package.json", + "/repos/some-app/node_modules/gulp/bower_components/readline/package.json", + "/repos/some-app/node_modules/react/package.json", + "/repos/some-app/package.json", + "/repos/some-app/packages/xzibit/package.json", + "/repos/some-app/packages/xzibit/node_modules/street-legal/package.json", + "/repos/some-app/packages/xzibit/node_modules/paint-colors/package.json", + "/repos/some-app/packages/xzibit/packages/yo-dawg/package.json", + "/repos/some-app/packages/xzibit/packages/yo-dawg/node_modules/meme/package.json", + "/repos/some-app/packages/xzibit/packages/yo-dawg/node_modules/yo-dawg/package.json", + "/repos/some-app/packages/colors/package.json", + "/repos/some-app/packages/faker/package.json", + "/repos/some-app/packages/left-pad/package.json", + "/repos/some-app/test/mocks/spanish-inquisition/package.json", + "/repos/some-app/tests/mocks/spanish-inquisition/package.json", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"packages/**/package.json"}, + excludePatterns: []string{"**/node_modules/", "**/bower_components/", "**/test/", "**/tests/"}, + }, + wantAll: []string{ + "/repos/some-app/packages/colors/package.json", + "/repos/some-app/packages/faker/package.json", + "/repos/some-app/packages/left-pad/package.json", + "/repos/some-app/packages/xzibit/package.json", + "/repos/some-app/packages/xzibit/packages/yo-dawg/package.json", + }, + wantFiles: []string{ + "/repos/some-app/packages/colors/package.json", + "/repos/some-app/packages/faker/package.json", + "/repos/some-app/packages/left-pad/package.json", + "/repos/some-app/packages/xzibit/package.json", + "/repos/some-app/packages/xzibit/packages/yo-dawg/package.json", + }, + }, + { + name: "includes do not override excludes", + files: []string{ + "/external/file.txt", + "/repos/some-app/apps/docs/package.json", + "/repos/some-app/apps/web/package.json", + "/repos/some-app/bower_components/readline/package.json", + "/repos/some-app/examples/package.json", + "/repos/some-app/node_modules/gulp/bower_components/readline/package.json", + "/repos/some-app/node_modules/react/package.json", + "/repos/some-app/package.json", + "/repos/some-app/packages/xzibit/package.json", + "/repos/some-app/packages/xzibit/node_modules/street-legal/package.json", + "/repos/some-app/packages/xzibit/node_modules/paint-colors/package.json", + "/repos/some-app/packages/xzibit/packages/yo-dawg/package.json", + "/repos/some-app/packages/xzibit/packages/yo-dawg/node_modules/meme/package.json", + "/repos/some-app/packages/xzibit/packages/yo-dawg/node_modules/yo-dawg/package.json", + "/repos/some-app/packages/colors/package.json", + "/repos/some-app/packages/faker/package.json", + "/repos/some-app/packages/left-pad/package.json", + "/repos/some-app/test/mocks/spanish-inquisition/package.json", + "/repos/some-app/tests/mocks/spanish-inquisition/package.json", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"packages/**/package.json", "tests/mocks/*/package.json"}, + excludePatterns: []string{"**/node_modules/", "**/bower_components/", "**/test/", "**/tests/"}, + }, + wantAll: []string{ + "/repos/some-app/packages/colors/package.json", + "/repos/some-app/packages/faker/package.json", + "/repos/some-app/packages/left-pad/package.json", + "/repos/some-app/packages/xzibit/package.json", + "/repos/some-app/packages/xzibit/packages/yo-dawg/package.json", + }, + wantFiles: []string{ + "/repos/some-app/packages/colors/package.json", + "/repos/some-app/packages/faker/package.json", + "/repos/some-app/packages/left-pad/package.json", + "/repos/some-app/packages/xzibit/package.json", + "/repos/some-app/packages/xzibit/packages/yo-dawg/package.json", + }, + }, + { + name: "output globbing grabs the desired content", + files: []string{ + "/external/file.txt", + "/repos/some-app/src/index.js", + "/repos/some-app/public/src/css/index.css", + "/repos/some-app/.turbo/turbo-build.log", + "/repos/some-app/.turbo/somebody-touched-this-file-into-existence.txt", + "/repos/some-app/.next/log.txt", + "/repos/some-app/.next/cache/db6a76a62043520e7aaadd0bb2104e78.txt", + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + "/repos/some-app/public/dist/css/index.css", + "/repos/some-app/public/dist/images/rick_astley.jpg", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{".turbo/turbo-build.log", "dist/**", ".next/**", "public/dist/**"}, + excludePatterns: []string{}, + }, + wantAll: []string{ + "/repos/some-app/.next", + "/repos/some-app/.next/cache", + "/repos/some-app/.next/cache/db6a76a62043520e7aaadd0bb2104e78.txt", + "/repos/some-app/.next/log.txt", + "/repos/some-app/.turbo/turbo-build.log", + "/repos/some-app/dist", + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules", + "/repos/some-app/dist/js/node_modules/browserify.js", + "/repos/some-app/public/dist", + "/repos/some-app/public/dist/css", + "/repos/some-app/public/dist/css/index.css", + "/repos/some-app/public/dist/images", + "/repos/some-app/public/dist/images/rick_astley.jpg", + }, + wantFiles: []string{ + "/repos/some-app/.next/cache/db6a76a62043520e7aaadd0bb2104e78.txt", + "/repos/some-app/.next/log.txt", + "/repos/some-app/.turbo/turbo-build.log", + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + "/repos/some-app/public/dist/css/index.css", + "/repos/some-app/public/dist/images/rick_astley.jpg", + }, + }, + { + name: "passing ** captures all children", + files: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"dist/**"}, + excludePatterns: []string{}, + }, + wantAll: []string{ + "/repos/some-app/dist", + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + wantFiles: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + }, + { + name: "passing just a directory captures no children", + files: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"dist"}, + excludePatterns: []string{}, + }, + wantAll: []string{"/repos/some-app/dist"}, + wantFiles: []string{}, + }, + { + name: "redundant includes do not duplicate", + files: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"**/*", "dist/**"}, + excludePatterns: []string{}, + }, + wantAll: []string{ + "/repos/some-app/dist", + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + wantFiles: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + }, + { + name: "exclude everything, include everything", + files: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"**"}, + excludePatterns: []string{"**"}, + }, + wantAll: []string{}, + wantFiles: []string{}, + }, + { + name: "passing just a directory to exclude prevents capture of children", + files: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"dist/**"}, + excludePatterns: []string{"dist/js"}, + }, + wantAll: []string{ + "/repos/some-app/dist", + "/repos/some-app/dist/index.html", + }, + wantFiles: []string{ + "/repos/some-app/dist/index.html", + }, + }, + { + name: "passing ** to exclude prevents capture of children", + files: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"dist/**"}, + excludePatterns: []string{"dist/js/**"}, + }, + wantAll: []string{ + "/repos/some-app/dist", + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js", + }, + wantFiles: []string{ + "/repos/some-app/dist/index.html", + }, + }, + { + name: "exclude everything with folder . applies at base path", + files: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"**"}, + excludePatterns: []string{"./"}, + }, + wantAll: []string{}, + wantFiles: []string{}, + }, + { + name: "exclude everything with traversal applies at a non-base path", + files: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"**"}, + excludePatterns: []string{"./dist"}, + }, + wantAll: []string{}, + wantFiles: []string{}, + }, + { + name: "exclude everything with folder traversal (..) applies at base path", + files: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"**"}, + excludePatterns: []string{"dist/../"}, + }, + wantAll: []string{}, + wantFiles: []string{}, + }, + { + name: "how do globs even work bad glob microformat", + files: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"**/**/**"}, + excludePatterns: []string{}, + }, + wantAll: []string{ + "/repos/some-app/dist", + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + wantFiles: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + }, + { + name: "directory traversal stops at base path", + files: []string{ + "/repos/spanish-inquisition/index.html", + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"../spanish-inquisition/**", "dist/**"}, + excludePatterns: []string{}, + }, + wantAll: []string{}, + wantFiles: []string{}, + wantErr: true, + }, + { + name: "globs and traversal and globs do not cross base path", + files: []string{ + "/repos/spanish-inquisition/index.html", + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"**/../../spanish-inquisition/**"}, + excludePatterns: []string{}, + }, + wantAll: []string{}, + wantFiles: []string{}, + wantErr: true, + }, + { + name: "traversal works within base path", + files: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"dist/js/../**"}, + excludePatterns: []string{}, + }, + wantAll: []string{ + "/repos/some-app/dist", + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + wantFiles: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + }, + { + name: "self-references (.) work", + files: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"dist/./././**"}, + excludePatterns: []string{}, + }, + wantAll: []string{ + "/repos/some-app/dist", + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + wantFiles: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + }, + { + name: "depth of 1 includes handles folders properly", + files: []string{ + "/repos/some-app/package.json", + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"*"}, + excludePatterns: []string{}, + }, + wantAll: []string{ + "/repos/some-app/dist", + "/repos/some-app/package.json", + }, + wantFiles: []string{"/repos/some-app/package.json"}, + }, + { + name: "depth of 1 excludes prevents capturing folders", + files: []string{ + "/repos/some-app/package.json", + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + args: args{ + basePath: "/repos/some-app/", + includePatterns: []string{"**"}, + excludePatterns: []string{"dist/*"}, + }, + wantAll: []string{ + "/repos/some-app/dist", + "/repos/some-app/package.json", + }, + wantFiles: []string{"/repos/some-app/package.json"}, + }, + { + name: "No-trailing slash basePath works", + files: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + args: args{ + basePath: "/repos/some-app", + includePatterns: []string{"dist/**"}, + excludePatterns: []string{}, + }, + wantAll: []string{ + "/repos/some-app/dist", + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + wantFiles: []string{ + "/repos/some-app/dist/index.html", + "/repos/some-app/dist/js/index.js", + "/repos/some-app/dist/js/lib.js", + "/repos/some-app/dist/js/node_modules/browserify.js", + }, + }, + { + name: "exclude single file", + files: []string{ + "/repos/some-app/included.txt", + "/repos/some-app/excluded.txt", + }, + args: args{ + basePath: "/repos/some-app", + includePatterns: []string{"*.txt"}, + excludePatterns: []string{"excluded.txt"}, + }, + wantAll: []string{ + "/repos/some-app/included.txt", + }, + wantFiles: []string{ + "/repos/some-app/included.txt", + }, + }, + { + name: "exclude nested single file", + files: []string{ + "/repos/some-app/one/included.txt", + "/repos/some-app/one/two/included.txt", + "/repos/some-app/one/two/three/included.txt", + "/repos/some-app/one/excluded.txt", + "/repos/some-app/one/two/excluded.txt", + "/repos/some-app/one/two/three/excluded.txt", + }, + args: args{ + basePath: "/repos/some-app", + includePatterns: []string{"**"}, + excludePatterns: []string{"**/excluded.txt"}, + }, + wantAll: []string{ + "/repos/some-app/one/included.txt", + "/repos/some-app/one/two/included.txt", + "/repos/some-app/one/two/three/included.txt", + "/repos/some-app/one", + "/repos/some-app/one/two", + "/repos/some-app/one/two/three", + }, + wantFiles: []string{ + "/repos/some-app/one/included.txt", + "/repos/some-app/one/two/included.txt", + "/repos/some-app/one/two/three/included.txt", + }, + }, + { + name: "exclude everything", + files: []string{ + "/repos/some-app/one/included.txt", + "/repos/some-app/one/two/included.txt", + "/repos/some-app/one/two/three/included.txt", + "/repos/some-app/one/excluded.txt", + "/repos/some-app/one/two/excluded.txt", + "/repos/some-app/one/two/three/excluded.txt", + }, + args: args{ + basePath: "/repos/some-app", + includePatterns: []string{"**"}, + excludePatterns: []string{"**"}, + }, + wantAll: []string{}, + wantFiles: []string{}, + }, + { + name: "exclude everything with slash", + files: []string{ + "/repos/some-app/one/included.txt", + "/repos/some-app/one/two/included.txt", + "/repos/some-app/one/two/three/included.txt", + "/repos/some-app/one/excluded.txt", + "/repos/some-app/one/two/excluded.txt", + "/repos/some-app/one/two/three/excluded.txt", + }, + args: args{ + basePath: "/repos/some-app", + includePatterns: []string{"**"}, + excludePatterns: []string{"**/"}, + }, + wantAll: []string{}, + wantFiles: []string{}, + }, + { + name: "exclude everything with leading **", + files: []string{ + "/repos/some-app/foo/bar", + "/repos/some-app/some-foo", + "/repos/some-app/some-foo/bar", + "/repos/some-app/included", + }, + args: args{ + basePath: "/repos/some-app", + includePatterns: []string{"**"}, + excludePatterns: []string{"**foo"}, + }, + wantAll: []string{ + "/repos/some-app/included", + }, + wantFiles: []string{ + "/repos/some-app/included", + }, + }, + { + name: "exclude everything with trailing **", + files: []string{ + "/repos/some-app/foo/bar", + "/repos/some-app/foo-file", + "/repos/some-app/foo-dir/bar", + "/repos/some-app/included", + }, + args: args{ + basePath: "/repos/some-app", + includePatterns: []string{"**"}, + excludePatterns: []string{"foo**"}, + }, + wantAll: []string{ + "/repos/some-app/included", + }, + wantFiles: []string{ + "/repos/some-app/included", + }, + }, + } + for _, tt := range tests { + fsysRoot := "/" + fsys := setup(fsysRoot, tt.files) + + t.Run(tt.name, func(t *testing.T) { + got, err := globFilesFs(fsys, fsysRoot, tt.args.basePath, tt.args.includePatterns, tt.args.excludePatterns) + + if (err != nil) != tt.wantErr { + t.Errorf("globFilesFs() error = %v, wantErr %v", err, tt.wantErr) + return + } + + gotToSlash := make([]string, len(got)) + for index, path := range got { + gotToSlash[index] = filepath.ToSlash(path) + } + + sort.Strings(gotToSlash) + + if !reflect.DeepEqual(gotToSlash, tt.wantFiles) { + t.Errorf("globFilesFs() = %v, want %v", gotToSlash, tt.wantFiles) + } + }) + + t.Run(tt.name, func(t *testing.T) { + got, err := globAllFs(fsys, fsysRoot, tt.args.basePath, tt.args.includePatterns, tt.args.excludePatterns) + + if (err != nil) != tt.wantErr { + t.Errorf("globAllFs() error = %v, wantErr %v", err, tt.wantErr) + return + } + + gotToSlash := make([]string, len(got)) + for index, path := range got { + gotToSlash[index] = filepath.ToSlash(path) + } + + sort.Strings(gotToSlash) + sort.Strings(tt.wantAll) + + if !reflect.DeepEqual(gotToSlash, tt.wantAll) { + t.Errorf("globAllFs() = %v, want %v", gotToSlash, tt.wantAll) + } + }) + } +} -- cgit v1.2.3-70-g09d2