mirror of
				https://github.com/therootcompany/go-gitver.git
				synced 2025-10-31 12:42:48 +00:00 
			
		
		
		
	v1.0.0: get version from git, or fail gracefully
This commit is contained in:
		
							parent
							
								
									e4a0e9576b
								
							
						
					
					
						commit
						f104a3155f
					
				
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,3 +1,5 @@ | ||||
| generated-version.go | ||||
| 
 | ||||
| # ---> Go | ||||
| # Binaries for programs and plugins | ||||
| *.exe | ||||
|  | ||||
							
								
								
									
										160
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										160
									
								
								README.md
									
									
									
									
									
								
							| @ -1,3 +1,161 @@ | ||||
| # git-version.go | ||||
| 
 | ||||
| Use git tags to add semver to your go package. | ||||
| Use git tags to add semver to your go package. | ||||
| 
 | ||||
| >     Goal: Either use an exact version like v1.0.0 | ||||
| >           or translate the git version like v1.0.0-4-g0000000 | ||||
| >           to a semver like v1.0.1-pre4+g0000000 | ||||
| > | ||||
| >           Fail gracefully when git repo isn't available. | ||||
| 
 | ||||
| # Demo | ||||
| 
 | ||||
| ```bash | ||||
| go run git.rootprojects.org/root/go-gitver | ||||
| ``` | ||||
| 
 | ||||
| # QuickStart | ||||
| 
 | ||||
| Add this to the top of your main file: | ||||
| 
 | ||||
| ```go | ||||
| //go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver | ||||
| 
 | ||||
| ``` | ||||
| 
 | ||||
| Add a file that imports go-gitver (for versioning) | ||||
| 
 | ||||
| ```go | ||||
| // build +tools | ||||
| 
 | ||||
| package example | ||||
| 
 | ||||
| import _ "git.rootprojects.org/root/go-gitver" | ||||
| ``` | ||||
| 
 | ||||
| Change you build instructions to be something like this: | ||||
| 
 | ||||
| ```bash | ||||
| go mod vendor | ||||
| go generate -mod=vendor ./... | ||||
| go build -mod=vendor -o example cmd/example/*.go | ||||
| ``` | ||||
| 
 | ||||
| You don't have to use `mod vendor`, but I highly recommend it. | ||||
| 
 | ||||
| # Options | ||||
| 
 | ||||
| ``` | ||||
| version   print version and exit | ||||
| --fail    will cause non-zero exit status on failure | ||||
| ``` | ||||
| 
 | ||||
| ENVs | ||||
| 
 | ||||
| ``` | ||||
| # Alias for --fail | ||||
| GITVER_FAIL=true | ||||
| ``` | ||||
| 
 | ||||
| For example: | ||||
| 
 | ||||
| ```go | ||||
| //go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver --fail | ||||
| 
 | ||||
| ``` | ||||
| 
 | ||||
| ```bash | ||||
| go run -mod=vendor git.rootprojects.org/root/go-gitver version | ||||
| ``` | ||||
| 
 | ||||
| # Usage | ||||
| 
 | ||||
| See `examples/basic` | ||||
| 
 | ||||
| 1. Create a `tools` package in your project | ||||
| 2. Guard it against regular builds with `// build +tools` | ||||
| 3. Include `_ "git.rootprojects.org/root/go-gitver"` in the imports | ||||
| 4. Declare `var GitRev, GitVersion, GitTimestamp string` in your `package main` | ||||
| 5. Include `//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver` as well | ||||
| 
 | ||||
| `tools/tools.go`: | ||||
| 
 | ||||
| ```go | ||||
| // build +tools | ||||
| 
 | ||||
| // This is a dummy package for build tooling | ||||
| package tools | ||||
| 
 | ||||
| import ( | ||||
| 	_ "git.rootprojects.org/root/go-gitver" | ||||
| ) | ||||
| ``` | ||||
| 
 | ||||
| `main.go`: | ||||
| 
 | ||||
