├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ ├── build.yml
│ └── release.yml
├── .gitignore
├── .goreleaser.yml
├── .svu.yml
├── Dockerfile
├── LICENSE.md
├── Makefile
├── README.md
├── art.txt
├── description.txt
├── example.svu.yaml
├── examples.sh
├── go.mod
├── go.sum
├── internal
├── git
│ ├── git.go
│ └── git_test.go
└── svu
│ ├── svu.go
│ └── svu_test.go
├── main.go
├── pkg
└── svu
│ └── svu.go
└── scripts
└── completions.sh
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [caarlos0]
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "gomod"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | time: "08:00"
8 | labels:
9 | - "dependencies"
10 | - package-ecosystem: "github-actions"
11 | directory: "/"
12 | schedule:
13 | interval: "daily"
14 | time: "08:00"
15 | labels:
16 | - "dependencies"
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches:
6 | - "main"
7 | tags:
8 | - "v*"
9 | pull_request:
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | - uses: actions/setup-go@v5
17 | with:
18 | go-version: stable
19 | - run: go mod tidy
20 | - run: go test -v ./...
21 | dependabot:
22 | needs: [build]
23 | runs-on: ubuntu-latest
24 | permissions:
25 | pull-requests: write
26 | contents: write
27 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}}
28 | steps:
29 | - id: metadata
30 | uses: dependabot/fetch-metadata@08eff52bf64351f401fb50d4972fa95b9f2c2d1b # v2
31 | with:
32 | github-token: "${{ secrets.GITHUB_TOKEN }}"
33 | - run: |
34 | gh pr review --approve "$PR_URL"
35 | gh pr merge --squash "$PR_URL"
36 | env:
37 | PR_URL: ${{github.event.pull_request.html_url}}
38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: goreleaser
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | tags:
8 | - "*"
9 |
10 | permissions:
11 | contents: write
12 | id-token: write
13 | packages: write
14 | attestations: write
15 |
16 | jobs:
17 | goreleaser:
18 | runs-on: ubuntu-latest
19 | steps:
20 | - if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
21 | run: echo "flags=--snapshot" >> $GITHUB_ENV
22 | - uses: actions/checkout@v4
23 | with:
24 | fetch-depth: 0
25 | - uses: actions/setup-go@v5
26 | with:
27 | go-version: stable
28 | - uses: docker/setup-qemu-action@v3
29 | - uses: docker/setup-buildx-action@v3
30 | - uses: docker/login-action@v3
31 | with:
32 | registry: ghcr.io
33 | username: ${{ github.repository_owner }}
34 | password: ${{ secrets.GH_PAT }}
35 | - uses: docker/login-action@v3
36 | with:
37 | registry: docker.io
38 | username: ${{ github.repository_owner }}
39 | password: ${{ secrets.DOCKER_PASSWORD }}
40 | - uses: sigstore/cosign-installer@v3.8.2
41 | - uses: anchore/sbom-action/download-syft@v0.20.0
42 | - uses: cachix/install-nix-action@v31
43 | with:
44 | github_access_token: ${{ secrets.GH_PAT }}
45 | - uses: goreleaser/goreleaser-action@v6
46 | with:
47 | distribution: goreleaser-pro
48 | version: "~> v2"
49 | args: release --clean ${{ env.flags }}
50 | env:
51 | GITHUB_TOKEN: ${{ secrets.GH_PAT }}
52 | FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
53 | GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
54 | AUR_KEY: ${{ secrets.AUR_KEY }}
55 | - uses: actions/attest-build-provenance@v2
56 | if: ${{ startsWith(github.ref, 'refs/tags/v') }}
57 | with:
58 | subject-checksums: ./dist/checksums.txt
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | completions/
3 | svu
4 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
2 |
3 | version: 2
4 |
5 | variables:
6 | description: Semantic Version Utility
7 | homepage: "https://github.com/caarlos0/svu"
8 |
9 | includes:
10 | - from_url:
11 | url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/build.yml
12 | - from_url:
13 | url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/package_with_completions_no_aur.yml
14 | - from_url:
15 | url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/release.yml
16 | - from_url:
17 | url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/docker.yml
18 | - from_url:
19 | url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/cosign_checksum.yml
20 | - from_url:
21 | url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/cosign_docker.yml
22 |
23 | furies:
24 | - account: caarlos0
25 |
26 | archives:
27 | - files:
28 | - README.md
29 | - LICENSE.md
30 | - completions/*
31 | format_overrides:
32 | - goos: windows
33 | formats: [zip]
34 |
35 | aurs:
36 | - maintainers:
37 | - "Carlos Alexandro Becker "
38 | - "Rafael Dominiquini "
39 | description: "{{ .Var.description }}"
40 | name: "svu-bin"
41 | homepage: "{{ .Var.homepage }}"
42 | license: MIT
43 | private_key: "{{ .Env.AUR_KEY }}"
44 | git_url: "ssh://aur@aur.archlinux.org/svu-bin.git"
45 | package: |-
46 | # bin
47 | install -Dm755 "./svu" "${pkgdir}/usr/bin/svu"
48 | # license
49 | install -Dm644 "./LICENSE.md" "${pkgdir}/usr/share/licenses/svu/LICENSE"
50 | # completions
51 | mkdir -p "${pkgdir}/usr/share/bash-completion/completions/"
52 | mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
53 | mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
54 | install -Dm644 "./completions/svu.bash" "${pkgdir}/usr/share/bash-completion/completions/svu"
55 | install -Dm644 "./completions/svu.zsh" "${pkgdir}/usr/share/zsh/site-functions/_svu"
56 | install -Dm644 "./completions/svu.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/svu.fish"
57 |
--------------------------------------------------------------------------------
/.svu.yml:
--------------------------------------------------------------------------------
1 | # svu configuration.
2 | #
3 | # https://github.com/caarlos0/svu
4 | tag.prefix: v
5 | always: true
6 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine
2 | RUN apk add -U git
3 | COPY svu*.apk /tmp/
4 | RUN git config --global safe.directory '*'
5 | RUN apk add --allow-untrusted /tmp/*.apk
6 | ENTRYPOINT ["svu"]
7 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017-2025 Carlos Alexandro Becker
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | major:
2 | git tag $$(svu major)
3 | git push --tags
4 | goreleaser --clean
5 | .PHONY: major
6 |
7 | minor:
8 | git tag $$(svu minor)
9 | git push --tags
10 | goreleaser --clean
11 | .PHONY: minor
12 |
13 | patch:
14 | git tag $$(svu patch)
15 | git push --tags
16 | goreleaser --clean
17 | .PHONY: patch
18 |
19 | .DEFAULT_GOAL := patch
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
semantic version utility
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | semantic version utility (svu) is a small helper for release scripts and workflows.
19 |
20 | It provides utility commands and functions to increase specific portions of the version.
21 | It can also figure the next version out automatically by looking through the git history.
22 |
23 | > [!TIP]
24 | > Read [the spec][Semver] for more information.
25 |
26 | ## usage
27 |
28 | Check `svu --help` for the list of sub-commands and flags.
29 |
30 | ### `next`, `n`
31 |
32 | This is probably the command you'll use the most.
33 |
34 | It checks your `git log`, and automatically increases and returns the new
35 | version based on this table:
36 |
37 | | Commit message | Tag increase |
38 | | -------------------------------------------------------------------------------------- | ------------ |
39 | | `chore: foo` | Nothing |
40 | | `fix: fixed something` | Patch |
41 | | `feat: added new button to do X` | Minor |
42 | | `fix: fixed thing xyz`
`BREAKING CHANGE: this will break users because of blah` | Major |
43 | | `fix!: fixed something` | Major |
44 | | `feat!: added blah` | Major |
45 |
46 | > [!TIP]
47 | > You can create an alias to create tags automatically:
48 | >
49 | > ```bash
50 | > alias gtn='git tag $(svu next)'
51 | > ```
52 |
53 | ## configuration
54 |
55 | Every flag option can also be set in a `.svu.yml` in the current
56 | directory/repository root folder, for example:
57 |
58 | ```yaml
59 | tag.prefix: ""
60 | always: true
61 | v0: true
62 | ```
63 |
64 | Names are the same as the flags themselves.
65 |
66 | ## install
67 |
68 | [](https://repology.org/project/svu/versions)
69 |
70 |
71 | macOS
72 |
73 | ```bash
74 | brew install caarlos0/tap/svu
75 | ```
76 |
77 |
78 |
79 |
80 | linux/apt
81 |
82 | ```bash
83 | echo 'deb [trusted=yes] https://apt.fury.io/caarlos0/ /' | sudo tee /etc/apt/sources.list.d/caarlos0.list
84 | sudo apt update
85 | sudo apt install svu
86 | ```
87 |
88 |
89 |
90 |
91 | linux/yum
92 |
93 | ```bash
94 | echo '[caarlos0]
95 | name=caarlos0
96 | baseurl=https://yum.fury.io/caarlos0/
97 | enabled=1
98 | gpgcheck=0' | sudo tee /etc/yum.repos.d/caarlos0.repo
99 | sudo yum install svu
100 | ```
101 |
102 |
103 |
104 |
105 | docker
106 |
107 | ```bash
108 | docker run --rm -v $PWD:/tmp --workdir /tmp ghcr.io/caarlos0/svu --help
109 | ```
110 |
111 |
112 |
113 |
114 | go install
115 |
116 | ```bash
117 | go install github.com/caarlos0/svu/v3@latest
118 | ```
119 |
120 |
121 |
122 |
123 | manually
124 |
125 | Or download one from the [releases tab](https://github.com/caarlos0/svu/releases) and install manually.
126 |
127 |
128 |
129 | ## stargazers over time
130 |
131 | [](https://starchart.cc/caarlos0/svu)
132 |
133 | [Semver]: https://semver.org
134 |
135 | ---
136 |
137 | Logo art and concept by [@carinebecker](https://github.com/carinebecker).
138 |
--------------------------------------------------------------------------------
/art.txt:
--------------------------------------------------------------------------------
1 | _____ ___ _
2 | / __\ \ / / | | |
3 | \__ \\ V /| |_| |
4 | |___/ \_/ \__,_|
5 |
--------------------------------------------------------------------------------
/description.txt:
--------------------------------------------------------------------------------
1 | Semantic Version Utility (svu) is a small helper that can be used in release
2 | scripts and workflows.
3 |
4 | It provides utility commands to increase specific portions of the version,
5 | and can also figure the next version out automatically by looking through the
6 | git history.
7 |
--------------------------------------------------------------------------------
/example.svu.yaml:
--------------------------------------------------------------------------------
1 | # svu configuration.
2 | #
3 | # https://github.com/caarlos0/svu
4 | verbose: false
5 | tag.pattern: ""
6 | tag.prefix: "v"
7 | tag.mode: all
8 | log.directory:
9 | - "."
10 | metadata: ""
11 | always: false
12 | v0: false
13 |
--------------------------------------------------------------------------------
/examples.sh:
--------------------------------------------------------------------------------
1 | # Automatic increase next version based on git log:
2 | svu next
3 |
4 | # Increase patch, minor, major:
5 | svu patch
6 | svu minor
7 | svu major
8 |
9 | # Show current version:
10 | svu current
11 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/caarlos0/svu/v3
2 |
3 | go 1.22
4 |
5 | require (
6 | github.com/Masterminds/semver/v3 v3.3.1
7 | github.com/caarlos0/go-version v0.2.0
8 | github.com/gobwas/glob v0.2.3
9 | github.com/spf13/cobra v1.9.1
10 | github.com/spf13/pflag v1.0.6
11 | github.com/spf13/viper v1.20.1
12 | github.com/stretchr/testify v1.10.0
13 | )
14 |
15 | require (
16 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
17 | github.com/fsnotify/fsnotify v1.8.0 // indirect
18 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
19 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
20 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect
21 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
22 | github.com/sagikazarmark/locafero v0.7.0 // indirect
23 | github.com/sourcegraph/conc v0.3.0 // indirect
24 | github.com/spf13/afero v1.12.0 // indirect
25 | github.com/spf13/cast v1.7.1 // indirect
26 | github.com/subosito/gotenv v1.6.0 // indirect
27 | go.uber.org/atomic v1.9.0 // indirect
28 | go.uber.org/multierr v1.9.0 // indirect
29 | golang.org/x/sys v0.29.0 // indirect
30 | golang.org/x/text v0.21.0 // indirect
31 | gopkg.in/yaml.v3 v3.0.1 // indirect
32 | )
33 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
2 | github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
3 | github.com/caarlos0/go-version v0.2.0 h1:TTD5dF3PBAtRHbfCKRE173SrVVpbE0yX95EDQ4BwTGs=
4 | github.com/caarlos0/go-version v0.2.0/go.mod h1:X+rI5VAtJDpcjCjeEIXpxGa5+rTcgur1FK66wS0/944=
5 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
9 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
11 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
12 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
13 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
14 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
15 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
16 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
17 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
18 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
19 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
20 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
21 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
22 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
23 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
24 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
25 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
26 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
27 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
28 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
29 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
30 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
31 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
32 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
33 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
34 | github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
35 | github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
36 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
37 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
38 | github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
39 | github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
40 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
41 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
42 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
43 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
44 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
45 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
46 | github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
47 | github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
48 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
49 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
50 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
51 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
52 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
53 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
54 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
55 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
56 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
57 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
58 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
59 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
60 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
61 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
62 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
63 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
64 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
65 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
66 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
67 |
--------------------------------------------------------------------------------
/internal/git/git.go:
--------------------------------------------------------------------------------
1 | package git
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os/exec"
7 | "strings"
8 |
9 | "github.com/gobwas/glob"
10 | )
11 |
12 | // Commit is a commit with a hash, title (first line of the message), and body
13 | // (rest of the message, not including the title).
14 | type Commit struct {
15 | SHA string
16 | Title string
17 | Body string
18 | }
19 |
20 | func (c Commit) String() string {
21 | return c.SHA + ": " + c.Title + "\n" + c.Body
22 | }
23 |
24 | const (
25 | TagModeAll = "all"
26 | TagModeCurrent = "current"
27 | )
28 |
29 | // copied from goreleaser
30 |
31 | // IsRepo returns true if current folder is a git repository
32 | func IsRepo() bool {
33 | out, err := run("rev-parse", "--is-inside-work-tree")
34 | return err == nil && strings.TrimSpace(out) == "true"
35 | }
36 |
37 | func Root() string {
38 | out, _ := run("rev-parse", "--show-toplevel")
39 | return strings.TrimSpace(out)
40 | }
41 |
42 | func getAllTags(args ...string) ([]string, error) {
43 | tags, err := run(append([]string{"-c", "versionsort.suffix=-", "tag", "--sort=-version:refname"}, args...)...)
44 | if err != nil {
45 | return nil, err
46 | }
47 | return strings.Split(tags, "\n"), nil
48 | }
49 |
50 | func DescribeTag(tagMode string, pattern string) (string, error) {
51 | args := []string{}
52 | if tagMode == TagModeCurrent {
53 | args = []string{"--merged"}
54 | }
55 | tags, err := getAllTags(args...)
56 | if err != nil {
57 | return "", err
58 | }
59 |
60 | if len(tags) == 0 {
61 | return "", nil
62 | }
63 | if pattern == "" {
64 | return tags[0], nil
65 | }
66 |
67 | g, err := glob.Compile(pattern)
68 | if err != nil {
69 | return "", err
70 | }
71 | for _, tag := range tags {
72 | if g.Match(tag) {
73 | return tag, nil
74 | }
75 | }
76 | return "", fmt.Errorf("no tags match '%s'", pattern)
77 | }
78 |
79 | func Changelog(tag string, dirs []string) ([]Commit, error) {
80 | if tag == "" {
81 | return gitLog(dirs, "HEAD")
82 | } else {
83 | return gitLog(dirs, fmt.Sprintf("tags/%s..HEAD", tag))
84 | }
85 | }
86 |
87 | func run(args ...string) (string, error) {
88 | extraArgs := []string{
89 | "-c", "log.showSignature=false",
90 | }
91 | args = append(extraArgs, args...)
92 | /* #nosec */
93 | cmd := exec.Command("git", args...)
94 | bts, err := cmd.CombinedOutput()
95 | if err != nil {
96 | return "", errors.New(string(bts))
97 | }
98 | return string(bts), nil
99 | }
100 |
101 | func gitLog(dirs []string, refs ...string) ([]Commit, error) {
102 | args := []string{"log", "--no-decorate", "--no-color", `--format=%H:%B`}
103 | args = append(args, refs...)
104 | if len(dirs) > 0 {
105 | args = append(args, "--")
106 | args = append(args, dirs...)
107 | }
108 | s, err := run(args...)
109 | if err != nil {
110 | return nil, err
111 | }
112 | var result []Commit
113 | for _, commit := range strings.Split(s, "") {
114 | commit = strings.TrimSpace(commit)
115 | if commit == "" { // accounts for the last split, which will be an empty line
116 | continue
117 | }
118 |
119 | hashEndIdx := strings.Index(commit, ":")
120 | titleEndIdx := strings.Index(commit, "\n")
121 | if titleEndIdx < 0 {
122 | titleEndIdx = len(commit)
123 | }
124 |
125 | result = append(result, Commit{
126 | commit[:hashEndIdx],
127 | commit[hashEndIdx+1 : titleEndIdx],
128 | commit[min(titleEndIdx+1, len(commit)):],
129 | })
130 | }
131 | return result, nil
132 | }
133 |
--------------------------------------------------------------------------------
/internal/git/git_test.go:
--------------------------------------------------------------------------------
1 | package git
2 |
3 | import (
4 | "os"
5 | "path"
6 | "testing"
7 | "time"
8 |
9 | "github.com/stretchr/testify/require"
10 | )
11 |
12 | func TestIsRepo(t *testing.T) {
13 | t.Run("is not a repo", func(t *testing.T) {
14 | tempdir(t)
15 | require.False(t, IsRepo()) // should not be arepo
16 | })
17 |
18 | t.Run("is a repo", func(t *testing.T) {
19 | tempdir(t)
20 | gitInit(t)
21 | require.True(t, IsRepo()) // should be arepo
22 | })
23 | }
24 |
25 | func TestDescribeTag(t *testing.T) {
26 | setup := func(tb testing.TB) {
27 | tb.Helper()
28 | tempdir(tb)
29 | gitInit(tb)
30 | gitCommit(tb, "chore: foobar")
31 | gitTag(tb, "pattern-1.2.3")
32 | gitCommit(tb, "lalalala")
33 | gitTag(tb, "v1.2.3")
34 | gitTag(tb, "v1.2.4") // multiple tags in a single commit
35 | gitCommit(tb, "chore: aaafoobar")
36 | gitCommit(tb, "docs: asdsad")
37 | gitCommit(tb, "fix: fooaaa")
38 | time.Sleep(time.Second) // TODO: no idea why, but without the sleep sometimes commits are in wrong order
39 | createBranch(tb, "not-main")
40 | gitCommit(tb, "docs: update")
41 | gitCommit(tb, "foo: bar")
42 | gitTag(tb, "v1.2.5")
43 | gitTag(tb, "v1.2.5-prerelease")
44 | switchToBranch(tb, "-")
45 | }
46 | t.Run(TagModeCurrent, func(t *testing.T) {
47 | setup(t)
48 | tag, err := DescribeTag(TagModeCurrent, "")
49 | require.NoError(t, err)
50 | require.Equal(t, "v1.2.4", tag)
51 | })
52 |
53 | t.Run(TagModeAll, func(t *testing.T) {
54 | setup(t)
55 | tag, err := DescribeTag(TagModeAll, "")
56 | require.NoError(t, err)
57 | require.Equal(t, "v1.2.5", tag)
58 | })
59 |
60 | t.Run("pattern", func(t *testing.T) {
61 | setup(t)
62 | tag, err := DescribeTag(TagModeCurrent, "pattern-*")
63 | require.NoError(t, err)
64 | require.Equal(t, "pattern-1.2.3", tag)
65 | })
66 | }
67 |
68 | func TestChangelog(t *testing.T) {
69 | tempdir(t)
70 | gitInit(t)
71 | gitCommit(t, "chore: foobar")
72 | gitCommit(t, "lalalala")
73 | gitTag(t, "v1.2.3")
74 | for _, msg := range []string{
75 | "chore: foobar",
76 | "fix: foo",
77 | "feat: foobar",
78 | } {
79 | gitCommit(t, msg)
80 | }
81 | log, err := Changelog("v1.2.3", nil)
82 | require.NoError(t, err)
83 | for _, title := range []string{
84 | "chore: foobar",
85 | "fix: foo",
86 | "feat: foobar",
87 | } {
88 | requireLogContains(t, log, title)
89 | }
90 | }
91 |
92 | func requireLogContains(tb testing.TB, log []Commit, title string) {
93 | tb.Helper()
94 | for _, commit := range log {
95 | if commit.Title == title {
96 | return
97 | }
98 | }
99 | tb.Errorf("expected %v to contain a commit with msg %q", log, title)
100 | }
101 |
102 | func requireLogNotContains(tb testing.TB, log []Commit, title string) {
103 | tb.Helper()
104 | for _, commit := range log {
105 | if commit.Title == title {
106 | tb.Errorf("expected %v to not contain a commit with msg %q", log, title)
107 | }
108 | }
109 | }
110 |
111 | func TestChangelogWithDirectory(t *testing.T) {
112 | tempDir := tempdir(t)
113 | localDir := dir(tempDir, t)
114 | file := tempfile(t, localDir)
115 | gitInit(t)
116 | gitCommit(t, "chore: foobar")
117 | gitCommit(t, "lalalala")
118 | gitTag(t, "v1.2.3")
119 | gitCommit(t, "feat: foobar")
120 | gitAdd(t, file)
121 | gitCommit(t, "chore: filtered dir")
122 | log, err := Changelog("v1.2.3", []string{localDir})
123 | require.NoError(t, err)
124 |
125 | requireLogContains(t, log, "chore: filtered dir")
126 | requireLogNotContains(t, log, "feat: foobar")
127 | }
128 |
129 | func switchToBranch(tb testing.TB, branch string) {
130 | _, err := fakeGitRun("switch", branch)
131 | require.NoError(tb, err)
132 | }
133 |
134 | func createBranch(tb testing.TB, branch string) {
135 | _, err := fakeGitRun("switch", "-c", branch)
136 | require.NoError(tb, err)
137 | }
138 |
139 | func gitTag(tb testing.TB, tag string) {
140 | _, err := fakeGitRun("tag", tag)
141 | require.NoError(tb, err)
142 | }
143 |
144 | func gitCommit(tb testing.TB, msg string) {
145 | _, err := fakeGitRun("commit", "--allow-empty", "-am", msg)
146 | require.NoError(tb, err)
147 | }
148 |
149 | func gitAdd(tb testing.TB, path string) {
150 | _, err := fakeGitRun("add", path)
151 | require.NoError(tb, err)
152 | }
153 |
154 | func gitInit(tb testing.TB) {
155 | _, err := fakeGitRun("init")
156 | require.NoError(tb, err)
157 | }
158 |
159 | func tempdir(tb testing.TB) string {
160 | previous, err := os.Getwd()
161 | require.NoError(tb, err)
162 | tb.Cleanup(func() {
163 | require.NoError(tb, os.Chdir(previous))
164 | })
165 | dir := tb.TempDir()
166 | require.NoError(tb, os.Chdir(dir))
167 | tb.Logf("cd into %s", dir)
168 | return dir
169 | }
170 |
171 | func dir(tempDir string, tb testing.TB) string {
172 | createdDir := path.Join(tempDir, "a-folder")
173 | err := os.Mkdir(createdDir, 0o755)
174 | require.NoError(tb, err)
175 | return createdDir
176 | }
177 |
178 | func tempfile(tb testing.TB, dir string) string {
179 | d1 := []byte("hello\ngo\n")
180 | file := path.Join(dir, "a-file.txt")
181 | err := os.WriteFile(file, d1, 0o644)
182 | require.NoError(tb, err)
183 | return file
184 | }
185 |
186 | func fakeGitRun(args ...string) (string, error) {
187 | allArgs := []string{
188 | "-c", "user.name='svu'",
189 | "-c", "user.email='svu@example.com'",
190 | "-c", "commit.gpgSign=false",
191 | "-c", "tag.gpgSign=false",
192 | "-c", "log.showSignature=false",
193 | }
194 | allArgs = append(allArgs, args...)
195 | return run(allArgs...)
196 | }
197 |
--------------------------------------------------------------------------------
/internal/svu/svu.go:
--------------------------------------------------------------------------------
1 | package svu
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "regexp"
7 | "strconv"
8 | "strings"
9 |
10 | "github.com/Masterminds/semver/v3"
11 | "github.com/caarlos0/svu/v3/internal/git"
12 | )
13 |
14 | type Action uint
15 |
16 | const (
17 | Next Action = iota
18 | Major
19 | Minor
20 | Patch
21 | Current
22 | PreRelease
23 | )
24 |
25 | var (
26 | breakingBody = regexp.MustCompile("(?m).*BREAKING[ -]CHANGE:.*")
27 | breaking = regexp.MustCompile(`(?im).*(\w+)(\(.*\))?!:.*`)
28 | feature = regexp.MustCompile(`(?im).*feat(\(.*\))?:.*`)
29 | patch = regexp.MustCompile(`(?im).*fix(\(.*\))?:.*`)
30 | )
31 |
32 | type Options struct {
33 | Action Action
34 | Pattern string
35 | Prefix string
36 | PreRelease string
37 | Metadata string
38 | Directories []string
39 | TagMode string
40 | Always bool
41 | KeepV0 bool
42 | }
43 |
44 | func Version(opts Options) (string, error) {
45 | tag, err := git.DescribeTag(string(opts.TagMode), opts.Pattern)
46 | if err != nil {
47 | return "", fmt.Errorf("failed to get current tag for repo: %w", err)
48 | }
49 |
50 | current, err := getCurrentVersion(tag, opts.Prefix)
51 | if err != nil {
52 | return "", fmt.Errorf("could not get current version from tag: '%s': %w", tag, err)
53 | }
54 |
55 | result, err := nextVersion(current, tag, opts)
56 | if err != nil {
57 | return "", fmt.Errorf("could not get next tag: '%s': %w", tag, err)
58 | }
59 |
60 | return opts.Prefix + result.String(), nil
61 | }
62 |
63 | func nextVersion(
64 | current *semver.Version,
65 | tag string,
66 | opts Options,
67 | ) (semver.Version, error) {
68 | if opts.Action == Current {
69 | return *current, nil
70 | }
71 |
72 | if opts.Always {
73 | c, _ := current.SetMetadata("")
74 | c, _ = c.SetPrerelease("")
75 | current = &c
76 | }
77 |
78 | var result semver.Version
79 | var err error
80 | switch opts.Action {
81 | case Next, PreRelease:
82 | result, err = findNextWithGitLog(current, tag, opts)
83 | case Major:
84 | result = current.IncMajor()
85 | case Minor:
86 | result = current.IncMinor()
87 | case Patch:
88 | result = current.IncPatch()
89 | }
90 | if err != nil {
91 | return result, err
92 | }
93 |
94 | if opts.Action == PreRelease {
95 | result, err = nextPreRelease(current, &result, opts.PreRelease)
96 | if err != nil {
97 | return result, err
98 | }
99 | } else {
100 | result, err = result.SetPrerelease(opts.PreRelease)
101 | if err != nil {
102 | return result, err
103 | }
104 | }
105 |
106 | result, err = result.SetMetadata(opts.Metadata)
107 | if err != nil {
108 | return result, err
109 | }
110 | return result, nil
111 | }
112 |
113 | func nextPreRelease(current, next *semver.Version, prerelease string) (semver.Version, error) {
114 | suffix := ""
115 | if prerelease != "" {
116 | // Check if the suffix already contains a version number, if it does assume the user wants to explicitly set the version so use that
117 | splitPreRelease := strings.Split(prerelease, ".")
118 | if len(splitPreRelease) > 1 {
119 | if _, err := strconv.Atoi(splitPreRelease[len(splitPreRelease)-1]); err == nil {
120 | return current.SetPrerelease(prerelease)
121 | }
122 | }
123 |
124 | suffix = prerelease
125 |
126 | // Check if the prerelease suffix is the same as the current prerelease
127 | preSuffix := strings.Split(current.Prerelease(), ".")[0]
128 | if preSuffix == prerelease {
129 | suffix = current.Prerelease()
130 | }
131 | } else if current.Prerelease() != "" {
132 | suffix = current.Prerelease()
133 | } else {
134 | return *current, fmt.Errorf(
135 | "--prerelease suffix is required to calculate next pre-release version as suffix could not be determined from current version: %s",
136 | current.String(),
137 | )
138 | }
139 |
140 | splitSuffix := strings.Split(suffix, ".")
141 | preReleaseName := splitSuffix[0]
142 | preReleaseVersion := 0
143 |
144 | currentWithoutPreRelease, _ := current.SetPrerelease("")
145 |
146 | if !next.GreaterThan(¤tWithoutPreRelease) {
147 | preReleaseVersion = -1
148 | if len(splitSuffix) == 2 {
149 | preReleaseName = splitSuffix[0]
150 | preReleaseVersion, _ = strconv.Atoi(splitSuffix[1])
151 | } else if len(splitSuffix) > 2 {
152 | preReleaseName = splitSuffix[len(splitSuffix)-1]
153 | }
154 |
155 | preReleaseVersion++
156 | }
157 |
158 | return next.SetPrerelease(fmt.Sprintf("%s.%d", preReleaseName, preReleaseVersion))
159 | }
160 |
161 | func getCurrentVersion(tag, prefix string) (*semver.Version, error) {
162 | var current *semver.Version
163 | var err error
164 | if tag == "" {
165 | current, err = semver.NewVersion(strings.TrimPrefix("0.0.0", prefix))
166 | } else {
167 | current, err = semver.NewVersion(strings.TrimPrefix(tag, prefix))
168 | }
169 | return current, err
170 | }
171 |
172 | func findNextWithGitLog(
173 | current *semver.Version,
174 | tag string,
175 | opts Options,
176 | ) (semver.Version, error) {
177 | log, err := git.Changelog(tag, opts.Directories)
178 | if err != nil {
179 | return semver.Version{}, fmt.Errorf("failed to get changelog: %w", err)
180 | }
181 |
182 | return findNext(current, log, opts), nil
183 | }
184 |
185 | func isBreaking(commit git.Commit) bool {
186 | return breakingBody.MatchString(commit.Body) || breaking.MatchString(commit.Title)
187 | }
188 |
189 | func isFeature(commit git.Commit) bool {
190 | return feature.MatchString(commit.Title)
191 | }
192 |
193 | func isPatch(commit git.Commit) bool {
194 | return patch.MatchString(commit.Title)
195 | }
196 |
197 | func findNext(current *semver.Version, changes []git.Commit, opts Options) semver.Version {
198 | var major, minor, patch *git.Commit
199 | for _, commit := range changes {
200 | if isBreaking(commit) {
201 | major = &commit
202 | break // no bigger change allowed, so we're done
203 | }
204 |
205 | if minor == nil && isFeature(commit) {
206 | minor = &commit
207 | }
208 |
209 | if patch == nil && isPatch(commit) {
210 | patch = &commit
211 | }
212 | }
213 |
214 | if major != nil {
215 | if current.Major() == 0 && opts.KeepV0 {
216 | log.Printf("found major change, but 'keep v0' is set: %s %s\n", major.SHA, major.Title)
217 | return current.IncMinor()
218 | }
219 | log.Printf("found major change: %s %s\n", major.SHA, major.Title)
220 | return current.IncMajor()
221 | }
222 |
223 | if minor != nil {
224 | log.Printf("found minor change: %s %s\n", minor.SHA, minor.Title)
225 | return current.IncMinor()
226 | }
227 |
228 | if patch != nil {
229 | log.Printf("found patch change: %s %s\n", patch.SHA, patch.Title)
230 | return current.IncPatch()
231 | }
232 |
233 | if opts.Always {
234 | log.Printf("found no changes, but 'always' is set")
235 | return current.IncPatch()
236 | }
237 | return *current
238 | }
239 |
--------------------------------------------------------------------------------
/internal/svu/svu_test.go:
--------------------------------------------------------------------------------
1 | package svu
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 |
7 | "github.com/Masterminds/semver/v3"
8 | "github.com/caarlos0/svu/v3/internal/git"
9 | "github.com/stretchr/testify/require"
10 | )
11 |
12 | func TestIsBreaking(t *testing.T) {
13 | for _, commit := range []git.Commit{
14 | {Title: "feat!: foo"},
15 | {Title: "chore(lala)!: foo"},
16 | {Title: "docs: lalala", Body: "BREAKING CHANGE: lalal"},
17 | {Title: "docs: lalala", Body: "BREAKING-CHANGE: lalal"},
18 | } {
19 | t.Run(commit.String(), func(t *testing.T) {
20 | require.True(t, isBreaking(commit)) // should be a major change
21 | })
22 | }
23 |
24 | for _, commit := range []git.Commit{
25 | {Title: "feat: foo"},
26 | {Title: "chore(lol): foo"},
27 | {Title: "docs: lalala"},
28 | {Title: "docs: BREAKING change: lalal"},
29 | {Title: "docs: breaking-change: aehijhk"},
30 | {Title: "docs: BREAKING_CHANGE: foo"},
31 | } {
32 | t.Run(commit.String(), func(t *testing.T) {
33 | require.True(t, !isBreaking(commit)) // should NOT be a major change
34 | })
35 | }
36 | }
37 |
38 | func TestIsFeature(t *testing.T) {
39 | for _, commit := range []git.Commit{
40 | {Title: "feat: foo"},
41 | {Title: "feat(lalal): foobar"},
42 | } {
43 | t.Run(commit.String(), func(t *testing.T) {
44 | require.True(t, isFeature(commit)) // should be a minor change
45 | })
46 | }
47 |
48 | for _, commit := range []git.Commit{
49 | {Title: "fix: foo"},
50 | {Title: "chore: foo"},
51 | {Title: "docs: lalala"},
52 | {Title: "ci: foo"},
53 | {Title: "test: foo"},
54 | {Title: "Merge remote-tracking branch 'origin/main'"},
55 | {Title: "refactor: foo bar"},
56 | } {
57 | t.Run(commit.String(), func(t *testing.T) {
58 | require.True(t, !isFeature(commit)) // should NOT be a minor change
59 | })
60 | }
61 | }
62 |
63 | func TestIsPatch(t *testing.T) {
64 | for _, commit := range []git.Commit{
65 | {Title: "fix: foo"},
66 | {Title: "fix(lalal): lalala"},
67 | } {
68 | t.Run(commit.String(), func(t *testing.T) {
69 | require.True(t, isPatch(commit)) // should be a patch change
70 | })
71 | }
72 |
73 | for _, commit := range []git.Commit{
74 | {Title: "chore: foobar"},
75 | {Title: "docs: something"},
76 | {Title: "invalid commit"},
77 | } {
78 | t.Run(commit.String(), func(t *testing.T) {
79 | require.True(t, !isPatch(commit)) // should NOT be a patch change
80 | })
81 | }
82 | }
83 |
84 | func TestFindNext(t *testing.T) {
85 | version0a := semver.MustParse("v0.4.5")
86 | version0b := semver.MustParse("v0.5.5")
87 | version1 := semver.MustParse("v1.2.3")
88 | version2 := semver.MustParse("v2.4.12")
89 | version3 := semver.MustParse("v3.4.5-beta34+ads")
90 | for expected, next := range map[string]semver.Version{
91 | "0.4.5": findNext(version0a, []git.Commit{{Title: "chore: should do nothing"}}, Options{}),
92 | "0.4.6": findNext(version0a, []git.Commit{{Title: "fix: inc patch"}}, Options{}),
93 | "0.5.0": findNext(version0a, []git.Commit{{Title: "feat: inc minor"}}, Options{}),
94 | "1.0.0": findNext(version0b, []git.Commit{{Title: "feat!: inc minor"}}, Options{}),
95 | "0.6.0": findNext(version0b, []git.Commit{{Title: "feat!: inc minor"}}, Options{KeepV0: true}),
96 | "1.2.3": findNext(version1, []git.Commit{{Title: "chore: should do nothing"}}, Options{}),
97 | "1.2.4": findNext(version1, []git.Commit{{Title: "chore: always"}}, Options{Always: true}),
98 | "1.3.0": findNext(version1, []git.Commit{{Title: "feat: inc major"}}, Options{}),
99 | "2.0.0": findNext(version1, []git.Commit{{Title: "chore!: hashbang incs major"}}, Options{}),
100 | "3.0.0": findNext(version2, []git.Commit{{Title: "feat: something", Body: "BREAKING CHANGE: increases major"}}, Options{}),
101 | "3.5.0": findNext(version3, []git.Commit{{Title: "feat: inc major"}}, Options{}),
102 | } {
103 | t.Run(expected, func(t *testing.T) {
104 | require.Equal(t, expected, next.String())
105 | })
106 | }
107 | }
108 |
109 | func TestCmd(t *testing.T) {
110 | ver := func() *semver.Version { return semver.MustParse("1.2.3-pre+123") }
111 | t.Run("current", func(t *testing.T) {
112 | t.Run("version has meta", func(t *testing.T) {
113 | v, err := nextVersion(ver(), "v1.2.3", Options{
114 | Action: Current,
115 | })
116 | require.NoError(t, err)
117 | require.Equal(t, "1.2.3-pre+123", v.String())
118 | })
119 | t.Run("version is clean", func(t *testing.T) {
120 | v, err := nextVersion(semver.MustParse("v1.2.3"), "v1.2.3", Options{
121 | Action: Current,
122 | })
123 | require.NoError(t, err)
124 | require.Equal(t, "1.2.3", v.String())
125 | })
126 | })
127 |
128 | t.Run("minor", func(t *testing.T) {
129 | t.Run("clean", func(t *testing.T) {
130 | v, err := nextVersion(ver(), "v1.2.3", Options{
131 | Action: Minor,
132 | })
133 | require.NoError(t, err)
134 | require.Equal(t, "1.3.0", v.String())
135 | })
136 | t.Run("metadata", func(t *testing.T) {
137 | v, err := nextVersion(ver(), "v1.2.3", Options{
138 | Action: Minor,
139 | Metadata: "124",
140 | })
141 | require.NoError(t, err)
142 | require.Equal(t, "1.3.0+124", v.String())
143 | })
144 | t.Run("prerelease", func(t *testing.T) {
145 | v, err := nextVersion(ver(), "v1.2.3", Options{
146 | Action: Minor,
147 | PreRelease: "alpha.1",
148 | })
149 | require.NoError(t, err)
150 | require.Equal(t, "1.3.0-alpha.1", v.String())
151 | })
152 | t.Run("all", func(t *testing.T) {
153 | v, err := nextVersion(ver(), "v1.2.3", Options{
154 | Action: Minor,
155 | PreRelease: "alpha.2",
156 | Metadata: "125",
157 | })
158 | require.NoError(t, err)
159 | require.Equal(t, "1.3.0-alpha.2+125", v.String())
160 | })
161 | })
162 |
163 | t.Run("patch", func(t *testing.T) {
164 | t.Run("clean", func(t *testing.T) {
165 | v, err := nextVersion(semver.MustParse("1.2.3"), "v1.2.3", Options{
166 | Action: Patch,
167 | })
168 | require.NoError(t, err)
169 | require.Equal(t, "1.2.4", v.String())
170 | })
171 | t.Run("previous had meta", func(t *testing.T) {
172 | v, err := nextVersion(semver.MustParse("1.2.3-alpha.1+1"), "v1.2.3", Options{
173 | Action: Patch,
174 | })
175 | require.NoError(t, err)
176 | require.Equal(t, "1.2.3", v.String())
177 | })
178 | t.Run("previous had meta + always", func(t *testing.T) {
179 | v, err := nextVersion(semver.MustParse("1.2.3-alpha.1+1"), "v1.2.3", Options{
180 | Action: Patch,
181 | Always: true,
182 | })
183 | require.NoError(t, err)
184 | require.Equal(t, "1.2.4", v.String())
185 | })
186 | t.Run("previous had meta + always, add meta", func(t *testing.T) {
187 | v, err := nextVersion(semver.MustParse("1.2.3-alpha.1+1"), "v1.2.3-alpha.1+1", Options{
188 | Action: Patch,
189 | Always: true,
190 | PreRelease: "alpha.2",
191 | Metadata: "10",
192 | })
193 | require.NoError(t, err)
194 | require.Equal(t, "1.2.4-alpha.2+10", v.String())
195 | })
196 | t.Run("previous had meta, change it", func(t *testing.T) {
197 | v, err := nextVersion(semver.MustParse("1.2.3-alpha.1+1"), "v1.2.3-alpha.1+1", Options{
198 | Action: Patch,
199 | PreRelease: "alpha.2",
200 | Metadata: "10",
201 | })
202 | require.NoError(t, err)
203 | require.Equal(t, "1.2.3-alpha.2+10", v.String())
204 | })
205 | t.Run("metadata", func(t *testing.T) {
206 | v, err := nextVersion(semver.MustParse("1.2.3"), "v1.2.3", Options{
207 | Action: Patch,
208 | Metadata: "124",
209 | })
210 | require.NoError(t, err)
211 | require.Equal(t, "1.2.4+124", v.String())
212 | })
213 | t.Run("prerelease", func(t *testing.T) {
214 | v, err := nextVersion(semver.MustParse("1.2.3"), "v1.2.3", Options{
215 | Action: Patch,
216 | PreRelease: "alpha.1",
217 | })
218 | require.NoError(t, err)
219 | require.Equal(t, "1.2.4-alpha.1", v.String())
220 | })
221 | t.Run("all meta", func(t *testing.T) {
222 | v, err := nextVersion(semver.MustParse("1.2.3"), "v1.2.3", Options{
223 | Action: Patch,
224 | Metadata: "125",
225 | PreRelease: "alpha.2",
226 | })
227 | require.NoError(t, err)
228 | require.Equal(t, "1.2.4-alpha.2+125", v.String())
229 | })
230 | })
231 |
232 | t.Run("major", func(t *testing.T) {
233 | t.Run("no meta", func(t *testing.T) {
234 | v, err := nextVersion(ver(), "v1.2.3", Options{
235 | Action: Major,
236 | })
237 | require.NoError(t, err)
238 | require.Equal(t, "2.0.0", v.String())
239 | })
240 | t.Run("metadata", func(t *testing.T) {
241 | v, err := nextVersion(ver(), "v1.2.3", Options{
242 | Action: Major,
243 | Metadata: "124",
244 | })
245 | require.NoError(t, err)
246 | require.Equal(t, "2.0.0+124", v.String())
247 | })
248 | t.Run("prerelease", func(t *testing.T) {
249 | v, err := nextVersion(ver(), "v1.2.3", Options{
250 | Action: Major,
251 | PreRelease: "alpha.1",
252 | })
253 | require.NoError(t, err)
254 | require.Equal(t, "2.0.0-alpha.1", v.String())
255 | })
256 | t.Run("all meta", func(t *testing.T) {
257 | v, err := nextVersion(ver(), "v1.2.3", Options{
258 | Action: Major,
259 | PreRelease: "alpha.2",
260 | Metadata: "125",
261 | })
262 | require.NoError(t, err)
263 | require.Equal(t, "2.0.0-alpha.2+125", v.String())
264 | })
265 | })
266 |
267 | t.Run("errors", func(t *testing.T) {
268 | t.Run("invalid build", func(t *testing.T) {
269 | _, err := nextVersion(semver.MustParse("1.2.3"), "v1.2.3", Options{})
270 | require.True(t, err != nil)
271 | })
272 | t.Run("invalid prerelease", func(t *testing.T) {
273 | _, err := nextVersion(semver.MustParse("1.2.3"), "v1.2.3", Options{})
274 | require.True(t, err != nil)
275 | })
276 | })
277 | }
278 |
279 | func Test_nextPreRelease(t *testing.T) {
280 | type args struct {
281 | current *semver.Version
282 | next *semver.Version
283 | preRelease string
284 | }
285 | tests := []struct {
286 | name string
287 | args args
288 | want semver.Version
289 | wantErr bool
290 | }{
291 | {
292 | name: "no current suffix and no suffix supplied",
293 | args: args{
294 | current: semver.MustParse("1.2.3"),
295 | next: semver.MustParse("1.3.0"),
296 | preRelease: "",
297 | },
298 | want: *semver.MustParse("1.3.0"),
299 | wantErr: true,
300 | },
301 | {
302 | name: "supplied suffix overrides current suffix",
303 | args: args{
304 | current: semver.MustParse("1.2.3-alpha.1"),
305 | next: semver.MustParse("1.3.0"),
306 | preRelease: "beta",
307 | },
308 | want: *semver.MustParse("1.3.0-beta.0"),
309 | wantErr: false,
310 | },
311 | {
312 | name: "current suffix is incremented",
313 | args: args{
314 | current: semver.MustParse("1.2.3-alpha.11"),
315 | next: semver.MustParse("1.2.3"),
316 | preRelease: "",
317 | },
318 | want: *semver.MustParse("1.2.3-alpha.12"),
319 | wantErr: false,
320 | },
321 | {
322 | name: "current suffix is incremented when supplied suffix matches current",
323 | args: args{
324 | current: semver.MustParse("1.2.3-alpha.11"),
325 | next: semver.MustParse("1.2.3"),
326 | preRelease: "alpha",
327 | },
328 | want: *semver.MustParse("1.2.3-alpha.12"),
329 | wantErr: false,
330 | },
331 | {
332 | name: "pre release version resets if next version changes",
333 | args: args{
334 | current: semver.MustParse("1.2.3-alpha.11"),
335 | next: semver.MustParse("1.2.4"),
336 | preRelease: "alpha",
337 | },
338 | want: *semver.MustParse("1.2.4-alpha.0"),
339 | wantErr: false,
340 | },
341 | {
342 | name: "increments a current tag that has build metadata",
343 | args: args{
344 | current: semver.MustParse("1.2.3-alpha.1+build.43"),
345 | next: semver.MustParse("1.2.3"),
346 | preRelease: "",
347 | },
348 | want: *semver.MustParse("1.2.3-alpha.2"),
349 | wantErr: false,
350 | },
351 | {
352 | name: "don't increment if explicit pre-release is supplied",
353 | args: args{
354 | current: semver.MustParse("1.2.3-alpha.1"),
355 | next: semver.MustParse("1.2.3"),
356 | preRelease: "alpha.10",
357 | },
358 | want: *semver.MustParse("1.2.3-alpha.10"),
359 | wantErr: false,
360 | },
361 | {
362 | name: "prerelease suffix contains a number",
363 | args: args{
364 | current: semver.MustParse("1.2.3-alpha123.1"),
365 | next: semver.MustParse("1.2.3"),
366 | preRelease: "alpha123",
367 | },
368 | want: *semver.MustParse("1.2.3-alpha123.2"),
369 | wantErr: false,
370 | },
371 | }
372 | for _, tt := range tests {
373 | t.Run(tt.name, func(t *testing.T) {
374 | got, err := nextPreRelease(tt.args.current, tt.args.next, tt.args.preRelease)
375 | if tt.wantErr {
376 | if err == nil {
377 | t.Errorf("nextPreRelease() error = %v, wantErr %v", err, tt.wantErr)
378 | }
379 | return
380 | }
381 | if !reflect.DeepEqual(got, tt.want) {
382 | t.Errorf("nextPreRelease() = %v, want %v", got, tt.want)
383 | }
384 | })
385 | }
386 | }
387 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | _ "embed"
5 | "fmt"
6 | "io"
7 | "log"
8 | "os"
9 | "strings"
10 |
11 | goversion "github.com/caarlos0/go-version"
12 | "github.com/caarlos0/svu/v3/internal/git"
13 | "github.com/caarlos0/svu/v3/internal/svu"
14 | "github.com/spf13/cobra"
15 | "github.com/spf13/pflag"
16 | "github.com/spf13/viper"
17 | )
18 |
19 | //go:embed description.txt
20 | var description []byte
21 |
22 | //go:embed examples.sh
23 | var examples []byte
24 |
25 | func main() {
26 | var verbose bool
27 | var opts svu.Options
28 |
29 | runFunc := func(cmd *cobra.Command) error {
30 | version, err := svu.Version(opts)
31 | if err != nil {
32 | return err
33 | }
34 | _, err = fmt.Fprintln(cmd.OutOrStdout(), version)
35 | return err
36 | }
37 |
38 | rootCmd := &cobra.Command{
39 | Use: "svu",
40 | Short: "Semantic Version Utility",
41 | Long: string(description),
42 | Version: buildVersion(version, commit, date, builtBy).String(),
43 | Example: paddingLeft(string(examples)),
44 | SilenceUsage: true,
45 | PersistentPreRunE: func(*cobra.Command, []string) error {
46 | switch opts.TagMode {
47 | case git.TagModeAll, git.TagModeCurrent:
48 | default:
49 | return fmt.Errorf(
50 | "invalid tag-mode: %q: valid options are %q and %q",
51 | opts.TagMode,
52 | git.TagModeCurrent,
53 | git.TagModeAll,
54 | )
55 | }
56 |
57 | if verbose {
58 | log.SetFlags(0)
59 | } else {
60 | log.SetOutput(io.Discard)
61 | }
62 | return nil
63 | },
64 | }
65 |
66 | prereleaseCmd := &cobra.Command{
67 | Use: "prerelease",
68 | Aliases: []string{"pr"},
69 | Short: "Increases the build portion of the prerelease",
70 | RunE: func(cmd *cobra.Command, args []string) error {
71 | opts.Action = svu.PreRelease
72 | return runFunc(cmd)
73 | },
74 | }
75 | nextCmd := &cobra.Command{
76 | Use: "next",
77 | Aliases: []string{"n"},
78 | Short: "Next version based on git history",
79 | RunE: func(cmd *cobra.Command, args []string) error {
80 | opts.Action = svu.Next
81 | return runFunc(cmd)
82 | },
83 | }
84 | majorCmd := &cobra.Command{
85 | Use: "major",
86 | Short: "New major release",
87 | RunE: func(cmd *cobra.Command, args []string) error {
88 | opts.Action = svu.Major
89 | return runFunc(cmd)
90 | },
91 | }
92 | minorCmd := &cobra.Command{
93 | Use: "minor",
94 | Short: "New minor release",
95 | Aliases: []string{"m"},
96 | RunE: func(cmd *cobra.Command, args []string) error {
97 | opts.Action = svu.Minor
98 | return runFunc(cmd)
99 | },
100 | }
101 | patchCmd := &cobra.Command{
102 | Use: "patch",
103 | Short: "New patch release",
104 | Aliases: []string{"p"},
105 | RunE: func(cmd *cobra.Command, args []string) error {
106 | opts.Action = svu.Patch
107 | return runFunc(cmd)
108 | },
109 | }
110 | currentCmd := &cobra.Command{
111 | Use: "current",
112 | Short: "Current version",
113 | Aliases: []string{"c"},
114 | RunE: func(cmd *cobra.Command, args []string) error {
115 | opts.Action = svu.Current
116 | return runFunc(cmd)
117 | },
118 | }
119 | initCmd := &cobra.Command{
120 | Use: "init",
121 | Short: "Creates a svu configuration file",
122 | Aliases: []string{"i"},
123 | RunE: func(cmd *cobra.Command, args []string) error {
124 | return os.WriteFile(".svu.yaml", exampleConfig, 0o644)
125 | },
126 | }
127 |
128 | rootCmd.SetVersionTemplate("{{.Version}}")
129 | rootCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "enable logs")
130 | rootCmd.AddCommand(initCmd)
131 | nextCmd.Flags().BoolVar(&opts.Always, "always", false, "if no commits trigger a version change, increment the patch")
132 | nextCmd.Flags().BoolVar(&opts.KeepV0, "v0", false, "prevent major version increments if current version is still v0")
133 |
134 | for _, cmd := range []*cobra.Command{
135 | nextCmd,
136 | majorCmd,
137 | minorCmd,
138 | patchCmd,
139 | currentCmd,
140 | prereleaseCmd,
141 | } {
142 | // init does not share these flags.
143 | cmd.Flags().StringVar(&opts.Pattern, "tag.pattern", "", "ignore tags that do not match the given pattern")
144 | cmd.Flags().StringVar(&opts.Prefix, "tag.prefix", "v", "sets a tag custom prefix")
145 | cmd.Flags().StringVar(&opts.TagMode, "tag.mode", git.TagModeAll, "determine if it should look for tags in all branches, or just the current one")
146 | cmd.Flags().StringVar(&opts.PreRelease, "prerelease", "", "sets the version prerelease")
147 | cmd.Flags().StringVar(&opts.Metadata, "metadata", "", "sets the version metadata")
148 | rootCmd.AddCommand(cmd)
149 | }
150 |
151 | for _, cmd := range []*cobra.Command{
152 | nextCmd,
153 | prereleaseCmd,
154 | } {
155 | cmd.Flags().StringSliceVar(&opts.Directories, "log.directory", nil, "only use commits that changed files in the given directories")
156 | }
157 |
158 | home, _ := os.UserHomeDir()
159 | config, _ := os.UserConfigDir()
160 | viper.AutomaticEnv()
161 | viper.SetEnvPrefix("svu")
162 | viper.AddConfigPath(".")
163 | viper.AddConfigPath(git.Root())
164 | viper.AddConfigPath(config)
165 | viper.AddConfigPath(home)
166 | viper.SetConfigName(".svu")
167 | viper.SetConfigType("yaml")
168 | cobra.OnInitialize(func() {
169 | if viper.ReadInConfig() == nil {
170 | presetRequiredFlags(rootCmd)
171 | }
172 | })
173 |
174 | if err := rootCmd.Execute(); err != nil {
175 | os.Exit(1)
176 | }
177 | }
178 |
179 | func presetRequiredFlags(cmd *cobra.Command) {
180 | viper.BindPFlags(cmd.Flags())
181 | cmd.Flags().VisitAll(func(f *pflag.Flag) {
182 | if viper.IsSet(f.Name) {
183 | cmd.Flags().Set(f.Name, viper.GetString(f.Name))
184 | }
185 | })
186 | for _, scmd := range cmd.Commands() {
187 | presetRequiredFlags(scmd)
188 | }
189 | }
190 |
191 | // nolint: gochecknoglobals
192 | var (
193 | version = ""
194 | commit = ""
195 | date = ""
196 | builtBy = ""
197 | treeState = ""
198 | )
199 |
200 | //go:embed art.txt
201 | var asciiArt string
202 |
203 | //go:embed example.svu.yaml
204 | var exampleConfig []byte
205 |
206 | func buildVersion(version, commit, date, builtBy string) goversion.Info {
207 | return goversion.GetVersionInfo(
208 | goversion.WithAppDetails("svu", "Semantic Version Utility", "https://github.com/caarlos0/svu"),
209 | goversion.WithASCIIName(asciiArt),
210 | func(i *goversion.Info) {
211 | if commit != "" {
212 | i.GitCommit = commit
213 | }
214 | if treeState != "" {
215 | i.GitTreeState = treeState
216 | }
217 | if date != "" {
218 | i.BuildDate = date
219 | }
220 | if version != "" {
221 | i.GitVersion = version
222 | }
223 | if builtBy != "" {
224 | i.BuiltBy = builtBy
225 | }
226 | },
227 | )
228 | }
229 |
230 | func paddingLeft(in string) string {
231 | var out []string
232 | for _, line := range strings.Split(in, "\n") {
233 | out = append(out, " "+line)
234 | }
235 | return strings.Join(out, "\n")
236 | }
237 |
--------------------------------------------------------------------------------
/pkg/svu/svu.go:
--------------------------------------------------------------------------------
1 | // Package svu provides a Go API to SVU.
2 | package svu
3 |
4 | import (
5 | "github.com/caarlos0/svu/v3/internal/git"
6 | "github.com/caarlos0/svu/v3/internal/svu"
7 | )
8 |
9 | type option func(o *svu.Options)
10 |
11 | // Next returns the next version based on the git log.
12 | func Next(opts ...option) (string, error) {
13 | return version(append(opts, cmd(svu.Next))...)
14 | }
15 |
16 | // Major increase the major part of the version.
17 | func Major(opts ...option) (string, error) {
18 | return version(append(opts, cmd(svu.Major))...)
19 | }
20 |
21 | // Minor increase the minor part of the version.
22 | func Minor(opts ...option) (string, error) {
23 | return version(append(opts, cmd(svu.Minor))...)
24 | }
25 |
26 | // Patch increase the patch part of the version.
27 | func Patch(opts ...option) (string, error) {
28 | return version(append(opts, cmd(svu.Patch))...)
29 | }
30 |
31 | // Current returns the current version.
32 | func Current(opts ...option) (string, error) {
33 | return version(append(opts, cmd(svu.Current))...)
34 | }
35 |
36 | // PreRelease returns the next pre-release version.
37 | func PreRelease(opts ...option) (string, error) {
38 | return version(append(opts, cmd(svu.PreRelease))...)
39 | }
40 |
41 | // WithPattern ignores tags that do not match the given pattern.
42 | func WithPattern(pattern string) option {
43 | return func(o *svu.Options) {
44 | o.Pattern = pattern
45 | }
46 | }
47 |
48 | // WithPrefix sets the version prefix.
49 | func WithPrefix(prefix string) option {
50 | return func(o *svu.Options) {
51 | o.Prefix = prefix
52 | }
53 | }
54 |
55 | // WithPreRelease sets the version prerelease.
56 | func WithPreRelease(prerelease string) option {
57 | return func(o *svu.Options) {
58 | o.PreRelease = prerelease
59 | }
60 | }
61 |
62 | // WithMetadata sets the version metadata.
63 | func WithMetadata(metadata string) option {
64 | return func(o *svu.Options) {
65 | o.Metadata = metadata
66 | }
67 | }
68 |
69 | // WithDirectories only use commits that changed files in the given directories.
70 | func WithDirectories(directories ...string) option {
71 | return func(o *svu.Options) {
72 | o.Directories = append(o.Directories, directories...)
73 | }
74 | }
75 |
76 | // ForCurrentBranch look for tags in the current branch only.
77 | func ForCurrentBranch() option {
78 | return func(o *svu.Options) {
79 | o.TagMode = git.TagModeCurrent
80 | }
81 | }
82 |
83 | // ForAllBranches look for tags in all branches.
84 | func ForAllBranches() option {
85 | return func(o *svu.Options) {
86 | o.TagMode = git.TagModeAll
87 | }
88 | }
89 |
90 | // Always if no commits would have increased the version, increase the
91 | // patch portion anyway.
92 | func Always() option {
93 | return func(o *svu.Options) {
94 | o.Always = true
95 | }
96 | }
97 |
98 | // KeepV0 prevents major upgrades if current version is a v0.
99 | func KeepV0() option {
100 | return func(o *svu.Options) {
101 | o.KeepV0 = true
102 | }
103 | }
104 |
105 | func version(opts ...option) (string, error) {
106 | options := &svu.Options{
107 | Action: svu.Next,
108 | Prefix: "v",
109 | TagMode: git.TagModeCurrent,
110 | }
111 | for _, opt := range opts {
112 | opt(options)
113 | }
114 | return svu.Version(*options)
115 | }
116 |
117 | func cmd(cmd svu.Action) option {
118 | return func(o *svu.Options) {
119 | o.Action = cmd
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/scripts/completions.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 | rm -rf completions
4 | mkdir completions
5 | go build -o svu .
6 | for sh in bash zsh fish; do
7 | ./svu completion "$sh" >"completions/svu.$sh"
8 | done
9 |
--------------------------------------------------------------------------------