Browse Source

v1.0.0: get version from git, or fail gracefully

tags/v1.0.0
AJ ONeal 8 months ago
parent
commit
f104a3155f
8 changed files with 440 additions and 1 deletions
  1. 2
    0
      .gitignore
  2. 159
    1
      README.md
  3. 30
    0
      examples/basic/README.md
  4. 3
    0
      examples/basic/go.mod
  5. 28
    0
      examples/basic/main.go
  6. 8
    0
      examples/basic/tools/tools.go
  7. 207
    0
      gitver.go
  8. 3
    0
      go.mod

+ 2
- 0
.gitignore View File

@@ -1,3 +1,5 @@
1
+generated-version.go
2
+
1 3
 # ---> Go
2 4
 # Binaries for programs and plugins
3 5
 *.exe

+ 159
- 1
README.md View File

@@ -1,3 +1,161 @@
1 1
 # git-version.go
2 2
 
3
-Use git tags to add semver to your go package.
3
+Use git tags to add semver to your go package.
4
+
5
+>     Goal: Either use an exact version like v1.0.0
6
+>           or translate the git version like v1.0.0-4-g0000000
7
+>           to a semver like v1.0.1-pre4+g0000000
8
+>
9
+>           Fail gracefully when git repo isn't available.
10
+
11
+# Demo
12
+
13
+```bash
14
+go run git.rootprojects.org/root/go-gitver
15
+```
16
+
17
+# QuickStart
18
+
19
+Add this to the top of your main file:
20
+
21
+```go
22
+//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver
23
+
24
+```
25
+
26
+Add a file that imports go-gitver (for versioning)
27
+
28
+```go
29
+// build +tools
30
+
31
+package example
32
+
33
+import _ "git.rootprojects.org/root/go-gitver"
34
+```
35
+
36
+Change you build instructions to be something like this:
37
+
38
+```bash
39
+go mod vendor
40
+go generate -mod=vendor ./...
41
+go build -mod=vendor -o example cmd/example/*.go
42
+```
43
+
44
+You don't have to use `mod vendor`, but I highly recommend it.
45
+
46
+# Options
47
+
48
+```
49
+version   print version and exit
50
+--fail    will cause non-zero exit status on failure
51
+```
52
+
53
+ENVs
54
+
55
+```
56
+# Alias for --fail
57
+GITVER_FAIL=true
58
+```
59
+
60
+For example:
61
+
62
+```go
63
+//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver --fail
64
+
65
+```
66
+
67
+```bash
68
+go run -mod=vendor git.rootprojects.org/root/go-gitver version
69
+```
70
+
71
+# Usage
72
+
73
+See `examples/basic`
74
+
75
+1. Create a `tools` package in your project
76
+2. Guard it against regular builds with `// build +tools`
77
+3. Include `_ "git.rootprojects.org/root/go-gitver"` in the imports
78
+4. Declare `var GitRev, GitVersion, GitTimestamp string` in your `package main`
79
+5. Include `//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver` as well
80
+
81
+`tools/tools.go`:
82
+
83
+```go
84
+// build +tools
85
+
86
+// This is a dummy package for build tooling
87
+package tools
88
+
89
+import (
90
+	_ "git.rootprojects.org/root/go-gitver"
91
+)
92
+```
93
+
94
+`main.go`:
95
+
96
+```go
97
+//go:generate go run git.rootprojects.org/root/go-gitver --fail
98
+
99
+package main
100
+
101
+import "fmt"
102
+
103
+var (
104
+	GitRev       = "0000000"
105
+	GitVersion   = "v0.0.0-pre0+0000000"
106
+	GitTimestamp = "0000-00-00T00:00:00+0000"
107
+)
108
+
109
+func main() {
110
+  fmt.Println(GitRev)
111
+  fmt.Println(GitVersion)
112
+  fmt.Println(GitTimestamp)
113
+}
114
+```
115
+
116
+If you're using `go mod vendor` (which I highly recommend that you do),
117
+you'd modify the `go:generate` ever so slightly:
118
+
119
+```go
120
+//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver --fail
121
+```
122
+
123
+The only reason I didn't do that in the example is that I'd be included
124
+the repository in itself and that would be... weird.
125
+
126
+# Why a tools package?
127
+
128
+>     import "git.rootprojects.org/root/go-gitver" is a program, not an importable package
129
+
130
+Having a tools package with a build tag that you don't use is a nice way to add exact
131
+versions of a command package used for tooling to your `go.mod` with `go mod tidy`,
132
+without getting the error above.
133
+
134
+# git: behind the curtain
135
+
136
+These are the commands that are used under the hood to produce the versions.
137
+
138
+Shows the git tag + description. Assumes that you're using the semver format `v1.0.0` for your base tags.
139
+
140
+```bash
141
+git describe --tags --dirty --always
142
+# v1.0.0
143
+# v1.0.0-1-g0000000
144
+# v1.0.0-dirty
145
+```
146
+
147
+Show the commit date (when the commit made it into the current tree).
148
+Internally we use the current date when the working tree is dirty.
149
+
150
+```bash
151
+git show v1.0.0-1-g0000000 --format=%cd --date=format:%Y-%m-%dT%H:%M:%SZ%z --no-patch
152
+# 2010-01-01T20:30:00Z-0600
153
+# fatal: ambiguous argument 'v1.0.0-1-g0000000-dirty': unknown revision or path not in the working tree.
154
+```
155
+
156
+Shows the most recent commit.
157
+
158
+```bash
159
+git rev-parse HEAD
160
+# 0000000000000000000000000000000000000000
161
+```

