cmd/gorelease: use CreateFromVCS instead of CreateFromDir This will ignore gitignored files during the zip file creation, which means gitignored files won't be included in the analysis. Fixes golang/go#37413 Change-Id: Id5df46408a48e0be53157d95333ef3c2e02765bc Reviewed-on: https://go-review.googlesource.com/c/exp/+/341930 Trust: Jean de Klerk <deklerk@google.com> Run-TryBot: Jean de Klerk <deklerk@google.com> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Jay Conrod <jayconrod@google.com>
diff --git a/cmd/gorelease/gorelease.go b/cmd/gorelease/gorelease.go index 825bf88..340d81f 100644 --- a/cmd/gorelease/gorelease.go +++ b/cmd/gorelease/gorelease.go
@@ -387,7 +387,7 @@ // as if it were published and downloaded. We'll detect any errors that would // occur (for example, invalid file names). We avoid loading it as the // main module. - tmpModRoot, err := copyModuleToTempDir(m.modPath, m.modRoot) + tmpModRoot, err := copyModuleToTempDir(repoRoot, m.modPath, m.modRoot) if err != nil { return moduleInfo{}, err } @@ -872,7 +872,7 @@ // An error is returned if the module contains any files or directories that // can't be included in a module zip file (due to special characters, // excessive sizes, etc.). -func copyModuleToTempDir(modPath, modRoot string) (dir string, err error) { +func copyModuleToTempDir(repoRoot, modPath, modRoot string) (dir string, err error) { // Generate a fake version consistent with modPath. We need a canonical // version to create a zip file. version := "v0.0.0-gorelease" @@ -902,13 +902,26 @@ } }() - if err := zip.CreateFromDir(zipFile, m, modRoot); err != nil { - var e zip.FileErrorList - if errors.As(err, &e) { - return "", e + var fallbackToDir bool + if repoRoot != "" { + var err error + fallbackToDir, err = tryCreateFromVCS(zipFile, m, modRoot, repoRoot) + if err != nil { + return "", err } - return "", err } + + if repoRoot == "" || fallbackToDir { + // Not a recognised repo: fall back to creating from dir. + if err := zip.CreateFromDir(zipFile, m, modRoot); err != nil { + var e zip.FileErrorList + if errors.As(err, &e) { + return "", e + } + return "", err + } + } + if err := zipFile.Close(); err != nil { return "", err } @@ -918,6 +931,40 @@ return dir, nil } +// tryCreateFromVCS tries to create a module zip file from VCS. If it succeeds, +// it returns fallBackToDir false and a nil err. If it fails in a recoverable +// way, it returns fallBackToDir true and a nil err. If it fails in an +// unrecoverable way, it returns a non-nil err. +func tryCreateFromVCS(zipFile io.Writer, m module.Version, modRoot, repoRoot string) (fallbackToDir bool, _ error) { + // We recognised a repo: create from VCS. + if !hasFilePathPrefix(modRoot, repoRoot) { + panic(fmt.Sprintf("repo root %q is not a prefix of mod root %q", repoRoot, modRoot)) + } + hasUncommitted, err := hasGitUncommittedChanges(repoRoot) + if err != nil { + // Fallback to CreateFromDir. + return true, nil + } + if hasUncommitted { + return false, fmt.Errorf("repo %s has uncommitted changes", repoRoot) + } + modRel := filepath.ToSlash(trimFilePathPrefix(modRoot, repoRoot)) + if err := zip.CreateFromVCS(zipFile, m, repoRoot, "HEAD", modRel); err != nil { + var fel zip.FileErrorList + if errors.As(err, &fel) { + return false, fel + } + var uve *zip.UnrecognizedVCSError + if errors.As(err, &uve) { + // Fallback to CreateFromDir. + return true, nil + } + return false, err + } + // Success! + return false, nil +} + // downloadModule downloads a specific version of a module to the // module cache using 'go mod download'. func downloadModule(ctx context.Context, m module.Version) (modRoot, goModPath string, err error) { @@ -1455,3 +1502,16 @@ // NOTE: the go.mod parser rejects invalid UTF-8, so we don't check that here. return rationale, true } + +// hasGitUncommittedChanges checks if the given directory has uncommitteed git +// changes. +func hasGitUncommittedChanges(dir string) (bool, error) { + stdout := &bytes.Buffer{} + cmd := exec.Command("git", "status", "--porcelain") + cmd.Dir = dir + cmd.Stdout = stdout + if err := cmd.Run(); err != nil { + return false, cleanCmdError(err) + } + return stdout.Len() != 0, nil +}
diff --git a/cmd/gorelease/gorelease_test.go b/cmd/gorelease/gorelease_test.go index 54087b8..96b3f9a 100644 --- a/cmd/gorelease/gorelease_test.go +++ b/cmd/gorelease/gorelease_test.go
@@ -15,6 +15,7 @@ "path/filepath" "strconv" "strings" + "sync" "testing" "golang.org/x/mod/module" @@ -26,6 +27,22 @@ updateGolden = flag.Bool("u", false, "update expected text in test files instead of failing") ) +var hasGitCache struct { + once sync.Once + found bool +} + +// hasGit reports whether the git executable exists on the PATH. +func hasGit() bool { + hasGitCache.once.Do(func() { + if _, err := exec.LookPath("git"); err != nil { + return + } + hasGitCache.found = true + }) + return hasGitCache.found +} + // prepareProxy creates a proxy dir and returns an associated ctx. // // proxyVersions must be a map of module version to true. If proxyVersions is @@ -130,6 +147,10 @@ // If it is not empty, each entry must be of the form <modpath>@v<version> // and exist in testdata/mod/. proxyVersions map[module.Version]bool + + // vcs is used to set the VCS that the root of the test should + // emulate. Allowed values are git, and hg. + vcs string } // readTest reads and parses a .test file with the given name. @@ -203,6 +224,8 @@ proxyVersions[mv] = true } t.proxyVersions = proxyVersions + case "vcs": + t.vcs = value default: return nil, fmt.Errorf("%s:%d: unknown key: %q", testPath, lineNum, key) } @@ -277,6 +300,33 @@ } } +func TestRelease_gitRepo_uncommittedChanges(t *testing.T) { + ctx := context.Background() + buf := &bytes.Buffer{} + releaseDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + + goModInit(t, releaseDir) + gitInit(t, releaseDir) + + // Create an uncommitted change. + bContents := `package b +const B = "b"` + if err := ioutil.WriteFile(filepath.Join(releaseDir, "b.go"), []byte(bContents), 0644); err != nil { + t.Fatal(err) + } + + success, err := runRelease(ctx, buf, releaseDir, nil) + if got, want := err.Error(), fmt.Sprintf("repo %s has uncommitted changes", releaseDir); got != want { + t.Errorf("runRelease:\ngot error:\n%q\nwant error\n%q", got, want) + } + if success { + t.Errorf("runRelease: expected failure, got success") + } +} + func testRelease(ctx context.Context, tests []*test, test *test) func(t *testing.T) { return func(t *testing.T) { if test.skip != "" { @@ -325,6 +375,21 @@ t.Fatal(err) } + switch test.vcs { + case "git": + // Convert testDir to a git repository with a single commit, to + // simulate a real user's module-in-a-git-repo. + gitInit(t, testDir) + case "hg": + // Convert testDir to a mercurial repository to simulate a real + // user's module-in-a-hg-repo. + hgInit(t, testDir) + case "": + // No VCS. + default: + t.Fatalf("unknown vcs %q", test.vcs) + } + // Generate the report and compare it against the expected text. var args []string if test.baseVersion != "" { @@ -373,3 +438,65 @@ } } } + +// hgInit initialises a directory as a mercurial repo. +func hgInit(t *testing.T, dir string) { + t.Helper() + + if err := os.Mkdir(filepath.Join(dir, ".hg"), 0777); err != nil { + t.Fatal(err) + } + + if err := ioutil.WriteFile(filepath.Join(dir, ".hg", "branch"), []byte("default"), 0777); err != nil { + t.Fatal(err) + } +} + +// gitInit initialises a directory as a git repo, and adds a simple commit. +func gitInit(t *testing.T, dir string) { + t.Helper() + + if !hasGit() { + t.Skip("PATH does not contain git") + } + + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + + for _, args := range [][]string{ + {"git", "init"}, + {"git", "checkout", "-b", "test"}, + {"git", "add", "-A"}, + {"git", "commit", "-m", "test"}, + } { + cmd := exec.Command(args[0], args[1:]...) + cmd.Dir = dir + cmd.Stdout = stdout + cmd.Stderr = stderr + if err := cmd.Run(); err != nil { + cmdArgs := strings.Join(args, " ") + t.Fatalf("%s\n%s\nerror running %q on dir %s: %v", stdout.String(), stderr.String(), cmdArgs, dir, err) + } + } +} + +// goModInit runs `go mod init` in the given directory. +func goModInit(t *testing.T, dir string) { + t.Helper() + + aContents := `package a +const A = "a"` + if err := ioutil.WriteFile(filepath.Join(dir, "a.go"), []byte(aContents), 0644); err != nil { + t.Fatal(err) + } + + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + cmd := exec.Command("go", "mod", "init", "example.com/uncommitted") + cmd.Stdout = stdout + cmd.Stderr = stderr + cmd.Dir = dir + if err := cmd.Run(); err != nil { + t.Fatalf("error running `go mod init`: %s, %v", stderr.String(), err) + } +}
diff --git a/cmd/gorelease/testdata/basic/v0_compatible_suggest_git.test b/cmd/gorelease/testdata/basic/v0_compatible_suggest_git.test new file mode 100644 index 0000000..080cafd --- /dev/null +++ b/cmd/gorelease/testdata/basic/v0_compatible_suggest_git.test
@@ -0,0 +1,16 @@ +mod=example.com/basic +version=v0.1.0 +base=v0.0.1 +proxyVersions=example.com/basic@v0.0.1 +vcs=git +-- want -- +# example.com/basic/a +## compatible changes +A2: added + +# example.com/basic/b +## compatible changes +package added + +# summary +Suggested version: v0.1.0
diff --git a/cmd/gorelease/testdata/basic/v0_compatible_suggest_hg.test b/cmd/gorelease/testdata/basic/v0_compatible_suggest_hg.test new file mode 100644 index 0000000..329ea88 --- /dev/null +++ b/cmd/gorelease/testdata/basic/v0_compatible_suggest_hg.test
@@ -0,0 +1,16 @@ +mod=example.com/basic +version=v0.1.0 +base=v0.0.1 +proxyVersions=example.com/basic@v0.0.1 +vcs=hg +-- want -- +# example.com/basic/a +## compatible changes +A2: added + +# example.com/basic/b +## compatible changes +package added + +# summary +Suggested version: v0.1.0
diff --git a/cmd/gorelease/testdata/errors/bad_filenames.test b/cmd/gorelease/testdata/errors/bad_filenames.test index 279740f..c8bf6e0 100644 --- a/cmd/gorelease/testdata/errors/bad_filenames.test +++ b/cmd/gorelease/testdata/errors/bad_filenames.test
@@ -2,10 +2,10 @@ dir=x base=none error=true +vcs=git -- want -- testdata/this_file_also_has_a_bad_filename'.txt: malformed file path "testdata/this_file_also_has_a_bad_filename'.txt": invalid char '\'' testdata/this_file_has_a_bad_filename'.txt: malformed file path "testdata/this_file_has_a_bad_filename'.txt": invalid char '\'' --- .git/HEAD -- -- x/go.mod -- module example.com/x
diff --git a/cmd/gorelease/testdata/patherrors/dup_roots_branch.test b/cmd/gorelease/testdata/patherrors/dup_roots_branch.test index 695d744..66f79bb 100644 --- a/cmd/gorelease/testdata/patherrors/dup_roots_branch.test +++ b/cmd/gorelease/testdata/patherrors/dup_roots_branch.test
@@ -1,8 +1,7 @@ dir=dup base=none success=false --- .git/empty -- -empty file to mark repository root +vcs=git -- dup/go.mod -- module example.com/dup/v2
diff --git a/cmd/gorelease/testdata/patherrors/dup_roots_dir.test b/cmd/gorelease/testdata/patherrors/dup_roots_dir.test index f369704..3898da4 100644 --- a/cmd/gorelease/testdata/patherrors/dup_roots_dir.test +++ b/cmd/gorelease/testdata/patherrors/dup_roots_dir.test
@@ -1,8 +1,7 @@ dir=dup/v2 base=none success=false --- .git/empty -- -empty file to mark repository root +vcs=git -- dup/go.mod -- module example.com/dup/v2
diff --git a/cmd/gorelease/testdata/patherrors/dup_roots_ok.test b/cmd/gorelease/testdata/patherrors/dup_roots_ok.test index bc71480..afd157c 100644 --- a/cmd/gorelease/testdata/patherrors/dup_roots_ok.test +++ b/cmd/gorelease/testdata/patherrors/dup_roots_ok.test
@@ -1,7 +1,6 @@ dir=dup/v2 base=none --- .git/empty -- -empty file to mark repository root +vcs=git -- dup/go.mod -- module example.com/dup
diff --git a/cmd/gorelease/testdata/patherrors/gopkginsub.test b/cmd/gorelease/testdata/patherrors/gopkginsub.test index 2cfcdd9..2c2fb7c 100644 --- a/cmd/gorelease/testdata/patherrors/gopkginsub.test +++ b/cmd/gorelease/testdata/patherrors/gopkginsub.test
@@ -2,6 +2,7 @@ base=none dir=yaml success=false +vcs=git -- want -- # diagnostics go.mod: go directive is missing @@ -11,6 +12,5 @@ Suggested version: v2.0.0 -- .mod -- module example.com/patherrors --- .git/HEAD -- -- yaml/go.mod -- module gopkg.in/yaml.v2
diff --git a/cmd/gorelease/testdata/patherrors/pathsub.test b/cmd/gorelease/testdata/patherrors/pathsub.test index 04dbdd6..000c02a 100644 --- a/cmd/gorelease/testdata/patherrors/pathsub.test +++ b/cmd/gorelease/testdata/patherrors/pathsub.test
@@ -2,6 +2,7 @@ dir=x base=none success=false +vcs=git -- want -- # diagnostics example.com/y: module path must end with "x", since it is in subdirectory "x" @@ -10,7 +11,6 @@ Suggested version: v0.1.0 -- .mod -- module example.com/patherrors --- .git/HEAD -- -- x/go.mod -- module example.com/y
diff --git a/cmd/gorelease/testdata/patherrors/pathsubv2.test b/cmd/gorelease/testdata/patherrors/pathsubv2.test index 76cc284..8ab2c80 100644 --- a/cmd/gorelease/testdata/patherrors/pathsubv2.test +++ b/cmd/gorelease/testdata/patherrors/pathsubv2.test
@@ -2,6 +2,7 @@ base=none dir=x success=false +vcs=git -- want -- # diagnostics example.com/y/v2: module path must end with "x" or "x/v2", since it is in subdirectory "x" @@ -10,7 +11,6 @@ Suggested version: v2.0.0 -- .mod -- module example.com/patherrors --- .git/HEAD -- -- x/go.mod -- module example.com/y/v2
diff --git a/cmd/gorelease/testdata/sub/nest.test b/cmd/gorelease/testdata/sub/nest.test index e8a2aae..c2a6436 100644 --- a/cmd/gorelease/testdata/sub/nest.test +++ b/cmd/gorelease/testdata/sub/nest.test
@@ -1,10 +1,10 @@ mod=example.com/sub/nest dir=nest base=v1.0.0 +vcs=git -- want -- # summary Suggested version: v1.0.1 (with tag nest/v1.0.1) --- .git/HEAD -- -- nest/go.mod -- module example.com/sub/nest
diff --git a/cmd/gorelease/testdata/sub/nest_v2.test b/cmd/gorelease/testdata/sub/nest_v2.test index 209029e..6d8bf0c 100644 --- a/cmd/gorelease/testdata/sub/nest_v2.test +++ b/cmd/gorelease/testdata/sub/nest_v2.test
@@ -1,10 +1,10 @@ mod=example.com/sub/nest/v2 dir=nest base=v2.0.0 +vcs=git -- want -- # summary Suggested version: v2.0.1 (with tag nest/v2.0.1) --- .git/HEAD -- -- nest/go.mod -- module example.com/sub/nest/v2
diff --git a/cmd/gorelease/testdata/sub/nest_v2_dir.test b/cmd/gorelease/testdata/sub/nest_v2_dir.test index 8d549e5..7e99c1e 100644 --- a/cmd/gorelease/testdata/sub/nest_v2_dir.test +++ b/cmd/gorelease/testdata/sub/nest_v2_dir.test
@@ -1,10 +1,10 @@ mod=example.com/sub/nest/v2 dir=nest/v2 base=v2.0.0 +vcs=git -- want -- # summary Suggested version: v2.0.1 (with tag nest/v2.0.1) --- .git/HEAD -- -- nest/v2/go.mod -- module example.com/sub/nest/v2
diff --git a/cmd/gorelease/testdata/sub/v2_dir.test b/cmd/gorelease/testdata/sub/v2_dir.test index feb690a..ed07eb9 100644 --- a/cmd/gorelease/testdata/sub/v2_dir.test +++ b/cmd/gorelease/testdata/sub/v2_dir.test
@@ -1,10 +1,10 @@ mod=example.com/sub/v2 dir=v2 base=v2.0.0 +vcs=git -- want -- # summary Suggested version: v2.0.1 --- .git/HEAD -- -- v2/go.mod -- module example.com/sub/v2
diff --git a/cmd/gorelease/testdata/sub/v2_root.test b/cmd/gorelease/testdata/sub/v2_root.test index ce6d68f..1a34c67 100644 --- a/cmd/gorelease/testdata/sub/v2_root.test +++ b/cmd/gorelease/testdata/sub/v2_root.test
@@ -1,9 +1,9 @@ mod=example.com/sub/v2 base=v2.0.0 +vcs=git -- want -- # summary Suggested version: v2.0.1 --- .git/HEAD -- -- go.mod -- module example.com/sub/v2
diff --git a/go.mod b/go.mod index e32b3f4..686f45b 100644 --- a/go.mod +++ b/go.mod
@@ -21,7 +21,7 @@ go.uber.org/zap v1.16.0 golang.org/x/image v0.0.0-20190802002840-cff245a6509b golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f - golang.org/x/mod v0.4.2 + golang.org/x/mod v0.5.1-0.20210830214625-1b1db11ec8f4 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 golang.org/x/tools v0.1.0 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
diff --git a/go.sum b/go.sum index 5d70f5f..6c1ce5d 100644 --- a/go.sum +++ b/go.sum
@@ -307,8 +307,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1-0.20210830214625-1b1db11ec8f4 h1:7Qds88gNaRx0Dz/1wOwXlR7asekh1B1u26wEwN6FcEI= +golang.org/x/mod v0.5.1-0.20210830214625-1b1db11ec8f4/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=