| ```go | ||||
| //go:generate go run git.rootprojects.org/root/go-gitver --fail | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import "fmt" | ||||
| 
 | ||||
| var ( | ||||
| 	GitRev       = "0000000" | ||||
| 	GitVersion   = "v0.0.0-pre0+0000000" | ||||
| 	GitTimestamp = "0000-00-00T00:00:00+0000" | ||||
| ) | ||||
| 
 | ||||
| func main() { | ||||
|   fmt.Println(GitRev) | ||||
|   fmt.Println(GitVersion) | ||||
|   fmt.Println(GitTimestamp) | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| If you're using `go mod vendor` (which I highly recommend that you do), | ||||
| you'd modify the `go:generate` ever so slightly: | ||||
| 
 | ||||
| ```go | ||||
| //go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver --fail | ||||
| ``` | ||||
| 
 | ||||
| The only reason I didn't do that in the example is that I'd be included | ||||
| the repository in itself and that would be... weird. | ||||
| 
 | ||||
| # Why a tools package? | ||||
| 
 | ||||
| >     import "git.rootprojects.org/root/go-gitver" is a program, not an importable package | ||||
| 
 | ||||
| Having a tools package with a build tag that you don't use is a nice way to add exact | ||||
| versions of a command package used for tooling to your `go.mod` with `go mod tidy`, | ||||
| without getting the error above. | ||||
| 
 | ||||
| # git: behind the curtain | ||||
| 
 | ||||
| These are the commands that are used under the hood to produce the versions. | ||||
| 
 | ||||
| Shows the git tag + description. Assumes that you're using the semver format `v1.0.0` for your base tags. | ||||
| 
 | ||||
| ```bash | ||||
| git describe --tags --dirty --always | ||||
| # v1.0.0 | ||||
| # v1.0.0-1-g0000000 | ||||
| # v1.0.0-dirty | ||||
| ``` | ||||
| 
 | ||||
| Show the commit date (when the commit made it into the current tree). | ||||
| Internally we use the current date when the working tree is dirty. | ||||
| 
 | ||||
| ```bash | ||||
| git show v1.0.0-1-g0000000 --format=%cd --date=format:%Y-%m-%dT%H:%M:%SZ%z --no-patch | ||||
| # 2010-01-01T20:30:00Z-0600 | ||||
| # fatal: ambiguous argument 'v1.0.0-1-g0000000-dirty': unknown revision or path not in the working tree. | ||||
| ``` | ||||
| 
 | ||||
| Shows the most recent commit. | ||||
| 
 | ||||
| ```bash | ||||
| git rev-parse HEAD | ||||
| # 0000000000000000000000000000000000000000 | ||||
| ``` | ||||
|  | ||||
							
								
								
									
										30
									
								
								examples/basic/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								examples/basic/README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| # Example | ||||
| 
 | ||||
| Prints the version or a nice message | ||||
| 
 | ||||
| # Build | ||||
| 
 | ||||
| Typically the developer would perform these steps | ||||
| and then commit the results (`go.mod`, `go.sum`, `vendor`). | ||||
| 
 | ||||
| However, since this is an example within the project directory, | ||||
| that seemed a little redundant. | ||||
| 
 | ||||
| ```bash | ||||
| go mod tidy | ||||
| go mod vendor | ||||
| ``` | ||||
| 
 | ||||
| These are the instructions that someone cloning the repo might use. | ||||
| 
 | ||||
| ```bash | ||||
| go generate -mod=vendor ./... | ||||
| go build -mod=vendor -o hello *.go | ||||
| ``` | ||||
| 
 | ||||
| Note: If the source is distributed in a non-git tarball then | ||||
| `generated-version.go` will not be output, and whatever | ||||
| version info is in `package main` will remain as-is. | ||||
| 
 | ||||
| If you would prefer the build process to fail (i.e. in a CI/CD pipeline), | ||||
| you can set the environment variable `GITVER_FAIL=true`. | ||||
							
								
								
									
										3
									
								
								examples/basic/go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								examples/basic/go.mod
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| module example.com/hello | ||||
| 
 | ||||
| go 1.12 | ||||
							
								
								
									
										28
									
								
								examples/basic/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								examples/basic/main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| //go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver --fail | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	GitRev       = "0000000" | ||||
| 	GitVersion   = "v0.0.0-pre0+0000000" | ||||
| 	GitTimestamp = "0000-00-00T00:00:00+0000" | ||||
| ) | ||||
| 
 | ||||
| func main() { | ||||
| 	showVersion := flag.Bool("version", false, "Print version and exit") | ||||
| 	flag.Parse() | ||||
| 
 | ||||
| 	if *showVersion { | ||||
| 		fmt.Println(GitRev) | ||||
| 		fmt.Println(GitVersion) | ||||
| 		fmt.Println(GitTimestamp) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	fmt.Println("Hello, World!") | ||||
| } | ||||
							
								
								
									
										8
									
								
								examples/basic/tools/tools.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								examples/basic/tools/tools.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| // build +tools | ||||
| 
 | ||||
| // This is a dummy package for build tooling | ||||
| package tools | ||||
| 
 | ||||
| import ( | ||||
| 	_ "git.rootprojects.org/root/go-gitver" | ||||
| ) | ||||
							
								
								
									
										207
									
								
								gitver.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								gitver.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,207 @@ | ||||
| //go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"go/format" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"regexp" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"text/template" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| var exitCode int | ||||
| var exactVer *regexp.Regexp | ||||
| var gitVer *regexp.Regexp | ||||
| var verFile = "generated-version.go" | ||||
| 
 | ||||
| var ( | ||||
| 	GitRev       = "0000000" | ||||
| 	GitVersion   = "v0.0.0-pre0+g0000000" | ||||
| 	GitTimestamp = "0000-00-00T00:00:00+0000" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	// exactly vX.Y.Z (go-compatible semver) | ||||
| 	exactVer = regexp.MustCompile(`^v\d+\.\d+\.\d+$`) | ||||
| 
 | ||||
| 	// vX.Y.Z-n-g0000000 git post-release, semver prerelease | ||||
| 	// vX.Y.Z-dirty git post-release, semver prerelease | ||||
| 	gitVer = regexp.MustCompile(`^(v\d+\.\d+)\.(\d+)(-(\d+))?(-(g[0-9a-f]+))?(-(dirty))?`) | ||||
| } | ||||
| 
 | ||||
| func main() { | ||||
| 	args := os.Args[1:] | ||||
| 	for i := range args { | ||||
| 		arg := args[i] | ||||
| 		if "-f" == arg || "--fail" == arg { | ||||
| 			exitCode = 1 | ||||
| 		} else if "-V" == arg || "version" == arg || "-version" == arg || "--version" == arg { | ||||
| 			fmt.Println(GitRev) | ||||
| 			fmt.Println(GitVersion) | ||||
| 			fmt.Println(GitTimestamp) | ||||
| 			os.Exit(0) | ||||
| 		} | ||||
| 	} | ||||
| 	if "" != os.Getenv("GITVER_FAIL") && "false" != os.Getenv("GITVER_FAIL") { | ||||
| 		exitCode = 1 | ||||
| 	} | ||||
| 
 | ||||
| 	desc, err := gitDesc() | ||||
| 	if nil != err { | ||||
| 		log.Fatalf("Failed to get git version: %s\n", err) | ||||
| 		os.Exit(exitCode) | ||||
| 	} | ||||
| 	rev := gitRev() | ||||
| 	ver := semVer(desc) | ||||
| 	ts, err := gitTimestamp(desc) | ||||
| 	if nil != err { | ||||
| 		ts = time.Now() | ||||
| 	} | ||||
| 
 | ||||
| 	v := struct { | ||||
| 		Timestamp string | ||||
| 		Version   string | ||||
| 		GitRev    string | ||||
| 	}{ | ||||
| 		Timestamp: ts.Format(time.RFC3339), | ||||
| 		Version:   ver, | ||||
| 		GitRev:    rev, | ||||
| 	} | ||||
| 
 | ||||
| 	// Create or overwrite the go file from template | ||||
| 	var buf bytes.Buffer | ||||
| 	if err := versionTpl.Execute(&buf, v); nil != err { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Format | ||||
| 	src, err := format.Source(buf.Bytes()) | ||||
| 	if nil != err { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Write to disk (in the Current Working Directory) | ||||
| 	f, err := os.Create(verFile) | ||||
| 	if nil != err { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	if _, err := f.Write(src); nil != err { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	if err := f.Close(); nil != err { | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func gitDesc() (string, error) { | ||||
| 	args := strings.Split("git describe --tags --dirty --always", " ") | ||||
| 	cmd := exec.Command(args[0], args[1:]...) | ||||
| 	out, err := cmd.CombinedOutput() | ||||
| 	if nil != err { | ||||
| 		// Don't panic, just carry on | ||||
| 		//out = []byte("v0.0.0-0-g0000000") | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return strings.TrimSpace(string(out)), nil | ||||
| } | ||||
| 
 | ||||
| func gitRev() string { | ||||
| 	args := strings.Split("git rev-parse HEAD", " ") | ||||
| 	cmd := exec.Command(args[0], args[1:]...) | ||||
| 	out, err := cmd.CombinedOutput() | ||||
| 	if nil != err { | ||||
| 		fmt.Fprintf(os.Stderr, | ||||
| 			"\nUnexpected Error\n\n"+ | ||||
| 				"Please open an issue at https://git.rootprojects.org/root/go-gitver/issues/new \n"+ | ||||
| 				"Please include the following:\n\n"+ | ||||
| 				"Command: %s\n"+ | ||||
| 				"Output: %s\n"+ | ||||
| 				"Error: %s\n"+ | ||||
| 				"\nPlease and Thank You.\n\n", strings.Join(args, " "), out, err) | ||||
| 		os.Exit(exitCode) | ||||
| 	} | ||||
| 	return strings.TrimSpace(string(out)) | ||||
| } | ||||
| 
 | ||||
| func semVer(desc string) string { | ||||
| 	if exactVer.MatchString(desc) { | ||||
| 		// v1.0.0 | ||||
| 		return desc | ||||
| 	} | ||||
| 
 | ||||
| 	if !gitVer.MatchString(desc) { | ||||
| 		return "" | ||||
| 	} | ||||
| 
 | ||||
| 	// (v1.0).(0)(-(1))(-(g0000000))(-(dirty)) | ||||
| 	vers := gitVer.FindStringSubmatch(desc) | ||||
| 	patch, err := strconv.Atoi(vers[2]) | ||||
| 	if nil != err { | ||||
| 		fmt.Fprintf(os.Stderr, | ||||
| 			"\nUnexpected Error\n\n"+ | ||||
| 				"Please open an issue at https://git.rootprojects.org/root/go-gitver/issues/new \n"+ | ||||
| 				"Please include the following:\n\n"+ | ||||
| 				"git description: %s\n"+ | ||||
| 				"RegExp: %#v\n"+ | ||||
| 				"Error: %s\n"+ | ||||
| 				"\nPlease and Thank You.\n\n", desc, gitVer, err) | ||||
| 		os.Exit(exitCode) | ||||
| 	} | ||||
| 
 | ||||
| 	// v1.0.1-pre1 | ||||
| 	// v1.0.1-pre1+g0000000 | ||||
| 	// v1.0.1-pre0+dirty | ||||
| 	// v1.0.1-pre0+g0000000-dirty | ||||
| 	if "" == vers[4] { | ||||
| 		vers[4] = "0" | ||||
| 	} | ||||
| 	ver := fmt.Sprintf("%s.%d-pre%s", vers[1], patch+1, vers[4]) | ||||
| 	if "" != vers[6] || "dirty" == vers[8] { | ||||
| 		ver += "+" | ||||
| 		if "" != vers[6] { | ||||
| 			ver += vers[6] | ||||
| 			if "" != vers[8] { | ||||
| 				ver += "-" | ||||
| 			} | ||||
| 		} | ||||
| 		ver += vers[8] | ||||
| 	} | ||||
| 
 | ||||
| 	return ver | ||||
| } | ||||
| 
 | ||||
| func gitTimestamp(desc string) (time.Time, error) { | ||||
| 	args := []string{ | ||||
| 		"git", | ||||
| 		"show", desc, | ||||
| 		"--format=%cd", | ||||
| 		"--date=format:%Y-%m-%dT%H:%M:%SZ%z", | ||||
| 		"--no-patch", | ||||
| 	} | ||||
| 	cmd := exec.Command(args[0], args[1:]...) | ||||
| 	out, err := cmd.CombinedOutput() | ||||
| 	if nil != err { | ||||
| 		// a dirty desc was probably used | ||||
| 		return time.Time{}, err | ||||
| 	} | ||||
| 	return time.Parse(time.RFC3339, strings.TrimSpace(string(out))) | ||||
| } | ||||
| 
 | ||||
| var versionTpl = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT. | ||||
| package main | ||||
| 
 | ||||
| func init() { | ||||
| 	GitRev = "{{ .GitRev }}" | ||||
| 	if "" != "{{ .Version }}" { | ||||
| 		GitVersion = "{{ .Version }}" | ||||
| 	} | ||||
| 	GitTimestamp = "{{ .Timestamp }}" | ||||
| } | ||||
| `)) | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user