AJ ONeal
5 anni fa
8 ha cambiato i file con 440 aggiunte e 1 eliminazioni
@ -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 |
|||
``` |
|||
|
@ -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 |
Caricamento…
Reference in new issue