zip: add ErrUnrecognizedVCS error, allowing fallback behavior

Add ErrUnrecognizedVCS, which allows calling functions (such as gorelease) to
fallback: usually to CreateFromDir.

Updates golang/go#37413

Change-Id: I846f72b1ce22bfc699e8cd83b28ea4529e73d6e9
Reviewed-on: https://go-review.googlesource.com/c/mod/+/345730
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/zip/zip.go b/zip/zip.go
index 40606d6..ca0f7ad 100644
--- a/zip/zip.go
+++ b/zip/zip.go
@@ -571,8 +571,8 @@
 // CreateFromVCS creates a module zip file for module m from the contents of a
 // VCS repository stored locally. The zip content is written to w.
 //
-// repo must be an absolute path to the base of the repository, such as
-// "/Users/some-user/my-repo".
+// repoRoot must be an absolute path to the base of the repository, such as
+// "/Users/some-user/some-repo".
 //
 // revision is the revision of the repository to create the zip from. Examples
 // include HEAD or SHA sums for git repositories.
@@ -580,32 +580,45 @@
 // subdir must be the relative path from the base of the repository, such as
 // "sub/dir". To create a zip from the base of the repository, pass an empty
 // string.
-func CreateFromVCS(w io.Writer, m module.Version, repo, revision, subdir string) (err error) {
+//
+// If CreateFromVCS returns ErrUnrecognizedVCS, consider falling back to
+// CreateFromDir.
+func CreateFromVCS(w io.Writer, m module.Version, repoRoot, revision, subdir string) (err error) {
 	defer func() {
 		if zerr, ok := err.(*zipError); ok {
-			zerr.path = repo
+			zerr.path = repoRoot
 		} else if err != nil {
-			err = &zipError{verb: "create zip from version control system", path: repo, err: err}
+			err = &zipError{verb: "create zip from version control system", path: repoRoot, err: err}
 		}
 	}()
 
 	var filesToCreate []File
 
 	switch {
-	case isGitRepo(repo):
-		files, err := filesInGitRepo(repo, revision, subdir)
+	case isGitRepo(repoRoot):
+		files, err := filesInGitRepo(repoRoot, revision, subdir)
 		if err != nil {
 			return err
 		}
 
 		filesToCreate = files
 	default:
-		return fmt.Errorf("%q does not use a recognised version control system", repo)
+		return &UnrecognizedVCSError{RepoRoot: repoRoot}
 	}
 
 	return Create(w, m, filesToCreate)
 }
 
+// UnrecognizedVCSError indicates that no recognized version control system was
+// found in the given directory.
+type UnrecognizedVCSError struct {
+	RepoRoot string
+}
+
+func (e *UnrecognizedVCSError) Error() string {
+	return fmt.Sprintf("could not find a recognized version control system at %q", e.RepoRoot)
+}
+
 // filterGitIgnored filters out any files that are git ignored in the directory.
 func filesInGitRepo(dir, rev, subdir string) ([]File, error) {
 	stderr := bytes.Buffer{}
diff --git a/zip/zip_test.go b/zip/zip_test.go
index 93e60fb..810c07a 100644
--- a/zip/zip_test.go
+++ b/zip/zip_test.go
@@ -9,6 +9,7 @@
 	"bytes"
 	"crypto/sha256"
 	"encoding/hex"
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -1669,8 +1670,15 @@
 
 	m := module.Version{Path: "example.com/foo/bar", Version: "v0.0.1"}
 
-	if err := modzip.CreateFromVCS(tmpZip, m, tmpDir, "HEAD", ""); err == nil {
-		t.Error("CreateFromVCS: expected error, got nil")
+	err = modzip.CreateFromVCS(tmpZip, m, tmpDir, "HEAD", "")
+	if err == nil {
+		t.Fatal("CreateFromVCS: expected error, got nil")
+	}
+	var gotErr *modzip.UnrecognizedVCSError
+	if !errors.As(err, &gotErr) {
+		t.Errorf("CreateFromVCS: returned error does not unwrap to modzip.ErrUnrecognisedVCS, but expected it to. returned error: %v", err)
+	} else if gotErr.RepoRoot != tmpDir {
+		t.Errorf("CreateFromVCS: returned error has RepoRoot %q, but want %q. returned error: %v", gotErr.RepoRoot, tmpDir, err)
 	}
 }