mirror of
				https://github.com/therootcompany/go-gitver.git
				synced 2025-11-04 14:42:47 +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