Repository: goplus/gop
Pull request #1055 (2021-12-12 13:17:36)

Add release tool
## Features:

* Support execute `go run cmd/make.go --tag <next-version>` command on ** any git branch** to release a new version of `gop`.
* The new version can be used by following Go+ build process.
* Full testcases to make sure it will do what that we expected.
* To build the Go+, the dir is requied to be a git repo or a `VERSION` file exists.

### What happend when `go run cmd/make.go --tag <next-version>` executed ?

0. The command could be executed on any git branch, the command will checkout to target release branch and checkout back to source branch automatically.
1. Store `new-version` into `VERSION` file, which can also be submitted to git repo, and will be kept on a release branch.
2. Auto execute `git tag new-version` command to tag the source code.

92hackers: #1055 
92hackers: #1018 
codecov-commenter: # [Codecov](https://codecov.io/gh/goplus/gop/pull/1055?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus) Report
> Merging [#1055](https://codecov.io/gh/goplus/gop/pull/1055?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus) (d6074dc) into [main](https://codecov.io/gh/goplus/gop/commit/bd85ee22bb79b8879aa2b59f5faffa08a02c2f66?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus) (bd85ee2) will **decrease** coverage by `0.01%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/goplus/gop/pull/1055/graphs/tree.svg?width=650&height=150&src=pr&token=4qln9Wk3Pn&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus)](https://codecov.io/gh/goplus/gop/pull/1055?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus)

```diff
@@            Coverage Diff             @@
##             main    #1055      +/-   ##
==========================================
- Coverage   85.71%   85.70%   -0.02%     
==========================================
  Files          23       23              
  Lines        9520     9520              
==========================================
- Hits         8160     8159       -1     
- Misses       1085     1086       +1     
  Partials      275      275              
```


| [Impacted Files](https://codecov.io/gh/goplus/gop/pull/1055?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus) | Coverage Δ | |
|---|---|---|
| [cl/stmt.go](https://codecov.io/gh/goplus/gop/pull/1055/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus#diff-Y2wvc3RtdC5nbw==) | `93.47% <0.00%> (-0.17%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goplus/gop/pull/1055?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/goplus/gop/pull/1055?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus). Last update [bd85ee2...d6074dc](https://codecov.io/gh/goplus/gop/pull/1055?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goplus).

92hackers: > ## Features:
> 1. Support execute `go run cmd/make.go --tag <next-version>` command on a **git release branch** to release a new version of `gop`.
> 2. Support execute `go run cmd/make.go --tag auto` command to generate an auto-incremented new version based on current version.
> 3. The new version can be used by following Go+ build process.
> 4. Full testcases to make sure it will do what that we expected.
> 5. You can't build the Go+ when the dir is nether a git repo or `VERSION` file not found.
> 
> ### What `go run cmd/make.go --tag <>` did ?
> 0. The command could only be executed on a release branch, whose name required to match `gop/env.MainVersion`.
> 1. Store `new-version` into `VERSION` file, which can also be submitted to git repo, and will be kept on a release branch.
> 2. Auto execute `git tag new-version` command to tag the source code.



Patch:
diff --git a/cmd/make.go b/cmd/make.go
index b510e7d2..8887024b 100644
--- a/cmd/make.go
+++ b/cmd/make.go
@@ -27,6 +27,7 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"regexp"
 	"runtime"
 	"strings"
 	"time"
@@ -41,6 +42,10 @@ func checkPathExist(path string, isDir bool) bool {
 	return isExists && !stat.IsDir()
 }
 
+func trimRight(s string) string {
+	return strings.TrimRight(s, " \t\r\n")
+}
+
 // Path returns single path to check
 type Path struct {
 	path  string
@@ -77,6 +82,7 @@ var commandExecuteEnv = initCommandExecuteEnv
 
 // Always put `gop` command as the first item, as it will be referenced by below code.
 var gopBinFiles = []string{"gop", "gopfmt"}
+var versionFile = filepath.Join(gopRoot, "VERSION")
 
 const (
 	inWindows = (runtime.GOOS == "windows")
@@ -101,23 +107,26 @@ func execCommand(command string, arg ...string) (string, string, error) {
 	return stdout.String(), stderr.String(), err
 }
 
-func getRevCommit(tag string) string {
-	commit, stderr, err := execCommand("git", "rev-parse", "--verify", tag)
-	if err != nil || stderr != "" {
+func getGitBranch() string {
+	branch, _, err := execCommand("git", "rev-parse", "--abbrev-ref", "HEAD")
+	if err != nil {
 		return ""
 	}
-	return strings.TrimRight(commit, "\n")
+	return trimRight(branch)
 }
 
-/*
-func getGitInfo() (string, bool) {
-	gitDir := filepath.Join(gopRoot, ".git")
-	if checkPathExist(gitDir, true) {
-		return getRevCommit("HEAD"), true
+func checkoutBranch(branch string) (string, error) {
+	_, stderr, err := execCommand("git", "checkout", branch)
+	return stderr, err
+}
+
+func isGitRepo() bool {
+	gitDir, _, err := execCommand("git", "rev-parse", "--git-dir")
+	if err != nil {
+		return false
 	}
-	return "", false
+	return checkPathExist(filepath.Join(gopRoot, trimRight(gitDir)), true)
 }
-*/
 
 func getBuildDateTime() string {
 	now := time.Now()
@@ -129,7 +138,7 @@ func getBuildVer() string {
 	if err != nil || tagErr != "" {
 		return ""
 	}
-	return strings.TrimRight(tagRet, "\n")
+	return trimRight(tagRet)
 }
 
 func getGopBuildFlags() string {
@@ -139,9 +148,10 @@ func getGopBuildFlags() string {
 	}
 	buildFlags := fmt.Sprintf("-X \"github.com/goplus/gop/env.defaultGopRoot=%s\"", defaultGopRoot)
 	buildFlags += fmt.Sprintf(" -X \"github.com/goplus/gop/env.buildDate=%s\"", getBuildDateTime())
-	if buildVer := getBuildVer(); buildVer != "" {
-		buildFlags += fmt.Sprintf(" -X github.com/goplus/gop/env.buildVersion=%s", buildVer)
-	}
+
+	version := findGopVersion()
+	buildFlags += fmt.Sprintf(" -X \"github.com/goplus/gop/env.buildVersion=%s\"", version)
+
 	return buildFlags
 }
 
@@ -332,12 +342,74 @@ func isInChina() bool {
 	return false
 }
 
+// findGopVersion returns current version of gop
+func findGopVersion() string {
+	// Read version from VERSION file
+	if checkPathExist(versionFile, false) {
+		data, err := os.ReadFile(versionFile)
+		if err == nil {
+			version := trimRight(string(data))
+			return version
+		}
+	}
+
+	// Read version from git repo
+	if !isGitRepo() {
+		log.Fatal("Error: must be a git repo or a VERSION file existed.")
+	}
+	version := getBuildVer() // Closet tag on git log
+	return version
+}
+
+// releaseNewVersion tags the repo with provided new tag, and writes new tag into VERSION file.
+func releaseNewVersion(tag string) {
+	if !isGitRepo() {
+		log.Fatal("Error: Releasing a new version could only be operated under a git repo.")
+	}
+	println("Start releasing new version")
+
+	version := tag
+
+	re := regexp.MustCompile(`^v\d+?\.\d+?`)
+	releaseBranch := re.FindString(version)
+	sourceBranch := getGitBranch()
+
+	if releaseBranch == "" {
+		log.Fatal("Error: A valid version should be has form: vx.y.z")
+	}
+
+	// Checkout to release breanch
+	if stderr, err := checkoutBranch(releaseBranch); err != nil {
+		log.Fatalf("Error: checkout to release branch: %s failed with error: %v.", releaseBranch, stderr)
+	}
+
+	fmt.Printf("\nReleasing new version: %s\n\n", version)
+
+	// Cache new version
+	if err := os.WriteFile(versionFile, []byte(version), 0666); err != nil {
+		log.Fatalf("Error: cache new version with error: %v\n", err)
+	}
+
+	// Tag the source code
+	if _, stderr, err := execCommand("git", "tag", version); err != nil {
+		log.Fatalf("Error: tag the source code with error: %v\n", stderr)
+	}
+
+	// Checkout back to source branch
+	if stderr, err := checkoutBranch(sourceBranch); err != nil {
+		log.Fatalf("Error: checkout to source branch: %s failed with error: %v.", sourceBranch, stderr)
+	}
+
+	println("End releasing new version:", tag)
+}
+
 func main() {
 	isInstall := flag.Bool("install", false, "Install Go+")
 	isTest := flag.Bool("test", false, "Run testcases")
 	isUninstall := flag.Bool("uninstall", false, "Uninstall Go+")
 	isGoProxy := flag.Bool("proxy", false, "Set GOPROXY for people in China")
 	isAutoProxy := flag.Bool("autoproxy", false, "Check to set GOPROXY automatically")
+	tag := flag.String("tag", "", "Release an new version with specified tag")
 
 	flag.Parse()
 
@@ -355,6 +427,11 @@ func main() {
 	flags := []*bool{isInstall, isTest, isUninstall}
 	hasActionDone := false
 
+	if *tag != "" {
+		releaseNewVersion(*tag)
+		hasActionDone = true
+	}
+
 	for _, flag := range flags {
 		if *flag {
 			flagActionMap[flag]()


Test patch:
diff --git a/cmd/make_test.go b/cmd/make_test.go
index b63bb311..e8e9f5e3 100644
--- a/cmd/make_test.go
+++ b/cmd/make_test.go
@@ -8,6 +8,7 @@ import (
 	"os/exec"
 	"path/filepath"
 	"runtime"
+	"strings"
 	"testing"
 )
 
@@ -18,6 +19,9 @@ const (
 var script = "all.bash"
 var gopRoot = ""
 var gopBinFiles = []string{"gop", "gopfmt"}
+var installer = "cmd/make.go"
+var versionFile = "VERSION"
+var mainVersionFile = "env/version.go"
 
 func checkPathExist(path string, isDir bool) bool {
 	stat, err := os.Stat(path)
@@ -28,6 +32,16 @@ func checkPathExist(path string, isDir bool) bool {
 	return isExists && !stat.IsDir()
 }
 
+func trimRight(s string) string {
+	return strings.TrimRight(s, " \t\r\n")
+}
+
+func getBranch() string {
+	gitCmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
+	b, _ := gitCmd.CombinedOutput()
+	return trimRight(string(b))
+}
+
 func detectGoBinPath() string {
 	goBin, ok := os.LookupEnv("GOBIN")
 	if ok {
@@ -50,6 +64,10 @@ func detectGoBinPath() string {
 func init() {
 	pwd, _ := os.Getwd()
 	gopRoot = filepath.Join(pwd, "..")
+	installer = filepath.Join(gopRoot, installer)
+	versionFile = filepath.Join(gopRoot, versionFile)
+	mainVersionFile = filepath.Join(gopRoot, mainVersionFile)
+
 	if inWindows {
 		script = "all.bat"
 		for index, file := range gopBinFiles {
@@ -59,6 +77,18 @@ func init() {
 	}
 }
 
+func cleanGopRunCacheFiles(t *testing.T) {
+	homeDir, _ := os.UserHomeDir()
+	runCacheDir := filepath.Join(homeDir, ".gop", "run")
+	files := []string{"go.mod", "go.sum"}
+	for _, file := range files {
+		fullPath := filepath.Join(runCacheDir, file)
+		if checkPathExist(fullPath, false) {
+			t.Fatalf("Failed: %s found in %s directory\n", file, runCacheDir)
+		}
+	}
+}
+
 func TestAllScript(t *testing.T) {
 	os.Chdir(gopRoot)
 	cmd := exec.Command(filepath.Join(gopRoot, script))
@@ -77,15 +107,224 @@ func TestAllScript(t *testing.T) {
 		}
 	}
 
+	cleanGopRunCacheFiles(t)
+
 	cmd = exec.Command(filepath.Join(gopRoot, "bin", gopBinFiles[0]), "version")
 	if output, err := cmd.CombinedOutput(); err != nil {
 		t.Fatalf("Failed: %v:\nOut: %s\n", err, output)
 	}
 }
 
+func TestTagFlagInGitRepo(t *testing.T) {
+	os.Chdir(gopRoot)
+
+	// Setup
+	tag := "v1.0.90"
+	tag2 := "v1.0.91"
+	bigtag := "v1.999.12"
+	releaseBranch := "v1.0"
+	nonExistBranch := "non-exist-branch"
+	sourceBranch := getBranch()
+
+	// Teardown
+	t.Cleanup(func() {
+		gitCmd := exec.Command("git", "tag", "-d", tag)
+		gitCmd.CombinedOutput()
+		gitCmd = exec.Command("git", "tag", "-d", tag2)
+		gitCmd.CombinedOutput()
+		gitCmd = exec.Command("git", "checkout", sourceBranch)
+		gitCmd.CombinedOutput()
+		gitCmd = exec.Command("git", "branch", "-d", nonExistBranch)
+		gitCmd.CombinedOutput()
+		if checkPathExist(versionFile, false) {
+			os.Remove(versionFile)
+		}
+	})
+
+	t.Run("release new version with bad tag", func(t *testing.T) {
+		cmd := exec.Command("go", "run", installer, "--tag", "xyz")
+		if _, err := cmd.CombinedOutput(); err == nil {
+			t.Fatal("Failed: release a bad tag should be failed.")
+		}
+	})
+
+	t.Run("empty tag should failed", func(t *testing.T) {
+		cmd := exec.Command("go", "run", installer, "--tag", "")
+		if out, err := cmd.CombinedOutput(); err != nil || !strings.Contains(string(out), "Usage") {
+			t.Fatalf("Failed: %v, out: %s\n", err, out)
+		}
+	})
+
+	t.Run("failed when release branch corresponding to tag does not exists", func(t *testing.T) {
+		cmd := exec.Command("go", "run", installer, "--tag", bigtag)
+		if _, err := cmd.CombinedOutput(); err == nil {
+			t.Fatal("Failed: a corresponding release branch does not exists should be failed.")
+		}
+	})
+
+	t.Run("release new version on release branch", func(t *testing.T) {
+		gitCmd := exec.Command("git", "checkout", "-b", releaseBranch)
+		if output, err := gitCmd.CombinedOutput(); err != nil {
+			if strings.Contains(string(output), "already exists") {
+				gitCmd = exec.Command("git", "checkout", releaseBranch)
+				if output, err := gitCmd.CombinedOutput(); err != nil {
+					t.Fatalf("Failed: %v, output: %s\n", err, output)
+				}
+			} else {
+				t.Fatalf("Failed: %v, output: %s\n", err, output)
+			}
+		}
+
+		cmd := exec.Command("go", "run", installer, "--tag", tag)
+		if _, err := cmd.CombinedOutput(); err != nil {
+			t.Fatalf("Failed: release tag: %s on branch: %s should not be failed", tag, releaseBranch)
+		}
+
+		if !checkPathExist(versionFile, false) {
+			t.Fatal("Failed: a VERSION file not found.")
+		}
+
+		if data, _ := os.ReadFile(versionFile); string(data) != tag {
+			t.Fatalf("Failed: content of VERSION file: '%s' not match tag: %s", data, tag)
+		}
+
+		// Make sure tag exists.
+		gitCmd = exec.Command("git", "tag")
+		if allTags, _ := gitCmd.CombinedOutput(); !strings.Contains(string(allTags), tag) {
+			t.Fatalf("Failed: %s tag not found in tag list of this git repo.\n", tag)
+		}
+	})
+
+	t.Run("release new version on non-release branch", func(t *testing.T) {
+		gitCmd := exec.Command("git", "checkout", "-b", nonExistBranch)
+		if output, err := gitCmd.CombinedOutput(); err != nil {
+			if strings.Contains(string(output), "already exists") {
+				gitCmd = exec.Command("git", "checkout", nonExistBranch)
+				if output, err := gitCmd.CombinedOutput(); err != nil {
+					t.Fatalf("Failed: %v, output: %s\n", err, output)
+				}
+			} else {
+				t.Fatalf("Failed: %v, output: %s\n", err, output)
+			}
+		}
+
+		cmd := exec.Command("go", "run", installer, "--tag", tag2)
+		if out, err := cmd.CombinedOutput(); err != nil {
+			t.Log(string(out))
+			t.Fatalf("Failed: release tag: %s on branch: %s should not be failed", tag2, nonExistBranch)
+		}
+
+		if !checkPathExist(versionFile, false) {
+			t.Fatal("Failed: a VERSION file not found.")
+		}
+
+		if data, _ := os.ReadFile(versionFile); string(data) != tag2 {
+			t.Fatalf("Failed: content of VERSION file: '%s' not match tag: %s", data, tag2)
+		}
+
+		gitCmd = exec.Command("git", "tag")
+		if allTags, _ := gitCmd.CombinedOutput(); !strings.Contains(string(allTags), tag2) {
+			t.Fatalf("Failed: %s tag not found in tag list of this git repo.\n", tag)
+		}
+	})
+}
+
+func TestTagFlagInNonGitRepo(t *testing.T) {
+	os.Chdir(gopRoot)
+
+	// Setup
+	gitDir := filepath.Join(gopRoot, ".git")
+	gitBackupDir := filepath.Join(gopRoot, ".gitBackup")
+	// Rename .git dir
+	if checkPathExist(gitDir, true) {
+		os.Rename(gitDir, gitBackupDir)
+	}
+
+	// Teardown
+	t.Cleanup(func() {
+		if checkPathExist(gitBackupDir, true) {
+			os.Rename(gitBackupDir, gitDir)
+		}
+	})
+
+	t.Run("specify new tag", func(t *testing.T) {
+		cmd := exec.Command("go", "run", installer, "--tag", "v1.0.98")
+		output, err := cmd.CombinedOutput()
+		if err == nil || !strings.Contains(string(output), "Error") {
+			t.Fatal("Failed: release on a non-git repo should be failed.")
+		}
+	})
+}
+
+func TestInstallInNonGitRepo(t *testing.T) {
+	os.Chdir(gopRoot)
+
+	// Setup
+	gitDir := filepath.Join(gopRoot, ".git")
+	gitBackupDir := filepath.Join(gopRoot, ".gitBackup")
+	// Rename .git dir
+	if checkPathExist(gitDir, true) {
+		os.Rename(gitDir, gitBackupDir)
+	}
+
+	// Teardown
+	t.Cleanup(func() {
+		if checkPathExist(gitBackupDir, true) {
+			os.Rename(gitBackupDir, gitDir)
+		}
+		if checkPathExist(versionFile, false) {
+			os.Remove(versionFile)
+		}
+	})
+
+	installCmd := func() *exec.Cmd {
+		return exec.Command("go", "run", installer, "--install")
+	}
+
+	t.Run("failed build operation", func(t *testing.T) {
+		cmd := installCmd()
+		output, err := cmd.CombinedOutput()
+		if err == nil || !strings.Contains(string(output), "Error") {
+			t.Fatal("Failed: build Go+ in a non-git repo and no VERSION file should be failed.")
+		}
+	})
+
+	t.Run("install with VERSION file", func(t *testing.T) {
+		version := "v1.0.98"
+		// Create VERSION file
+		if err := os.WriteFile(versionFile, []byte(version), 0666); err != nil {
+			t.Fatal(err)
+		}
+
+		cmd := installCmd()
+		if output, err := cmd.CombinedOutput(); err != nil {
+			t.Fatalf("Failed: %v, output: %s\n", err, output)
+		}
+
+		cmd = exec.Command(filepath.Join(gopRoot, "bin", gopBinFiles[0]), "version")
+		output, err := cmd.CombinedOutput()
+		if err != nil || !strings.Contains(string(output), version) {
+			t.Fatalf("Failed: %v, output: %s\n", err, output)
+		}
+	})
+}
+
 func TestHandleMultiFlags(t *testing.T) {
 	os.Chdir(gopRoot)
-	cmd := exec.Command("go", "run", filepath.Join(gopRoot, "cmd/make.go"), "--install", "--test", "--uninstall")
+
+	// Setup
+	nextVersion := "v1.0.988"
+
+	// Teardown
+	t.Cleanup(func() {
+		gitCmd := exec.Command("git", "tag", "-d", nextVersion)
+		gitCmd.CombinedOutput()
+		if checkPathExist(versionFile, false) {
+			os.Remove(versionFile)
+		}
+	})
+
+	cmd := exec.Command("go", "run", installer, "--install", "--test", "--uninstall", "--tag", nextVersion)
 	if output, err := cmd.CombinedOutput(); err != nil {
 		t.Fatalf("Failed: %v:\nOut: %s\n", err, output)
 	}
@@ -101,4 +340,20 @@ func TestHandleMultiFlags(t *testing.T) {
 			t.Fatalf("Failed: %s found in %s/bin directory\n", file, goBinPath)
 		}
 	}
+
+	// Test if git tag list contains nextVersion
+	cmd = exec.Command("git", "tag")
+	if allTags, err := cmd.CombinedOutput(); err != nil {
+		if !strings.Contains(string(allTags), nextVersion) {
+			t.Fatalf("Failed: %s tag not found in this git repo.\n", nextVersion)
+		}
+	}
+
+	if !checkPathExist(versionFile, false) {
+		t.Fatal("Failed: a VERSION file not found.")
+	}
+
+	if data, _ := os.ReadFile(versionFile); string(data) != nextVersion {
+		t.Fatalf("Failed: content of VERSION file: '%s' not match tag: %s", data, nextVersion)
+	}
 }