aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/cli/internal/hashing
diff options
context:
space:
mode:
author简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:55 +0800
committer简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:55 +0800
commitfc8c5fdce62fb229202659408798a7b6c98f6e8b (patch)
tree7554f80e50de4af6fd255afa7c21bcdd58a7af34 /cli/internal/hashing
parentdd84b9d64fb98746a230cd24233ff50a562c39c9 (diff)
downloadHydroRoll-fc8c5fdce62fb229202659408798a7b6c98f6e8b.tar.gz
HydroRoll-fc8c5fdce62fb229202659408798a7b6c98f6e8b.zip
Diffstat (limited to 'cli/internal/hashing')
-rw-r--r--cli/internal/hashing/package_deps_hash.go461
-rw-r--r--cli/internal/hashing/package_deps_hash_test.go386
2 files changed, 0 insertions, 847 deletions
diff --git a/cli/internal/hashing/package_deps_hash.go b/cli/internal/hashing/package_deps_hash.go
deleted file mode 100644
index 517cddd..0000000
--- a/cli/internal/hashing/package_deps_hash.go
+++ /dev/null
@@ -1,461 +0,0 @@
-package hashing
-
-import (
- "bufio"
- "fmt"
- "io"
- "os/exec"
- "path/filepath"
- "strings"
- "sync"
-
- "github.com/pkg/errors"
- "github.com/vercel/turbo/cli/internal/encoding/gitoutput"
- "github.com/vercel/turbo/cli/internal/fs"
- "github.com/vercel/turbo/cli/internal/globby"
- "github.com/vercel/turbo/cli/internal/turbopath"
- "github.com/vercel/turbo/cli/internal/util"
-)
-
-// PackageDepsOptions are parameters for getting git hashes for a filesystem
-type PackageDepsOptions struct {
- // PackagePath is the folder path to derive the package dependencies from. This is typically the folder
- // containing package.json. If omitted, the default value is the current working directory.
- PackagePath turbopath.AnchoredSystemPath
-
- InputPatterns []string
-}
-
-// GetPackageDeps Builds an object containing git hashes for the files under the specified `packagePath` folder.
-func GetPackageDeps(rootPath turbopath.AbsoluteSystemPath, p *PackageDepsOptions) (map[turbopath.AnchoredUnixPath]string, error) {
- pkgPath := rootPath.UntypedJoin(p.PackagePath.ToStringDuringMigration())
- // Add all the checked in hashes.
- var result map[turbopath.AnchoredUnixPath]string
-
- // make a copy of the inputPatterns array, because we may be appending to it later.
- calculatedInputs := make([]string, len(p.InputPatterns))
- copy(calculatedInputs, p.InputPatterns)
-
- if len(calculatedInputs) == 0 {
- gitLsTreeOutput, err := gitLsTree(pkgPath)
- if err != nil {
- return nil, fmt.Errorf("could not get git hashes for files in package %s: %w", p.PackagePath, err)
- }
- result = gitLsTreeOutput
-
- // Update the checked in hashes with the current repo status
- // The paths returned from this call are anchored at the package directory
- gitStatusOutput, err := gitStatus(pkgPath, calculatedInputs)
- if err != nil {
- return nil, fmt.Errorf("Could not get git hashes from git status: %v", err)
- }
-
- var filesToHash []turbopath.AnchoredSystemPath
- for filePath, status := range gitStatusOutput {
- if status.isDelete() {
- delete(result, filePath)
- } else {
- filesToHash = append(filesToHash, filePath.ToSystemPath())
- }
- }
-
- hashes, err := gitHashObject(turbopath.AbsoluteSystemPathFromUpstream(pkgPath.ToString()), filesToHash)
- if err != nil {
- return nil, err
- }
-
- // Zip up file paths and hashes together
- for filePath, hash := range hashes {
- result[filePath] = hash
- }
- } else {
- // Add in package.json and turbo.json to input patterns. Both file paths are relative to pkgPath
- //
- // - package.json is an input because if the `scripts` in
- // the package.json change (i.e. the tasks that turbo executes), we want
- // a cache miss, since any existing cache could be invalid.
- // - turbo.json because it's the definition of the tasks themselves. The root turbo.json
- // is similarly included in the global hash. This file may not exist in the workspace, but
- // that is ok, because it will get ignored downstream.
- calculatedInputs = append(calculatedInputs, "package.json")
- calculatedInputs = append(calculatedInputs, "turbo.json")
-
- // The input patterns are relative to the package.
- // However, we need to change the globbing to be relative to the repo root.
- // Prepend the package path to each of the input patterns.
- prefixedInputPatterns := []string{}
- prefixedExcludePatterns := []string{}
- for _, pattern := range calculatedInputs {
- if len(pattern) > 0 && pattern[0] == '!' {
- rerooted, err := rootPath.PathTo(pkgPath.UntypedJoin(pattern[1:]))
- if err != nil {
- return nil, err
- }
- prefixedExcludePatterns = append(prefixedExcludePatterns, rerooted)
- } else {
- rerooted, err := rootPath.PathTo(pkgPath.UntypedJoin(pattern))
- if err != nil {
- return nil, err
- }
- prefixedInputPatterns = append(prefixedInputPatterns, rerooted)
- }
- }
- absoluteFilesToHash, err := globby.GlobFiles(rootPath.ToStringDuringMigration(), prefixedInputPatterns, prefixedExcludePatterns)
-
- if err != nil {
- return nil, errors.Wrapf(err, "failed to resolve input globs %v", calculatedInputs)
- }
-
- filesToHash := make([]turbopath.AnchoredSystemPath, len(absoluteFilesToHash))
- for i, rawPath := range absoluteFilesToHash {
- relativePathString, err := pkgPath.RelativePathString(rawPath)
-
- if err != nil {
- return nil, errors.Wrapf(err, "not relative to package: %v", rawPath)
- }
-
- filesToHash[i] = turbopath.AnchoredSystemPathFromUpstream(relativePathString)
- }
-
- hashes, err := gitHashObject(turbopath.AbsoluteSystemPathFromUpstream(pkgPath.ToStringDuringMigration()), filesToHash)
- if err != nil {
- return nil, errors.Wrap(err, "failed hashing resolved inputs globs")
- }
- result = hashes
- // Note that in this scenario, we don't need to check git status, we're using hash-object directly which
- // hashes the current state, not state at a commit
- }
-
- return result, nil
-}
-
-func manuallyHashFiles(rootPath turbopath.AbsoluteSystemPath, files []turbopath.AnchoredSystemPath) (map[turbopath.AnchoredUnixPath]string, error) {
- hashObject := make(map[turbopath.AnchoredUnixPath]string)
- for _, file := range files {
- hash, err := fs.GitLikeHashFile(file.ToString())
- if err != nil {
- return nil, fmt.Errorf("could not hash file %v. \n%w", file.ToString(), err)
- }
-
- hashObject[file.ToUnixPath()] = hash
- }
- return hashObject, nil
-}
-
-// GetHashableDeps hashes the list of given files, then returns a map of normalized path to hash
-// this map is suitable for cross-platform caching.
-func GetHashableDeps(rootPath turbopath.AbsoluteSystemPath, files []turbopath.AbsoluteSystemPath) (map[turbopath.AnchoredUnixPath]string, error) {
- output := make([]turbopath.AnchoredSystemPath, len(files))
- convertedRootPath := turbopath.AbsoluteSystemPathFromUpstream(rootPath.ToString())
-
- for index, file := range files {
- anchoredSystemPath, err := file.RelativeTo(convertedRootPath)
- if err != nil {
- return nil, err
- }
- output[index] = anchoredSystemPath
- }
- hashObject, err := gitHashObject(convertedRootPath, output)
- if err != nil {
- manuallyHashedObject, err := manuallyHashFiles(convertedRootPath, output)
- if err != nil {
- return nil, err
- }
- hashObject = manuallyHashedObject
- }
-
- return hashObject, nil
-}
-
-// gitHashObject returns a map of paths to their SHA hashes calculated by passing the paths to `git hash-object`.
-// `git hash-object` expects paths to use Unix separators, even on Windows.
-//
-// Note: paths of files to hash passed to `git hash-object` are processed as relative to the given anchor.
-// For that reason we convert all input paths and make them relative to the anchor prior to passing them
-// to `git hash-object`.
-func gitHashObject(anchor turbopath.AbsoluteSystemPath, filesToHash []turbopath.AnchoredSystemPath) (map[turbopath.AnchoredUnixPath]string, error) {
- fileCount := len(filesToHash)
- output := make(map[turbopath.AnchoredUnixPath]string, fileCount)
-
- if fileCount > 0 {
- cmd := exec.Command(
- "git", // Using `git` from $PATH,
- "hash-object", // hash a file,
- "--stdin-paths", // using a list of newline-separated paths from stdin.
- )
- cmd.Dir = anchor.ToString() // Start at this directory.
-
- // The functionality for gitHashObject is different enough that it isn't reasonable to
- // generalize the behavior for `runGitCmd`. In fact, it doesn't even use the `gitoutput`
- // encoding library, instead relying on its own separate `bufio.Scanner`.
-
- // We're going to send the list of files in via `stdin`, so we grab that pipe.
- // This prevents a huge number of encoding issues and shell compatibility issues
- // before they even start.
- stdinPipe, stdinPipeError := cmd.StdinPipe()
- if stdinPipeError != nil {
- return nil, stdinPipeError
- }
-
- // Kick the processing off in a goroutine so while that is doing its thing we can go ahead
- // and wire up the consumer of `stdout`.
- go func() {
- defer util.CloseAndIgnoreError(stdinPipe)
-
- // `git hash-object` understands all relative paths to be relative to the repository.
- // This function's result needs to be relative to `rootPath`.
- // We convert all files to absolute paths and assume that they will be inside of the repository.
- for _, file := range filesToHash {
- converted := file.RestoreAnchor(anchor)
-
- // `git hash-object` expects paths to use Unix separators, even on Windows.
- // `git hash-object` expects paths to be one per line so we must escape newlines.
- // In order to understand the escapes, the path must be quoted.
- // In order to quote the path, the quotes in the path must be escaped.
- // Other than that, we just write everything with full Unicode.
- stringPath := converted.ToString()
- toSlashed := filepath.ToSlash(stringPath)
- escapedNewLines := strings.ReplaceAll(toSlashed, "\n", "\\n")
- escapedQuotes := strings.ReplaceAll(escapedNewLines, "\"", "\\\"")
- prepared := fmt.Sprintf("\"%s\"\n", escapedQuotes)
- _, err := io.WriteString(stdinPipe, prepared)
- if err != nil {
- return
- }
- }
- }()
-
- // This gives us an io.ReadCloser so that we never have to read the entire input in
- // at a single time. It is doing stream processing instead of string processing.
- stdoutPipe, stdoutPipeError := cmd.StdoutPipe()
- if stdoutPipeError != nil {
- return nil, fmt.Errorf("failed to read `git hash-object`: %w", stdoutPipeError)
- }
-
- startError := cmd.Start()
- if startError != nil {
- return nil, fmt.Errorf("failed to read `git hash-object`: %w", startError)
- }
-
- // The output of `git hash-object` is a 40-character SHA per input, then a newline.
- // We need to track the SHA that corresponds to the input file path.
- index := 0
- hashes := make([]string, len(filesToHash))
- scanner := bufio.NewScanner(stdoutPipe)
-
- // Read the output line-by-line (which is our separator) until exhausted.
- for scanner.Scan() {
- bytes := scanner.Bytes()
-
- scanError := scanner.Err()
- if scanError != nil {
- return nil, fmt.Errorf("failed to read `git hash-object`: %w", scanError)
- }
-
- hashError := gitoutput.CheckObjectName(bytes)
- if hashError != nil {
- return nil, fmt.Errorf("failed to read `git hash-object`: %s", "invalid hash received")
- }
-
- // Worked, save it off.
- hashes[index] = string(bytes)
- index++
- }
-
- // Waits until stdout is closed before proceeding.
- waitErr := cmd.Wait()
- if waitErr != nil {
- return nil, fmt.Errorf("failed to read `git hash-object`: %w", waitErr)
- }
-
- // Make sure we end up with a matching number of files and hashes.
- hashCount := len(hashes)
- if fileCount != hashCount {
- return nil, fmt.Errorf("failed to read `git hash-object`: %d files %d hashes", fileCount, hashCount)
- }
-
- // The API of this method specifies that we return a `map[turbopath.AnchoredUnixPath]string`.
- for i, hash := range hashes {
- filePath := filesToHash[i]
- output[filePath.ToUnixPath()] = hash
- }
- }
-
- return output, nil
-}
-
-// runGitCommand provides boilerplate command handling for `ls-tree`, `ls-files`, and `status`
-// Rather than doing string processing, it does stream processing of `stdout`.
-func runGitCommand(cmd *exec.Cmd, commandName string, handler func(io.Reader) *gitoutput.Reader) ([][]string, error) {
- stdoutPipe, pipeError := cmd.StdoutPipe()
- if pipeError != nil {
- return nil, fmt.Errorf("failed to read `git %s`: %w", commandName, pipeError)
- }
-
- startError := cmd.Start()
- if startError != nil {
- return nil, fmt.Errorf("failed to read `git %s`: %w", commandName, startError)
- }
-
- reader := handler(stdoutPipe)
- entries, readErr := reader.ReadAll()
- if readErr != nil {
- return nil, fmt.Errorf("failed to read `git %s`: %w", commandName, readErr)
- }
-
- waitErr := cmd.Wait()
- if waitErr != nil {
- return nil, fmt.Errorf("failed to read `git %s`: %w", commandName, waitErr)
- }
-
- return entries, nil
-}
-
-// gitLsTree returns a map of paths to their SHA hashes starting at a particular directory
-// that are present in the `git` index at a particular revision.
-func gitLsTree(rootPath turbopath.AbsoluteSystemPath) (map[turbopath.AnchoredUnixPath]string, error) {
- cmd := exec.Command(
- "git", // Using `git` from $PATH,
- "ls-tree", // list the contents of the git index,
- "-r", // recursively,
- "-z", // with each file path relative to the invocation directory and \000-terminated,
- "HEAD", // at this specified version.
- )
- cmd.Dir = rootPath.ToString() // Include files only from this directory.
-
- entries, err := runGitCommand(cmd, "ls-tree", gitoutput.NewLSTreeReader)
- if err != nil {
- return nil, err
- }
-
- output := make(map[turbopath.AnchoredUnixPath]string, len(entries))
-
- for _, entry := range entries {
- lsTreeEntry := gitoutput.LsTreeEntry(entry)
- output[turbopath.AnchoredUnixPathFromUpstream(lsTreeEntry.GetField(gitoutput.Path))] = lsTreeEntry[2]
- }
-
- return output, nil
-}
-
-// getTraversePath gets the distance of the current working directory to the repository root.
-// This is used to convert repo-relative paths to cwd-relative paths.
-//
-// `git rev-parse --show-cdup` always returns Unix paths, even on Windows.
-func getTraversePath(rootPath turbopath.AbsoluteSystemPath) (turbopath.RelativeUnixPath, error) {
- cmd := exec.Command("git", "rev-parse", "--show-cdup")
- cmd.Dir = rootPath.ToString()
-
- traversePath, err := cmd.Output()
- if err != nil {
- return "", err
- }
-
- trimmedTraversePath := strings.TrimSuffix(string(traversePath), "\n")
-
- return turbopath.RelativeUnixPathFromUpstream(trimmedTraversePath), nil
-}
-
-// Don't shell out if we already know where you are in the repository.
-// `memoize` is a good candidate for generics.
-func memoizeGetTraversePath() func(turbopath.AbsoluteSystemPath) (turbopath.RelativeUnixPath, error) {
- cacheMutex := &sync.RWMutex{}
- cachedResult := map[turbopath.AbsoluteSystemPath]turbopath.RelativeUnixPath{}
- cachedError := map[turbopath.AbsoluteSystemPath]error{}
-
- return func(rootPath turbopath.AbsoluteSystemPath) (turbopath.RelativeUnixPath, error) {
- cacheMutex.RLock()
- result, resultExists := cachedResult[rootPath]
- err, errExists := cachedError[rootPath]
- cacheMutex.RUnlock()
-
- if resultExists && errExists {
- return result, err
- }
-
- invokedResult, invokedErr := getTraversePath(rootPath)
- cacheMutex.Lock()
- cachedResult[rootPath] = invokedResult
- cachedError[rootPath] = invokedErr
- cacheMutex.Unlock()
-
- return invokedResult, invokedErr
- }
-}
-
-var memoizedGetTraversePath = memoizeGetTraversePath()
-
-// statusCode represents the two-letter status code from `git status` with two "named" fields, x & y.
-// They have different meanings based upon the actual state of the working tree. Using x & y maps
-// to upstream behavior.
-type statusCode struct {
- x string
- y string
-}
-
-func (s statusCode) isDelete() bool {
- return s.x == "D" || s.y == "D"
-}
-
-// gitStatus returns a map of paths to their `git` status code. This can be used to identify what should
-// be done with files that do not currently match what is in the index.
-//
-// Note: `git status -z`'s relative path results are relative to the repository's location.
-// We need to calculate where the repository's location is in order to determine what the full path is
-// before we can return those paths relative to the calling directory, normalizing to the behavior of
-// `ls-files` and `ls-tree`.
-func gitStatus(rootPath turbopath.AbsoluteSystemPath, patterns []string) (map[turbopath.AnchoredUnixPath]statusCode, error) {
- cmd := exec.Command(
- "git", // Using `git` from $PATH,
- "status", // tell me about the status of the working tree,
- "--untracked-files", // including information about untracked files,
- "--no-renames", // do not detect renames,
- "-z", // with each file path relative to the repository root and \000-terminated,
- "--", // and any additional argument you see is a path, promise.
- )
- if len(patterns) == 0 {
- cmd.Args = append(cmd.Args, ".") // Operate in the current directory instead of the root of the working tree.
- } else {
- // FIXME: Globbing is using `git`'s globbing rules which are not consistent with `doublestar``.
- cmd.Args = append(cmd.Args, patterns...) // Pass in input patterns as arguments.
- }
- cmd.Dir = rootPath.ToString() // Include files only from this directory.
-
- entries, err := runGitCommand(cmd, "status", gitoutput.NewStatusReader)
- if err != nil {
- return nil, err
- }
-
- output := make(map[turbopath.AnchoredUnixPath]statusCode, len(entries))
- convertedRootPath := turbopath.AbsoluteSystemPathFromUpstream(rootPath.ToString())
-
- traversePath, err := memoizedGetTraversePath(convertedRootPath)
- if err != nil {
- return nil, err
- }
-
- for _, entry := range entries {
- statusEntry := gitoutput.StatusEntry(entry)
- // Anchored at repository.
- pathFromStatus := turbopath.AnchoredUnixPathFromUpstream(statusEntry.GetField(gitoutput.Path))
- var outputPath turbopath.AnchoredUnixPath
-
- if len(traversePath) > 0 {
- repositoryPath := convertedRootPath.Join(traversePath.ToSystemPath())
- fileFullPath := pathFromStatus.ToSystemPath().RestoreAnchor(repositoryPath)
-
- relativePath, err := fileFullPath.RelativeTo(convertedRootPath)
- if err != nil {
- return nil, err
- }
-
- outputPath = relativePath.ToUnixPath()
- } else {
- outputPath = pathFromStatus
- }
-
- output[outputPath] = statusCode{x: statusEntry.GetField(gitoutput.StatusX), y: statusEntry.GetField(gitoutput.StatusY)}
- }
-
- return output, nil
-}
diff --git a/cli/internal/hashing/package_deps_hash_test.go b/cli/internal/hashing/package_deps_hash_test.go
deleted file mode 100644
index 8f68d38..0000000
--- a/cli/internal/hashing/package_deps_hash_test.go
+++ /dev/null
@@ -1,386 +0,0 @@
-package hashing
-
-import (
- "errors"
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "reflect"
- "runtime"
- "strings"
- "testing"
-
- "github.com/vercel/turbo/cli/internal/fs"
- "github.com/vercel/turbo/cli/internal/turbopath"
- "gotest.tools/v3/assert"
-)
-
-func getFixture(id int) turbopath.AbsoluteSystemPath {
- cwd, _ := os.Getwd()
- root := turbopath.AbsoluteSystemPath(filepath.VolumeName(cwd) + string(os.PathSeparator))
- checking := turbopath.AbsoluteSystemPath(cwd)
-
- for checking != root {
- fixtureDirectory := checking.Join("fixtures")
- _, err := os.Stat(fixtureDirectory.ToString())
- if !errors.Is(err, os.ErrNotExist) {
- // Found the fixture directory!
- files, _ := os.ReadDir(fixtureDirectory.ToString())
-
- // Grab the specified fixture.
- for _, file := range files {
- fileName := turbopath.RelativeSystemPath(file.Name())
- if strings.Index(fileName.ToString(), fmt.Sprintf("%02d-", id)) == 0 {
- return turbopath.AbsoluteSystemPath(fixtureDirectory.Join(fileName))
- }
- }
- }
- checking = checking.Join("..")
- }
-
- panic("fixtures not found!")
-}
-
-func TestSpecialCharacters(t *testing.T) {
- if runtime.GOOS == "windows" {
- return
- }
-
- fixturePath := getFixture(1)
- newlinePath := turbopath.AnchoredUnixPath("new\nline").ToSystemPath()
- quotePath := turbopath.AnchoredUnixPath("\"quote\"").ToSystemPath()
- newline := newlinePath.RestoreAnchor(fixturePath)
- quote := quotePath.RestoreAnchor(fixturePath)
-
- // Setup
- one := os.WriteFile(newline.ToString(), []byte{}, 0644)
- two := os.WriteFile(quote.ToString(), []byte{}, 0644)
-
- // Cleanup
- defer func() {
- one := os.Remove(newline.ToString())
- two := os.Remove(quote.ToString())
-
- if one != nil || two != nil {
- return
- }
- }()
-
- // Setup error check
- if one != nil || two != nil {
- return
- }
-
- tests := []struct {
- name string
- rootPath turbopath.AbsoluteSystemPath
- filesToHash []turbopath.AnchoredSystemPath
- want map[turbopath.AnchoredUnixPath]string
- wantErr bool
- }{
- {
- name: "Quotes",
- rootPath: fixturePath,
- filesToHash: []turbopath.AnchoredSystemPath{
- quotePath,
- },
- want: map[turbopath.AnchoredUnixPath]string{
- quotePath.ToUnixPath(): "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
- },
- },
- {
- name: "Newlines",
- rootPath: fixturePath,
- filesToHash: []turbopath.AnchoredSystemPath{
- newlinePath,
- },
- want: map[turbopath.AnchoredUnixPath]string{
- newlinePath.ToUnixPath(): "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, err := gitHashObject(tt.rootPath, tt.filesToHash)
- if (err != nil) != tt.wantErr {
- t.Errorf("gitHashObject() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if !reflect.DeepEqual(got, tt.want) {
- t.Errorf("gitHashObject() = %v, want %v", got, tt.want)
- }
- })
- }
-}
-
-func Test_gitHashObject(t *testing.T) {
- fixturePath := getFixture(1)
- traversePath, err := getTraversePath(fixturePath)
- if err != nil {
- return
- }
-
- tests := []struct {
- name string
- rootPath turbopath.AbsoluteSystemPath
- filesToHash []turbopath.AnchoredSystemPath
- want map[turbopath.AnchoredUnixPath]string
- wantErr bool
- }{
- {
- name: "No paths",
- rootPath: fixturePath,
- filesToHash: []turbopath.AnchoredSystemPath{},
- want: map[turbopath.AnchoredUnixPath]string{},
- },
- {
- name: "Absolute paths come back relative to rootPath",
- rootPath: fixturePath.Join("child"),
- filesToHash: []turbopath.AnchoredSystemPath{
- turbopath.AnchoredUnixPath("../root.json").ToSystemPath(),
- turbopath.AnchoredUnixPath("child.json").ToSystemPath(),
- turbopath.AnchoredUnixPath("grandchild/grandchild.json").ToSystemPath(),
- },
- want: map[turbopath.AnchoredUnixPath]string{
- "../root.json": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
- "child.json": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
- "grandchild/grandchild.json": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
- },
- },
- {
- name: "Traverse outside of the repo",
- rootPath: fixturePath.Join(traversePath.ToSystemPath(), ".."),
- filesToHash: []turbopath.AnchoredSystemPath{
- turbopath.AnchoredUnixPath("null.json").ToSystemPath(),
- },
- want: nil,
- wantErr: true,
- },
- {
- name: "Nonexistent file",
- rootPath: fixturePath,
- filesToHash: []turbopath.AnchoredSystemPath{
- turbopath.AnchoredUnixPath("nonexistent.json").ToSystemPath(),
- },
- want: nil,
- wantErr: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, err := gitHashObject(tt.rootPath, tt.filesToHash)
- if (err != nil) != tt.wantErr {
- t.Errorf("gitHashObject() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if !reflect.DeepEqual(got, tt.want) {
- t.Errorf("gitHashObject() = %v, want %v", got, tt.want)
- }
- })
- }
-}
-
-func Test_getTraversePath(t *testing.T) {
- fixturePath := getFixture(1)
-
- tests := []struct {
- name string
- rootPath turbopath.AbsoluteSystemPath
- want turbopath.RelativeUnixPath
- wantErr bool
- }{
- {
- name: "From fixture location",
- rootPath: fixturePath,
- want: turbopath.RelativeUnixPath("../../../"),
- wantErr: false,
- },
- {
- name: "Traverse out of git repo",
- rootPath: fixturePath.UntypedJoin("..", "..", "..", ".."),
- want: "",
- wantErr: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, err := getTraversePath(tt.rootPath)
- if (err != nil) != tt.wantErr {
- t.Errorf("getTraversePath() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if !reflect.DeepEqual(got, tt.want) {
- t.Errorf("getTraversePath() = %v, want %v", got, tt.want)
- }
- })
- }
-}
-
-func requireGitCmd(t *testing.T, repoRoot turbopath.AbsoluteSystemPath, args ...string) {
- t.Helper()
- cmd := exec.Command("git", args...)
- cmd.Dir = repoRoot.ToString()
- out, err := cmd.CombinedOutput()
- if err != nil {
- t.Fatalf("git commit failed: %v %v", err, string(out))
- }
-}
-
-func TestGetPackageDeps(t *testing.T) {
- // Directory structure:
- // <root>/
- // new-root-file <- new file not added to git
- // my-pkg/
- // committed-file
- // deleted-file
- // uncommitted-file <- new file not added to git
- // dir/
- // nested-file
-
- repoRoot := fs.AbsoluteSystemPathFromUpstream(t.TempDir())
- myPkgDir := repoRoot.UntypedJoin("my-pkg")
-
- // create the dir first
- err := myPkgDir.MkdirAll(0775)
- assert.NilError(t, err, "CreateDir")
-
- // create file 1
- committedFilePath := myPkgDir.UntypedJoin("committed-file")
- err = committedFilePath.WriteFile([]byte("committed bytes"), 0644)
- assert.NilError(t, err, "WriteFile")
-
- // create file 2
- deletedFilePath := myPkgDir.UntypedJoin("deleted-file")
- err = deletedFilePath.WriteFile([]byte("delete-me"), 0644)
- assert.NilError(t, err, "WriteFile")
-
- // create file 3
- nestedPath := myPkgDir.UntypedJoin("dir", "nested-file")
- assert.NilError(t, nestedPath.EnsureDir(), "EnsureDir")
- assert.NilError(t, nestedPath.WriteFile([]byte("nested"), 0644), "WriteFile")
-
- // create a package.json
- packageJSONPath := myPkgDir.UntypedJoin("package.json")
- err = packageJSONPath.WriteFile([]byte("{}"), 0644)
- assert.NilError(t, err, "WriteFile")
-
- // set up git repo and commit all
- requireGitCmd(t, repoRoot, "init", ".")
- requireGitCmd(t, repoRoot, "config", "--local", "user.name", "test")
- requireGitCmd(t, repoRoot, "config", "--local", "user.email", "test@example.com")
- requireGitCmd(t, repoRoot, "add", ".")
- requireGitCmd(t, repoRoot, "commit", "-m", "foo")
-
- // remove a file
- err = deletedFilePath.Remove()
- assert.NilError(t, err, "Remove")
-
- // create another untracked file in git
- uncommittedFilePath := myPkgDir.UntypedJoin("uncommitted-file")
- err = uncommittedFilePath.WriteFile([]byte("uncommitted bytes"), 0644)
- assert.NilError(t, err, "WriteFile")
-
- // create an untracked file in git up a level
- rootFilePath := repoRoot.UntypedJoin("new-root-file")
- err = rootFilePath.WriteFile([]byte("new-root bytes"), 0644)
- assert.NilError(t, err, "WriteFile")
-
- tests := []struct {
- opts *PackageDepsOptions
- expected map[turbopath.AnchoredUnixPath]string
- }{
- // base case. when inputs aren't specified, all files hashes are computed
- {
- opts: &PackageDepsOptions{
- PackagePath: "my-pkg",
- },
- expected: map[turbopath.AnchoredUnixPath]string{
- "committed-file": "3a29e62ea9ba15c4a4009d1f605d391cdd262033",
- "uncommitted-file": "4e56ad89387e6379e4e91ddfe9872cf6a72c9976",
- "package.json": "9e26dfeeb6e641a33dae4961196235bdb965b21b",
- "dir/nested-file": "bfe53d766e64d78f80050b73cd1c88095bc70abb",
- },
- },
- // with inputs, only the specified inputs are hashed
- {
- opts: &PackageDepsOptions{
- PackagePath: "my-pkg",
- InputPatterns: []string{"uncommitted-file"},
- },
- expected: map[turbopath.AnchoredUnixPath]string{
- "package.json": "9e26dfeeb6e641a33dae4961196235bdb965b21b",
- "uncommitted-file": "4e56ad89387e6379e4e91ddfe9872cf6a72c9976",
- },
- },
- // inputs with glob pattern also works
- {
- opts: &PackageDepsOptions{
- PackagePath: "my-pkg",
- InputPatterns: []string{"**/*-file"},
- },
- expected: map[turbopath.AnchoredUnixPath]string{
- "committed-file": "3a29e62ea9ba15c4a4009d1f605d391cdd262033",
- "uncommitted-file": "4e56ad89387e6379e4e91ddfe9872cf6a72c9976",
- "package.json": "9e26dfeeb6e641a33dae4961196235bdb965b21b",
- "dir/nested-file": "bfe53d766e64d78f80050b73cd1c88095bc70abb",
- },
- },
- // inputs with traversal work
- {
- opts: &PackageDepsOptions{
- PackagePath: "my-pkg",
- InputPatterns: []string{"../**/*-file"},
- },
- expected: map[turbopath.AnchoredUnixPath]string{
- "../new-root-file": "8906ddcdd634706188bd8ef1c98ac07b9be3425e",
- "committed-file": "3a29e62ea9ba15c4a4009d1f605d391cdd262033",
- "uncommitted-file": "4e56ad89387e6379e4e91ddfe9872cf6a72c9976",
- "package.json": "9e26dfeeb6e641a33dae4961196235bdb965b21b",
- "dir/nested-file": "bfe53d766e64d78f80050b73cd1c88095bc70abb",
- },
- },
- // inputs with another glob pattern works
- {
- opts: &PackageDepsOptions{
- PackagePath: "my-pkg",
- InputPatterns: []string{"**/{uncommitted,committed}-file"},
- },
- expected: map[turbopath.AnchoredUnixPath]string{
- "committed-file": "3a29e62ea9ba15c4a4009d1f605d391cdd262033",
- "package.json": "9e26dfeeb6e641a33dae4961196235bdb965b21b",
- "uncommitted-file": "4e56ad89387e6379e4e91ddfe9872cf6a72c9976",
- },
- },
- // inputs with another glob pattern + traversal work
- {
- opts: &PackageDepsOptions{
- PackagePath: "my-pkg",
- InputPatterns: []string{"../**/{new-root,uncommitted,committed}-file"},
- },
- expected: map[turbopath.AnchoredUnixPath]string{
- "../new-root-file": "8906ddcdd634706188bd8ef1c98ac07b9be3425e",
- "committed-file": "3a29e62ea9ba15c4a4009d1f605d391cdd262033",
- "package.json": "9e26dfeeb6e641a33dae4961196235bdb965b21b",
- "uncommitted-file": "4e56ad89387e6379e4e91ddfe9872cf6a72c9976",
- },
- },
- }
- for _, tt := range tests {
- got, err := GetPackageDeps(repoRoot, tt.opts)
- if err != nil {
- t.Errorf("GetPackageDeps got error %v", err)
- continue
- }
- assert.DeepEqual(t, got, tt.expected)
- }
-}
-
-func Test_memoizedGetTraversePath(t *testing.T) {
- fixturePath := getFixture(1)
-
- gotOne, _ := memoizedGetTraversePath(fixturePath)
- gotTwo, _ := memoizedGetTraversePath(fixturePath)
-
- assert.Check(t, gotOne == gotTwo, "The strings are identical.")
-}