From 4133bae2e15da303318a849aee9d23d02022fd80 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 12 Oct 2015 16:16:01 -0700 Subject: [PATCH] copy from terraform --- copy_dir.go | 76 ++++ detect.go | 92 +++++ detect_bitbucket.go | 66 +++ detect_bitbucket_test.go | 67 +++ detect_file.go | 60 +++ detect_file_test.go | 88 ++++ detect_github.go | 73 ++++ detect_github_test.go | 55 +++ detect_test.go | 51 +++ folder_storage.go | 65 +++ folder_storage_test.go | 48 +++ get.go | 207 ++++++++++ get_file.go | 46 +++ get_file_test.go | 104 +++++ get_git.go | 74 ++++ get_git_test.go | 143 +++++++ get_hg.go | 89 ++++ get_hg_test.go | 81 ++++ get_http.go | 173 ++++++++ get_http_test.go | 155 +++++++ get_test.go | 128 ++++++ module.go | 7 + module_test.go | 57 +++ storage.go | 25 ++ test-fixtures/basic-dot/.test/foo.tf | 1 + test-fixtures/basic-dot/main.tf | 5 + test-fixtures/basic-git/DOTgit/COMMIT_EDITMSG | 7 + test-fixtures/basic-git/DOTgit/HEAD | 1 + test-fixtures/basic-git/DOTgit/config | 7 + test-fixtures/basic-git/DOTgit/description | 1 + .../DOTgit/hooks/applypatch-msg.sample | 15 + .../basic-git/DOTgit/hooks/commit-msg.sample | 24 ++ .../basic-git/DOTgit/hooks/post-update.sample | 8 + .../DOTgit/hooks/pre-applypatch.sample | 14 + .../basic-git/DOTgit/hooks/pre-commit.sample | 49 +++ .../basic-git/DOTgit/hooks/pre-push.sample | 54 +++ .../basic-git/DOTgit/hooks/pre-rebase.sample | 169 ++++++++ .../DOTgit/hooks/prepare-commit-msg.sample | 36 ++ .../basic-git/DOTgit/hooks/update.sample | 128 ++++++ test-fixtures/basic-git/DOTgit/index | Bin 0 -> 184 bytes test-fixtures/basic-git/DOTgit/info/exclude | 6 + test-fixtures/basic-git/DOTgit/logs/HEAD | 7 + .../basic-git/DOTgit/logs/refs/heads/master | 4 + .../DOTgit/logs/refs/heads/test-branch | 2 + .../14/6492b04efe0aae2b8288c5c0aef6a951030fde | Bin 0 -> 170 bytes .../1d/3d6744266642cb7623e2c678c33c77b075c49f | Bin 0 -> 84 bytes .../1f/31e97f053caeb5d6b7bffa3faf82941c99efa2 | Bin 0 -> 167 bytes .../24/3f0fc5c4e586d1a3daa54c981b6f34e9ab1085 | Bin 0 -> 164 bytes .../38/30637158f774a20edcc0bf1c4d07b0bf87c43d | Bin 0 -> 59 bytes .../40/4618c9d96dfa0a5d365b518e0dfbb5a387c649 | Bin 0 -> 84 bytes .../49/7bc37401eb3c9b11865b1768725b64066eccee | 2 + .../7b/7614f8759ac8b5e4b02be65ad8e2667be6dd87 | 2 + .../8c/1a79ca1f98b6d00f5bf5c6cc9e8d3c092dd3ba | Bin 0 -> 51 bytes .../96/43088174e25a9bd91c27970a580af0085c9f32 | Bin 0 -> 52 bytes .../b7/757b6a3696ad036e9aa2f5b4856d09e7f17993 | Bin 0 -> 82 bytes .../e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 | Bin 0 -> 15 bytes .../basic-git/DOTgit/refs/heads/master | 1 + .../basic-git/DOTgit/refs/heads/test-branch | 1 + test-fixtures/basic-git/DOTgit/refs/tags/v1.0 | 1 + test-fixtures/basic-git/main.tf | 5 + test-fixtures/basic-git/subdir/sub.tf | 0 test-fixtures/basic-hg/.hg/00changelog.i | Bin 0 -> 57 bytes test-fixtures/basic-hg/.hg/branch | 1 + .../basic-hg/.hg/cache/branch2-served | 3 + test-fixtures/basic-hg/.hg/cache/tags | 2 + test-fixtures/basic-hg/.hg/dirstate | Bin 0 -> 64 bytes test-fixtures/basic-hg/.hg/last-message.txt | 2 + test-fixtures/basic-hg/.hg/requires | 4 + .../basic-hg/.hg/store/00changelog.i | Bin 0 -> 355 bytes test-fixtures/basic-hg/.hg/store/00manifest.i | Bin 0 -> 246 bytes .../basic-hg/.hg/store/data/main.tf.i | Bin 0 -> 112 bytes .../basic-hg/.hg/store/data/main__branch.tf.i | Bin 0 -> 64 bytes test-fixtures/basic-hg/.hg/store/fncache | 2 + test-fixtures/basic-hg/.hg/store/phaseroots | 1 + test-fixtures/basic-hg/.hg/store/undo | Bin 0 -> 59 bytes .../basic-hg/.hg/store/undo.phaseroots | 1 + test-fixtures/basic-hg/.hg/undo.bookmarks | 0 test-fixtures/basic-hg/.hg/undo.branch | 1 + test-fixtures/basic-hg/.hg/undo.desc | 2 + test-fixtures/basic-hg/.hg/undo.dirstate | Bin 0 -> 95 bytes test-fixtures/basic-hg/main.tf | 5 + test-fixtures/basic-parent/a/a.tf | 3 + test-fixtures/basic-parent/c/c.tf | 1 + test-fixtures/basic-parent/main.tf | 3 + .../basic-subdir/foo/sub/baz/main.tf | 0 test-fixtures/basic-subdir/foo/sub/main.tf | 3 + test-fixtures/basic-subdir/main.tf | 3 + test-fixtures/basic/foo/main.tf | 1 + test-fixtures/basic/main.tf | 5 + test-fixtures/basic/subdir/sub.tf | 0 test-fixtures/child/foo/bar/main.tf | 2 + test-fixtures/child/foo/main.tf | 5 + test-fixtures/child/main.tf | 5 + test-fixtures/dup/foo/main.tf | 0 test-fixtures/dup/main.tf | 7 + .../child/main.tf | 1 + .../validate-bad-output-to-module/main.tf | 8 + .../validate-bad-output/child/main.tf | 0 test-fixtures/validate-bad-output/main.tf | 7 + test-fixtures/validate-bad-var/child/main.tf | 0 test-fixtures/validate-bad-var/main.tf | 5 + .../validate-child-bad/child/main.tf | 3 + test-fixtures/validate-child-bad/main.tf | 3 + .../validate-child-good/child/main.tf | 3 + test-fixtures/validate-child-good/main.tf | 8 + .../validate-required-var/child/main.tf | 1 + test-fixtures/validate-required-var/main.tf | 3 + test-fixtures/validate-root-bad/main.tf | 3 + tree.go | 385 ++++++++++++++++++ tree_gob.go | 57 +++ tree_gob_test.go | 37 ++ tree_test.go | 309 ++++++++++++++ 112 files changed, 3475 insertions(+) create mode 100644 copy_dir.go create mode 100644 detect.go create mode 100644 detect_bitbucket.go create mode 100644 detect_bitbucket_test.go create mode 100644 detect_file.go create mode 100644 detect_file_test.go create mode 100644 detect_github.go create mode 100644 detect_github_test.go create mode 100644 detect_test.go create mode 100644 folder_storage.go create mode 100644 folder_storage_test.go create mode 100644 get.go create mode 100644 get_file.go create mode 100644 get_file_test.go create mode 100644 get_git.go create mode 100644 get_git_test.go create mode 100644 get_hg.go create mode 100644 get_hg_test.go create mode 100644 get_http.go create mode 100644 get_http_test.go create mode 100644 get_test.go create mode 100644 module.go create mode 100644 module_test.go create mode 100644 storage.go create mode 100644 test-fixtures/basic-dot/.test/foo.tf create mode 100644 test-fixtures/basic-dot/main.tf create mode 100644 test-fixtures/basic-git/DOTgit/COMMIT_EDITMSG create mode 100644 test-fixtures/basic-git/DOTgit/HEAD create mode 100644 test-fixtures/basic-git/DOTgit/config create mode 100644 test-fixtures/basic-git/DOTgit/description create mode 100755 test-fixtures/basic-git/DOTgit/hooks/applypatch-msg.sample create mode 100755 test-fixtures/basic-git/DOTgit/hooks/commit-msg.sample create mode 100755 test-fixtures/basic-git/DOTgit/hooks/post-update.sample create mode 100755 test-fixtures/basic-git/DOTgit/hooks/pre-applypatch.sample create mode 100755 test-fixtures/basic-git/DOTgit/hooks/pre-commit.sample create mode 100755 test-fixtures/basic-git/DOTgit/hooks/pre-push.sample create mode 100755 test-fixtures/basic-git/DOTgit/hooks/pre-rebase.sample create mode 100755 test-fixtures/basic-git/DOTgit/hooks/prepare-commit-msg.sample create mode 100755 test-fixtures/basic-git/DOTgit/hooks/update.sample create mode 100644 test-fixtures/basic-git/DOTgit/index create mode 100644 test-fixtures/basic-git/DOTgit/info/exclude create mode 100644 test-fixtures/basic-git/DOTgit/logs/HEAD create mode 100644 test-fixtures/basic-git/DOTgit/logs/refs/heads/master create mode 100644 test-fixtures/basic-git/DOTgit/logs/refs/heads/test-branch create mode 100644 test-fixtures/basic-git/DOTgit/objects/14/6492b04efe0aae2b8288c5c0aef6a951030fde create mode 100644 test-fixtures/basic-git/DOTgit/objects/1d/3d6744266642cb7623e2c678c33c77b075c49f create mode 100644 test-fixtures/basic-git/DOTgit/objects/1f/31e97f053caeb5d6b7bffa3faf82941c99efa2 create mode 100644 test-fixtures/basic-git/DOTgit/objects/24/3f0fc5c4e586d1a3daa54c981b6f34e9ab1085 create mode 100644 test-fixtures/basic-git/DOTgit/objects/38/30637158f774a20edcc0bf1c4d07b0bf87c43d create mode 100644 test-fixtures/basic-git/DOTgit/objects/40/4618c9d96dfa0a5d365b518e0dfbb5a387c649 create mode 100644 test-fixtures/basic-git/DOTgit/objects/49/7bc37401eb3c9b11865b1768725b64066eccee create mode 100644 test-fixtures/basic-git/DOTgit/objects/7b/7614f8759ac8b5e4b02be65ad8e2667be6dd87 create mode 100644 test-fixtures/basic-git/DOTgit/objects/8c/1a79ca1f98b6d00f5bf5c6cc9e8d3c092dd3ba create mode 100644 test-fixtures/basic-git/DOTgit/objects/96/43088174e25a9bd91c27970a580af0085c9f32 create mode 100644 test-fixtures/basic-git/DOTgit/objects/b7/757b6a3696ad036e9aa2f5b4856d09e7f17993 create mode 100644 test-fixtures/basic-git/DOTgit/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 create mode 100644 test-fixtures/basic-git/DOTgit/refs/heads/master create mode 100644 test-fixtures/basic-git/DOTgit/refs/heads/test-branch create mode 100644 test-fixtures/basic-git/DOTgit/refs/tags/v1.0 create mode 100644 test-fixtures/basic-git/main.tf create mode 100644 test-fixtures/basic-git/subdir/sub.tf create mode 100644 test-fixtures/basic-hg/.hg/00changelog.i create mode 100644 test-fixtures/basic-hg/.hg/branch create mode 100644 test-fixtures/basic-hg/.hg/cache/branch2-served create mode 100644 test-fixtures/basic-hg/.hg/cache/tags create mode 100644 test-fixtures/basic-hg/.hg/dirstate create mode 100644 test-fixtures/basic-hg/.hg/last-message.txt create mode 100644 test-fixtures/basic-hg/.hg/requires create mode 100644 test-fixtures/basic-hg/.hg/store/00changelog.i create mode 100644 test-fixtures/basic-hg/.hg/store/00manifest.i create mode 100644 test-fixtures/basic-hg/.hg/store/data/main.tf.i create mode 100644 test-fixtures/basic-hg/.hg/store/data/main__branch.tf.i create mode 100644 test-fixtures/basic-hg/.hg/store/fncache create mode 100644 test-fixtures/basic-hg/.hg/store/phaseroots create mode 100644 test-fixtures/basic-hg/.hg/store/undo create mode 100644 test-fixtures/basic-hg/.hg/store/undo.phaseroots create mode 100644 test-fixtures/basic-hg/.hg/undo.bookmarks create mode 100644 test-fixtures/basic-hg/.hg/undo.branch create mode 100644 test-fixtures/basic-hg/.hg/undo.desc create mode 100644 test-fixtures/basic-hg/.hg/undo.dirstate create mode 100644 test-fixtures/basic-hg/main.tf create mode 100644 test-fixtures/basic-parent/a/a.tf create mode 100644 test-fixtures/basic-parent/c/c.tf create mode 100644 test-fixtures/basic-parent/main.tf create mode 100644 test-fixtures/basic-subdir/foo/sub/baz/main.tf create mode 100644 test-fixtures/basic-subdir/foo/sub/main.tf create mode 100644 test-fixtures/basic-subdir/main.tf create mode 100644 test-fixtures/basic/foo/main.tf create mode 100644 test-fixtures/basic/main.tf create mode 100644 test-fixtures/basic/subdir/sub.tf create mode 100644 test-fixtures/child/foo/bar/main.tf create mode 100644 test-fixtures/child/foo/main.tf create mode 100644 test-fixtures/child/main.tf create mode 100644 test-fixtures/dup/foo/main.tf create mode 100644 test-fixtures/dup/main.tf create mode 100644 test-fixtures/validate-bad-output-to-module/child/main.tf create mode 100644 test-fixtures/validate-bad-output-to-module/main.tf create mode 100644 test-fixtures/validate-bad-output/child/main.tf create mode 100644 test-fixtures/validate-bad-output/main.tf create mode 100644 test-fixtures/validate-bad-var/child/main.tf create mode 100644 test-fixtures/validate-bad-var/main.tf create mode 100644 test-fixtures/validate-child-bad/child/main.tf create mode 100644 test-fixtures/validate-child-bad/main.tf create mode 100644 test-fixtures/validate-child-good/child/main.tf create mode 100644 test-fixtures/validate-child-good/main.tf create mode 100644 test-fixtures/validate-required-var/child/main.tf create mode 100644 test-fixtures/validate-required-var/main.tf create mode 100644 test-fixtures/validate-root-bad/main.tf create mode 100644 tree.go create mode 100644 tree_gob.go create mode 100644 tree_gob_test.go create mode 100644 tree_test.go diff --git a/copy_dir.go b/copy_dir.go new file mode 100644 index 000000000..45da6d7cd --- /dev/null +++ b/copy_dir.go @@ -0,0 +1,76 @@ +package getter + +import ( + "io" + "os" + "path/filepath" + "strings" +) + +// copyDir copies the src directory contents into dst. Both directories +// should already exist. +func copyDir(dst, src string) error { + src, err := filepath.EvalSymlinks(src) + if err != nil { + return err + } + + walkFn := func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if path == src { + return nil + } + + if strings.HasPrefix(filepath.Base(path), ".") { + // Skip any dot files + if info.IsDir() { + return filepath.SkipDir + } else { + return nil + } + } + + // The "path" has the src prefixed to it. We need to join our + // destination with the path without the src on it. + dstPath := filepath.Join(dst, path[len(src):]) + + // If we have a directory, make that subdirectory, then continue + // the walk. + if info.IsDir() { + if path == filepath.Join(src, dst) { + // dst is in src; don't walk it. + return nil + } + + if err := os.MkdirAll(dstPath, 0755); err != nil { + return err + } + + return nil + } + + // If we have a file, copy the contents. + srcF, err := os.Open(path) + if err != nil { + return err + } + defer srcF.Close() + + dstF, err := os.Create(dstPath) + if err != nil { + return err + } + defer dstF.Close() + + if _, err := io.Copy(dstF, srcF); err != nil { + return err + } + + // Chmod it + return os.Chmod(dstPath, info.Mode()) + } + + return filepath.Walk(src, walkFn) +} diff --git a/detect.go b/detect.go new file mode 100644 index 000000000..0b9780acf --- /dev/null +++ b/detect.go @@ -0,0 +1,92 @@ +package getter + +import ( + "fmt" + "path/filepath" + + "github.com/hashicorp/terraform/helper/url" +) + +// Detector defines the interface that an invalid URL or a URL with a blank +// scheme is passed through in order to determine if its shorthand for +// something else well-known. +type Detector interface { + // Detect will detect whether the string matches a known pattern to + // turn it into a proper URL. + Detect(string, string) (string, bool, error) +} + +// Detectors is the list of detectors that are tried on an invalid URL. +// This is also the order they're tried (index 0 is first). +var Detectors []Detector + +func init() { + Detectors = []Detector{ + new(GitHubDetector), + new(BitBucketDetector), + new(FileDetector), + } +} + +// Detect turns a source string into another source string if it is +// detected to be of a known pattern. +// +// This is safe to be called with an already valid source string: Detect +// will just return it. +func Detect(src string, pwd string) (string, error) { + getForce, getSrc := getForcedGetter(src) + + // Separate out the subdir if there is one, we don't pass that to detect + getSrc, subDir := getDirSubdir(getSrc) + + u, err := url.Parse(getSrc) + if err == nil && u.Scheme != "" { + // Valid URL + return src, nil + } + + for _, d := range Detectors { + result, ok, err := d.Detect(getSrc, pwd) + if err != nil { + return "", err + } + if !ok { + continue + } + + var detectForce string + detectForce, result = getForcedGetter(result) + result, detectSubdir := getDirSubdir(result) + + // If we have a subdir from the detection, then prepend it to our + // requested subdir. + if detectSubdir != "" { + if subDir != "" { + subDir = filepath.Join(detectSubdir, subDir) + } else { + subDir = detectSubdir + } + } + if subDir != "" { + u, err := url.Parse(result) + if err != nil { + return "", fmt.Errorf("Error parsing URL: %s", err) + } + u.Path += "//" + subDir + result = u.String() + } + + // Preserve the forced getter if it exists. We try to use the + // original set force first, followed by any force set by the + // detector. + if getForce != "" { + result = fmt.Sprintf("%s::%s", getForce, result) + } else if detectForce != "" { + result = fmt.Sprintf("%s::%s", detectForce, result) + } + + return result, nil + } + + return "", fmt.Errorf("invalid source string: %s", src) +} diff --git a/detect_bitbucket.go b/detect_bitbucket.go new file mode 100644 index 000000000..a183a17df --- /dev/null +++ b/detect_bitbucket.go @@ -0,0 +1,66 @@ +package getter + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" +) + +// BitBucketDetector implements Detector to detect BitBucket URLs and turn +// them into URLs that the Git or Hg Getter can understand. +type BitBucketDetector struct{} + +func (d *BitBucketDetector) Detect(src, _ string) (string, bool, error) { + if len(src) == 0 { + return "", false, nil + } + + if strings.HasPrefix(src, "bitbucket.org/") { + return d.detectHTTP(src) + } + + return "", false, nil +} + +func (d *BitBucketDetector) detectHTTP(src string) (string, bool, error) { + u, err := url.Parse("https://" + src) + if err != nil { + return "", true, fmt.Errorf("error parsing BitBucket URL: %s", err) + } + + // We need to get info on this BitBucket repository to determine whether + // it is Git or Hg. + var info struct { + SCM string `json:"scm"` + } + infoUrl := "https://api.bitbucket.org/1.0/repositories" + u.Path + resp, err := http.Get(infoUrl) + if err != nil { + return "", true, fmt.Errorf("error looking up BitBucket URL: %s", err) + } + if resp.StatusCode == 403 { + // A private repo + return "", true, fmt.Errorf( + "shorthand BitBucket URL can't be used for private repos, " + + "please use a full URL") + } + dec := json.NewDecoder(resp.Body) + if err := dec.Decode(&info); err != nil { + return "", true, fmt.Errorf("error looking up BitBucket URL: %s", err) + } + + switch info.SCM { + case "git": + if !strings.HasSuffix(u.Path, ".git") { + u.Path += ".git" + } + + return "git::" + u.String(), true, nil + case "hg": + return "hg::" + u.String(), true, nil + default: + return "", true, fmt.Errorf("unknown BitBucket SCM type: %s", info.SCM) + } +} diff --git a/detect_bitbucket_test.go b/detect_bitbucket_test.go new file mode 100644 index 000000000..202c93256 --- /dev/null +++ b/detect_bitbucket_test.go @@ -0,0 +1,67 @@ +package getter + +import ( + "net/http" + "strings" + "testing" +) + +const testBBUrl = "https://bitbucket.org/hashicorp/tf-test-git" + +func TestBitBucketDetector(t *testing.T) { + t.Parallel() + + if _, err := http.Get(testBBUrl); err != nil { + t.Log("internet may not be working, skipping BB tests") + t.Skip() + } + + cases := []struct { + Input string + Output string + }{ + // HTTP + { + "bitbucket.org/hashicorp/tf-test-git", + "git::https://bitbucket.org/hashicorp/tf-test-git.git", + }, + { + "bitbucket.org/hashicorp/tf-test-git.git", + "git::https://bitbucket.org/hashicorp/tf-test-git.git", + }, + { + "bitbucket.org/hashicorp/tf-test-hg", + "hg::https://bitbucket.org/hashicorp/tf-test-hg", + }, + } + + pwd := "/pwd" + f := new(BitBucketDetector) + for i, tc := range cases { + var err error + for i := 0; i < 3; i++ { + var output string + var ok bool + output, ok, err = f.Detect(tc.Input, pwd) + if err != nil { + if strings.Contains(err.Error(), "invalid character") { + continue + } + + t.Fatalf("err: %s", err) + } + if !ok { + t.Fatal("not ok") + } + + if output != tc.Output { + t.Fatalf("%d: bad: %#v", i, output) + } + + break + } + if i >= 3 { + t.Fatalf("failure from bitbucket: %s", err) + } + } +} diff --git a/detect_file.go b/detect_file.go new file mode 100644 index 000000000..ddeedc1dd --- /dev/null +++ b/detect_file.go @@ -0,0 +1,60 @@ +package getter + +import ( + "fmt" + "os" + "path/filepath" + "runtime" +) + +// FileDetector implements Detector to detect file paths. +type FileDetector struct{} + +func (d *FileDetector) Detect(src, pwd string) (string, bool, error) { + if len(src) == 0 { + return "", false, nil + } + + if !filepath.IsAbs(src) { + if pwd == "" { + return "", true, fmt.Errorf( + "relative paths require a module with a pwd") + } + + // Stat the pwd to determine if its a symbolic link. If it is, + // then the pwd becomes the original directory. Otherwise, + // `filepath.Join` below does some weird stuff. + // + // We just ignore if the pwd doesn't exist. That error will be + // caught later when we try to use the URL. + if fi, err := os.Lstat(pwd); !os.IsNotExist(err) { + if err != nil { + return "", true, err + } + if fi.Mode()&os.ModeSymlink != 0 { + pwd, err = os.Readlink(pwd) + if err != nil { + return "", true, err + } + } + } + + src = filepath.Join(pwd, src) + } + + return fmtFileURL(src), true, nil +} + +func fmtFileURL(path string) string { + if runtime.GOOS == "windows" { + // Make sure we're using "/" on Windows. URLs are "/"-based. + path = filepath.ToSlash(path) + return fmt.Sprintf("file://%s", path) + } + + // Make sure that we don't start with "/" since we add that below. + if path[0] == '/' { + path = path[1:] + } + return fmt.Sprintf("file:///%s", path) +} diff --git a/detect_file_test.go b/detect_file_test.go new file mode 100644 index 000000000..aa4ff2aaf --- /dev/null +++ b/detect_file_test.go @@ -0,0 +1,88 @@ +package getter + +import ( + "runtime" + "testing" +) + +type fileTest struct { + in, pwd, out string + err bool +} + +var fileTests = []fileTest{ + {"./foo", "/pwd", "file:///pwd/foo", false}, + {"./foo?foo=bar", "/pwd", "file:///pwd/foo?foo=bar", false}, + {"foo", "/pwd", "file:///pwd/foo", false}, +} + +var unixFileTests = []fileTest{ + {"/foo", "/pwd", "file:///foo", false}, + {"/foo?bar=baz", "/pwd", "file:///foo?bar=baz", false}, +} + +var winFileTests = []fileTest{ + {"/foo", "/pwd", "file:///pwd/foo", false}, + {`C:\`, `/pwd`, `file://C:/`, false}, + {`C:\?bar=baz`, `/pwd`, `file://C:/?bar=baz`, false}, +} + +func TestFileDetector(t *testing.T) { + if runtime.GOOS == "windows" { + fileTests = append(fileTests, winFileTests...) + } else { + fileTests = append(fileTests, unixFileTests...) + } + + f := new(FileDetector) + for i, tc := range fileTests { + out, ok, err := f.Detect(tc.in, tc.pwd) + if err != nil { + t.Fatalf("err: %s", err) + } + if !ok { + t.Fatal("not ok") + } + + if out != tc.out { + t.Fatalf("%d: bad: %#v", i, out) + } + } +} + +var noPwdFileTests = []fileTest{ + {in: "./foo", pwd: "", out: "", err: true}, + {in: "foo", pwd: "", out: "", err: true}, +} + +var noPwdUnixFileTests = []fileTest{ + {in: "/foo", pwd: "", out: "file:///foo", err: false}, +} + +var noPwdWinFileTests = []fileTest{ + {in: "/foo", pwd: "", out: "", err: true}, + {in: `C:\`, pwd: ``, out: `file://C:/`, err: false}, +} + +func TestFileDetector_noPwd(t *testing.T) { + if runtime.GOOS == "windows" { + noPwdFileTests = append(noPwdFileTests, noPwdWinFileTests...) + } else { + noPwdFileTests = append(noPwdFileTests, noPwdUnixFileTests...) + } + + f := new(FileDetector) + for i, tc := range noPwdFileTests { + out, ok, err := f.Detect(tc.in, tc.pwd) + if err != nil != tc.err { + t.Fatalf("%d: err: %s", i, err) + } + if !ok { + t.Fatal("not ok") + } + + if out != tc.out { + t.Fatalf("%d: bad: %#v", i, out) + } + } +} diff --git a/detect_github.go b/detect_github.go new file mode 100644 index 000000000..c084ad9ac --- /dev/null +++ b/detect_github.go @@ -0,0 +1,73 @@ +package getter + +import ( + "fmt" + "net/url" + "strings" +) + +// GitHubDetector implements Detector to detect GitHub URLs and turn +// them into URLs that the Git Getter can understand. +type GitHubDetector struct{} + +func (d *GitHubDetector) Detect(src, _ string) (string, bool, error) { + if len(src) == 0 { + return "", false, nil + } + + if strings.HasPrefix(src, "github.com/") { + return d.detectHTTP(src) + } else if strings.HasPrefix(src, "git@github.com:") { + return d.detectSSH(src) + } + + return "", false, nil +} + +func (d *GitHubDetector) detectHTTP(src string) (string, bool, error) { + parts := strings.Split(src, "/") + if len(parts) < 3 { + return "", false, fmt.Errorf( + "GitHub URLs should be github.com/username/repo") + } + + urlStr := fmt.Sprintf("https://%s", strings.Join(parts[:3], "/")) + url, err := url.Parse(urlStr) + if err != nil { + return "", true, fmt.Errorf("error parsing GitHub URL: %s", err) + } + + if !strings.HasSuffix(url.Path, ".git") { + url.Path += ".git" + } + + if len(parts) > 3 { + url.Path += "//" + strings.Join(parts[3:], "/") + } + + return "git::" + url.String(), true, nil +} + +func (d *GitHubDetector) detectSSH(src string) (string, bool, error) { + idx := strings.Index(src, ":") + qidx := strings.Index(src, "?") + if qidx == -1 { + qidx = len(src) + } + + var u url.URL + u.Scheme = "ssh" + u.User = url.User("git") + u.Host = "github.com" + u.Path = src[idx+1 : qidx] + if qidx < len(src) { + q, err := url.ParseQuery(src[qidx+1:]) + if err != nil { + return "", true, fmt.Errorf("error parsing GitHub SSH URL: %s", err) + } + + u.RawQuery = q.Encode() + } + + return "git::" + u.String(), true, nil +} diff --git a/detect_github_test.go b/detect_github_test.go new file mode 100644 index 000000000..43ed9fcc6 --- /dev/null +++ b/detect_github_test.go @@ -0,0 +1,55 @@ +package getter + +import ( + "testing" +) + +func TestGitHubDetector(t *testing.T) { + cases := []struct { + Input string + Output string + }{ + // HTTP + {"github.com/hashicorp/foo", "git::https://github.com/hashicorp/foo.git"}, + {"github.com/hashicorp/foo.git", "git::https://github.com/hashicorp/foo.git"}, + { + "github.com/hashicorp/foo/bar", + "git::https://github.com/hashicorp/foo.git//bar", + }, + { + "github.com/hashicorp/foo?foo=bar", + "git::https://github.com/hashicorp/foo.git?foo=bar", + }, + { + "github.com/hashicorp/foo.git?foo=bar", + "git::https://github.com/hashicorp/foo.git?foo=bar", + }, + + // SSH + {"git@github.com:hashicorp/foo.git", "git::ssh://git@github.com/hashicorp/foo.git"}, + { + "git@github.com:hashicorp/foo.git//bar", + "git::ssh://git@github.com/hashicorp/foo.git//bar", + }, + { + "git@github.com:hashicorp/foo.git?foo=bar", + "git::ssh://git@github.com/hashicorp/foo.git?foo=bar", + }, + } + + pwd := "/pwd" + f := new(GitHubDetector) + for i, tc := range cases { + output, ok, err := f.Detect(tc.Input, pwd) + if err != nil { + t.Fatalf("err: %s", err) + } + if !ok { + t.Fatal("not ok") + } + + if output != tc.Output { + t.Fatalf("%d: bad: %#v", i, output) + } + } +} diff --git a/detect_test.go b/detect_test.go new file mode 100644 index 000000000..a7f6d9a39 --- /dev/null +++ b/detect_test.go @@ -0,0 +1,51 @@ +package getter + +import ( + "testing" +) + +func TestDetect(t *testing.T) { + cases := []struct { + Input string + Pwd string + Output string + Err bool + }{ + {"./foo", "/foo", "file:///foo/foo", false}, + {"git::./foo", "/foo", "git::file:///foo/foo", false}, + { + "git::github.com/hashicorp/foo", + "", + "git::https://github.com/hashicorp/foo.git", + false, + }, + { + "./foo//bar", + "/foo", + "file:///foo/foo//bar", + false, + }, + { + "git::github.com/hashicorp/foo//bar", + "", + "git::https://github.com/hashicorp/foo.git//bar", + false, + }, + { + "git::https://github.com/hashicorp/consul.git", + "", + "git::https://github.com/hashicorp/consul.git", + false, + }, + } + + for i, tc := range cases { + output, err := Detect(tc.Input, tc.Pwd) + if err != nil != tc.Err { + t.Fatalf("%d: bad err: %s", i, err) + } + if output != tc.Output { + t.Fatalf("%d: bad output: %s\nexpected: %s", i, output, tc.Output) + } + } +} diff --git a/folder_storage.go b/folder_storage.go new file mode 100644 index 000000000..647ccf459 --- /dev/null +++ b/folder_storage.go @@ -0,0 +1,65 @@ +package getter + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "os" + "path/filepath" +) + +// FolderStorage is an implementation of the Storage interface that manages +// modules on the disk. +type FolderStorage struct { + // StorageDir is the directory where the modules will be stored. + StorageDir string +} + +// Dir implements Storage.Dir +func (s *FolderStorage) Dir(key string) (d string, e bool, err error) { + d = s.dir(key) + _, err = os.Stat(d) + if err == nil { + // Directory exists + e = true + return + } + if os.IsNotExist(err) { + // Directory doesn't exist + d = "" + e = false + err = nil + return + } + + // An error + d = "" + e = false + return +} + +// Get implements Storage.Get +func (s *FolderStorage) Get(key string, source string, update bool) error { + dir := s.dir(key) + if !update { + if _, err := os.Stat(dir); err == nil { + // If the directory already exists, then we're done since + // we're not updating. + return nil + } else if !os.IsNotExist(err) { + // If the error we got wasn't a file-not-exist error, then + // something went wrong and we should report it. + return fmt.Errorf("Error reading module directory: %s", err) + } + } + + // Get the source. This always forces an update. + return Get(dir, source) +} + +// dir returns the directory name internally that we'll use to map to +// internally. +func (s *FolderStorage) dir(key string) string { + sum := md5.Sum([]byte(key)) + return filepath.Join(s.StorageDir, hex.EncodeToString(sum[:])) +} diff --git a/folder_storage_test.go b/folder_storage_test.go new file mode 100644 index 000000000..feb8d3425 --- /dev/null +++ b/folder_storage_test.go @@ -0,0 +1,48 @@ +package getter + +import ( + "os" + "path/filepath" + "testing" +) + +func TestFolderStorage_impl(t *testing.T) { + var _ Storage = new(FolderStorage) +} + +func TestFolderStorage(t *testing.T) { + s := &FolderStorage{StorageDir: tempDir(t)} + + module := testModule("basic") + + // A module shouldn't exist at first... + _, ok, err := s.Dir(module) + if err != nil { + t.Fatalf("err: %s", err) + } + if ok { + t.Fatal("should not exist") + } + + key := "foo" + + // We can get it + err = s.Get(key, module, false) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Now the module exists + dir, ok, err := s.Dir(key) + if err != nil { + t.Fatalf("err: %s", err) + } + if !ok { + t.Fatal("should exist") + } + + mainPath := filepath.Join(dir, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} diff --git a/get.go b/get.go new file mode 100644 index 000000000..295a76f88 --- /dev/null +++ b/get.go @@ -0,0 +1,207 @@ +package getter + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/url" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "syscall" + + urlhelper "github.com/hashicorp/terraform/helper/url" +) + +// Getter defines the interface that schemes must implement to download +// and update modules. +type Getter interface { + // Get downloads the given URL into the given directory. This always + // assumes that we're updating and gets the latest version that it can. + // + // The directory may already exist (if we're updating). If it is in a + // format that isn't understood, an error should be returned. Get shouldn't + // simply nuke the directory. + Get(string, *url.URL) error +} + +// Getters is the mapping of scheme to the Getter implementation that will +// be used to get a dependency. +var Getters map[string]Getter + +// forcedRegexp is the regular expression that finds forced getters. This +// syntax is schema::url, example: git::https://foo.com +var forcedRegexp = regexp.MustCompile(`^([A-Za-z]+)::(.+)$`) + +func init() { + httpGetter := new(HttpGetter) + + Getters = map[string]Getter{ + "file": new(FileGetter), + "git": new(GitGetter), + "hg": new(HgGetter), + "http": httpGetter, + "https": httpGetter, + } +} + +// Get downloads the module specified by src into the folder specified by +// dst. If dst already exists, Get will attempt to update it. +// +// src is a URL, whereas dst is always just a file path to a folder. This +// folder doesn't need to exist. It will be created if it doesn't exist. +func Get(dst, src string) error { + var force string + force, src = getForcedGetter(src) + + // If there is a subdir component, then we download the root separately + // and then copy over the proper subdir. + var realDst string + src, subDir := getDirSubdir(src) + if subDir != "" { + tmpDir, err := ioutil.TempDir("", "tf") + if err != nil { + return err + } + if err := os.RemoveAll(tmpDir); err != nil { + return err + } + defer os.RemoveAll(tmpDir) + + realDst = dst + dst = tmpDir + } + + u, err := urlhelper.Parse(src) + if err != nil { + return err + } + if force == "" { + force = u.Scheme + } + + g, ok := Getters[force] + if !ok { + return fmt.Errorf( + "module download not supported for scheme '%s'", force) + } + + err = g.Get(dst, u) + if err != nil { + err = fmt.Errorf("error downloading module '%s': %s", src, err) + return err + } + + // If we have a subdir, copy that over + if subDir != "" { + if err := os.RemoveAll(realDst); err != nil { + return err + } + if err := os.MkdirAll(realDst, 0755); err != nil { + return err + } + + return copyDir(realDst, filepath.Join(dst, subDir)) + } + + return nil +} + +// GetCopy is the same as Get except that it downloads a copy of the +// module represented by source. +// +// This copy will omit and dot-prefixed files (such as .git/, .hg/) and +// can't be updated on its own. +func GetCopy(dst, src string) error { + // Create the temporary directory to do the real Get to + tmpDir, err := ioutil.TempDir("", "tf") + if err != nil { + return err + } + if err := os.RemoveAll(tmpDir); err != nil { + return err + } + defer os.RemoveAll(tmpDir) + + // Get to that temporary dir + if err := Get(tmpDir, src); err != nil { + return err + } + + // Make sure the destination exists + if err := os.MkdirAll(dst, 0755); err != nil { + return err + } + + // Copy to the final location + return copyDir(dst, tmpDir) +} + +// getRunCommand is a helper that will run a command and capture the output +// in the case an error happens. +func getRunCommand(cmd *exec.Cmd) error { + var buf bytes.Buffer + cmd.Stdout = &buf + cmd.Stderr = &buf + err := cmd.Run() + if err == nil { + return nil + } + if exiterr, ok := err.(*exec.ExitError); ok { + // The program has exited with an exit code != 0 + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + return fmt.Errorf( + "%s exited with %d: %s", + cmd.Path, + status.ExitStatus(), + buf.String()) + } + } + + return fmt.Errorf("error running %s: %s", cmd.Path, buf.String()) +} + +// getDirSubdir takes a source and returns a tuple of the URL without +// the subdir and the URL with the subdir. +func getDirSubdir(src string) (string, string) { + // Calcaulate an offset to avoid accidentally marking the scheme + // as the dir. + var offset int + if idx := strings.Index(src, "://"); idx > -1 { + offset = idx + 3 + } + + // First see if we even have an explicit subdir + idx := strings.Index(src[offset:], "//") + if idx == -1 { + return src, "" + } + + idx += offset + subdir := src[idx+2:] + src = src[:idx] + + // Next, check if we have query parameters and push them onto the + // URL. + if idx = strings.Index(subdir, "?"); idx > -1 { + query := subdir[idx:] + subdir = subdir[:idx] + src += query + } + + return src, subdir +} + +// getForcedGetter takes a source and returns the tuple of the forced +// getter and the raw URL (without the force syntax). +func getForcedGetter(src string) (string, string) { + var forced string + if ms := forcedRegexp.FindStringSubmatch(src); ms != nil { + forced = ms[1] + src = ms[2] + } + + return forced, src +} diff --git a/get_file.go b/get_file.go new file mode 100644 index 000000000..092028072 --- /dev/null +++ b/get_file.go @@ -0,0 +1,46 @@ +package getter + +import ( + "fmt" + "net/url" + "os" + "path/filepath" +) + +// FileGetter is a Getter implementation that will download a module from +// a file scheme. +type FileGetter struct{} + +func (g *FileGetter) Get(dst string, u *url.URL) error { + // The source path must exist and be a directory to be usable. + if fi, err := os.Stat(u.Path); err != nil { + return fmt.Errorf("source path error: %s", err) + } else if !fi.IsDir() { + return fmt.Errorf("source path must be a directory") + } + + fi, err := os.Lstat(dst) + if err != nil && !os.IsNotExist(err) { + return err + } + + // If the destination already exists, it must be a symlink + if err == nil { + mode := fi.Mode() + if mode&os.ModeSymlink == 0 { + return fmt.Errorf("destination exists and is not a symlink") + } + + // Remove the destination + if err := os.Remove(dst); err != nil { + return err + } + } + + // Create all the parent directories + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return err + } + + return os.Symlink(u.Path, dst) +} diff --git a/get_file_test.go b/get_file_test.go new file mode 100644 index 000000000..80cd47f0f --- /dev/null +++ b/get_file_test.go @@ -0,0 +1,104 @@ +package getter + +import ( + "os" + "path/filepath" + "testing" +) + +func TestFileGetter_impl(t *testing.T) { + var _ Getter = new(FileGetter) +} + +func TestFileGetter(t *testing.T) { + g := new(FileGetter) + dst := tempDir(t) + + // With a dir that doesn't exist + if err := g.Get(dst, testModuleURL("basic")); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the destination folder is a symlink + fi, err := os.Lstat(dst) + if err != nil { + t.Fatalf("err: %s", err) + } + if fi.Mode()&os.ModeSymlink == 0 { + t.Fatal("destination is not a symlink") + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestFileGetter_sourceFile(t *testing.T) { + g := new(FileGetter) + dst := tempDir(t) + + // With a source URL that is a path to a file + u := testModuleURL("basic") + u.Path += "/main.tf" + if err := g.Get(dst, u); err == nil { + t.Fatal("should error") + } +} + +func TestFileGetter_sourceNoExist(t *testing.T) { + g := new(FileGetter) + dst := tempDir(t) + + // With a source URL that doesn't exist + u := testModuleURL("basic") + u.Path += "/main" + if err := g.Get(dst, u); err == nil { + t.Fatal("should error") + } +} + +func TestFileGetter_dir(t *testing.T) { + g := new(FileGetter) + dst := tempDir(t) + + if err := os.MkdirAll(dst, 0755); err != nil { + t.Fatalf("err: %s", err) + } + + // With a dir that exists that isn't a symlink + if err := g.Get(dst, testModuleURL("basic")); err == nil { + t.Fatal("should error") + } +} + +func TestFileGetter_dirSymlink(t *testing.T) { + g := new(FileGetter) + dst := tempDir(t) + dst2 := tempDir(t) + + // Make parents + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + t.Fatalf("err: %s", err) + } + if err := os.MkdirAll(dst2, 0755); err != nil { + t.Fatalf("err: %s", err) + } + + // Make a symlink + if err := os.Symlink(dst2, dst); err != nil { + t.Fatalf("err: %s", err) + } + + // With a dir that exists that isn't a symlink + if err := g.Get(dst, testModuleURL("basic")); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} diff --git a/get_git.go b/get_git.go new file mode 100644 index 000000000..d1a2fd35d --- /dev/null +++ b/get_git.go @@ -0,0 +1,74 @@ +package getter + +import ( + "fmt" + "net/url" + "os" + "os/exec" +) + +// GitGetter is a Getter implementation that will download a module from +// a git repository. +type GitGetter struct{} + +func (g *GitGetter) Get(dst string, u *url.URL) error { + if _, err := exec.LookPath("git"); err != nil { + return fmt.Errorf("git must be available and on the PATH") + } + + // Extract some query parameters we use + var ref string + q := u.Query() + if len(q) > 0 { + ref = q.Get("ref") + q.Del("ref") + + // Copy the URL + var newU url.URL = *u + u = &newU + u.RawQuery = q.Encode() + } + + // First: clone or update the repository + _, err := os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + return err + } + if err == nil { + err = g.update(dst, u) + } else { + err = g.clone(dst, u) + } + if err != nil { + return err + } + + // Next: check out the proper tag/branch if it is specified, and checkout + if ref == "" { + return nil + } + + return g.checkout(dst, ref) +} + +func (g *GitGetter) checkout(dst string, ref string) error { + cmd := exec.Command("git", "checkout", ref) + cmd.Dir = dst + return getRunCommand(cmd) +} + +func (g *GitGetter) clone(dst string, u *url.URL) error { + cmd := exec.Command("git", "clone", u.String(), dst) + return getRunCommand(cmd) +} + +func (g *GitGetter) update(dst string, u *url.URL) error { + // We have to be on a branch to pull + if err := g.checkout(dst, "master"); err != nil { + return err + } + + cmd := exec.Command("git", "pull", "--ff-only") + cmd.Dir = dst + return getRunCommand(cmd) +} diff --git a/get_git_test.go b/get_git_test.go new file mode 100644 index 000000000..0e5d687ae --- /dev/null +++ b/get_git_test.go @@ -0,0 +1,143 @@ +package getter + +import ( + "os" + "os/exec" + "path/filepath" + "testing" +) + +var testHasGit bool + +func init() { + if _, err := exec.LookPath("git"); err == nil { + testHasGit = true + } +} + +func TestGitGetter_impl(t *testing.T) { + var _ Getter = new(GitGetter) +} + +func TestGitGetter(t *testing.T) { + if !testHasGit { + t.Log("git not found, skipping") + t.Skip() + } + + g := new(GitGetter) + dst := tempDir(t) + + // Git doesn't allow nested ".git" directories so we do some hackiness + // here to get around that... + moduleDir := filepath.Join(fixtureDir, "basic-git") + oldName := filepath.Join(moduleDir, "DOTgit") + newName := filepath.Join(moduleDir, ".git") + if err := os.Rename(oldName, newName); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Rename(newName, oldName) + + // With a dir that doesn't exist + if err := g.Get(dst, testModuleURL("basic-git")); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestGitGetter_branch(t *testing.T) { + if !testHasGit { + t.Log("git not found, skipping") + t.Skip() + } + + g := new(GitGetter) + dst := tempDir(t) + + // Git doesn't allow nested ".git" directories so we do some hackiness + // here to get around that... + moduleDir := filepath.Join(fixtureDir, "basic-git") + oldName := filepath.Join(moduleDir, "DOTgit") + newName := filepath.Join(moduleDir, ".git") + if err := os.Rename(oldName, newName); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Rename(newName, oldName) + + url := testModuleURL("basic-git") + q := url.Query() + q.Add("ref", "test-branch") + url.RawQuery = q.Encode() + + if err := g.Get(dst, url); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main_branch.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } + + // Get again should work + if err := g.Get(dst, url); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath = filepath.Join(dst, "main_branch.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestGitGetter_tag(t *testing.T) { + if !testHasGit { + t.Log("git not found, skipping") + t.Skip() + } + + g := new(GitGetter) + dst := tempDir(t) + + // Git doesn't allow nested ".git" directories so we do some hackiness + // here to get around that... + moduleDir := filepath.Join(fixtureDir, "basic-git") + oldName := filepath.Join(moduleDir, "DOTgit") + newName := filepath.Join(moduleDir, ".git") + if err := os.Rename(oldName, newName); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Rename(newName, oldName) + + url := testModuleURL("basic-git") + q := url.Query() + q.Add("ref", "v1.0") + url.RawQuery = q.Encode() + + if err := g.Get(dst, url); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main_tag1.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } + + // Get again should work + if err := g.Get(dst, url); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath = filepath.Join(dst, "main_tag1.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} diff --git a/get_hg.go b/get_hg.go new file mode 100644 index 000000000..f84071b77 --- /dev/null +++ b/get_hg.go @@ -0,0 +1,89 @@ +package getter + +import ( + "fmt" + "net/url" + "os" + "os/exec" + "runtime" + + urlhelper "github.com/hashicorp/terraform/helper/url" +) + +// HgGetter is a Getter implementation that will download a module from +// a Mercurial repository. +type HgGetter struct{} + +func (g *HgGetter) Get(dst string, u *url.URL) error { + if _, err := exec.LookPath("hg"); err != nil { + return fmt.Errorf("hg must be available and on the PATH") + } + + newURL, err := urlhelper.Parse(u.String()) + if err != nil { + return err + } + if fixWindowsDrivePath(newURL) { + // See valid file path form on http://www.selenic.com/hg/help/urls + newURL.Path = fmt.Sprintf("/%s", newURL.Path) + } + + // Extract some query parameters we use + var rev string + q := newURL.Query() + if len(q) > 0 { + rev = q.Get("rev") + q.Del("rev") + + newURL.RawQuery = q.Encode() + } + + _, err = os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + return err + } + if err != nil { + if err := g.clone(dst, newURL); err != nil { + return err + } + } + + if err := g.pull(dst, newURL); err != nil { + return err + } + + return g.update(dst, newURL, rev) +} + +func (g *HgGetter) clone(dst string, u *url.URL) error { + cmd := exec.Command("hg", "clone", "-U", u.String(), dst) + return getRunCommand(cmd) +} + +func (g *HgGetter) pull(dst string, u *url.URL) error { + cmd := exec.Command("hg", "pull") + cmd.Dir = dst + return getRunCommand(cmd) +} + +func (g *HgGetter) update(dst string, u *url.URL, rev string) error { + args := []string{"update"} + if rev != "" { + args = append(args, rev) + } + + cmd := exec.Command("hg", args...) + cmd.Dir = dst + return getRunCommand(cmd) +} + +func fixWindowsDrivePath(u *url.URL) bool { + // hg assumes a file:/// prefix for Windows drive letter file paths. + // (e.g. file:///c:/foo/bar) + // If the URL Path does not begin with a '/' character, the resulting URL + // path will have a file:// prefix. (e.g. file://c:/foo/bar) + // See http://www.selenic.com/hg/help/urls and the examples listed in + // http://selenic.com/repo/hg-stable/file/1265a3a71d75/mercurial/util.py#l1936 + return runtime.GOOS == "windows" && u.Scheme == "file" && + len(u.Path) > 1 && u.Path[0] != '/' && u.Path[1] == ':' +} diff --git a/get_hg_test.go b/get_hg_test.go new file mode 100644 index 000000000..b45dbeb44 --- /dev/null +++ b/get_hg_test.go @@ -0,0 +1,81 @@ +package getter + +import ( + "os" + "os/exec" + "path/filepath" + "testing" +) + +var testHasHg bool + +func init() { + if _, err := exec.LookPath("hg"); err == nil { + testHasHg = true + } +} + +func TestHgGetter_impl(t *testing.T) { + var _ Getter = new(HgGetter) +} + +func TestHgGetter(t *testing.T) { + t.Parallel() + + if !testHasHg { + t.Log("hg not found, skipping") + t.Skip() + } + + g := new(HgGetter) + dst := tempDir(t) + + // With a dir that doesn't exist + if err := g.Get(dst, testModuleURL("basic-hg")); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestHgGetter_branch(t *testing.T) { + t.Parallel() + + if !testHasHg { + t.Log("hg not found, skipping") + t.Skip() + } + + g := new(HgGetter) + dst := tempDir(t) + + url := testModuleURL("basic-hg") + q := url.Query() + q.Add("rev", "test-branch") + url.RawQuery = q.Encode() + + if err := g.Get(dst, url); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main_branch.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } + + // Get again should work + if err := g.Get(dst, url); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath = filepath.Join(dst, "main_branch.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} diff --git a/get_http.go b/get_http.go new file mode 100644 index 000000000..27bf8e96f --- /dev/null +++ b/get_http.go @@ -0,0 +1,173 @@ +package getter + +import ( + "encoding/xml" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" +) + +// HttpGetter is a Getter implementation that will download a module from +// an HTTP endpoint. The protocol for downloading a module from an HTTP +// endpoing is as follows: +// +// An HTTP GET request is made to the URL with the additional GET parameter +// "terraform-get=1". This lets you handle that scenario specially if you +// wish. The response must be a 2xx. +// +// First, a header is looked for "X-Terraform-Get" which should contain +// a source URL to download. +// +// If the header is not present, then a meta tag is searched for named +// "terraform-get" and the content should be a source URL. +// +// The source URL, whether from the header or meta tag, must be a fully +// formed URL. The shorthand syntax of "github.com/foo/bar" or relative +// paths are not allowed. +type HttpGetter struct{} + +func (g *HttpGetter) Get(dst string, u *url.URL) error { + // Copy the URL so we can modify it + var newU url.URL = *u + u = &newU + + // Add terraform-get to the parameter. + q := u.Query() + q.Add("terraform-get", "1") + u.RawQuery = q.Encode() + + // Get the URL + resp, err := http.Get(u.String()) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("bad response code: %d", resp.StatusCode) + } + + // Extract the source URL + var source string + if v := resp.Header.Get("X-Terraform-Get"); v != "" { + source = v + } else { + source, err = g.parseMeta(resp.Body) + if err != nil { + return err + } + } + if source == "" { + return fmt.Errorf("no source URL was returned") + } + + // If there is a subdir component, then we download the root separately + // into a temporary directory, then copy over the proper subdir. + source, subDir := getDirSubdir(source) + if subDir == "" { + return Get(dst, source) + } + + // We have a subdir, time to jump some hoops + return g.getSubdir(dst, source, subDir) +} + +// getSubdir downloads the source into the destination, but with +// the proper subdir. +func (g *HttpGetter) getSubdir(dst, source, subDir string) error { + // Create a temporary directory to store the full source + td, err := ioutil.TempDir("", "tf") + if err != nil { + return err + } + defer os.RemoveAll(td) + + // Download that into the given directory + if err := Get(td, source); err != nil { + return err + } + + // Make sure the subdir path actually exists + sourcePath := filepath.Join(td, subDir) + if _, err := os.Stat(sourcePath); err != nil { + return fmt.Errorf( + "Error downloading %s: %s", source, err) + } + + // Copy the subdirectory into our actual destination. + if err := os.RemoveAll(dst); err != nil { + return err + } + + // Make the final destination + if err := os.MkdirAll(dst, 0755); err != nil { + return err + } + + return copyDir(dst, sourcePath) +} + +// parseMeta looks for the first meta tag in the given reader that +// will give us the source URL. +func (g *HttpGetter) parseMeta(r io.Reader) (string, error) { + d := xml.NewDecoder(r) + d.CharsetReader = charsetReader + d.Strict = false + var err error + var t xml.Token + for { + t, err = d.Token() + if err != nil { + if err == io.EOF { + err = nil + } + return "", err + } + if e, ok := t.(xml.StartElement); ok && strings.EqualFold(e.Name.Local, "body") { + return "", nil + } + if e, ok := t.(xml.EndElement); ok && strings.EqualFold(e.Name.Local, "head") { + return "", nil + } + e, ok := t.(xml.StartElement) + if !ok || !strings.EqualFold(e.Name.Local, "meta") { + continue + } + if attrValue(e.Attr, "name") != "terraform-get" { + continue + } + if f := attrValue(e.Attr, "content"); f != "" { + return f, nil + } + } +} + +// attrValue returns the attribute value for the case-insensitive key +// `name', or the empty string if nothing is found. +func attrValue(attrs []xml.Attr, name string) string { + for _, a := range attrs { + if strings.EqualFold(a.Name.Local, name) { + return a.Value + } + } + return "" +} + +// charsetReader returns a reader for the given charset. Currently +// it only supports UTF-8 and ASCII. Otherwise, it returns a meaningful +// error which is printed by go get, so the user can find why the package +// wasn't downloaded if the encoding is not supported. Note that, in +// order to reduce potential errors, ASCII is treated as UTF-8 (i.e. characters +// greater than 0x7f are not rejected). +func charsetReader(charset string, input io.Reader) (io.Reader, error) { + switch strings.ToLower(charset) { + case "ascii": + return input, nil + default: + return nil, fmt.Errorf("can't decode XML document using charset %q", charset) + } +} diff --git a/get_http_test.go b/get_http_test.go new file mode 100644 index 000000000..05d8b8584 --- /dev/null +++ b/get_http_test.go @@ -0,0 +1,155 @@ +package getter + +import ( + "fmt" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "testing" +) + +func TestHttpGetter_impl(t *testing.T) { + var _ Getter = new(HttpGetter) +} + +func TestHttpGetter_header(t *testing.T) { + ln := testHttpServer(t) + defer ln.Close() + + g := new(HttpGetter) + dst := tempDir(t) + + var u url.URL + u.Scheme = "http" + u.Host = ln.Addr().String() + u.Path = "/header" + + // Get it! + if err := g.Get(dst, &u); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestHttpGetter_meta(t *testing.T) { + ln := testHttpServer(t) + defer ln.Close() + + g := new(HttpGetter) + dst := tempDir(t) + + var u url.URL + u.Scheme = "http" + u.Host = ln.Addr().String() + u.Path = "/meta" + + // Get it! + if err := g.Get(dst, &u); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestHttpGetter_metaSubdir(t *testing.T) { + ln := testHttpServer(t) + defer ln.Close() + + g := new(HttpGetter) + dst := tempDir(t) + + var u url.URL + u.Scheme = "http" + u.Host = ln.Addr().String() + u.Path = "/meta-subdir" + + // Get it! + if err := g.Get(dst, &u); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the main file exists + mainPath := filepath.Join(dst, "sub.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestHttpGetter_none(t *testing.T) { + ln := testHttpServer(t) + defer ln.Close() + + g := new(HttpGetter) + dst := tempDir(t) + + var u url.URL + u.Scheme = "http" + u.Host = ln.Addr().String() + u.Path = "/none" + + // Get it! + if err := g.Get(dst, &u); err == nil { + t.Fatal("should error") + } +} + +func testHttpServer(t *testing.T) net.Listener { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("err: %s", err) + } + + mux := http.NewServeMux() + mux.HandleFunc("/header", testHttpHandlerHeader) + mux.HandleFunc("/meta", testHttpHandlerMeta) + mux.HandleFunc("/meta-subdir", testHttpHandlerMetaSubdir) + + var server http.Server + server.Handler = mux + go server.Serve(ln) + + return ln +} + +func testHttpHandlerHeader(w http.ResponseWriter, r *http.Request) { + w.Header().Add("X-Terraform-Get", testModuleURL("basic").String()) + w.WriteHeader(200) +} + +func testHttpHandlerMeta(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(fmt.Sprintf(testHttpMetaStr, testModuleURL("basic").String()))) +} + +func testHttpHandlerMetaSubdir(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(fmt.Sprintf(testHttpMetaStr, testModuleURL("basic//subdir").String()))) +} + +func testHttpHandlerNone(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(testHttpNoneStr)) +} + +const testHttpMetaStr = ` + + + + + +` + +const testHttpNoneStr = ` + + + + +` diff --git a/get_test.go b/get_test.go new file mode 100644 index 000000000..78234f8e6 --- /dev/null +++ b/get_test.go @@ -0,0 +1,128 @@ +package getter + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestGet_badSchema(t *testing.T) { + dst := tempDir(t) + u := testModule("basic") + u = strings.Replace(u, "file", "nope", -1) + + if err := Get(dst, u); err == nil { + t.Fatal("should error") + } +} + +func TestGet_file(t *testing.T) { + dst := tempDir(t) + u := testModule("basic") + + if err := Get(dst, u); err != nil { + t.Fatalf("err: %s", err) + } + + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestGet_fileForced(t *testing.T) { + dst := tempDir(t) + u := testModule("basic") + u = "file::" + u + + if err := Get(dst, u); err != nil { + t.Fatalf("err: %s", err) + } + + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestGet_fileSubdir(t *testing.T) { + dst := tempDir(t) + u := testModule("basic//subdir") + + if err := Get(dst, u); err != nil { + t.Fatalf("err: %s", err) + } + + mainPath := filepath.Join(dst, "sub.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestGetCopy_dot(t *testing.T) { + dst := tempDir(t) + u := testModule("basic-dot") + + if err := GetCopy(dst, u); err != nil { + t.Fatalf("err: %s", err) + } + + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } + + mainPath = filepath.Join(dst, "foo.tf") + if _, err := os.Stat(mainPath); err == nil { + t.Fatal("should not have foo.tf") + } +} + +func TestGetCopy_file(t *testing.T) { + dst := tempDir(t) + u := testModule("basic") + + if err := GetCopy(dst, u); err != nil { + t.Fatalf("err: %s", err) + } + + mainPath := filepath.Join(dst, "main.tf") + if _, err := os.Stat(mainPath); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestGetDirSubdir(t *testing.T) { + cases := []struct { + Input string + Dir, Sub string + }{ + { + "hashicorp.com", + "hashicorp.com", "", + }, + { + "hashicorp.com//foo", + "hashicorp.com", "foo", + }, + { + "hashicorp.com//foo?bar=baz", + "hashicorp.com?bar=baz", "foo", + }, + { + "file://foo//bar", + "file://foo", "bar", + }, + } + + for i, tc := range cases { + adir, asub := getDirSubdir(tc.Input) + if adir != tc.Dir { + t.Fatalf("%d: bad dir: %#v", i, adir) + } + if asub != tc.Sub { + t.Fatalf("%d: bad sub: %#v", i, asub) + } + } +} diff --git a/module.go b/module.go new file mode 100644 index 000000000..3ebecdb4d --- /dev/null +++ b/module.go @@ -0,0 +1,7 @@ +package getter + +// Module represents the metadata for a single module. +type Module struct { + Name string + Source string +} diff --git a/module_test.go b/module_test.go new file mode 100644 index 000000000..3b50e32e5 --- /dev/null +++ b/module_test.go @@ -0,0 +1,57 @@ +package getter + +import ( + "io/ioutil" + "net/url" + "os" + "path/filepath" + "testing" + + "github.com/hashicorp/terraform/config" + urlhelper "github.com/hashicorp/terraform/helper/url" +) + +const fixtureDir = "./test-fixtures" + +func tempDir(t *testing.T) string { + dir, err := ioutil.TempDir("", "tf") + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.RemoveAll(dir); err != nil { + t.Fatalf("err: %s", err) + } + + return dir +} + +func testConfig(t *testing.T, n string) *config.Config { + c, err := config.LoadDir(filepath.Join(fixtureDir, n)) + if err != nil { + t.Fatalf("err: %s", err) + } + + return c +} + +func testModule(n string) string { + p := filepath.Join(fixtureDir, n) + p, err := filepath.Abs(p) + if err != nil { + panic(err) + } + return fmtFileURL(p) +} + +func testModuleURL(n string) *url.URL { + u, err := urlhelper.Parse(testModule(n)) + if err != nil { + panic(err) + } + + return u +} + +func testStorage(t *testing.T) Storage { + return &FolderStorage{StorageDir: tempDir(t)} +} diff --git a/storage.go b/storage.go new file mode 100644 index 000000000..4d2a9fccc --- /dev/null +++ b/storage.go @@ -0,0 +1,25 @@ +package getter + +// Storage is an interface that knows how to lookup downloaded modules +// as well as download and update modules from their sources into the +// proper location. +type Storage interface { + // Dir returns the directory on local disk where the modulue source + // can be loaded from. + Dir(string) (string, bool, error) + + // Get will download and optionally update the given module. + Get(string, string, bool) error +} + +func getStorage(s Storage, key string, src string, mode GetMode) (string, bool, error) { + // Get the module with the level specified if we were told to. + if mode > GetModeNone { + if err := s.Get(key, src, mode == GetModeUpdate); err != nil { + return "", false, err + } + } + + // Get the directory where the module is. + return s.Dir(key) +} diff --git a/test-fixtures/basic-dot/.test/foo.tf b/test-fixtures/basic-dot/.test/foo.tf new file mode 100644 index 000000000..76f177f19 --- /dev/null +++ b/test-fixtures/basic-dot/.test/foo.tf @@ -0,0 +1 @@ +# Hi diff --git a/test-fixtures/basic-dot/main.tf b/test-fixtures/basic-dot/main.tf new file mode 100644 index 000000000..383063715 --- /dev/null +++ b/test-fixtures/basic-dot/main.tf @@ -0,0 +1,5 @@ +# Hello + +module "foo" { + source = "./foo" +} diff --git a/test-fixtures/basic-git/DOTgit/COMMIT_EDITMSG b/test-fixtures/basic-git/DOTgit/COMMIT_EDITMSG new file mode 100644 index 000000000..a580d5737 --- /dev/null +++ b/test-fixtures/basic-git/DOTgit/COMMIT_EDITMSG @@ -0,0 +1,7 @@ +add subdir +# Please enter the commit message for your changes. Lines starting +# with '#' will be ignored, and an empty message aborts the commit. +# On branch master +# Changes to be committed: +# new file: subdir/sub.tf +# diff --git a/test-fixtures/basic-git/DOTgit/HEAD b/test-fixtures/basic-git/DOTgit/HEAD new file mode 100644 index 000000000..cb089cd89 --- /dev/null +++ b/test-fixtures/basic-git/DOTgit/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/test-fixtures/basic-git/DOTgit/config b/test-fixtures/basic-git/DOTgit/config new file mode 100644 index 000000000..6c9406b7d --- /dev/null +++ b/test-fixtures/basic-git/DOTgit/config @@ -0,0 +1,7 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true + precomposeunicode = true diff --git a/test-fixtures/basic-git/DOTgit/description b/test-fixtures/basic-git/DOTgit/description new file mode 100644 index 000000000..498b267a8 --- /dev/null +++ b/test-fixtures/basic-git/DOTgit/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/test-fixtures/basic-git/DOTgit/hooks/applypatch-msg.sample b/test-fixtures/basic-git/DOTgit/hooks/applypatch-msg.sample new file mode 100755 index 000000000..8b2a2fe84 --- /dev/null +++ b/test-fixtures/basic-git/DOTgit/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +test -x "$GIT_DIR/hooks/commit-msg" && + exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"} +: diff --git a/test-fixtures/basic-git/DOTgit/hooks/commit-msg.sample b/test-fixtures/basic-git/DOTgit/hooks/commit-msg.sample new file mode 100755 index 000000000..b58d1184a --- /dev/null +++ b/test-fixtures/basic-git/DOTgit/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/test-fixtures/basic-git/DOTgit/hooks/post-update.sample b/test-fixtures/basic-git/DOTgit/hooks/post-update.sample new file mode 100755 index 000000000..ec17ec193 --- /dev/null +++ b/test-fixtures/basic-git/DOTgit/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/test-fixtures/basic-git/DOTgit/hooks/pre-applypatch.sample b/test-fixtures/basic-git/DOTgit/hooks/pre-applypatch.sample new file mode 100755 index 000000000..b1f187c2e --- /dev/null +++ b/test-fixtures/basic-git/DOTgit/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +test -x "$GIT_DIR/hooks/pre-commit" && + exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"} +: diff --git a/test-fixtures/basic-git/DOTgit/hooks/pre-commit.sample b/test-fixtures/basic-git/DOTgit/hooks/pre-commit.sample new file mode 100755 index 000000000..68d62d544 --- /dev/null +++ b/test-fixtures/basic-git/DOTgit/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/test-fixtures/basic-git/DOTgit/hooks/pre-push.sample b/test-fixtures/basic-git/DOTgit/hooks/pre-push.sample new file mode 100755 index 000000000..1f3bcebfd --- /dev/null +++ b/test-fixtures/basic-git/DOTgit/hooks/pre-push.sample @@ -0,0 +1,54 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +z40=0000000000000000000000000000000000000000 + +IFS=' ' +while read local_ref local_sha remote_ref remote_sha +do + if [ "$local_sha" = $z40 ] + then + # Handle delete + : + else + if [ "$remote_sha" = $z40 ] + then + # New branch, examine all commits + range="$local_sha" + else + # Update to existing branch, examine new commits + range="$remote_sha..$local_sha" + fi + + # Check for WIP commit + commit=`git rev-list -n 1 --grep '^WIP' "$range"` + if [ -n "$commit" ] + then + echo "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/test-fixtures/basic-git/DOTgit/hooks/pre-rebase.sample b/test-fixtures/basic-git/DOTgit/hooks/pre-rebase.sample new file mode 100755 index 000000000..9773ed4cb --- /dev/null +++ b/test-fixtures/basic-git/DOTgit/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up-to-date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +exit 0 + +################################################################ + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". diff --git a/test-fixtures/basic-git/DOTgit/hooks/prepare-commit-msg.sample b/test-fixtures/basic-git/DOTgit/hooks/prepare-commit-msg.sample new file mode 100755 index 000000000..f093a02ec --- /dev/null +++ b/test-fixtures/basic-git/DOTgit/hooks/prepare-commit-msg.sample @@ -0,0 +1,36 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first comments out the +# "Conflicts:" part of a merge commit. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +case "$2,$3" in + merge,) + /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; + +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$1" ;; + + *) ;; +esac + +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" diff --git a/test-fixtures/basic-git/DOTgit/hooks/update.sample b/test-fixtures/basic-git/DOTgit/hooks/update.sample new file mode 100755 index 000000000..d84758373 --- /dev/null +++ b/test-fixtures/basic-git/DOTgit/hooks/update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to blocks unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --bool hooks.allowunannotated) +allowdeletebranch=$(git config --bool hooks.allowdeletebranch) +denycreatebranch=$(git config --bool hooks.denycreatebranch) +allowdeletetag=$(git config --bool hooks.allowdeletetag) +allowmodifytag=$(git config --bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero="0000000000000000000000000000000000000000" +if [ "$newrev" = "$zero" ]; then + newrev_type=delete +else + newrev_type=$(git cat-file -t $newrev) +fi + +case "$refname","$newrev_type" in + refs/tags/*,commit) + # un-annotated tag + short_refname=${refname##refs/tags/} + if [ "$allowunannotated" != "true" ]; then + echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/test-fixtures/basic-git/DOTgit/index b/test-fixtures/basic-git/DOTgit/index new file mode 100644 index 0000000000000000000000000000000000000000..99b358e271f626bc5ee9ad355678185228a62198 GIT binary patch literal 184 zcmZ?q402{*U|<5_5CyjoAk6@y85tN@7-Gyi7#JFtFfcHF1xkqkvA%^ta$&^xl0|%X z4(yllW#6#B{fI3Cdv0Q8o?b~BNWbcB4 1410850637 -0700 commit (initial): A commit +497bc37401eb3c9b11865b1768725b64066eccee 243f0fc5c4e586d1a3daa54c981b6f34e9ab1085 Mitchell Hashimoto 1410886526 -0700 commit: tag1 +243f0fc5c4e586d1a3daa54c981b6f34e9ab1085 1f31e97f053caeb5d6b7bffa3faf82941c99efa2 Mitchell Hashimoto 1410886536 -0700 commit: remove tag1 +1f31e97f053caeb5d6b7bffa3faf82941c99efa2 1f31e97f053caeb5d6b7bffa3faf82941c99efa2 Mitchell Hashimoto 1410886909 -0700 checkout: moving from master to test-branch +1f31e97f053caeb5d6b7bffa3faf82941c99efa2 7b7614f8759ac8b5e4b02be65ad8e2667be6dd87 Mitchell Hashimoto 1410886913 -0700 commit: Branch +7b7614f8759ac8b5e4b02be65ad8e2667be6dd87 1f31e97f053caeb5d6b7bffa3faf82941c99efa2 Mitchell Hashimoto 1410886916 -0700 checkout: moving from test-branch to master +1f31e97f053caeb5d6b7bffa3faf82941c99efa2 146492b04efe0aae2b8288c5c0aef6a951030fde Mitchell Hashimoto 1411767116 -0700 commit: add subdir diff --git a/test-fixtures/basic-git/DOTgit/logs/refs/heads/master b/test-fixtures/basic-git/DOTgit/logs/refs/heads/master new file mode 100644 index 000000000..f30b1d9d3 --- /dev/null +++ b/test-fixtures/basic-git/DOTgit/logs/refs/heads/master @@ -0,0 +1,4 @@ +0000000000000000000000000000000000000000 497bc37401eb3c9b11865b1768725b64066eccee Mitchell Hashimoto 1410850637 -0700 commit (initial): A commit +497bc37401eb3c9b11865b1768725b64066eccee 243f0fc5c4e586d1a3daa54c981b6f34e9ab1085 Mitchell Hashimoto 1410886526 -0700 commit: tag1 +243f0fc5c4e586d1a3daa54c981b6f34e9ab1085 1f31e97f053caeb5d6b7bffa3faf82941c99efa2 Mitchell Hashimoto 1410886536 -0700 commit: remove tag1 +1f31e97f053caeb5d6b7bffa3faf82941c99efa2 146492b04efe0aae2b8288c5c0aef6a951030fde Mitchell Hashimoto 1411767116 -0700 commit: add subdir diff --git a/test-fixtures/basic-git/DOTgit/logs/refs/heads/test-branch b/test-fixtures/basic-git/DOTgit/logs/refs/heads/test-branch new file mode 100644 index 000000000..937067a2a --- /dev/null +++ b/test-fixtures/basic-git/DOTgit/logs/refs/heads/test-branch @@ -0,0 +1,2 @@ +0000000000000000000000000000000000000000 1f31e97f053caeb5d6b7bffa3faf82941c99efa2 Mitchell Hashimoto 1410886909 -0700 branch: Created from HEAD +1f31e97f053caeb5d6b7bffa3faf82941c99efa2 7b7614f8759ac8b5e4b02be65ad8e2667be6dd87 Mitchell Hashimoto 1410886913 -0700 commit: Branch diff --git a/test-fixtures/basic-git/DOTgit/objects/14/6492b04efe0aae2b8288c5c0aef6a951030fde b/test-fixtures/basic-git/DOTgit/objects/14/6492b04efe0aae2b8288c5c0aef6a951030fde new file mode 100644 index 0000000000000000000000000000000000000000..2a713ec7cf4938e12e84fc7f0aa6231c45c9a946 GIT binary patch literal 170 zcmV;b09F5Z0j^8 zP`O@wiXbUzWpDU7*Rk7Mo3iS**>8TVt&chE=W*a|=k?F7_s(rQ%jjPgA^;%)p#5;< YoH_I;rvA&A;Zp45nM>Qv3xnxYMYiKtt^fc4 literal 0 HcmV?d00001 diff --git a/test-fixtures/basic-git/DOTgit/objects/1d/3d6744266642cb7623e2c678c33c77b075c49f b/test-fixtures/basic-git/DOTgit/objects/1d/3d6744266642cb7623e2c678c33c77b075c49f new file mode 100644 index 0000000000000000000000000000000000000000..2518fd6ac51a5a3b76ebf290ea8866b81746de0c GIT binary patch literal 84 zcmV-a0IUCa0V^p=O;s?nU@$Z=Ff%bx$W6@5(<@11urNq2jQC!%i0{sU{W8An8}_#! qu{ALO0)^tzq?F7eh90TPQ}Q#mUEq)YdhE=+UK>u`%ew&du^oyiI4LLq literal 0 HcmV?d00001 diff --git a/test-fixtures/basic-git/DOTgit/objects/1f/31e97f053caeb5d6b7bffa3faf82941c99efa2 b/test-fixtures/basic-git/DOTgit/objects/1f/31e97f053caeb5d6b7bffa3faf82941c99efa2 new file mode 100644 index 0000000000000000000000000000000000000000..5793a840b76235dafcb144ed66d1b8d274ed76bf GIT binary patch literal 167 zcmV;Y09gNc0jde)V4 V5S{)q#u}CSCf;!s>;oX7Q=N++Phyo>KhY@))4ka?%IV3*@oJ1_}!Me>)YD$BHN~~{94vhTh*t2;A_M3v&+5kx(&IOvjz`l>`{yQXvi4V SwJ)0dC8iqRL45!N?@<9%)ls?t literal 0 HcmV?d00001 diff --git a/test-fixtures/basic-git/DOTgit/objects/38/30637158f774a20edcc0bf1c4d07b0bf87c43d b/test-fixtures/basic-git/DOTgit/objects/38/30637158f774a20edcc0bf1c4d07b0bf87c43d new file mode 100644 index 0000000000000000000000000000000000000000..ef8ebf7282975b2da2da3cfe6cef1477fcca395c GIT binary patch literal 59 zcmV-B0L1@z0ZYosPf{>3XHZt~NX^N~=iAXD~D{Ff%bx$W6@5(<@11urNq2jQC!%i0{sU{W8An8}_#! qu|-l6pH!5Xmz)7o`E2f^*_$q2bN24mTvr}-x_U-g&xG EByA >/)}kT….ѸSl HjqH %D \ No newline at end of file diff --git a/test-fixtures/basic-git/DOTgit/objects/7b/7614f8759ac8b5e4b02be65ad8e2667be6dd87 b/test-fixtures/basic-git/DOTgit/objects/7b/7614f8759ac8b5e4b02be65ad8e2667be6dd87 new file mode 100644 index 000000000..abe281a74 --- /dev/null +++ b/test-fixtures/basic-git/DOTgit/objects/7b/7614f8759ac8b5e4b02be65ad8e2667be6dd87 @@ -0,0 +1,2 @@ +xK +0aYE6`I7#'.&FbܿEpN?'Zg#r->l&`&Qdр.Y:nKR#aT&"(s23(2Ru7xi?򨰬Sj̥{G`k-S \ No newline at end of file diff --git a/test-fixtures/basic-git/DOTgit/objects/8c/1a79ca1f98b6d00f5bf5c6cc9e8d3c092dd3ba b/test-fixtures/basic-git/DOTgit/objects/8c/1a79ca1f98b6d00f5bf5c6cc9e8d3c092dd3ba new file mode 100644 index 0000000000000000000000000000000000000000..656ae8e33e7cb6dd2220cec705ebf707caad03c0 GIT binary patch literal 51 zcmbc HOYk88vc(gk literal 0 HcmV?d00001 diff --git a/test-fixtures/basic-git/DOTgit/objects/96/43088174e25a9bd91c27970a580af0085c9f32 b/test-fixtures/basic-git/DOTgit/objects/96/43088174e25a9bd91c27970a580af0085c9f32 new file mode 100644 index 0000000000000000000000000000000000000000..387943288da570c5373bc4f0cb7b8005536653e7 GIT binary patch literal 52 zcmV-40L%Y)0V^p=O;s>9WiT`_Ff%bx$W6@5(<@11urNq2jQC!%i0{sU{W8An8}_#! Ku>}Anzz?PeB^DI` literal 0 HcmV?d00001 diff --git a/test-fixtures/basic-git/DOTgit/objects/b7/757b6a3696ad036e9aa2f5b4856d09e7f17993 b/test-fixtures/basic-git/DOTgit/objects/b7/757b6a3696ad036e9aa2f5b4856d09e7f17993 new file mode 100644 index 0000000000000000000000000000000000000000..10192566594b6bfa1e9d04c558444006bb2f27fe GIT binary patch literal 82 zcmV-Y0ImOc0V^p=O;s>AWiT`_Ff%bx$W6@5(<@11urNq2jQC!%i0{sU{W8An8}_#! ou|-l6Uy_(^2vYZK?xWe8E?#r??$%sa9(Ci;lb+y-0LsiEdb9E;RsaA1 literal 0 HcmV?d00001 diff --git a/test-fixtures/basic-git/DOTgit/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 b/test-fixtures/basic-git/DOTgit/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 new file mode 100644 index 0000000000000000000000000000000000000000..711223894375fe1186ac5bfffdc48fb1fa1e65cc GIT binary patch literal 15 Wcmb=^>}vhYRG!~=>ejxFxg7V^;JV-L`jXd&PhOer=+rTh@j}%%hBW{9?W+v?SN{IR z{@{bE57=&yOBoo7fp{vI#Q^aG$jQgzX7-lU|K9k|T86DEAkBO8@7N_!HDIb@PI3YR zv#)^P79V5JjV5N>P0anhjJ6qgZ}Zq_;$`M-WN75M$-vNWlaJ394$K# z%*;#-O*U-UU|?zE~8+H~68-ISJn6r11$%Jn~3hUUC O%+xn%FqHDjM*;v@nt`|g literal 0 HcmV?d00001 diff --git a/test-fixtures/basic-hg/.hg/store/00manifest.i b/test-fixtures/basic-hg/.hg/store/00manifest.i new file mode 100644 index 0000000000000000000000000000000000000000..e35c6bf121b04e9a520ae6fcb95a06e381132f86 GIT binary patch literal 246 zcmZQzWME`~03#q}2xT+;hXRIf+oy9R`GtjZS|7jmaf#B=uR&8JsyZaZ;PC)>%p#d_9?E$&b?Fbd>K5VinW9-mZ{n3tRZc7ugMN~&p! mp|ORzsfDSDVX|qOfvJ(1X<~|@iAiFTWm=+zS&Csw5*Gmeyggk2 literal 0 HcmV?d00001 diff --git a/test-fixtures/basic-hg/.hg/store/data/main.tf.i b/test-fixtures/basic-hg/.hg/store/data/main.tf.i new file mode 100644 index 0000000000000000000000000000000000000000..f45ddc33f19db6ef2a8f723ce4e36e10db12a436 GIT binary patch literal 112 zcmZQzWME`~00SVU4`nm_hk{)i%ZqQMPS2Zr>zb=wCx2OxuW=Ym4TvsPR`5v8$;s#9 n%FRzH%}G^IO3TkzQmE!q0D|KD(xT*41zQCrJ$;ZcS1lI+ym=or literal 0 HcmV?d00001 diff --git a/test-fixtures/basic-hg/.hg/store/data/main__branch.tf.i b/test-fixtures/basic-hg/.hg/store/data/main__branch.tf.i new file mode 100644 index 0000000000000000000000000000000000000000..a6bdf46f1091be151835fda73ea8ccb9f8c70c21 GIT binary patch literal 64 ocmZQzWME{#1dRWoUQPrLGn6+WD*P;%#K*02?S1MgRZ+ literal 0 HcmV?d00001 diff --git a/test-fixtures/basic-hg/.hg/store/fncache b/test-fixtures/basic-hg/.hg/store/fncache new file mode 100644 index 000000000..a1babe068 --- /dev/null +++ b/test-fixtures/basic-hg/.hg/store/fncache @@ -0,0 +1,2 @@ +data/main.tf.i +data/main_branch.tf.i diff --git a/test-fixtures/basic-hg/.hg/store/phaseroots b/test-fixtures/basic-hg/.hg/store/phaseroots new file mode 100644 index 000000000..a08565294 --- /dev/null +++ b/test-fixtures/basic-hg/.hg/store/phaseroots @@ -0,0 +1 @@ +1 dcaed7754d58264cb9a5916215a5442377307bd1 diff --git a/test-fixtures/basic-hg/.hg/store/undo b/test-fixtures/basic-hg/.hg/store/undo new file mode 100644 index 0000000000000000000000000000000000000000..cf2be297d7083584b62cd4d06b4f2bb835f193ab GIT binary patch literal 59 zcmYdEEJ@VQP0Y-TPbx~xOU}?MNz=<@FyJyU$W6@4OiL{;0dfruO@IQ)8Hsu6sX6)S JAQ5vjE&xq(5=sC7 literal 0 HcmV?d00001 diff --git a/test-fixtures/basic-hg/.hg/store/undo.phaseroots b/test-fixtures/basic-hg/.hg/store/undo.phaseroots new file mode 100644 index 000000000..a08565294 --- /dev/null +++ b/test-fixtures/basic-hg/.hg/store/undo.phaseroots @@ -0,0 +1 @@ +1 dcaed7754d58264cb9a5916215a5442377307bd1 diff --git a/test-fixtures/basic-hg/.hg/undo.bookmarks b/test-fixtures/basic-hg/.hg/undo.bookmarks new file mode 100644 index 000000000..e69de29bb diff --git a/test-fixtures/basic-hg/.hg/undo.branch b/test-fixtures/basic-hg/.hg/undo.branch new file mode 100644 index 000000000..a81bc2dd2 --- /dev/null +++ b/test-fixtures/basic-hg/.hg/undo.branch @@ -0,0 +1 @@ +test-branch \ No newline at end of file diff --git a/test-fixtures/basic-hg/.hg/undo.desc b/test-fixtures/basic-hg/.hg/undo.desc new file mode 100644 index 000000000..d678f64de --- /dev/null +++ b/test-fixtures/basic-hg/.hg/undo.desc @@ -0,0 +1,2 @@ +1 +commit diff --git a/test-fixtures/basic-hg/.hg/undo.dirstate b/test-fixtures/basic-hg/.hg/undo.dirstate new file mode 100644 index 0000000000000000000000000000000000000000..62e4ca2e911f538d8040be23a460d47b8f786da2 GIT binary patch literal 95 zcmcb!?s}" for the root +// tree and then the module name given for any children. +func (t *Tree) Name() string { + if t.name == "" { + return RootName + } + + return t.name +} + +// Load loads the configuration of the entire tree. +// +// The parameters are used to tell the tree where to find modules and +// whether it can download/update modules along the way. +// +// Calling this multiple times will reload the tree. +// +// Various semantic-like checks are made along the way of loading since +// module trees inherently require the configuration to be in a reasonably +// sane state: no circular dependencies, proper module sources, etc. A full +// suite of validations can be done by running Validate (after loading). +func (t *Tree) Load(s Storage, mode GetMode) error { + t.lock.Lock() + defer t.lock.Unlock() + + // Reset the children if we have any + t.children = nil + + modules := t.Modules() + children := make(map[string]*Tree) + + // Go through all the modules and get the directory for them. + for _, m := range modules { + if _, ok := children[m.Name]; ok { + return fmt.Errorf( + "module %s: duplicated. module names must be unique", m.Name) + } + + // Determine the path to this child + path := make([]string, len(t.path), len(t.path)+1) + copy(path, t.path) + path = append(path, m.Name) + + // Split out the subdir if we have one + source, subDir := getDirSubdir(m.Source) + + source, err := Detect(source, t.config.Dir) + if err != nil { + return fmt.Errorf("module %s: %s", m.Name, err) + } + + // Check if the detector introduced something new. + source, subDir2 := getDirSubdir(source) + if subDir2 != "" { + subDir = filepath.Join(subDir2, subDir) + } + + // Get the directory where this module is so we can load it + key := strings.Join(path, ".") + key = "root." + key + dir, ok, err := getStorage(s, key, source, mode) + if err != nil { + return err + } + if !ok { + return fmt.Errorf( + "module %s: not found, may need to be downloaded", m.Name) + } + + // If we have a subdirectory, then merge that in + if subDir != "" { + dir = filepath.Join(dir, subDir) + } + + // Load the configurations.Dir(source) + children[m.Name], err = NewTreeModule(m.Name, dir) + if err != nil { + return fmt.Errorf( + "module %s: %s", m.Name, err) + } + + // Set the path of this child + children[m.Name].path = path + } + + // Go through all the children and load them. + for _, c := range children { + if err := c.Load(s, mode); err != nil { + return err + } + } + + // Set our tree up + t.children = children + + return nil +} + +// Path is the full path to this tree. +func (t *Tree) Path() []string { + return t.path +} + +// String gives a nice output to describe the tree. +func (t *Tree) String() string { + var result bytes.Buffer + path := strings.Join(t.path, ", ") + if path != "" { + path = fmt.Sprintf(" (path: %s)", path) + } + result.WriteString(t.Name() + path + "\n") + + cs := t.Children() + if cs == nil { + result.WriteString(" not loaded") + } else { + // Go through each child and get its string value, then indent it + // by two. + for _, c := range cs { + r := strings.NewReader(c.String()) + scanner := bufio.NewScanner(r) + for scanner.Scan() { + result.WriteString(" ") + result.WriteString(scanner.Text()) + result.WriteString("\n") + } + } + } + + return result.String() +} + +// Validate does semantic checks on the entire tree of configurations. +// +// This will call the respective config.Config.Validate() functions as well +// as verifying things such as parameters/outputs between the various modules. +// +// Load must be called prior to calling Validate or an error will be returned. +func (t *Tree) Validate() error { + if !t.Loaded() { + return fmt.Errorf("tree must be loaded before calling Validate") + } + + // If something goes wrong, here is our error template + newErr := &TreeError{Name: []string{t.Name()}} + + // Validate our configuration first. + if err := t.config.Validate(); err != nil { + newErr.Err = err + return newErr + } + + // Get the child trees + children := t.Children() + + // Validate all our children + for _, c := range children { + err := c.Validate() + if err == nil { + continue + } + + verr, ok := err.(*TreeError) + if !ok { + // Unknown error, just return... + return err + } + + // Append ourselves to the error and then return + verr.Name = append(verr.Name, t.Name()) + return verr + } + + // Go over all the modules and verify that any parameters are valid + // variables into the module in question. + for _, m := range t.config.Modules { + tree, ok := children[m.Name] + if !ok { + // This should never happen because Load watches us + panic("module not found in children: " + m.Name) + } + + // Build the variables that the module defines + requiredMap := make(map[string]struct{}) + varMap := make(map[string]struct{}) + for _, v := range tree.config.Variables { + varMap[v.Name] = struct{}{} + + if v.Required() { + requiredMap[v.Name] = struct{}{} + } + } + + // Compare to the keys in our raw config for the module + for k, _ := range m.RawConfig.Raw { + if _, ok := varMap[k]; !ok { + newErr.Err = fmt.Errorf( + "module %s: %s is not a valid parameter", + m.Name, k) + return newErr + } + + // Remove the required + delete(requiredMap, k) + } + + // If we have any required left over, they aren't set. + for k, _ := range requiredMap { + newErr.Err = fmt.Errorf( + "module %s: required variable %s not set", + m.Name, k) + return newErr + } + } + + // Go over all the variables used and make sure that any module + // variables represent outputs properly. + for source, vs := range t.config.InterpolatedVariables() { + for _, v := range vs { + mv, ok := v.(*config.ModuleVariable) + if !ok { + continue + } + + tree, ok := children[mv.Name] + if !ok { + // This should never happen because Load watches us + panic("module not found in children: " + mv.Name) + } + + found := false + for _, o := range tree.config.Outputs { + if o.Name == mv.Field { + found = true + break + } + } + if !found { + newErr.Err = fmt.Errorf( + "%s: %s is not a valid output for module %s", + source, mv.Field, mv.Name) + return newErr + } + } + } + + return nil +} + +// TreeError is an error returned by Tree.Validate if an error occurs +// with validation. +type TreeError struct { + Name []string + Err error +} + +func (e *TreeError) Error() string { + // Build up the name + var buf bytes.Buffer + for _, n := range e.Name { + buf.WriteString(n) + buf.WriteString(".") + } + buf.Truncate(buf.Len() - 1) + + // Format the value + return fmt.Sprintf("module %s: %s", buf.String(), e.Err) +} diff --git a/tree_gob.go b/tree_gob.go new file mode 100644 index 000000000..fcf5be254 --- /dev/null +++ b/tree_gob.go @@ -0,0 +1,57 @@ +package getter + +import ( + "bytes" + "encoding/gob" + + "github.com/hashicorp/terraform/config" +) + +func (t *Tree) GobDecode(bs []byte) error { + t.lock.Lock() + defer t.lock.Unlock() + + // Decode the gob data + var data treeGob + dec := gob.NewDecoder(bytes.NewReader(bs)) + if err := dec.Decode(&data); err != nil { + return err + } + + // Set the fields + t.name = data.Name + t.config = data.Config + t.children = data.Children + t.path = data.Path + + return nil +} + +func (t *Tree) GobEncode() ([]byte, error) { + data := &treeGob{ + Config: t.config, + Children: t.children, + Name: t.name, + Path: t.path, + } + + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + if err := enc.Encode(data); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// treeGob is used as a structure to Gob encode a tree. +// +// This structure is private so it can't be referenced but the fields are +// public, allowing Gob to properly encode this. When we decode this, we are +// able to turn it into a Tree. +type treeGob struct { + Config *config.Config + Children map[string]*Tree + Name string + Path []string +} diff --git a/tree_gob_test.go b/tree_gob_test.go new file mode 100644 index 000000000..7f2470e9c --- /dev/null +++ b/tree_gob_test.go @@ -0,0 +1,37 @@ +package getter + +import ( + "bytes" + "encoding/gob" + "strings" + "testing" +) + +func TestTreeEncodeDecodeGob(t *testing.T) { + storage := testStorage(t) + tree := NewTree("", testConfig(t, "basic")) + + // This should get things + if err := tree.Load(storage, GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + // Encode it. + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + if err := enc.Encode(tree); err != nil { + t.Fatalf("err: %s", err) + } + + dec := gob.NewDecoder(&buf) + var actual Tree + if err := dec.Decode(&actual); err != nil { + t.Fatalf("err: %s", err) + } + + actualStr := strings.TrimSpace(actual.String()) + expectedStr := strings.TrimSpace(tree.String()) + if actualStr != expectedStr { + t.Fatalf("\n%s\n\nexpected:\n\n%s", actualStr, expectedStr) + } +} diff --git a/tree_test.go b/tree_test.go new file mode 100644 index 000000000..519c7bfc3 --- /dev/null +++ b/tree_test.go @@ -0,0 +1,309 @@ +package getter + +import ( + "reflect" + "strings" + "testing" +) + +func TestTreeChild(t *testing.T) { + storage := testStorage(t) + tree := NewTree("", testConfig(t, "child")) + if err := tree.Load(storage, GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + // Should be able to get the root child + if c := tree.Child([]string{}); c == nil { + t.Fatal("should not be nil") + } else if c.Name() != "root" { + t.Fatalf("bad: %#v", c.Name()) + } else if !reflect.DeepEqual(c.Path(), []string(nil)) { + t.Fatalf("bad: %#v", c.Path()) + } + + // Should be able to get the root child + if c := tree.Child(nil); c == nil { + t.Fatal("should not be nil") + } else if c.Name() != "root" { + t.Fatalf("bad: %#v", c.Name()) + } else if !reflect.DeepEqual(c.Path(), []string(nil)) { + t.Fatalf("bad: %#v", c.Path()) + } + + // Should be able to get the foo child + if c := tree.Child([]string{"foo"}); c == nil { + t.Fatal("should not be nil") + } else if c.Name() != "foo" { + t.Fatalf("bad: %#v", c.Name()) + } else if !reflect.DeepEqual(c.Path(), []string{"foo"}) { + t.Fatalf("bad: %#v", c.Path()) + } + + // Should be able to get the nested child + if c := tree.Child([]string{"foo", "bar"}); c == nil { + t.Fatal("should not be nil") + } else if c.Name() != "bar" { + t.Fatalf("bad: %#v", c.Name()) + } else if !reflect.DeepEqual(c.Path(), []string{"foo", "bar"}) { + t.Fatalf("bad: %#v", c.Path()) + } +} + +func TestTreeLoad(t *testing.T) { + storage := testStorage(t) + tree := NewTree("", testConfig(t, "basic")) + + if tree.Loaded() { + t.Fatal("should not be loaded") + } + + // This should error because we haven't gotten things yet + if err := tree.Load(storage, GetModeNone); err == nil { + t.Fatal("should error") + } + + if tree.Loaded() { + t.Fatal("should not be loaded") + } + + // This should get things + if err := tree.Load(storage, GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + if !tree.Loaded() { + t.Fatal("should be loaded") + } + + // This should no longer error + if err := tree.Load(storage, GetModeNone); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(tree.String()) + expected := strings.TrimSpace(treeLoadStr) + if actual != expected { + t.Fatalf("bad: \n\n%s", actual) + } +} + +func TestTreeLoad_duplicate(t *testing.T) { + storage := testStorage(t) + tree := NewTree("", testConfig(t, "dup")) + + if tree.Loaded() { + t.Fatal("should not be loaded") + } + + // This should get things + if err := tree.Load(storage, GetModeGet); err == nil { + t.Fatalf("should error") + } +} + +func TestTreeLoad_parentRef(t *testing.T) { + storage := testStorage(t) + tree := NewTree("", testConfig(t, "basic-parent")) + + if tree.Loaded() { + t.Fatal("should not be loaded") + } + + // This should error because we haven't gotten things yet + if err := tree.Load(storage, GetModeNone); err == nil { + t.Fatal("should error") + } + + if tree.Loaded() { + t.Fatal("should not be loaded") + } + + // This should get things + if err := tree.Load(storage, GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + if !tree.Loaded() { + t.Fatal("should be loaded") + } + + // This should no longer error + if err := tree.Load(storage, GetModeNone); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(tree.String()) + expected := strings.TrimSpace(treeLoadParentStr) + if actual != expected { + t.Fatalf("bad: \n\n%s", actual) + } +} + +func TestTreeLoad_subdir(t *testing.T) { + storage := testStorage(t) + tree := NewTree("", testConfig(t, "basic-subdir")) + + if tree.Loaded() { + t.Fatal("should not be loaded") + } + + // This should error because we haven't gotten things yet + if err := tree.Load(storage, GetModeNone); err == nil { + t.Fatal("should error") + } + + if tree.Loaded() { + t.Fatal("should not be loaded") + } + + // This should get things + if err := tree.Load(storage, GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + if !tree.Loaded() { + t.Fatal("should be loaded") + } + + // This should no longer error + if err := tree.Load(storage, GetModeNone); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(tree.String()) + expected := strings.TrimSpace(treeLoadSubdirStr) + if actual != expected { + t.Fatalf("bad: \n\n%s", actual) + } +} + +func TestTreeModules(t *testing.T) { + tree := NewTree("", testConfig(t, "basic")) + actual := tree.Modules() + + expected := []*Module{ + &Module{Name: "foo", Source: "./foo"}, + } + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } +} + +func TestTreeName(t *testing.T) { + tree := NewTree("", testConfig(t, "basic")) + actual := tree.Name() + + if actual != RootName { + t.Fatalf("bad: %#v", actual) + } +} + +func TestTreeValidate_badChild(t *testing.T) { + tree := NewTree("", testConfig(t, "validate-child-bad")) + + if err := tree.Load(testStorage(t), GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + if err := tree.Validate(); err == nil { + t.Fatal("should error") + } +} + +func TestTreeValidate_badChildOutput(t *testing.T) { + tree := NewTree("", testConfig(t, "validate-bad-output")) + + if err := tree.Load(testStorage(t), GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + if err := tree.Validate(); err == nil { + t.Fatal("should error") + } +} + +func TestTreeValidate_badChildOutputToModule(t *testing.T) { + tree := NewTree("", testConfig(t, "validate-bad-output-to-module")) + + if err := tree.Load(testStorage(t), GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + if err := tree.Validate(); err == nil { + t.Fatal("should error") + } +} + +func TestTreeValidate_badChildVar(t *testing.T) { + tree := NewTree("", testConfig(t, "validate-bad-var")) + + if err := tree.Load(testStorage(t), GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + if err := tree.Validate(); err == nil { + t.Fatal("should error") + } +} + +func TestTreeValidate_badRoot(t *testing.T) { + tree := NewTree("", testConfig(t, "validate-root-bad")) + + if err := tree.Load(testStorage(t), GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + if err := tree.Validate(); err == nil { + t.Fatal("should error") + } +} + +func TestTreeValidate_good(t *testing.T) { + tree := NewTree("", testConfig(t, "validate-child-good")) + + if err := tree.Load(testStorage(t), GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + if err := tree.Validate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestTreeValidate_notLoaded(t *testing.T) { + tree := NewTree("", testConfig(t, "basic")) + + if err := tree.Validate(); err == nil { + t.Fatal("should error") + } +} + +func TestTreeValidate_requiredChildVar(t *testing.T) { + tree := NewTree("", testConfig(t, "validate-required-var")) + + if err := tree.Load(testStorage(t), GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + if err := tree.Validate(); err == nil { + t.Fatal("should error") + } +} + +const treeLoadStr = ` +root + foo (path: foo) +` + +const treeLoadParentStr = ` +root + a (path: a) + b (path: a, b) +` +const treeLoadSubdirStr = ` +root + foo (path: foo) + bar (path: foo, bar) +`