+ 30
- 0
examples/basic/README.md View File

@@ -0,0 +1,30 @@
1
+# Example
2
+
3
+Prints the version or a nice message
4
+
5
+# Build
6
+
7
+Typically the developer would perform these steps
8
+and then commit the results (`go.mod`, `go.sum`, `vendor`).
9
+
10
+However, since this is an example within the project directory,
11
+that seemed a little redundant.
12
+
13
+```bash
14
+go mod tidy
15
+go mod vendor
16
+```
17
+
18
+These are the instructions that someone cloning the repo might use.
19
+
20
+```bash
21
+go generate -mod=vendor ./...
22
+go build -mod=vendor -o hello *.go
23
+```
24
+
25
+Note: If the source is distributed in a non-git tarball then
26
+`generated-version.go` will not be output, and whatever
27
+version info is in `package main` will remain as-is.
28
+
29
+If you would prefer the build process to fail (i.e. in a CI/CD pipeline),
30
+you can set the environment variable `GITVER_FAIL=true`.

+ 3
- 0
examples/basic/go.mod View File

@@ -0,0 +1,3 @@
1
+module example.com/hello
2
+
3
+go 1.12

+ 28
- 0
examples/basic/main.go View File

@@ -0,0 +1,28 @@
1
+//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver --fail
2
+
3
+package main
4
+
5
+import (
6
+	"flag"
7
+	"fmt"
8
+)
9
+
10
+var (
11
+	GitRev       = "0000000"
12
+	GitVersion   = "v0.0.0-pre0+0000000"
13
+	GitTimestamp = "0000-00-00T00:00:00+0000"
14
+)
15
+
16
+func main() {
17
+	showVersion := flag.Bool("version", false, "Print version and exit")
18
+	flag.Parse()
19
+
20
+	if *showVersion {
21
+		fmt.Println(GitRev)
22
+		fmt.Println(GitVersion)
23
+		fmt.Println(GitTimestamp)
24
+		return
25
+	}
26
+
27
+	fmt.Println("Hello, World!")
28
+}

+ 8
- 0
examples/basic/tools/tools.go View File

@@ -0,0 +1,8 @@
1
+// build +tools
2
+
3
+// This is a dummy package for build tooling
4
+package tools
5
+
6
+import (
7
+	_ "git.rootprojects.org/root/go-gitver"
8
+)

