├── .gitignore ├── Dockerfile ├── .editorconfig ├── Makefile ├── main.go ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── go.mod ├── cmd ├── root_test.go └── root.go ├── LICENSE ├── .goreleaser.yml ├── internal ├── generate.go ├── semver_test.go ├── semver.go ├── generate_test.go ├── git.go └── git_test.go ├── action.yaml ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | coverage.out 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | ENTRYPOINT ["/bin/git-describe-semver"] 3 | COPY git-describe-semver /bin/git-describe-semver 4 | WORKDIR /workdir 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.go] 12 | indent_style = tab 13 | indent_size = 4 14 | 15 | [Makefile] 16 | indent_style = tab 17 | indent_size = 4 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: * 2 | 3 | run: 4 | go run . 5 | 6 | test: 7 | go test -v ./... 8 | 9 | test-watch: 10 | watch -n1 go test -v ./... 11 | 12 | test-cover: 13 | go test -coverprofile=coverage.out ./... 14 | go tool cover -func=coverage.out 15 | go tool cover -html=coverage.out 16 | 17 | build: 18 | goreleaser release --rm-dist --skip-publish --snapshot 19 | 20 | release: 21 | goreleaser release --rm-dist 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/choffmeister/git-describe-semver/cmd" 8 | ) 9 | 10 | // nolint: gochecknoglobals 11 | var ( 12 | version = "dev" 13 | commit = "" 14 | date = "" 15 | builtBy = "" 16 | ) 17 | 18 | func main() { 19 | if err := cmd.Execute(cmd.FullVersion{Version: version, Commit: commit, Date: date, BuiltBy: builtBy}); err != nil { 20 | fmt.Printf("%v\n", err) 21 | os.Exit(1) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/setup-go@v3 11 | with: 12 | go-version: 1.24.x 13 | - uses: docker/setup-qemu-action@v2 14 | - uses: docker/setup-buildx-action@v2 15 | - uses: docker/login-action@v2 16 | with: 17 | registry: ghcr.io 18 | username: choffmeister 19 | password: ${{ secrets.GITHUB_TOKEN }} 20 | - uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | - name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v3 25 | with: 26 | distribution: goreleaser 27 | args: release --rm-dist 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/choffmeister/git-describe-semver 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/Microsoft/go-winio v0.6.0 // indirect 7 | github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 // indirect 8 | github.com/go-git/go-git/v5 v5.4.2 9 | github.com/jessevdk/go-flags v1.5.0 10 | github.com/kevinburke/ssh_config v1.2.0 // indirect 11 | github.com/sergi/go-diff v1.2.0 // indirect 12 | github.com/stretchr/testify v1.7.0 13 | github.com/xanzy/ssh-agent v0.3.2 // indirect 14 | golang.org/x/crypto v0.3.0 // indirect 15 | golang.org/x/net v0.2.0 // indirect 16 | golang.org/x/sys v0.2.0 // indirect 17 | ) 18 | 19 | require ( 20 | github.com/acomagu/bufpipe v1.0.3 // indirect 21 | github.com/cloudflare/circl v1.3.0 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/emirpasic/gods v1.18.1 // indirect 24 | github.com/go-git/gcfg v1.5.0 // indirect 25 | github.com/go-git/go-billy/v5 v5.3.1 // indirect 26 | github.com/imdario/mergo v0.3.13 // indirect 27 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 28 | github.com/mitchellh/go-homedir v1.1.0 // indirect 29 | github.com/pmezard/go-difflib v1.0.0 // indirect 30 | golang.org/x/mod v0.7.0 // indirect 31 | golang.org/x/tools v0.3.0 // indirect 32 | gopkg.in/warnings.v0 v0.1.2 // indirect 33 | gopkg.in/yaml.v3 v3.0.0 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /cmd/root_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/choffmeister/git-describe-semver/internal" 8 | "github.com/go-git/go-git/v5" 9 | "github.com/go-git/go-git/v5/plumbing/object" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestRun(t *testing.T) { 14 | assert := assert.New(t) 15 | dir, _ := ioutil.TempDir("", "example") 16 | author := object.Signature{Name: "Test", Email: "test@test.com"} 17 | _, err := run(dir, internal.GenerateVersionOptions{PrereleasePrefix: "dev"}) 18 | assert.Error(err) 19 | 20 | repo, _ := git.PlainInit(dir, false) 21 | worktree, _ := repo.Worktree() 22 | _, err = run(dir, internal.GenerateVersionOptions{PrereleasePrefix: "dev"}) 23 | assert.Error(err) 24 | 25 | commit1, _ := worktree.Commit("first", &git.CommitOptions{Author: &author}) 26 | repo.CreateTag("invalid", commit1, nil) 27 | _, err = run(dir, internal.GenerateVersionOptions{PrereleasePrefix: "dev"}) 28 | assert.Error(err) 29 | 30 | commit2, _ := worktree.Commit("first", &git.CommitOptions{Author: &author}) 31 | repo.CreateTag("v1.0.0", commit2, nil) 32 | 33 | commit3, _ := worktree.Commit("second", &git.CommitOptions{Author: &author}) 34 | result, err := run(dir, internal.GenerateVersionOptions{PrereleasePrefix: "dev"}) 35 | assert.NoError(err) 36 | assert.Equal("v1.0.1-dev.1.g"+commit3.String()[0:7], *result) 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Christian Hoffmeister. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Christian Hoffmeister nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | - go mod tidy 6 | builds: 7 | - env: 8 | - CGO_ENABLED=0 9 | goos: 10 | - linux 11 | - windows 12 | - darwin 13 | goarch: 14 | - amd64 15 | - arm64 16 | dockers: 17 | - image_templates: 18 | - "ghcr.io/choffmeister/{{ .ProjectName }}:{{ .Version }}-amd64" 19 | - "ghcr.io/choffmeister/{{ .ProjectName }}:latest-amd64" 20 | use: buildx 21 | goarch: amd64 22 | dockerfile: Dockerfile 23 | build_flag_templates: 24 | - "--platform=linux/amd64" 25 | - image_templates: 26 | - "ghcr.io/choffmeister/{{ .ProjectName }}:{{ .Version }}-arm64v8" 27 | - "ghcr.io/choffmeister/{{ .ProjectName }}:latest-arm64v8" 28 | use: buildx 29 | goarch: arm64 30 | dockerfile: Dockerfile 31 | build_flag_templates: 32 | - "--platform=linux/arm64/v8" 33 | docker_manifests: 34 | - name_template: ghcr.io/choffmeister/{{ .ProjectName }}:{{ .Version }} 35 | image_templates: 36 | - ghcr.io/choffmeister/{{ .ProjectName }}:{{ .Version }}-amd64 37 | - ghcr.io/choffmeister/{{ .ProjectName }}:{{ .Version }}-arm64v8 38 | - name_template: ghcr.io/choffmeister/{{ .ProjectName }}:latest 39 | image_templates: 40 | - ghcr.io/choffmeister/{{ .ProjectName }}:latest-amd64 41 | - ghcr.io/choffmeister/{{ .ProjectName }}:latest-arm64v8 42 | checksum: 43 | name_template: 'checksums.txt' 44 | snapshot: 45 | name_template: "0.0.0-dev" 46 | changelog: 47 | sort: asc 48 | filters: 49 | exclude: 50 | - '^docs:' 51 | - '^test:' 52 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: 5 | - "*" 6 | pull_request: 7 | branches: 8 | - main 9 | env: 10 | GO111MODULE: on 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/setup-go@v2 16 | with: 17 | go-version: 1.24.x 18 | - uses: actions/checkout@v3 19 | - run: go test ./... 20 | test-action-latest: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | with: 25 | fetch-depth: 0 26 | - id: git-describe-semver 27 | uses: choffmeister/git-describe-semver@main 28 | with: 29 | version: latest 30 | dir: . 31 | fallback: v0.0.0-init 32 | drop-prefix: true 33 | prerelease-prefix: prefix 34 | prerelease-suffix: suffix 35 | prerelease-timestamped: true 36 | - name: Verify output 37 | run: | 38 | if [[ -z "${{ steps.git-describe-semver.outputs.version }}" ]]; then 39 | echo Output version is empty! 40 | exit 1 41 | fi 42 | test-action-0-3-11: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v3 46 | with: 47 | fetch-depth: 0 48 | - id: git-describe-semver 49 | uses: choffmeister/git-describe-semver@main 50 | with: 51 | version: 0.3.11 52 | dir: . 53 | fallback: v0.0.0-init 54 | drop-prefix: true 55 | prerelease-prefix: prefix 56 | prerelease-suffix: suffix 57 | prerelease-timestamped: true 58 | - name: Verify output 59 | run: | 60 | if [[ -z "${{ steps.git-describe-semver.outputs.version }}" ]]; then 61 | echo Output version is empty! 62 | exit 1 63 | fi 64 | test-action-0-4-0: 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v3 68 | with: 69 | fetch-depth: 0 70 | - id: git-describe-semver 71 | uses: choffmeister/git-describe-semver@main 72 | with: 73 | version: 0.4.0 74 | dir: . 75 | fallback: v0.0.0-init 76 | drop-prefix: true 77 | prerelease-prefix: prefix 78 | prerelease-suffix: suffix 79 | prerelease-timestamped: true 80 | next-release: major 81 | - name: Verify output 82 | run: | 83 | if [[ -z "${{ steps.git-describe-semver.outputs.version }}" ]]; then 84 | echo Output version is empty! 85 | exit 1 86 | fi 87 | -------------------------------------------------------------------------------- /internal/generate.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // GenerateVersionOptions ... 11 | type GenerateVersionOptions struct { 12 | FallbackTagName string 13 | DropTagNamePrefix bool 14 | PrereleaseSuffix string 15 | PrereleasePrefix string 16 | PrereleaseTimestamped bool 17 | NextRelease string 18 | Format string 19 | } 20 | 21 | // GenerateVersion ... 22 | func GenerateVersion(tagName string, counter int, headHash string, timestamp time.Time, opts GenerateVersionOptions) (*string, error) { 23 | devPrerelease := []string{opts.PrereleasePrefix, strconv.Itoa(counter), "g" + (headHash)[0:7]} 24 | if opts.PrereleaseTimestamped { 25 | timestampUTC := timestamp.UTC() 26 | timestampSegments := []string{ 27 | strconv.FormatInt(timestampUTC.UnixMilli()/1000, 10), 28 | } 29 | devPrerelease = []string{opts.PrereleasePrefix, strings.Join(timestampSegments, ""), "g" + (headHash)[0:7]} 30 | } 31 | if opts.PrereleaseSuffix != "" { 32 | devPrerelease[len(devPrerelease)-1] = devPrerelease[len(devPrerelease)-1] + "-" + opts.PrereleaseSuffix 33 | } 34 | version := &SemVer{} 35 | if tagName == "" { 36 | version = SemVerParse(opts.FallbackTagName) 37 | if version == nil { 38 | return nil, fmt.Errorf("unable to parse fallback tag") 39 | } 40 | version.Prerelease = devPrerelease 41 | } else { 42 | version = SemVerParse(tagName) 43 | if version == nil { 44 | return nil, fmt.Errorf("unable to parse tag") 45 | } 46 | if counter > 0 { 47 | if len(version.Prerelease) > 0 { 48 | version = &SemVer{ 49 | Prefix: version.Prefix, 50 | Major: version.Major, 51 | Minor: version.Minor, 52 | Patch: version.Patch, 53 | Prerelease: append(version.Prerelease, devPrerelease...), 54 | BuildMetadata: append([]string{}, version.BuildMetadata...), 55 | } 56 | } else { 57 | version = &SemVer{ 58 | Prefix: version.Prefix, 59 | Major: version.Major, 60 | Minor: version.Minor, 61 | Patch: version.Patch + 1, 62 | Prerelease: devPrerelease, 63 | BuildMetadata: append([]string{}, version.BuildMetadata...), 64 | } 65 | } 66 | } 67 | } 68 | version.Bump(opts.NextRelease) 69 | if opts.DropTagNamePrefix { 70 | version.Prefix = "" 71 | } 72 | result := version.String() 73 | if opts.Format != "" { 74 | result = strings.ReplaceAll(opts.Format, "", result) 75 | } 76 | return &result, nil 77 | } 78 | -------------------------------------------------------------------------------- /internal/semver_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSemVerString(t *testing.T) { 10 | assert := assert.New(t) 11 | test := func(input SemVer, expected string) { 12 | actual := input.String() 13 | assert.Equal(expected, actual) 14 | } 15 | 16 | test(SemVer{}, "0.0.0") 17 | test(SemVer{Prefix: "v"}, "v0.0.0") 18 | test(SemVer{Major: 1, Minor: 2, Patch: 3}, "1.2.3") 19 | test(SemVer{Prerelease: []string{"rc", "1"}}, "0.0.0-rc.1") 20 | test(SemVer{Prerelease: []string{"alpha-version", "1"}}, "0.0.0-alpha-version.1") 21 | test(SemVer{BuildMetadata: []string{"foo", "bar"}}, "0.0.0+foo.bar") 22 | test(SemVer{Prefix: "v", Major: 1, Minor: 2, Patch: 3, Prerelease: []string{"rc", "1"}, BuildMetadata: []string{"foo", "bar"}}, "v1.2.3-rc.1+foo.bar") 23 | } 24 | 25 | func TestSemVerParse(t *testing.T) { 26 | assert := assert.New(t) 27 | test := func(input string, expected *SemVer) { 28 | actual := SemVerParse(input) 29 | assert.Equal(expected, actual) 30 | } 31 | 32 | test("0.0.0", &SemVer{}) 33 | test("v0.0.0", &SemVer{Prefix: "v"}) 34 | test("1.2.3", &SemVer{Major: 1, Minor: 2, Patch: 3}) 35 | test("0.0.0-rc.1", &SemVer{Prerelease: []string{"rc", "1"}}) 36 | test("0.0.0-alpha-version.1", &SemVer{Prerelease: []string{"alpha-version", "1"}}) 37 | test("0.0.0+foo.bar", &SemVer{BuildMetadata: []string{"foo", "bar"}}) 38 | test("v1.2.3-rc.1+foo.bar", &SemVer{Prefix: "v", Major: 1, Minor: 2, Patch: 3, Prerelease: []string{"rc", "1"}, BuildMetadata: []string{"foo", "bar"}}) 39 | test("invalid", nil) 40 | } 41 | 42 | func TestSemVerEqual(t *testing.T) { 43 | assert := assert.New(t) 44 | test := func(a SemVer, b SemVer, expected bool) { 45 | actual := a.Equal(b) 46 | assert.Equal(expected, actual) 47 | } 48 | 49 | test(SemVer{}, SemVer{}, true) 50 | test(SemVer{Major: 1}, SemVer{Major: 2}, false) 51 | test(SemVer{Minor: 1}, SemVer{Minor: 2}, false) 52 | test(SemVer{Patch: 1}, SemVer{Patch: 2}, false) 53 | test(SemVer{Prerelease: []string{"foo"}}, SemVer{Prerelease: []string{"foo"}}, true) 54 | test(SemVer{Prerelease: []string{"foo"}}, SemVer{Prerelease: []string{"bar"}}, false) 55 | test(SemVer{Prerelease: []string{"foo"}}, SemVer{}, false) 56 | test(SemVer{}, SemVer{Prerelease: []string{"bar"}}, false) 57 | test(SemVer{BuildMetadata: []string{"foo"}}, SemVer{BuildMetadata: []string{"foo"}}, true) 58 | test(SemVer{BuildMetadata: []string{"foo"}}, SemVer{BuildMetadata: []string{"bar"}}, false) 59 | test(SemVer{BuildMetadata: []string{"foo"}}, SemVer{}, false) 60 | test(SemVer{}, SemVer{BuildMetadata: []string{"bar"}}, false) 61 | } 62 | -------------------------------------------------------------------------------- /action.yaml: -------------------------------------------------------------------------------- 1 | name: 'Git describe semver' 2 | description: '' 3 | inputs: 4 | version: 5 | description: 'Which git-describe-semver release should be used?' 6 | default: 'latest' 7 | dir: 8 | description: 'Git worktree directory (defaults to current directory)' 9 | default: '.' 10 | fallback: 11 | description: 'Fallback to given tag name if no tag is available' 12 | default: 'v0.0.0' 13 | drop-prefix: 14 | description: 'Drop any present prefix (like "v") from the output' 15 | default: 'false' 16 | prerelease-prefix: 17 | description: 'Adds a dash-separated prefix to the prerelease part' 18 | default: 'dev' 19 | prerelease-suffix: 20 | description: 'Adds a dash-separated suffix to the prerelease part' 21 | default: '' 22 | prerelease-timestamped: 23 | description: 'Use timestamp instead of commit count for prerelease' 24 | default: 'false' 25 | next-release: 26 | description: 'Bump current version to next release (choices: "major", "minor", "patch")' 27 | default: '' 28 | outputs: 29 | version: 30 | description: 'Version output from git-describe-semver' 31 | value: ${{ steps.git-describe-semver.outputs.version }} 32 | runs: 33 | using: 'composite' 34 | steps: 35 | - if: inputs.version == 'latest' 36 | run: | 37 | cd /tmp 38 | URL=$(curl -fsSL https://api.github.com/repos/choffmeister/git-describe-semver/releases/latest | jq -r '.assets[] | select(.name|test("linux_amd64\\.tar\\.gz$")) | .browser_download_url') 39 | curl -fsSL "$URL" | tar xz 40 | mv git-describe-semver /usr/local/bin 41 | shell: bash 42 | - if: inputs.version != 'latest' 43 | run: | 44 | cd /tmp 45 | VERSION="$(echo ${{ inputs.version }} | sed 's#^v##')" 46 | URL="https://github.com/choffmeister/git-describe-semver/releases/download/v${VERSION}/git-describe-semver_${VERSION}_linux_amd64.tar.gz" 47 | curl -fsSL "$URL" | tar xz 48 | mv git-describe-semver /usr/local/bin 49 | shell: bash 50 | - id: git-describe-semver 51 | run: | 52 | git-describe-semver \ 53 | ${{ format('--dir="{0}"', inputs.dir) }} \ 54 | ${{ format('--fallback="{0}"', inputs.fallback) }} \ 55 | ${{ inputs.drop-prefix == 'true' && format('--drop-prefix') || '' }} \ 56 | ${{ format('--prerelease-prefix="{0}"', inputs.prerelease-prefix) }} \ 57 | ${{ format('--prerelease-suffix="{0}"', inputs.prerelease-suffix) }} \ 58 | ${{ inputs.prerelease-timestamped == 'true' && format('--prerelease-timestamped') || '' }} \ 59 | ${{ inputs.next-release != '' && format('--next-release="{0}"', inputs.next-release) || '' }} \ 60 | --format="version=" \ 61 | $GITHUB_OUTPUT 62 | shell: bash 63 | - run: echo ${{ steps.git-describe-semver.outputs.version }} 64 | shell: bash 65 | -------------------------------------------------------------------------------- /internal/semver.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // SemVer ... 11 | type SemVer struct { 12 | Prefix string 13 | Major int 14 | Minor int 15 | Patch int 16 | Prerelease []string 17 | BuildMetadata []string 18 | } 19 | 20 | // Equal ... 21 | func (v SemVer) Equal(v2 SemVer) bool { 22 | return true && 23 | v.Prefix == v2.Prefix && 24 | v.Major == v2.Major && 25 | v.Minor == v2.Minor && 26 | v.Patch == v2.Patch && 27 | equalStringSlice(v.Prerelease, v2.Prerelease) && 28 | equalStringSlice(v.BuildMetadata, v2.BuildMetadata) 29 | } 30 | 31 | // Bump ... 32 | func (v *SemVer) Bump(nextRelease string) { 33 | if nextRelease == "" { 34 | return 35 | } 36 | isPrerelease := len(v.Prerelease) > 0 37 | patch := v.Patch 38 | if nextRelease == "patch" && !isPrerelease { 39 | patch++ 40 | } 41 | minor := v.Minor 42 | if nextRelease == "minor" { 43 | if v.Patch != 0 || !isPrerelease { 44 | minor++ 45 | } 46 | patch = 0 47 | } 48 | major := v.Major 49 | if nextRelease == "major" { 50 | if v.Patch != 0 || !isPrerelease { 51 | major++ 52 | } 53 | minor = 0 54 | patch = 0 55 | } 56 | v.Major = major 57 | v.Minor = minor 58 | v.Patch = patch 59 | v.Prerelease = []string{} 60 | v.BuildMetadata = []string{} 61 | } 62 | 63 | // String ... 64 | func (v SemVer) String() string { 65 | str := fmt.Sprintf("%s%d.%d.%d", v.Prefix, v.Major, v.Minor, v.Patch) 66 | if len(v.Prerelease) > 0 { 67 | str = str + "-" + strings.Join(v.Prerelease, ".") 68 | } 69 | if len(v.BuildMetadata) > 0 { 70 | str = str + "+" + strings.Join(v.BuildMetadata, ".") 71 | } 72 | return str 73 | } 74 | 75 | var semVerRegexp = regexp.MustCompile(`^([A-Za-z]+)?(\d+)\.(\d+)\.(\d+)(?:-((?:[0-9A-Za-z-]+)(?:\.[0-9A-Za-z-]+)*))?(?:\+((?:[0-9A-Za-z-]+)(?:\.[0-9A-Za-z-]+)*))?$`) 76 | 77 | // SemVerParse ... 78 | func SemVerParse(str string) *SemVer { 79 | match := semVerRegexp.FindStringSubmatch(str) 80 | if len(match) == 0 { 81 | return nil 82 | } 83 | 84 | prefix := match[1] 85 | major, _ := strconv.Atoi(match[2]) 86 | minor, _ := strconv.Atoi(match[3]) 87 | patch, _ := strconv.Atoi(match[4]) 88 | prerelease := stringToSlice(match[5], ".") 89 | buildMetadata := stringToSlice(match[6], ".") 90 | 91 | return &SemVer{ 92 | Prefix: prefix, 93 | Major: major, 94 | Minor: minor, 95 | Patch: patch, 96 | Prerelease: prerelease, 97 | BuildMetadata: buildMetadata, 98 | } 99 | } 100 | 101 | func stringToSlice(s string, sep string) []string { 102 | temp := strings.Split(s, sep) 103 | if temp[0] == "" { 104 | return []string(nil) 105 | } 106 | return temp 107 | } 108 | 109 | func equalStringSlice(a, b []string) bool { 110 | if len(a) != len(b) { 111 | return false 112 | } 113 | for i, v := range a { 114 | if v != b[i] { 115 | return false 116 | } 117 | } 118 | return true 119 | } 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-describe-semver 2 | 3 | Replacement for `git describe --tags` that produces [semver](https://semver.org/) compatible versions that follow to semver sorting rules. 4 | 5 | ## Comparison 6 | 7 | | Previous git tag | git describe --tags | git-describe-semver --fallback v0.0.0 | 8 | |--------------------|--------------------------------|---------------------------------------| 9 | | `v1.2.3` | `v1.2.3` | `v1.2.3` | 10 | | `v1.2.3` | `v1.2.3-23-gabc1234` | `v1.2.4-dev.23.gabc1234` | 11 | | `v1.3.0-rc.1` | `v1.3.0-rc.1-23-gabc1234` | `v1.3.0-rc.1.dev.23.gabc1234` | 12 | | `v1.3.0-rc.1+info` | `v1.3.0-rc.1+info-23-gabc1234` | `v1.3.0-rc.1.dev.23.gabc1234+info` | 13 | | none | fail | `v0.0.0-dev.23.gabc1234` | 14 | 15 | ## Next Release 16 | 17 | | Previous git tag | git describe --tags | git-describe-semver --fallback v0.0.0 | --next-release | 18 | |------------------|---------------------------|---------------------------------------|-----------------------| 19 | | `v1.2.3` | `v1.2.3` | `v1.2.4` | patch | 20 | | `v1.2.3` | `v1.2.3-23-gabc1234` | `v1.2.4` | patch | 21 | | `v1.3.0-rc.1` | `v1.3.0-rc.1-23-gabc1234` | `v1.3.0` | patch / minor | 22 | | `v1.3.1-rc.1` | `v1.3.1-rc.1-23-gabc1234` | `v1.4.0` | minor | 23 | | `v1.0.0-rc.1` | `v1.0.0-rc.1-23-gabc1234` | `v1.0.0` | major | 24 | | `v1.0.0` | `v1.0.0` | `v2.0.0` | major | 25 | | `v1.0.1-rc.1` | `v1.0.1-rc.1-23-gabc1234` | `v2.0.0` | major | 26 | | none | fail | `v0.0.0` | patch / minor / major | 27 | 28 | ## Usage 29 | 30 | * Flag `--dir /some/git/worktree`: Git worktree directory (defaults to current directory `.`) 31 | * Flag `--fallback v0.0.0`: Fallback to given tag name if no tag is available 32 | * Flag `--drop-prefix`: Drop any present prefix (like `v`) from the output 33 | * Flag `--prerelease-suffix`: Adds a dash-separated suffix to the prerelease part 34 | * Flag `--prerelease-prefix`: Adds a dash-separated prefix to the prerelease part (defaults to `dev`) 35 | * Flag `--prerelease-timestamped`: Use timestamp instead of commit count for prerelease 36 | * Flag `--next-release`: Bump current version to next release (choices: `major`, `minor`, `patch`) 37 | * Flag `--format`: Changes output (use `` as placeholder) 38 | 39 | ### Docker 40 | 41 | ```bash 42 | cd my-git-directory 43 | docker pull ghcr.io/choffmeister/git-describe-semver:latest 44 | docker run --rm -v $PWD:/workdir ghcr.io/choffmeister/git-describe-semver:latest 45 | ``` 46 | 47 | ### GitHub action 48 | 49 | ```yaml 50 | # .github/workflows/build.yml 51 | name: build 52 | jobs: 53 | update: 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v3 57 | with: 58 | fetch-depth: 0 59 | - id: git-describe-semver 60 | uses: choffmeister/git-describe-semver@main 61 | with: 62 | version: latest 63 | dir: . 64 | fallback: v0.0.0 65 | drop-prefix: true 66 | prerelease-prefix: dev 67 | prerelease-suffix: SNAPSHOT 68 | prerelease-timestamped: true 69 | next-release: '' 70 | - run: echo This is the version ${{ steps.git-describe-semver.outputs.version }} 71 | ``` 72 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "runtime/debug" 8 | "strings" 9 | "time" 10 | 11 | "github.com/choffmeister/git-describe-semver/internal" 12 | "github.com/jessevdk/go-flags" 13 | ) 14 | 15 | func run(dir string, opts internal.GenerateVersionOptions) (*string, error) { 16 | repo, err := internal.OpenRepository(dir) 17 | if err != nil { 18 | return nil, fmt.Errorf("unable to open git repository: %v", err) 19 | } 20 | tagName, counter, headHash, err := internal.GitDescribe(*repo) 21 | if err != nil { 22 | return nil, fmt.Errorf("unable to describe commit: %v", err) 23 | } 24 | result, err := internal.GenerateVersion(*tagName, *counter, *headHash, time.Now(), opts) 25 | if err != nil { 26 | return nil, fmt.Errorf("unable to generate version: %v", err) 27 | } 28 | return result, nil 29 | } 30 | 31 | func openStdoutOrFile(file string) (io.WriteCloser, error) { 32 | if file == "-" { 33 | return os.Stdout, nil 34 | } 35 | return os.OpenFile(file, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) 36 | } 37 | 38 | type ParserOptions struct { 39 | Dir string `long:"dir" default:"." description:"The git worktree directory"` 40 | Fallback string `long:"fallback" description:"The first version to fallback to should there be no tag"` 41 | DropPrefix bool `long:"drop-prefix" description:"Drop prefix from output"` 42 | PrereleaseSuffix string `long:"prerelease-suffix" description:"Suffix to add to prereleases"` 43 | PrereleasePrefix string `long:"prerelease-prefix" default:"dev" description:"Prefix to use as start of prerelease"` 44 | PrereleaseTimestamped bool `long:"prerelease-timestamped" description:"Use timestamp instead of commit count for prerelease"` 45 | NextRelease string `long:"next-release" description:"Bump current version to next release" choice:"major" choice:"minor" choice:"patch"` 46 | Format string `long:"format" description:"Format of output (use as placeholder)"` 47 | } 48 | 49 | func Execute(version FullVersion) error { 50 | var options ParserOptions 51 | parser := flags.NewParser(&options, flags.Default) 52 | args, err := parser.Parse() 53 | if err != nil { 54 | switch flagsErr := err.(type) { 55 | case flags.ErrorType: 56 | if flagsErr == flags.ErrHelp { 57 | os.Exit(0) 58 | } 59 | os.Exit(1) 60 | default: 61 | os.Exit(1) 62 | } 63 | } 64 | 65 | opts := internal.GenerateVersionOptions{ 66 | FallbackTagName: options.Fallback, 67 | DropTagNamePrefix: options.DropPrefix, 68 | PrereleaseSuffix: options.PrereleaseSuffix, 69 | PrereleasePrefix: options.PrereleasePrefix, 70 | PrereleaseTimestamped: options.PrereleaseTimestamped, 71 | NextRelease: options.NextRelease, 72 | Format: options.Format, 73 | } 74 | result, err := run(options.Dir, opts) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | file := "-" 80 | if len(args) == 1 { 81 | arg := args[0] 82 | if strings.HasPrefix(arg, "$") { 83 | file = os.Getenv(strings.TrimPrefix(arg, "$")) 84 | } else { 85 | file = arg 86 | } 87 | } 88 | output, err := openStdoutOrFile(file) 89 | if err != nil { 90 | return err 91 | } 92 | defer output.Close() 93 | fmt.Fprintf(output, "%s\n", *result) 94 | 95 | return nil 96 | } 97 | 98 | type FullVersion struct { 99 | Version string 100 | Commit string 101 | Date string 102 | BuiltBy string 103 | } 104 | 105 | func (v FullVersion) ToString() string { 106 | result := v.Version 107 | if v.Commit != "" { 108 | result = fmt.Sprintf("%s\ncommit: %s", result, v.Commit) 109 | } 110 | if v.Date != "" { 111 | result = fmt.Sprintf("%s\nbuilt at: %s", result, v.Date) 112 | } 113 | if v.BuiltBy != "" { 114 | result = fmt.Sprintf("%s\nbuilt by: %s", result, v.BuiltBy) 115 | } 116 | if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" { 117 | result = fmt.Sprintf("%s\nmodule version: %s, checksum: %s", result, info.Main.Version, info.Main.Sum) 118 | } 119 | return result 120 | } 121 | -------------------------------------------------------------------------------- /internal/generate_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGenerateVersion(t *testing.T) { 11 | now, _ := time.Parse(time.RFC822Z, "01 Jan 20 02:03 -0000") 12 | assert := assert.New(t) 13 | test := func(inputTagName string, inputCounter int, inputHeadHash string, inputOpts GenerateVersionOptions, expected string) { 14 | actual, err := GenerateVersion(inputTagName, inputCounter, inputHeadHash, now, inputOpts) 15 | if assert.NoError(err) { 16 | assert.Equal(expected, *actual) 17 | } 18 | } 19 | 20 | test("0.0.0", 0, "abc1234", GenerateVersionOptions{PrereleasePrefix: "dev"}, "0.0.0") 21 | test("0.0.0", 1, "abc1234", GenerateVersionOptions{PrereleasePrefix: "dev"}, "0.0.1-dev.1.gabc1234") 22 | test("0.0.0-rc1", 1, "abc1234", GenerateVersionOptions{PrereleasePrefix: "dev"}, "0.0.0-rc1.dev.1.gabc1234") 23 | test("0.0.0-rc.1", 1, "abc1234", GenerateVersionOptions{PrereleasePrefix: "dev"}, "0.0.0-rc.1.dev.1.gabc1234") 24 | test("0.0.0-rc.1+foobar", 1, "abc1234", GenerateVersionOptions{PrereleasePrefix: "dev"}, "0.0.0-rc.1.dev.1.gabc1234+foobar") 25 | test("v0.0.0-rc.1+foobar", 1, "abc1234", GenerateVersionOptions{PrereleasePrefix: "dev"}, "v0.0.0-rc.1.dev.1.gabc1234+foobar") 26 | 27 | test("", 1, "abc1234", GenerateVersionOptions{FallbackTagName: "v0.0.0", NextRelease: "patch"}, "v0.0.0") 28 | test("", 1, "abc1234", GenerateVersionOptions{FallbackTagName: "v0.0.0", NextRelease: "minor"}, "v0.0.0") 29 | test("", 1, "abc1234", GenerateVersionOptions{FallbackTagName: "v0.0.0", NextRelease: "major"}, "v0.0.0") 30 | test("v0.0.0-rc.1+foobar", 1, "abc1234", GenerateVersionOptions{NextRelease: "patch"}, "v0.0.0") 31 | test("v0.0.1+foobar", 1, "abc1234", GenerateVersionOptions{NextRelease: "patch"}, "v0.0.2") 32 | test("v1.2.3", 1, "abc1234", GenerateVersionOptions{NextRelease: "patch"}, "v1.2.4") 33 | test("v0.1.0-rc1", 1, "abc1234", GenerateVersionOptions{NextRelease: "minor"}, "v0.1.0") 34 | test("v0.1.0", 1, "abc1234", GenerateVersionOptions{NextRelease: "minor", DropTagNamePrefix: true}, "0.2.0") 35 | test("v0.1.1-rc1", 1, "abc1234", GenerateVersionOptions{NextRelease: "minor"}, "v0.2.0") 36 | test("v1.2.3", 1, "abc1234", GenerateVersionOptions{NextRelease: "minor"}, "v1.3.0") 37 | test("v1.0.0-rc1", 1, "abc1234", GenerateVersionOptions{NextRelease: "major"}, "v1.0.0") 38 | test("v1.0.0", 1, "abc1234", GenerateVersionOptions{NextRelease: "major"}, "v2.0.0") 39 | test("v1.0.1-rc1", 1, "abc1234", GenerateVersionOptions{NextRelease: "major"}, "v2.0.0") 40 | test("v1.2.3", 1, "abc1234", GenerateVersionOptions{NextRelease: "major", Format: "v", DropTagNamePrefix: true}, "v2.0.0") 41 | 42 | test("", 1, "abc1234", GenerateVersionOptions{FallbackTagName: "0.0.0", PrereleasePrefix: "dev"}, "0.0.0-dev.1.gabc1234") 43 | test("", 1, "abc1234", GenerateVersionOptions{FallbackTagName: "v0.0.0", PrereleasePrefix: "dev"}, "v0.0.0-dev.1.gabc1234") 44 | 45 | test("v0.0.0", 0, "abc1234", GenerateVersionOptions{PrereleaseSuffix: "SNAPSHOT", PrereleasePrefix: "dev"}, "v0.0.0") 46 | test("v0.0.0", 1, "abc1234", GenerateVersionOptions{PrereleaseSuffix: "SNAPSHOT", PrereleasePrefix: "dev"}, "v0.0.1-dev.1.gabc1234-SNAPSHOT") 47 | 48 | test("v0.0.0", 0, "abc1234", GenerateVersionOptions{DropTagNamePrefix: true, PrereleasePrefix: "dev"}, "0.0.0") 49 | test("v0.0.0-rc.1", 1, "abc1234", GenerateVersionOptions{DropTagNamePrefix: true, PrereleasePrefix: "dev"}, "0.0.0-rc.1.dev.1.gabc1234") 50 | test("v0.0.0-rc.1+foobar", 1, "abc1234", GenerateVersionOptions{DropTagNamePrefix: true, PrereleasePrefix: "dev"}, "0.0.0-rc.1.dev.1.gabc1234+foobar") 51 | test("", 1, "abc1234", GenerateVersionOptions{FallbackTagName: "v0.0.0", DropTagNamePrefix: true, PrereleasePrefix: "dev"}, "0.0.0-dev.1.gabc1234") 52 | 53 | test("0.0.0", 0, "abc1234", GenerateVersionOptions{PrereleasePrefix: "custom"}, "0.0.0") 54 | test("0.0.0", 1, "abc1234", GenerateVersionOptions{PrereleasePrefix: "custom"}, "0.0.1-custom.1.gabc1234") 55 | 56 | test("0.0.0", 0, "abc1234", GenerateVersionOptions{PrereleasePrefix: "dev", PrereleaseTimestamped: true}, "0.0.0") 57 | test("0.0.0", 1, "abc1234", GenerateVersionOptions{PrereleasePrefix: "dev", PrereleaseTimestamped: false}, "0.0.1-dev.1.gabc1234") 58 | test("0.0.0", 1, "abc1234", GenerateVersionOptions{PrereleasePrefix: "dev", PrereleaseTimestamped: true}, "0.0.1-dev.1577844180.gabc1234") 59 | 60 | _, err := GenerateVersion("", 1, "abc1234", now, GenerateVersionOptions{PrereleasePrefix: "dev"}) 61 | assert.Error(err) 62 | } 63 | -------------------------------------------------------------------------------- /internal/git.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/go-git/go-git/v5" 11 | "github.com/go-git/go-git/v5/plumbing" 12 | "github.com/go-git/go-git/v5/plumbing/object" 13 | ) 14 | 15 | const ( 16 | // Prefix found in .git files that point to another location 17 | GitDirPrefix = "gitdir: " 18 | 19 | GitDirName = ".git" 20 | CommonDirName = "commondir" 21 | ) 22 | 23 | // GitTagMap ... 24 | func GitTagMap(repo git.Repository) (*map[string]string, error) { 25 | iter, err := repo.Tags() 26 | if err != nil { 27 | return nil, err 28 | } 29 | tagMap := map[string]string{} 30 | err = iter.ForEach(func(r *plumbing.Reference) error { 31 | tag, _ := repo.TagObject(r.Hash()) 32 | if SemVerParse(r.Name().Short()) == nil { 33 | // Filter out tags that are not semver 34 | return nil 35 | } 36 | if tag == nil { 37 | tagMap[r.Hash().String()] = r.Name().Short() 38 | } else { 39 | c, err := tag.Commit() 40 | if err != nil { 41 | return err 42 | } 43 | tagMap[c.Hash.String()] = r.Name().Short() 44 | } 45 | return nil 46 | }) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return &tagMap, nil 51 | } 52 | 53 | // GitDescribe ... 54 | func GitDescribe(repo git.Repository) (*string, *int, *string, error) { 55 | type gitDescribeNode struct { 56 | Commit object.Commit 57 | Distance int 58 | } 59 | 60 | head, err := repo.Head() 61 | if err != nil { 62 | return nil, nil, nil, fmt.Errorf("unable to find head: %v", err) 63 | } 64 | headHash := head.Hash().String() 65 | tags, err := GitTagMap(repo) 66 | if err != nil { 67 | return nil, nil, nil, fmt.Errorf("unable to get tags: %v", err) 68 | } 69 | commits, err := repo.Log(&git.LogOptions{ 70 | From: head.Hash(), 71 | Order: git.LogOrderCommitterTime, 72 | }) 73 | if err != nil { 74 | return nil, nil, nil, fmt.Errorf("unable to get log: %v", err) 75 | } 76 | state := map[string]gitDescribeNode{} 77 | counter := 0 78 | tagHash := "" 79 | commits.ForEach(func(c *object.Commit) error { 80 | node, found := state[c.Hash.String()] 81 | if !found { 82 | node = gitDescribeNode{ 83 | Commit: *c, 84 | Distance: 0, 85 | } 86 | state[c.Hash.String()] = node 87 | } 88 | c.Parents().ForEach(func(p *object.Commit) error { 89 | _, found := state[p.Hash.String()] 90 | if !found { 91 | state[p.Hash.String()] = gitDescribeNode{ 92 | Commit: *p, 93 | Distance: node.Distance + 1, 94 | } 95 | } 96 | return nil 97 | }) 98 | 99 | _, foundTag := (*tags)[c.Hash.String()] 100 | if tagHash == "" && foundTag { 101 | counter = state[c.Hash.String()].Distance 102 | tagHash = c.Hash.String() 103 | } 104 | return nil 105 | }) 106 | if tagHash == "" { 107 | for _, node := range state { 108 | if node.Distance+1 > counter { 109 | counter = node.Distance + 1 110 | } 111 | } 112 | tagName := "" 113 | return &tagName, &counter, &headHash, nil 114 | } 115 | tagName := (*tags)[tagHash] 116 | return &tagName, &counter, &headHash, nil 117 | } 118 | 119 | func OpenRepository(dir string) (*git.Repository, error) { 120 | gitDir, err := FindGitDir(dir) 121 | if err != nil { 122 | return nil, err 123 | } 124 | enableCommonDir, err := shouldEnableCommondDir(gitDir) 125 | if err != nil { 126 | return nil, err 127 | } 128 | openOpts := &git.PlainOpenOptions{EnableDotGitCommonDir: enableCommonDir} 129 | return git.PlainOpenWithOptions(dir, openOpts) 130 | } 131 | 132 | func shouldEnableCommondDir(gitDir string) (bool, error) { 133 | cdPath := filepath.Join(gitDir, CommonDirName) 134 | st, err := os.Stat(cdPath) 135 | if err != nil { 136 | if errors.Is(err, os.ErrNotExist) { 137 | return false, nil 138 | } 139 | return false, err 140 | } 141 | if st.IsDir() { 142 | return false, fmt.Errorf("expected to be a file, not directory: %s", cdPath) 143 | } 144 | return true, nil 145 | } 146 | 147 | func FindGitDir(dir string) (string, error) { 148 | gitDirPath := filepath.Join(dir, GitDirName) 149 | st, err := os.Stat(gitDirPath) 150 | if err != nil { 151 | return "", err 152 | } 153 | if st.IsDir() { 154 | return gitDirPath, nil 155 | } 156 | // It is a file, read the contents 157 | contents, err := os.ReadFile(gitDirPath) 158 | if err != nil { 159 | return "", err 160 | } 161 | 162 | line := string(contents) 163 | if !strings.HasPrefix(line, GitDirPrefix) { 164 | return "", fmt.Errorf(".git file has no %s prefix", GitDirPrefix) 165 | } 166 | 167 | gitdir := strings.Split(line[len(GitDirPrefix):], "\n")[0] 168 | gitdir = strings.TrimSpace(gitdir) 169 | return gitdir, nil 170 | } 171 | -------------------------------------------------------------------------------- /internal/git_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/go-git/go-git/v5" 10 | "github.com/go-git/go-git/v5/plumbing" 11 | "github.com/go-git/go-git/v5/plumbing/object" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestGitTagMap(t *testing.T) { 17 | assert := assert.New(t) 18 | dir, _ := ioutil.TempDir("", "example") 19 | author := object.Signature{Name: "Test", Email: "test@test.com"} 20 | repo, _ := git.PlainInit(dir, false) 21 | worktree, _ := repo.Worktree() 22 | 23 | tags, _ := GitTagMap(*repo) 24 | assert.Equal(map[string]string{}, *tags) 25 | 26 | commit1, _ := worktree.Commit("first", &git.CommitOptions{Author: &author}) 27 | tag1, _ := repo.CreateTag("v1.0.0", commit1, nil) 28 | tags, _ = GitTagMap(*repo) 29 | assert.Equal(commit1.String(), tag1.Hash().String()) 30 | assert.Equal(map[string]string{ 31 | tag1.Hash().String(): "v1.0.0", 32 | }, *tags) 33 | 34 | commit2, _ := worktree.Commit("second", &git.CommitOptions{Author: &author}) 35 | tag2, _ := repo.CreateTag("v2.0.0", commit2, &git.CreateTagOptions{ 36 | Tagger: &object.Signature{ 37 | Name: "Foo Bar", 38 | Email: "foo@bar.com", 39 | }, 40 | Message: "Version 2.0.0", 41 | }) 42 | assert.NotEqual(commit2.String(), tag2.Hash().String()) 43 | tags, _ = GitTagMap(*repo) 44 | assert.Equal(map[string]string{ 45 | commit1.String(): "v1.0.0", 46 | commit2.String(): "v2.0.0", 47 | }, *tags) 48 | 49 | commit3, _ := worktree.Commit("third", &git.CommitOptions{Author: &author}) 50 | tag3, _ := repo.CreateTag("fum", commit3, &git.CreateTagOptions{ 51 | Tagger: &object.Signature{ 52 | Name: "Fum", 53 | Email: "fum@example.com", 54 | }, 55 | Message: "Not a semver version tag", 56 | }) 57 | assert.NotEqual(commit3.String(), tag3.Hash().String()) 58 | tags, _ = GitTagMap(*repo) 59 | assert.Equal(map[string]string{ 60 | commit1.String(): "v1.0.0", 61 | commit2.String(): "v2.0.0", 62 | }, *tags) 63 | } 64 | 65 | func TestGitDescribe(t *testing.T) { 66 | assert := assert.New(t) 67 | dir, _ := ioutil.TempDir("", "example") 68 | author := object.Signature{Name: "Test", Email: "test@test.com"} 69 | repo, _ := git.PlainInit(dir, false) 70 | worktree, _ := repo.Worktree() 71 | _, _, _, err := GitDescribe(*repo) 72 | assert.Error(err) 73 | test := func(expectedTagName string, expectedCounter int, expectedHeadHash string) { 74 | actualTagName, actualCounter, actualHeadHash, err := GitDescribe(*repo) 75 | assert.NoError(err) 76 | assert.Equal(expectedTagName, *actualTagName) 77 | assert.Equal(expectedCounter, *actualCounter) 78 | assert.Equal(expectedHeadHash, *actualHeadHash) 79 | } 80 | 81 | commit1, _ := worktree.Commit("first", &git.CommitOptions{Author: &author}) 82 | test("", 1, commit1.String()) 83 | 84 | repo.CreateTag("v1.0.0", commit1, nil) 85 | test("v1.0.0", 0, commit1.String()) 86 | 87 | commit2, _ := worktree.Commit("second", &git.CommitOptions{Author: &author}) 88 | test("v1.0.0", 1, commit2.String()) 89 | 90 | commit3, _ := worktree.Commit("third", &git.CommitOptions{Author: &author}) 91 | test("v1.0.0", 2, commit3.String()) 92 | 93 | repo.CreateTag("v2.0.0", commit3, nil) 94 | test("v2.0.0", 0, commit3.String()) 95 | } 96 | 97 | func TestGitDescribeWithBranch(t *testing.T) { 98 | assert := assert.New(t) 99 | dir, _ := ioutil.TempDir("", "example") 100 | author := object.Signature{Name: "Test", Email: "test@test.com"} 101 | repo, _ := git.PlainInit(dir, false) 102 | worktree, _ := repo.Worktree() 103 | _, _, _, err := GitDescribe(*repo) 104 | assert.Error(err) 105 | test := func(expectedTagName string, expectedCounter int, expectedHeadHash string) { 106 | actualTagName, actualCounter, actualHeadHash, err := GitDescribe(*repo) 107 | assert.NoError(err) 108 | assert.Equal(expectedTagName, *actualTagName) 109 | assert.Equal(expectedCounter, *actualCounter) 110 | assert.Equal(expectedHeadHash, *actualHeadHash) 111 | } 112 | 113 | commit1, _ := worktree.Commit("first", &git.CommitOptions{Author: &author}) 114 | test("", 1, commit1.String()) 115 | 116 | repo.CreateTag("v1.0.0", commit1, nil) 117 | test("v1.0.0", 0, commit1.String()) 118 | 119 | commit2, _ := worktree.Commit("second", &git.CommitOptions{Author: &author}) 120 | test("v1.0.0", 1, commit2.String()) 121 | 122 | worktree.Checkout(&git.CheckoutOptions{Hash: commit1}) 123 | 124 | commit3, _ := worktree.Commit("third", &git.CommitOptions{Author: &author}) 125 | test("v1.0.0", 1, commit3.String()) 126 | 127 | commit4, _ := worktree.Commit("forth", &git.CommitOptions{Author: &author, Parents: []plumbing.Hash{commit2, commit3}}) 128 | test("v1.0.0", 2, commit4.String()) 129 | repo.CreateTag("v2.0.0", commit3, nil) 130 | test("v2.0.0", 1, commit4.String()) 131 | } 132 | 133 | func setUpDotGitDirTest(assert *assert.Assertions) (string, string) { 134 | testDir, err := os.MkdirTemp("", "test") 135 | assert.NoError(err, "failed to create temp dir") 136 | 137 | gitDirPath := filepath.Join(testDir, GitDirName) 138 | err = os.Mkdir(gitDirPath, 0750) 139 | assert.NoError(err, "failed to create git dir") 140 | 141 | return testDir, gitDirPath 142 | } 143 | 144 | func setUpDotGitFileTest(assert *assert.Assertions) (string, string, string) { 145 | testDir, err := os.MkdirTemp("", "test") 146 | assert.NoError(err, "failed to create temp dir") 147 | 148 | actualDotGitPath := filepath.Join(testDir, "actual") 149 | err = os.Mkdir(actualDotGitPath, 0750) 150 | assert.NoError(err, "failed to create actual git dir") 151 | 152 | wtPath := filepath.Join(testDir, "my_worktree") 153 | err = os.Mkdir(wtPath, 0750) 154 | assert.NoError(err, "failed to create worktree dir") 155 | 156 | wtDotGitPath := filepath.Join(wtPath, GitDirName) 157 | contents := GitDirPrefix + actualDotGitPath 158 | err = os.WriteFile(wtDotGitPath, []byte(contents), 0666) 159 | assert.NoError(err, "failed to write git dir file in worktree") 160 | 161 | return testDir, actualDotGitPath, wtPath 162 | } 163 | 164 | func TestFindGitDir(t *testing.T) { 165 | t.Run(".git is a directory", func(t *testing.T) { 166 | assert := assert.New(t) 167 | testDir, gitDirPath := setUpDotGitDirTest(assert) 168 | defer os.RemoveAll(testDir) 169 | 170 | result, err := FindGitDir(testDir) 171 | assert.NoError(err, "failed to find git dir") 172 | assert.Equal(gitDirPath, result) 173 | }) 174 | t.Run(".git is a file pointing to another directory", func(t *testing.T) { 175 | assert := assert.New(t) 176 | testDir, actualDotGitPath, wtPath := setUpDotGitFileTest(assert) 177 | defer os.RemoveAll(testDir) 178 | 179 | result, err := FindGitDir(wtPath) 180 | assert.NoError(err, "failed to find git dir in worktree") 181 | assert.Equal(actualDotGitPath, result) 182 | }) 183 | } 184 | 185 | func TestShouldEnableCommonDir(t *testing.T) { 186 | t.Run(".git is a directory", func(t *testing.T) { 187 | assert := assert.New(t) 188 | testDir, gitDirPath := setUpDotGitDirTest(assert) 189 | defer os.RemoveAll(testDir) 190 | 191 | result, err := shouldEnableCommondDir(gitDirPath) 192 | assert.NoError(err, "failed evaluating whether to enable commond dir") 193 | assert.False(result) 194 | }) 195 | t.Run(".git is a file pointing to another directory", func(t *testing.T) { 196 | assert := assert.New(t) 197 | testDir, actualDotGitPath, _ := setUpDotGitFileTest(assert) 198 | defer os.RemoveAll(testDir) 199 | 200 | cdPath := filepath.Join(actualDotGitPath, CommonDirName) 201 | contents := "../my_worktree" 202 | err := os.WriteFile(cdPath, []byte(contents), 0666) 203 | assert.NoError(err, "failed writing commondir file") 204 | 205 | result, err := shouldEnableCommondDir(actualDotGitPath) 206 | assert.NoError(err, "failed evaluating whether to enable commond dir") 207 | assert.True(result) 208 | }) 209 | } 210 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= 2 | github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= 3 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 4 | github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= 5 | github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= 6 | github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= 7 | github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 h1:ra2OtmuW0AE5csawV4YXMNGNQQXvLRps3z2Z59OPO+I= 8 | github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8= 9 | github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= 10 | github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= 11 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= 12 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 13 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 14 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 15 | github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 16 | github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= 17 | github.com/cloudflare/circl v1.3.0 h1:Anq00jxDtoyX3+aCaYUZ0vXC5r4k4epberfWGDXV1zE= 18 | github.com/cloudflare/circl v1.3.0/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw= 19 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 20 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 22 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 24 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 25 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 26 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 27 | github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= 28 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 29 | github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= 30 | github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= 31 | github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= 32 | github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34= 33 | github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= 34 | github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8= 35 | github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= 36 | github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= 37 | github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= 38 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 39 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 40 | github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 41 | github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= 42 | github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= 43 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 44 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 45 | github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= 46 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 47 | github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 48 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 49 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 50 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 51 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 52 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 53 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 54 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 55 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 56 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 57 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 58 | github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= 59 | github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= 60 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 61 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 62 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 63 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 64 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 65 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 66 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 67 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 68 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 69 | github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= 70 | github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 71 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 72 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 73 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 74 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 75 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 76 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 77 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 78 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 79 | github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= 80 | github.com/xanzy/ssh-agent v0.3.2 h1:eKj4SX2Fe7mui28ZgnFW5fmTz1EIr7ugo5s6wDxdHBM= 81 | github.com/xanzy/ssh-agent v0.3.2/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 82 | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 83 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 84 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 85 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 86 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 87 | golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= 88 | golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 89 | golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= 90 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 91 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 92 | golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= 93 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 94 | golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= 95 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 96 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 97 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 98 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 99 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 100 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 101 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 102 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 103 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 104 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 105 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 106 | golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 107 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 108 | golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 109 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 110 | golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 111 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 112 | golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= 113 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 114 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 115 | golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM= 116 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 117 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 118 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 119 | golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= 120 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 121 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 122 | golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM= 123 | golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= 124 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 125 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 126 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 127 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 128 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 129 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 130 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 131 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 132 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 133 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 134 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 135 | gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= 136 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 137 | --------------------------------------------------------------------------------