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=