AJ ONeal
5 years ago
8 changed files with 440 additions and 1 deletions
@ -1,3 +1,161 @@ |
|||||
# git-version.go |
# 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 |
||||
|
``` |
||||
|
@ -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`. |
@ -0,0 +1,3 @@ |
|||||
|
module example.com/hello |
||||
|
|
||||
|
go 1.12 |
@ -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!") |
||||
|
} |
@ -0,0 +1,8 @@ |
|||||
|
// build +tools
|
||||
|
|
||||
|
// This is a dummy package for build tooling
|
||||
|
package tools |
||||
|
|
||||
|
import ( |
||||
|
_ "git.rootprojects.org/root/go-gitver" |
||||
|
) |
@ -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 }}" |
||||
|
} |
||||
|
`)) |
@ -0,0 +1,3 @@ |
|||||
|
module git.rootprojects.org/root/go-gitver |
||||
|
|
||||
|
go 1.12 |
Loading…
Reference in new issue