├── .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 | svu Logo 3 |

semantic version utility

4 |

5 | 6 |
7 | 8 |

9 | Release 10 | Software License 11 | Build status 12 | Go Doc 13 | GoReportCard 14 | Conventional Commits 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 | [![Packaging status](https://repology.org/badge/vertical-allrepos/svu.svg)](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 | [![Stargazers over time](https://starchart.cc/caarlos0/svu.svg?variant=adaptive)](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 | --------------------------------------------------------------------------------