+ 207
- 0
gitver.go View File

@@ -0,0 +1,207 @@
1
+//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver
2
+
3
+package main
4
+
5
+import (
6
+	"bytes"
7
+	"fmt"
8
+	"go/format"
9
+	"log"
10
+	"os"
11
+	"os/exec"
12
+	"regexp"
13
+	"strconv"
14
+	"strings"
15
+	"text/template"
16
+	"time"
17
+)
18
+
19
+var exitCode int
20
+var exactVer *regexp.Regexp
21
+var gitVer *regexp.Regexp
22
+var verFile = "generated-version.go"
23
+
24
+var (
25
+	GitRev       = "0000000"
26
+	GitVersion   = "v0.0.0-pre0+g0000000"
27
+	GitTimestamp = "0000-00-00T00:00:00+0000"
28
+)
29
+
30
+func init() {
31
+	// exactly vX.Y.Z (go-compatible semver)
32
+	exactVer = regexp.MustCompile(`^v\d+\.\d+\.\d+$`)
33
+
34
+	// vX.Y.Z-n-g0000000 git post-release, semver prerelease
35
+	// vX.Y.Z-dirty git post-release, semver prerelease
36
+	gitVer = regexp.MustCompile(`^(v\d+\.\d+)\.(\d+)(-(\d+))?(-(g[0-9a-f]+))?(-(dirty))?`)
37
+}
38
+
39
+func main() {
40
+	args := os.Args[1:]
41
+	for i := range args {
42
+		arg := args[i]
43
+		if "-f" == arg || "--fail" == arg {
44
+			exitCode = 1
45
+		} else if "-V" == arg || "version" == arg || "-version" == arg || "--version" == arg {
46
+			fmt.Println(GitRev)
47
+			fmt.Println(GitVersion)
48
+			fmt.Println(GitTimestamp)
49
+			os.Exit(0)
50
+		}
51
+	}
52
+	if "" != os.Getenv("GITVER_FAIL") && "false" != os.Getenv("GITVER_FAIL") {
53
+		exitCode = 1
54
+	}
55
+
56
+	desc, err := gitDesc()
57
+	if nil != err {
58
+		log.Fatalf("Failed to get git version: %s\n", err)
59
+		os.Exit(exitCode)
60
+	}
61
+	rev := gitRev()
62
+	ver := semVer(desc)
63
+	ts, err := gitTimestamp(desc)
64
+	if nil != err {
65
+		ts = time.Now()
66
+	}
67
+
68
+	v := struct {
69
+		Timestamp string
70
+		Version   string
71
+		GitRev    string
72
+	}{
73
+		Timestamp: ts.Format(time.RFC3339),
74
+		Version:   ver,
75
+		GitRev:    rev,
76
+	}
77
+
78
+	// Create or overwrite the go file from template
79
+	var buf bytes.Buffer
80
+	if err := versionTpl.Execute(&buf, v); nil != err {
81
+		panic(err)
82
+	}
83
+
84
+	// Format
85
+	src, err := format.Source(buf.Bytes())
86
+	if nil != err {
87
+		panic(err)
88
+	}
89
+
90
+	// Write to disk (in the Current Working Directory)
91
+	f, err := os.Create(verFile)
92
+	if nil != err {
93
+		panic(err)
94
+	}
95
+	if _, err := f.Write(src); nil != err {
96
+		panic(err)
97
+	}
98
+	if err := f.Close(); nil != err {
99
+		panic(err)
100
+	}
101
+}
102
+
103
+func gitDesc() (string, error) {
104
+	args := strings.Split("git describe --tags --dirty --always", " ")
105
+	cmd := exec.Command(args[0], args[1:]...)
106
+	out, err := cmd.CombinedOutput()
107
+	if nil != err {
108
+		// Don't panic, just carry on
109
+		//out = []byte("v0.0.0-0-g0000000")
110
+		return "", err
111
+	}
112
+	return strings.TrimSpace(string(out)), nil
113
+}
114
+
115
+func gitRev() string {
116
+	args := strings.Split("git rev-parse HEAD", " ")
117
+	cmd := exec.Command(args[0], args[1:]...)
118
+	out, err := cmd.CombinedOutput()
119
+	if nil != err {
120
+		fmt.Fprintf(os.Stderr,
121
+			"\nUnexpected Error\n\n"+
122
+				"Please open an issue at https://git.rootprojects.org/root/go-gitver/issues/new \n"+
123
+				"Please include the following:\n\n"+
124
+				"Command: %s\n"+
125
+				"Output: %s\n"+
126
+				"Error: %s\n"+
127
+				"\nPlease and Thank You.\n\n", strings.Join(args, " "), out, err)
128
+		os.Exit(exitCode)
129
+	}
130
+	return strings.TrimSpace(string(out))
131
+}
132
+
133
+func semVer(desc string) string {
134
+	if exactVer.MatchString(desc) {
135
+		// v1.0.0
136
+		return desc
137
+	}
138
+
139
+	if !gitVer.MatchString(desc) {
140
+		return ""
141
+	}
142
+
143
+	// (v1.0).(0)(-(1))(-(g0000000))(-(dirty))
144
+	vers := gitVer.FindStringSubmatch(desc)
145
+	patch, err := strconv.Atoi(vers[2])
146
+	if nil != err {
147
+		fmt.Fprintf(os.Stderr,
148
+			"\nUnexpected Error\n\n"+
149
+				"Please open an issue at https://git.rootprojects.org/root/go-gitver/issues/new \n"+
150
+				"Please include the following:\n\n"+
151
+				"git description: %s\n"+
152
+				"RegExp: %#v\n"+
153
+				"Error: %s\n"+
154
+				"\nPlease and Thank You.\n\n", desc, gitVer, err)
155
+		os.Exit(exitCode)
156
+	}
157
+
158
+	// v1.0.1-pre1
159
+	// v1.0.1-pre1+g0000000
160
+	// v1.0.1-pre0+dirty
161
+	// v1.0.1-pre0+g0000000-dirty
162
+	if "" == vers[4] {
163
+		vers[4] = "0"
164
+	}
165
+	ver := fmt.Sprintf("%s.%d-pre%s", vers[1], patch+1, vers[4])
166
+	if "" != vers[6] || "dirty" == vers[8] {
167
+		ver += "+"
168
+		if "" != vers[6] {
169
+			ver += vers[6]
170
+			if "" != vers[8] {
171
+				ver += "-"
172
+			}
173
+		}
174
+		ver += vers[8]
175
+	}
176
+
177
+	return ver
178
+}
179
+
180
+func gitTimestamp(desc string) (time.Time, error) {
181
+	args := []string{
182
+		"git",
183
+		"show", desc,
184
+		"--format=%cd",
185
+		"--date=format:%Y-%m-%dT%H:%M:%SZ%z",
186
+		"--no-patch",
187
+	}
188
+	cmd := exec.Command(args[0], args[1:]...)
189
+	out, err := cmd.CombinedOutput()
190
+	if nil != err {
191
+		// a dirty desc was probably used
192
+		return time.Time{}, err
193
+	}
194
+	return time.Parse(time.RFC3339, strings.TrimSpace(string(out)))
195
+}
196
+
197
+var versionTpl = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
198
+package main
199
+
200
+func init() {
201
+	GitRev = "{{ .GitRev }}"
202
+	if "" != "{{ .Version }}" {
203
+		GitVersion = "{{ .Version }}"
204
+	}
205
+	GitTimestamp = "{{ .Timestamp }}"
206
+}
207
+`))

+ 3
- 0
go.mod View File

@@ -0,0 +1,3 @@
1
+module git.rootprojects.org/root/go-gitver
2
+
3
+go 1.12

Loading…
Cancel
Save