initial commit after fork

This commit is contained in:
Robert Kaussow 2023-10-12 16:18:25 +02:00
parent 20e64f8fcf
commit 00e2fe31e5
Signed by: xoxys
GPG Key ID: 4E692A2EAECC03C0
43 changed files with 2129 additions and 748 deletions

View File

@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"

71
.github/settings.yml vendored Normal file
View File

@ -0,0 +1,71 @@
repository:
name: git-sv
description: Woodpecker CI plugin to perform git actions
homepage: https://woodpecker-plugins.geekdocs.de/plugins/git-sv
topics: woodpecker-ci, woodpecker, woodpecker-plugin
private: false
has_issues: true
has_wiki: false
has_downloads: true
default_branch: main
allow_squash_merge: true
allow_merge_commit: true
allow_rebase_merge: true
labels:
- name: bug
color: d73a4a
description: Something isn't working
- name: documentation
color: 0075ca
description: Improvements or additions to documentation
- name: duplicate
color: cfd3d7
description: This issue or pull request already exists
- name: enhancement
color: a2eeef
description: New feature or request
- name: good first issue
color: 7057ff
description: Good for newcomers
- name: help wanted
color: 008672
description: Extra attention is needed
- name: invalid
color: e4e669
description: This doesn't seem right
- name: question
color: d876e3
description: Further information is requested
- name: wontfix
color: ffffff
description: This will not be worked on
branches:
- name: main
protection:
required_pull_request_reviews: null
required_status_checks:
strict: false
contexts:
- ci/woodpecker/pr/test
- ci/woodpecker/pr/build-package
- ci/woodpecker/pr/build-container
- ci/woodpecker/pr/docs
enforce_admins: false
required_linear_history: true
restrictions: null
- name: docs
protection:
required_pull_request_reviews: null
required_status_checks: null
enforce_admins: true
required_linear_history: true
restrictions:
apps: []
users: []
teams:
- bot

View File

@ -1,102 +0,0 @@
name: ci
on:
push:
branches: [master]
paths-ignore:
- "**.md"
- "**/.gitignore"
- ".github/workflows/**"
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Run golangci lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ^1.19
- name: Build
run: make build
tag:
name: Tag
runs-on: ubuntu-latest
needs: [lint, build]
steps:
- name: Check out code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set GitHub Actions as commit author
shell: bash
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Setup sv4git
run: |
curl -s https://api.github.com/repos/bvieira/sv4git/releases/latest | jq -r '.assets[] | select(.browser_download_url | contains("linux")) | .browser_download_url' | wget -O /tmp/sv4git.tar.gz -qi - \
&& tar -C /usr/local/bin -xzf /tmp/sv4git.tar.gz
- name: Create tag
id: create-tag
run: |
git sv tag
VERSION=$(git sv cv)
echo "::set-output name=tag::v$VERSION"
outputs:
tag: ${{ steps.create-tag.outputs.tag }}
release:
name: Release
runs-on: ubuntu-latest
needs: [tag]
steps:
- name: Check out code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup sv4git
run: |
curl -s https://api.github.com/repos/bvieira/sv4git/releases/latest | jq -r '.assets[] | select(.browser_download_url | contains("linux")) | .browser_download_url' | wget -O /tmp/sv4git.tar.gz -qi - \
&& tar -C /usr/local/bin -xzf /tmp/sv4git.tar.gz
- name: Set up Go
id: go
uses: actions/setup-go@v3
with:
go-version: ^1.19
- name: Create release notes
run: |
git sv rn -t "${{ needs.tag.outputs.tag }}" > release-notes.md
- name: Build releases
run: make release-all
- name: Release
uses: softprops/action-gh-release@v1
with:
body_path: release-notes.md
tag_name: ${{ needs.tag.outputs.tag }}
fail_on_unmatched_files: true
files: |
bin/git-sv_*

View File

@ -1,34 +0,0 @@
name: pull_request
on:
pull_request:
branches: [ master ]
paths-ignore:
- '**/.gitignore'
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Run golangci lint
uses: golangci/golangci-lint-action@v2
with:
version: latest
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: ^1.19
id: go
- name: Build
run: make build

27
.gitignore vendored
View File

@ -1,23 +1,6 @@
# Binaries for programs and plugins
bin/
*.exe
*.exe~
*.dll
*.so
*.dylib
/dist
/release
/git-sv*
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
*.sample
todo
# Additional generated artifacts
artifacts/
# Mac metadata
.DS_Store
coverage.out
CHANGELOG.md

47
.gitsv/config.yml Normal file
View File

@ -0,0 +1,47 @@
---
version: "1.1"
versioning:
update-major: []
update-minor: [feat]
update-patch: [fix, perf, refactor, chore, test, ci, docs]
tag:
pattern: "v%d.%d.%d"
release-notes:
sections:
- name: Features
commit-types: [feat]
section-type: commits
- name: Bug Fixes
commit-types: [fix]
section-type: commits
- name: Performance Improvements
commit-types: [perf]
section-type: commits
- name: Code Refactoring
commit-types: [refactor]
section-type: commits
- name: Others
commit-types: [chore]
section-type: commits
- name: Testing
commit-types: [test]
section-type: commits
- name: CI Pipeline
commit-types: [ci]
section-type: commits
- name: Documentation
commit-types: [docs]
section-type: commits
- name: Breaking Changes
section-type: breaking-changes
commit-message:
footer:
issue:
key: issue
add-value-prefix: "#"
issue:
regex: "#?[0-9]+"

View File

@ -1,35 +1,109 @@
linters:
enable-all: false
disable-all: true
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- typecheck
- unused
- asasalint
- asciicheck
- bidichk
- bodyclose
- containedctx
- contextcheck
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- errchkjson
- errname
- errorlint
- execinquery
# - exhaustive
- exportloopref
- forcetypeassert
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
- gocognit
- goconst
- gocritic
- gocyclo
- godot
# - godox
- goerr113
- gofmt
- gofumpt
- goheader
- goimports
- gomnd
- gomoddirectives
- gomodguard
- goprintffuncname
- gosec
- grouper
- importas
- interfacebloat
- ireturn
- lll
- loggercheck
- maintidx
- makezero
- misspell
- musttag
- nakedret
- nestif
- nilerr
- nilnil
- nlreturn
- noctx
- nolintlint
- nonamedreturns
- nosprintfhostport
- prealloc
- predeclared
- promlinter
- reassign
- revive
# - rowserrcheck
# - sqlclosecheck
# - structcheck
- stylecheck
- tagliatelle
- tenv
- testableexamples
- thelper
- tparallel
- unconvert
- unparam
- usestdlibvars
# - wastedassign
- whitespace
- wsl
fast: false
run:
skip-dirs:
- build
- artifacts
timeout: 3m
linters-settings:
tagliatelle:
case:
use-field-name: true
rules:
json: camel
yaml: kebab
xml: camel
bson: camel
avro: snake
mapstructure: kebab
gofumpt:
extra-rules: true
lang-version: "1.21"
issues:
exclude-rules:
- path: _test\.go
- path: (.+)_test.go
linters:
- gocyclo
- errcheck
- dupl
- gosec
- gochecknoglobals
- testpackage
- path: cmd/git-sv/main.go
linters:
- gochecknoglobals
- funlen
- prealloc

6
.markdownlint.yml Normal file
View File

@ -0,0 +1,6 @@
---
default: True
MD013: False
MD041: False
MD004:
style: dash

2
.prettierignore Normal file
View File

@ -0,0 +1,2 @@
*.tpl.md
LICENSE

View File

@ -1,31 +0,0 @@
version: "1.1"
versioning:
update-major: []
update-minor: [feat]
update-patch: [build, ci, chore, fix, perf, refactor, test]
tag:
pattern: "v%d.%d.%d"
release-notes:
sections:
- name: Features
section-type: commits
commit-types: [feat]
- name: Bug Fixes
section-type: commits
commit-types: [fix]
- name: Misc
section-type: commits
commit-types: [build]
- name: Breaking Changes
section-type: breaking-changes
commit-message:
footer:
issue:
key: issue
add-value-prefix: "#"
issue:
regex: "#?[0-9]+"

View File

@ -0,0 +1,66 @@
---
when:
- event: [pull_request, tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
steps:
dryrun:
image: quay.io/thegeeklab/wp-docker-buildx:1
settings:
containerfile: Containerfile.multiarch
dry_run: true
platforms:
- linux/amd64
- linux/arm64
provenance: false
repo: ${CI_REPO}
when:
- event: [pull_request]
publish-dockerhub:
group: container
image: quay.io/thegeeklab/wp-docker-buildx:1
settings:
auto_tag: true
containerfile: Containerfile.multiarch
password:
from_secret: docker_password
platforms:
- linux/amd64
- linux/arm64
provenance: false
repo: ${CI_REPO}
username:
from_secret: docker_username
when:
- event: [tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
publish-quay:
group: container
image: quay.io/thegeeklab/wp-docker-buildx:1
settings:
auto_tag: true
containerfile: Containerfile.multiarch
password:
from_secret: quay_password
platforms:
- linux/amd64
- linux/arm64
provenance: false
registry: quay.io
repo: quay.io/${CI_REPO}
username:
from_secret: quay_username
when:
- event: [tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
depends_on:
- test

View File

@ -0,0 +1,44 @@
---
when:
- event: [pull_request, tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
steps:
build:
image: docker.io/techknowlogick/xgo:go-1.21.x
commands:
- ln -s $(pwd) /source
- make release
executable:
image: quay.io/thegeeklab/alpine-tools
commands:
- $(find dist/ -executable -type f -iname ${CI_REPO_NAME}-linux-amd64) --help
# changelog-generate:
# image: quay.io/thegeeklab/git-chglog
# commands:
# - git fetch -tq
# - git-chglog --no-color --no-emoji -o CHANGELOG.md ${CI_COMMIT_TAG:---next-tag unreleased unreleased}
# changelog-format:
# image: quay.io/thegeeklab/alpine-tools
# commands:
# - prettier CHANGELOG.md
# - prettier -w CHANGELOG.md
publish-github:
image: docker.io/plugins/github-release
settings:
api_key:
from_secret: github_token
note: CHANGELOG.md
overwrite: true
title: ${CI_COMMIT_TAG}
when:
- event: [tag]
depends_on:
- test

72
.woodpecker/docs.yml Normal file
View File

@ -0,0 +1,72 @@
---
when:
- event: [pull_request, tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
steps:
markdownlint:
image: quay.io/thegeeklab/markdownlint-cli
commands:
- markdownlint 'README.md' 'CONTRIBUTING.md'
spellcheck:
image: quay.io/thegeeklab/alpine-tools
commands:
- spellchecker --files '_docs/**/*.md' 'README.md' 'CONTRIBUTING.md' -d .dictionary -p spell indefinite-article syntax-urls
environment:
FORCE_COLOR: "true"
NPM_CONFIG_LOGLEVEL: "error"
publish:
image: quay.io/thegeeklab/git-sv
settings:
action:
- pages
author_email: bot@thegeeklab.de
author_name: thegeeklab-bot
branch: docs
message: "[skip ci] auto-update documentation"
netrc_password:
from_secret: github_token
pages_directory: _docs/
when:
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
pushrm-dockerhub:
image: docker.io/chko/docker-pushrm:1
secrets:
- source: docker_password
target: DOCKER_PASS
- source: docker_username
target: DOCKER_USER
environment:
PUSHRM_FILE: README.md
PUSHRM_SHORT: Woodpecker CI plugin to perform git actions
PUSHRM_TARGET: ${CI_REPO}
when:
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
status: [success]
pushrm-quay:
image: docker.io/chko/docker-pushrm:1
secrets:
- source: quay_token
target: APIKEY__QUAY_IO
environment:
PUSHRM_FILE: README.md
PUSHRM_TARGET: quay.io/${CI_REPO}
when:
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
status: [success]
depends_on:
- build-package
- build-container

26
.woodpecker/notify.yml Normal file
View File

@ -0,0 +1,26 @@
---
when:
- event: [tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
runs_on: [success, failure]
steps:
matrix:
image: quay.io/thegeeklab/wp-matrix
settings:
homeserver:
from_secret: matrix_homeserver
password:
from_secret: matrix_password
roomid:
from_secret: matrix_roomid
username:
from_secret: matrix_username
when:
- status: [success, failure]
depends_on:
- docs

17
.woodpecker/test.yml Normal file
View File

@ -0,0 +1,17 @@
---
when:
- event: [pull_request, tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
steps:
lint:
image: docker.io/library/golang:1.21
commands:
- make lint
test:
image: docker.io/library/golang:1.21
commands:
- make test

25
Containerfile.multiarch Normal file
View File

@ -0,0 +1,25 @@
FROM --platform=$BUILDPLATFORM golang:1.21@sha256:19600fdcae402165dcdab18cb9649540bde6be7274dedb5d205b2f84029fe909 as build
ARG TARGETOS
ARG TARGETARCH
ADD . /src
WORKDIR /src
RUN make build
FROM alpine:3.18@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978
LABEL maintainer="Robert Kaussow <mail@thegeeklab.de>"
LABEL org.opencontainers.image.authors="Robert Kaussow <mail@thegeeklab.de>"
LABEL org.opencontainers.image.title="git-sv"
LABEL org.opencontainers.image.url="https://github.com/thegeeklab/git-sv"
LABEL org.opencontainers.image.source="https://github.com/thegeeklab/git-sv"
LABEL org.opencontainers.image.documentation="https://github.com/thegeeklab/git-sv"
RUN apk --update add --no-cache git && \
rm -rf /var/cache/apk/* && \
rm -rf /tmp/*
COPY --from=build /src/dist/git-sv /bin/git-sv
ENTRYPOINT ["/bin/git-sv"]

22
LICENSE
View File

@ -1,21 +1,21 @@
MIT License
Copyright (c) 2019 Beatriz Vieira
Copyright (c) 2022 Robert Kaussow <mail@thegeeklab.de>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

168
Makefile
View File

@ -1,89 +1,101 @@
.PHONY: usage build lint lint-autofix test test-coverage test-show-coverage run tidy release release-all
# renovate: datasource=github-releases depName=mvdan/gofumpt
GOFUMPT_PACKAGE_VERSION := v0.5.0
# renovate: datasource=github-releases depName=golangci/golangci-lint
GOLANGCI_LINT_PACKAGE_VERSION := v1.54.2
OK_COLOR=\033[32;01m
NO_COLOR=\033[0m
ERROR_COLOR=\033[31;01m
WARN_COLOR=\033[33;01m
EXECUTABLE := git-sv
PKGS = $(shell go list ./...)
BIN = git-sv
DIST := dist
DIST_DIRS := $(DIST)
IMPORT := github.com/thegeeklab/$(EXECUTABLE)
ECHOFLAGS ?=
GO ?= go
CWD ?= $(shell pwd)
PACKAGES ?= $(shell go list ./...)
SOURCES ?= $(shell find . -name "*.go" -type f)
BUILD_TIME = $(shell date +"%Y%m%d%H%M")
VERSION ?= dev-$(BUILD_TIME)
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@$(GOFUMPT_PACKAGE_VERSION)
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_PACKAGE_VERSION)
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
BUILDOS ?= linux
BUILDARCH ?= amd64
BUILDENVS ?= CGO_ENABLED=0 GOOS=$(BUILDOS) GOARCH=$(BUILDARCH)
BUILDFLAGS ?= -a -installsuffix cgo --ldflags '-X main.Version=$(VERSION) -extldflags "-lm -lstdc++ -static"'
GENERATE ?=
XGO_VERSION := go-1.21.x
XGO_TARGETS ?= linux/amd64,linux/arm-6,linux/arm-7,linux/arm64
COMPRESS_TYPE ?= targz
TARGETOS ?= linux
TARGETARCH ?= amd64
ifneq ("$(TARGETVARIANT)","")
GOARM ?= $(subst v,,$(TARGETVARIANT))
endif
TAGS ?= netgo,osusergo
usage: Makefile
@echo $(ECHOFLAGS) "to use make call:"
@echo $(ECHOFLAGS) " make <action>"
@echo $(ECHOFLAGS) ""
@echo $(ECHOFLAGS) "list of available actions:"
@sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'
## build: build git-sv
build: test
@echo $(ECHOFLAGS) "$(OK_COLOR)==> Building binary ($(BUILDOS)/$(BUILDARCH)/$(BIN))...$(NO_COLOR)"
@$(BUILDENVS) go build -v $(BUILDFLAGS) -o bin/$(BUILDOS)_$(BUILDARCH)/$(BIN) ./cmd/git-sv
## lint: run golangci-lint without autofix
lint:
@echo $(ECHOFLAGS) "$(OK_COLOR)==> Running golangci-lint...$(NO_COLOR)"
@golangci-lint run ./... --config .golangci.yml
## lint-autofix: run golangci-lint with autofix enabled
lint-autofix:
@echo $(ECHOFLAGS) "$(OK_COLOR)==> Running golangci-lint...$(NO_COLOR)"
@golangci-lint run ./... --config .golangci.yml --fix
## test: run unit tests
test:
@echo $(ECHOFLAGS) "$(OK_COLOR)==> Running tests...$(NO_COLOR)"
@go test $(PKGS)
## test-coverage: run tests with coverage
test-coverage:
@echo $(ECHOFLAGS) "$(OK_COLOR)==> Running tests with coverage...$(NO_COLOR)"
@go test -race -covermode=atomic -coverprofile coverage.out ./...
## test-show-coverage: show coverage
test-show-coverage: test-coverage
@echo $(ECHOFLAGS) "$(OK_COLOR)==> Show test coverage...$(NO_COLOR)"
@go tool cover -html coverage.out
## run: run git-sv
run:
@echo $(ECHOFLAGS) "$(OK_COLOR)==> Running bin/$(BUILDOS)_$(BUILDARCH)/$(BIN)...$(NO_COLOR)"
@./bin/$(BUILDOS)_$(BUILDARCH)/$(BIN) $(args)
## tidy: execute go mod tidy
tidy:
@echo $(ECHOFLAGS) "$(OK_COLOR)==> runing tidy"
@go mod tidy
## release: prepare binary for release
release:
make build
ifeq ($(COMPRESS_TYPE), zip)
@zip -j bin/git-sv_$(VERSION)_$(BUILDOS)_$(BUILDARCH).zip bin/$(BUILDOS)_$(BUILDARCH)/$(BIN)
else
@tar -czf bin/git-sv_$(VERSION)_$(BUILDOS)_$(BUILDARCH).tar.gz -C bin/$(BUILDOS)_$(BUILDARCH)/ $(BIN)
ifndef VERSION
ifneq ($(CI_COMMIT_TAG),)
VERSION ?= $(subst v,,$(CI_COMMIT_TAG))
else
VERSION ?= $(shell git rev-parse --short HEAD)
endif
endif
## release-all: prepare linux, darwin and windows binary for release (requires sv4git)
release-all:
@rm -rf bin
ifndef DATE
DATE := $(shell date -u +"%Y-%m-%dT%H:%M:%S%z")
endif
VERSION=$(shell git sv nv) BUILDOS=linux BUILDARCH=amd64 make release
VERSION=$(shell git sv nv) BUILDOS=darwin BUILDARCH=amd64 make release
VERSION=$(shell git sv nv) COMPRESS_TYPE=zip BUILDOS=windows BUILDARCH=amd64 make release
LDFLAGS += -s -w -X "main.BuildVersion=$(VERSION)" -X "main.BuildDate=$(DATE)"
VERSION=$(shell git sv nv) BUILDOS=linux BUILDARCH=arm64 make release
VERSION=$(shell git sv nv) BUILDOS=darwin BUILDARCH=arm64 make release
VERSION=$(shell git sv nv) COMPRESS_TYPE=zip BUILDOS=windows BUILDARCH=arm64 make release
.PHONY: all
all: clean build
.PHONY: clean
clean:
$(GO) clean -i ./...
rm -rf $(DIST_DIRS)
.PHONY: fmt
fmt:
$(GO) run $(GOFUMPT_PACKAGE) -extra -w $(SOURCES)
.PHONY: golangci-lint
golangci-lint:
$(GO) run $(GOLANGCI_LINT_PACKAGE) run
.PHONY: lint
lint: golangci-lint
.PHONY: generate
generate:
$(GO) generate $(GENERATE)
.PHONY: test
test:
$(GO) test -v -coverprofile coverage.out $(PACKAGES)
.PHONY: build
build: $(DIST)/$(EXECUTABLE)
$(DIST)/$(EXECUTABLE): $(SOURCES)
GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) GOARM=$(GOARM) $(GO) build -v -tags '$(TAGS)' -ldflags '-extldflags "-static" $(LDFLAGS)' -o $@ ./cmd/$(EXECUTABLE)
$(DIST_DIRS):
mkdir -p $(DIST_DIRS)
.PHONY: xgo
xgo: | $(DIST_DIRS)
$(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -v -ldflags '-extldflags "-static" $(LDFLAGS)' -tags '$(TAGS)' -targets '$(XGO_TARGETS)' -out $(EXECUTABLE) --pkg cmd/$(EXECUTABLE) .
cp /build/* $(CWD)/$(DIST)
ls -l $(CWD)/$(DIST)
.PHONY: checksum
checksum:
cd $(DIST); $(foreach file,$(wildcard $(DIST)/$(EXECUTABLE)-*),sha256sum $(notdir $(file)) > $(notdir $(file)).sha256;)
ls -l $(CWD)/$(DIST)
.PHONY: release
release: xgo checksum
.PHONY: deps
deps:
$(GO) mod download
$(GO) install $(GOFUMPT_PACKAGE)
$(GO) install $(GOLANGCI_LINT_PACKAGE)
$(GO) install $(XGO_PACKAGE)

148
README.md
View File

@ -1,18 +1,6 @@
<p align="center">
<h1 align="center">sv4git</h1>
<p align="center">A command line tool (CLI) to validate commit messages, bump version, create tags and generate changelogs!</p>
<p align="center">
<a href="https://github.com/bvieira/sv4git/releases/latest"><img alt="Release" src="https://img.shields.io/github/release/bvieira/sv4git.svg?style=for-the-badge"></a>
<a href="https://pkg.go.dev/github.com/bvieira/sv4git/v2"><img alt="Go Reference" src="https://img.shields.io/badge/-Reference-blue?style=for-the-badge&logo=go&labelColor=gray"></a>
<a href="https://github.com/bvieira/sv4git/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/bvieira/sv4git?style=for-the-badge"></a>
<a href="https://github.com/bvieira/sv4git/releases/latest"><img alt="GitHub release (latest by date)" src="https://img.shields.io/github/downloads/bvieira/sv4git/latest/total?color=blue&style=for-the-badge"></a>
<a href="https://github.com/bvieira/sv4git/releases/latest"><img alt="GitHub all releases" src="https://img.shields.io/github/downloads/bvieira/sv4git/total?color=blue&style=for-the-badge"></a>
<a href="/LICENSE"><img alt="Software License" src="https://img.shields.io/badge/license-MIT-informational.svg?style=for-the-badge"></a>
<a href="https://github.com/bvieira/sv4git/actions?workflow=ci"><img alt="GitHub Actions Status" src="https://img.shields.io/github/actions/workflow/status/bvieira/sv4git/ci.yml?style=for-the-badge"></a>
<a href="https://goreportcard.com/report/github.com/bvieira/sv4git"><img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/bvieira/sv4git?style=for-the-badge"></a>
<a href="https://conventionalcommits.org"><img alt="Conventional Commits" src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-informational.svg?style=for-the-badge"></a>
</p>
</p>
# git-sv
A command line tool (CLI) to validate commit messages, bump version, create tags and generate changelogs.
## Getting Started
@ -23,23 +11,23 @@
### Installing
- Download the latest release and add the binary to your path.
- Optional: Set `SV4GIT_HOME` to define user configs. Check the [Config](#config) topic for more information.
- Optional: Set `GITSV_HOME` to define user configs. Check the [Config](#config) topic for more information.
If you want to install from source using `go install`, just run:
```bash
# keep in mind that with this, it will compile from source and won't show the version on cli -h.
go install github.com/bvieira/sv4git/v2/cmd/git-sv@latest
go install github.com/thegeeklab/git-sv/v2/cmd/git-sv@latest
# if you want to add the version on the binary, run this command instead.
SV4GIT_VERSION=$(go list -f '{{ .Version }}' -m github.com/bvieira/sv4git/v2@latest | sed 's/v//') && go install --ldflags "-X main.Version=$SV4GIT_VERSION" github.com/bvieira/sv4git/v2/cmd/git-sv@v$SV4GIT_VERSION
GITSV_VERSION=$(go list -f '{{ .Version }}' -m github.com/thegeeklab/git-sv/v2@latest | sed 's/v//') && go install --ldflags "-X main.Version=$SGITSV_VERSION" github.com/thegeeklab/git-sv/v2/cmd/git-sv@v$GITSV_VERSION
```
### Config
#### YAML
There are 3 config levels when using sv4git: [default](#default), [user](#user), [repository](#repository). All of them are merged considering the follow priority: **repository > user > default**.
There are 3 config levels when using git-sv: [default](#default), [user](#user), [repository](#repository). All of them are merged considering the follow priority: **repository > user > default**.
To see the current config, run:
@ -59,22 +47,22 @@ git sv cfg default
###### User
For user config, it is necessary to define the `SV4GIT_HOME` environment variable, eg.:
For user config, it is necessary to define the `GITSV_HOME` environment variable, eg.:
```bash
SV4GIT_HOME=/home/myuser/.sv4git # myuser is just an example.
GITSV_HOME=/home/myuser/.gitsv # myuser is just an example.
```
And create a `config.yml` file inside it, eg.:
```bash
.sv4git
.gitsv
└── config.yml
```
###### Repository
Create a `.sv4git.yml` file on the root of your repository, eg.: [.sv4git.yml](.sv4git.yml).
Create a `.gitsv/config.yml` file on the root of your repository, eg. [.gitsv/config.yml](.gitsv/config.yml).
##### Configuration format
@ -82,73 +70,85 @@ Create a `.sv4git.yml` file on the root of your repository, eg.: [.sv4git.yml](.
version: "1.1" #config version
versioning: # versioning bump
update-major: [] # Commit types used to bump major.
update-minor: [feat] # Commit types used to bump minor.
update-patch: [build, ci, chore, fix, perf, refactor, test] # Commit types used to bump patch.
# When type is not present on update rules and is unknown (not mapped on commit message types);
# if ignore-unknown=false bump patch, if ignore-unknown=true do not bump version
ignore-unknown: false
update-major: [] # Commit types used to bump major.
update-minor: [feat] # Commit types used to bump minor.
update-patch: [build, ci, chore, fix, perf, refactor, test] # Commit types used to bump patch.
# When type is not present on update rules and is unknown (not mapped on commit message types);
# if ignore-unknown=false bump patch, if ignore-unknown=true do not bump version
ignore-unknown: false
tag:
pattern: '%d.%d.%d' # Pattern used to create git tag.
filter: '' # Enables you to filter for considerable tags using git pattern syntax
pattern: "%d.%d.%d" # Pattern used to create git tag.
filter: "" # Enables you to filter for considerable tags using git pattern syntax
release-notes:
# Deprecated!!! please use 'sections' instead!
# Headers names for release notes markdown. To disable a section just remove the header
# line. It's possible to add other commit types, the release note will be created
# respecting the following order: feat, fix, refactor, perf, test, build, ci, chore, docs, style, breaking-change.
headers:
breaking-change: Breaking Changes
feat: Features
fix: Bug Fixes
sections: # Array with each section of release note. Check template section for more information.
- name: Features # Name used on section.
section-type: commits # Type of the section, supported types: commits, breaking-changes.
commit-types: [feat] # Commit types for commit section-type, one commit type cannot be in more than one section.
- name: Bug Fixes
section-type: commits
commit-types: [fix]
- name: Breaking Changes
section-type: breaking-changes
# Deprecated!!! please use 'sections' instead!
# Headers names for release notes markdown. To disable a section just remove the header
# line. It's possible to add other commit types, the release note will be created
# respecting the following order: feat, fix, refactor, perf, test, build, ci, chore, docs, style, breaking-change.
headers:
breaking-change: Breaking Changes
feat: Features
fix: Bug Fixes
sections: # Array with each section of release note. Check template section for more information.
- name: Features # Name used on section.
section-type: commits # Type of the section, supported types: commits, breaking-changes.
commit-types: [feat] # Commit types for commit section-type, one commit type cannot be in more than one section.
- name: Bug Fixes
section-type: commits
commit-types: [fix]
- name: Breaking Changes
section-type: breaking-changes
branches: # Git branches config.
prefix: ([a-z]+\/)? # Prefix used on branch name, it should be a regex group.
suffix: (-.*)? # Suffix used on branch name, it should be a regex group.
disable-issue: false # Set true if there is no need to recover issue id from branch name.
skip: [master, main, developer] # List of branch names ignored on commit message validation.
skip-detached: false # Set true if a detached branch should be ignored on commit message validation.
prefix: ([a-z]+\/)? # Prefix used on branch name, it should be a regex group.
suffix: (-.*)? # Suffix used on branch name, it should be a regex group.
disable-issue: false # Set true if there is no need to recover issue id from branch name.
skip: [master, main, developer] # List of branch names ignored on commit message validation.
skip-detached: false # Set true if a detached branch should be ignored on commit message validation.
commit-message:
types: [build, ci, chore, docs, feat, fix, perf, refactor, revert, style, test] # Supported commit types.
header-selector: '' # You can put in a regex here to select only a certain part of the commit message. Please define a regex group 'header'.
scope:
# Define supported scopes, if blank, scope will not be validated, if not, only scope listed will be valid.
# Don't forget to add "" on your list if you need to define scopes and keep it optional.
values: []
footer:
issue: # Use "issue: {}" if you wish to disable issue footer.
key: jira # Name used to define an issue on footer metadata.
key-synonyms: [Jira, JIRA] # Supported variations for footer metadata.
use-hash: false # If false, use :<space> separator. If true, use <space># separator.
add-value-prefix: '' # Add a prefix to issue value.
issue:
regex: '[A-Z]+-[0-9]+' # Regex for issue id.
types: [
build,
ci,
chore,
docs,
feat,
fix,
perf,
refactor,
revert,
style,
test,
] # Supported commit types.
header-selector: "" # You can put in a regex here to select only a certain part of the commit message. Please define a regex group 'header'.
scope:
# Define supported scopes, if blank, scope will not be validated, if not, only scope listed will be valid.
# Don't forget to add "" on your list if you need to define scopes and keep it optional.
values: []
footer:
issue: # Use "issue: {}" if you wish to disable issue footer.
key: jira # Name used to define an issue on footer metadata.
key-synonyms: [Jira, JIRA] # Supported variations for footer metadata.
use-hash: false # If false, use :<space> separator. If true, use <space># separator.
add-value-prefix: "" # Add a prefix to issue value.
issue:
regex: "[A-Z]+-[0-9]+" # Regex for issue id.
```
#### Templates
**sv4git** uses *go templates* to format the output for `release-notes` and `changelog`, to see how the default template is configured check [template directory](cmd/git-sv/resources/templates). On v2.7.0+, its possible to overwrite the default configuration by adding `.sv4git/templates` on your repository. The cli expects that at least 2 files exists on your directory: `changelog-md.tpl` and `releasenotes-md.tpl`.
**git-sv** uses _go templates_ to format the output for `release-notes` and `changelog`, to see how the default template is configured check [template directory](cmd/git-sv/resources/templates). It's possible to overwrite the default configuration by adding `.gitsv/templates` on your repository. The cli expects that at least 2 files exists on your directory: `changelog-md.tpl` and `releasenotes-md.tpl`.
```bash
.sv4git
.gitsv
└── templates
├── changelog-md.tpl
└── releasenotes-md.tpl
```
Everything inside `.sv4git/templates` will be loaded, so it's possible to add more files to be used as needed.
Everything inside `.gitsv/templates` will be loaded, so it's possible to add more files to be used as needed.
##### Variables
@ -156,9 +156,9 @@ To execute the template the `releasenotes-md.tpl` will receive a single **Releas
Each **ReleaseNoteSection** will be configured according with `release-notes.section` from config file. The order for each section will be maintained and the **SectionType** is defined according with `section-type` attribute as described on the table below.
| section-type | ReleaseNoteSection |
| -- | -- |
| commits | ReleaseNoteCommitsSection |
| section-type | ReleaseNoteSection |
| ---------------- | -------------------------------- |
| commits | ReleaseNoteCommitsSection |
| breaking-changes | ReleaseNoteBreakingChangeSection |
> :warning: currently only `commits` and `breaking-changes` are supported as `section-types`, using a different value for this field will make the section to be removed from the template variables.

View File

@ -4,27 +4,27 @@ import (
"fmt"
"log"
"os"
"os/exec"
"reflect"
"strings"
"github.com/bvieira/sv4git/v2/sv"
"github.com/imdario/mergo"
"dario.cat/mergo"
"github.com/kelseyhightower/envconfig"
"github.com/thegeeklab/git-sv/v2/sv"
"gopkg.in/yaml.v3"
)
// EnvConfig env vars for cli configuration.
type EnvConfig struct {
Home string `envconfig:"SV4GIT_HOME" default:""`
Home string `envconfig:"GITSV_HOME" default:""`
}
func loadEnvConfig() EnvConfig {
var c EnvConfig
err := envconfig.Process("", &c)
if err != nil {
log.Fatal("failed to load env config, error: ", err.Error())
}
return c
}
@ -38,20 +38,6 @@ type Config struct {
CommitMessage sv.CommitMessageConfig `yaml:"commit-message"`
}
func getRepoPath() (string, error) {
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
out, err := cmd.CombinedOutput()
if err != nil {
return "", combinedOutputErr(err, out)
}
return strings.TrimSpace(string(out)), nil
}
func combinedOutputErr(err error, out []byte) error {
msg := strings.Split(string(out), "\n")
return fmt.Errorf("%v - %s", err, msg[0])
}
func readConfig(filepath string) (Config, error) {
content, rerr := os.ReadFile(filepath)
if rerr != nil {
@ -59,9 +45,10 @@ func readConfig(filepath string) (Config, error) {
}
var cfg Config
cerr := yaml.Unmarshal(content, &cfg)
if cerr != nil {
return Config{}, fmt.Errorf("could not parse config from path: %s, error: %v", filepath, cerr)
return Config{}, fmt.Errorf("could not parse config from path: %s, error: %w", filepath, cerr)
}
return cfg, nil
@ -71,6 +58,7 @@ func defaultConfig() Config {
skipDetached := false
pattern := "%d.%d.%d"
filter := ""
return Config{
Version: "1.1",
Versioning: sv.VersioningConfig{
@ -116,6 +104,7 @@ func merge(dst *Config, src Config) error {
dst.ReleaseNotes.Headers = src.ReleaseNotes.Headers
}
}
return err
}
@ -127,6 +116,7 @@ func (t *mergeTransformer) Transformer(typ reflect.Type) func(dst, src reflect.V
if dst.CanSet() && !src.IsNil() {
dst.Set(src)
}
return nil
}
}
@ -136,9 +126,11 @@ func (t *mergeTransformer) Transformer(typ reflect.Type) func(dst, src reflect.V
if dst.CanSet() && !src.IsNil() {
dst.Set(src)
}
return nil
}
}
return nil
}
@ -146,6 +138,7 @@ func migrateConfig(cfg Config, filename string) Config {
if cfg.ReleaseNotes.Headers == nil {
return cfg
}
warnf("config 'release-notes.headers' on %s is deprecated, please use 'sections' instead!", filename)
return Config{
@ -162,14 +155,29 @@ func migrateConfig(cfg Config, filename string) Config {
func migrateReleaseNotesConfig(headers map[string]string) []sv.ReleaseNotesSectionConfig {
order := []string{"feat", "fix", "refactor", "perf", "test", "build", "ci", "chore", "docs", "style"}
var sections []sv.ReleaseNotesSectionConfig
for _, key := range order {
if name, exists := headers[key]; exists {
sections = append(sections, sv.ReleaseNotesSectionConfig{Name: name, SectionType: sv.ReleaseNotesSectionTypeCommits, CommitTypes: []string{key}})
sections = append(
sections,
sv.ReleaseNotesSectionConfig{
Name: name,
SectionType: sv.ReleaseNotesSectionTypeCommits,
CommitTypes: []string{key},
})
}
}
if name, exists := headers["breaking-change"]; exists {
sections = append(sections, sv.ReleaseNotesSectionConfig{Name: name, SectionType: sv.ReleaseNotesSectionTypeBreakingChanges})
sections = append(
sections,
sv.ReleaseNotesSectionConfig{
Name: name,
SectionType: sv.ReleaseNotesSectionTypeBreakingChanges,
})
}
return sections
}

View File

@ -4,7 +4,7 @@ import (
"reflect"
"testing"
"github.com/bvieira/sv4git/v2/sv"
"github.com/thegeeklab/git-sv/v2/sv"
)
func Test_merge(t *testing.T) {
@ -20,24 +20,135 @@ func Test_merge(t *testing.T) {
want Config
wantErr bool
}{
{"overwrite string", Config{Version: "a"}, Config{Version: "b"}, Config{Version: "b"}, false},
{"default string", Config{Version: "a"}, Config{Version: ""}, Config{Version: "a"}, false},
{
"overwrite string",
Config{Version: "a"},
Config{Version: "b"},
Config{Version: "b"},
false,
},
{
"default string",
Config{Version: "a"},
Config{Version: ""},
Config{Version: "a"},
false,
},
{
"overwrite list",
Config{Branches: sv.BranchesConfig{Skip: []string{"a", "b"}}},
Config{Branches: sv.BranchesConfig{Skip: []string{"c", "d"}}},
Config{Branches: sv.BranchesConfig{Skip: []string{"c", "d"}}},
false,
},
{
"overwrite list with empty",
Config{Branches: sv.BranchesConfig{Skip: []string{"a", "b"}}},
Config{Branches: sv.BranchesConfig{Skip: make([]string, 0)}},
Config{Branches: sv.BranchesConfig{Skip: make([]string, 0)}},
false,
},
{
"default list",
Config{Branches: sv.BranchesConfig{Skip: []string{"a", "b"}}},
Config{Branches: sv.BranchesConfig{Skip: nil}},
Config{Branches: sv.BranchesConfig{Skip: []string{"a", "b"}}},
false,
},
{"overwrite list", Config{Branches: sv.BranchesConfig{Skip: []string{"a", "b"}}}, Config{Branches: sv.BranchesConfig{Skip: []string{"c", "d"}}}, Config{Branches: sv.BranchesConfig{Skip: []string{"c", "d"}}}, false},
{"overwrite list with empty", Config{Branches: sv.BranchesConfig{Skip: []string{"a", "b"}}}, Config{Branches: sv.BranchesConfig{Skip: make([]string, 0)}}, Config{Branches: sv.BranchesConfig{Skip: make([]string, 0)}}, false},
{"default list", Config{Branches: sv.BranchesConfig{Skip: []string{"a", "b"}}}, Config{Branches: sv.BranchesConfig{Skip: nil}}, Config{Branches: sv.BranchesConfig{Skip: []string{"a", "b"}}}, false},
{"overwrite pointer bool false", Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}}, false},
{"overwrite pointer bool true", Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}}, false},
{"default pointer bool", Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}}, Config{Branches: sv.BranchesConfig{SkipDetached: nil}}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}}, false},
{"merge maps", Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}}}}, Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{"issue2": {Key: "jira2"}}}}, Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}, "issue2": {Key: "jira2"}}}}, false},
{"default maps", Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}}}}, Config{CommitMessage: sv.CommitMessageConfig{Footer: nil}}, Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}}}}, false},
{"merge empty maps", Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}}}}, Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{}}}, Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}}}}, false},
{"overwrite release notes header", Config{ReleaseNotes: sv.ReleaseNotesConfig{Headers: map[string]string{"a": "aa"}}}, Config{ReleaseNotes: sv.ReleaseNotesConfig{Headers: map[string]string{"b": "bb"}}}, Config{ReleaseNotes: sv.ReleaseNotesConfig{Headers: map[string]string{"b": "bb"}}}, false},
{"overwrite tag config", Config{Version: "a", Tag: sv.TagConfig{Pattern: &nonEmptyStr, Filter: &nonEmptyStr}}, Config{Version: "", Tag: sv.TagConfig{Pattern: &emptyStr, Filter: &emptyStr}}, Config{Version: "a", Tag: sv.TagConfig{Pattern: &emptyStr, Filter: &emptyStr}}, false},
{
"overwrite pointer bool false",
Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}},
Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}},
Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}},
false,
},
{
"overwrite pointer bool true",
Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}},
Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}},
Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}},
false,
},
{
"default pointer bool",
Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}},
Config{Branches: sv.BranchesConfig{SkipDetached: nil}},
Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}},
false,
},
{
"merge maps",
Config{CommitMessage: sv.CommitMessageConfig{
Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}},
}},
Config{CommitMessage: sv.CommitMessageConfig{
Footer: map[string]sv.CommitMessageFooterConfig{"issue2": {Key: "jira2"}},
}},
Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{
"issue": {Key: "jira"},
"issue2": {Key: "jira2"},
}}},
false,
},
{
"default maps",
Config{CommitMessage: sv.CommitMessageConfig{
Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}},
}},
Config{CommitMessage: sv.CommitMessageConfig{
Footer: nil,
}},
Config{CommitMessage: sv.CommitMessageConfig{
Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}},
}},
false,
},
{
"merge empty maps",
Config{CommitMessage: sv.CommitMessageConfig{
Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}},
}},
Config{CommitMessage: sv.CommitMessageConfig{
Footer: map[string]sv.CommitMessageFooterConfig{},
}},
Config{CommitMessage: sv.CommitMessageConfig{
Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}},
}},
false,
},
{
"overwrite release notes header",
Config{ReleaseNotes: sv.ReleaseNotesConfig{Headers: map[string]string{"a": "aa"}}},
Config{ReleaseNotes: sv.ReleaseNotesConfig{Headers: map[string]string{"b": "bb"}}},
Config{ReleaseNotes: sv.ReleaseNotesConfig{Headers: map[string]string{"b": "bb"}}},
false,
},
{
"overwrite tag config",
Config{
Version: "a",
Tag: sv.TagConfig{
Pattern: &nonEmptyStr,
Filter: &nonEmptyStr,
},
},
Config{
Version: "",
Tag: sv.TagConfig{
Pattern: &emptyStr,
Filter: &emptyStr,
},
},
Config{
Version: "a",
Tag: sv.TagConfig{
Pattern: &emptyStr,
Filter: &emptyStr,
},
},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -2,6 +2,7 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
@ -10,19 +11,32 @@ import (
"time"
"github.com/Masterminds/semver/v3"
"github.com/bvieira/sv4git/v2/sv"
"github.com/thegeeklab/git-sv/v2/sv"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v3"
)
const laxFilePerm = 0o644
var (
errCanNotCreateTagFlag = errors.New("cannot define tag flag with range, start or end flags")
errUnknownTag = errors.New("unknown tag")
errReadCommitMessage = errors.New("failed to read commit message")
errAppendFooter = errors.New("failed to append meta-informations on footer")
errInvalidRange = errors.New("invalid log range")
)
func configDefaultHandler() func(c *cli.Context) error {
cfg := defaultConfig()
return func(c *cli.Context) error {
content, err := yaml.Marshal(&cfg)
if err != nil {
return err
}
fmt.Println(string(content))
return nil
}
}
@ -33,7 +47,9 @@ func configShowHandler(cfg Config) func(c *cli.Context) error {
if err != nil {
return err
}
fmt.Println(string(content))
return nil
}
}
@ -44,9 +60,11 @@ func currentVersionHandler(git sv.Git) func(c *cli.Context) error {
currentVer, err := sv.ToVersion(lastTag)
if err != nil {
return fmt.Errorf("error parsing version: %s from git tag, message: %v", lastTag, err)
return fmt.Errorf("error parsing version: %s from git tag, message: %w", lastTag, err)
}
fmt.Printf("%d.%d.%d\n", currentVer.Major(), currentVer.Minor(), currentVer.Patch())
return nil
}
}
@ -57,30 +75,62 @@ func nextVersionHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) f
currentVer, err := sv.ToVersion(lastTag)
if err != nil {
return fmt.Errorf("error parsing version: %s from git tag, message: %v", lastTag, err)
return fmt.Errorf("error parsing version: %s from git tag, message: %w", lastTag, err)
}
commits, err := git.Log(sv.NewLogRange(sv.TagRange, lastTag, ""))
if err != nil {
return fmt.Errorf("error getting git log, message: %v", err)
return fmt.Errorf("error getting git log, message: %w", err)
}
nextVer, _ := semverProcessor.NextVersion(currentVer, commits)
fmt.Printf("%d.%d.%d\n", nextVer.Major(), nextVer.Minor(), nextVer.Patch())
return nil
}
}
func commitLogFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "t",
Aliases: []string{"tag"},
Usage: "get commit log from a specific tag",
},
&cli.StringFlag{
Name: "r",
Aliases: []string{"range"},
Usage: "type of range of commits, use: tag, date or hash",
Value: string(sv.TagRange),
},
&cli.StringFlag{
Name: "s",
Aliases: []string{"start"},
Usage: "start range of git log revision range, if date, the value is used on since flag instead",
},
&cli.StringFlag{
Name: "e",
Aliases: []string{"end"},
Usage: "end range of git log revision range, if date, the value is used on until flag instead",
},
}
}
func commitLogHandler(git sv.Git) func(c *cli.Context) error {
return func(c *cli.Context) error {
var commits []sv.GitCommitLog
var err error
var (
commits []sv.GitCommitLog
err error
)
tagFlag := c.String("t")
rangeFlag := c.String("r")
startFlag := c.String("s")
endFlag := c.String("e")
if tagFlag != "" && (rangeFlag != string(sv.TagRange) || startFlag != "" || endFlag != "") {
return fmt.Errorf("cannot define tag flag with range, start or end flags")
return errCanNotCreateTagFlag
}
if tagFlag != "" {
@ -92,8 +142,9 @@ func commitLogHandler(git sv.Git) func(c *cli.Context) error {
}
commits, err = git.Log(r)
}
if err != nil {
return fmt.Errorf("error getting git log, message: %v", err)
return fmt.Errorf("error getting git log, message: %w", err)
}
for _, commit := range commits {
@ -101,8 +152,10 @@ func commitLogHandler(git sv.Git) func(c *cli.Context) error {
if err != nil {
return err
}
fmt.Println(string(content))
}
return nil
}
}
@ -112,6 +165,7 @@ func getTagCommits(git sv.Git, tag string) ([]sv.GitCommitLog, error) {
if err != nil {
return nil, err
}
return git.Log(sv.NewLogRange(sv.TagRange, prev, tag))
}
@ -124,15 +178,45 @@ func logRange(git sv.Git, rangeFlag, startFlag, endFlag string) (sv.LogRange, er
case string(sv.HashRange):
return sv.NewLogRange(sv.HashRange, startFlag, endFlag), nil
default:
return sv.LogRange{}, fmt.Errorf("invalid range: %s, expected: %s, %s or %s", rangeFlag, sv.TagRange, sv.DateRange, sv.HashRange)
return sv.LogRange{}, fmt.Errorf(
"%w: %s, expected: %s, %s or %s",
errInvalidRange,
rangeFlag,
sv.TagRange,
sv.DateRange,
sv.HashRange,
)
}
}
func commitNotesHandler(git sv.Git, rnProcessor sv.ReleaseNoteProcessor, outputFormatter sv.OutputFormatter) func(c *cli.Context) error {
func commitNotesFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "r", Aliases: []string{"range"},
Usage: "type of range of commits, use: tag, date or hash",
Required: true,
},
&cli.StringFlag{
Name: "s",
Aliases: []string{"start"},
Usage: "start range of git log revision range, if date, the value is used on since flag instead",
},
&cli.StringFlag{
Name: "e",
Aliases: []string{"end"},
Usage: "end range of git log revision range, if date, the value is used on until flag instead",
},
}
}
func commitNotesHandler(
git sv.Git, rnProcessor sv.ReleaseNoteProcessor, outputFormatter sv.OutputFormatter,
) func(c *cli.Context) error {
return func(c *cli.Context) error {
var date time.Time
rangeFlag := c.String("r")
lr, err := logRange(git, rangeFlag, c.String("s"), c.String("e"))
if err != nil {
return err
@ -140,7 +224,7 @@ func commitNotesHandler(git sv.Git, rnProcessor sv.ReleaseNoteProcessor, outputF
commits, err := git.Log(lr)
if err != nil {
return fmt.Errorf("error getting git log from range: %s, message: %v", rangeFlag, err)
return fmt.Errorf("error getting git log from range: %s, message: %w", rangeFlag, err)
}
if len(commits) > 0 {
@ -149,20 +233,39 @@ func commitNotesHandler(git sv.Git, rnProcessor sv.ReleaseNoteProcessor, outputF
output, err := outputFormatter.FormatReleaseNote(rnProcessor.Create(nil, "", date, commits))
if err != nil {
return fmt.Errorf("could not format release notes, message: %v", err)
return fmt.Errorf("could not format release notes, message: %w", err)
}
fmt.Println(output)
return nil
}
}
func releaseNotesHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnProcessor sv.ReleaseNoteProcessor, outputFormatter sv.OutputFormatter) func(c *cli.Context) error {
func releaseNotesFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "t",
Aliases: []string{"tag"},
Usage: "get release note from tag",
},
}
}
func releaseNotesHandler(
git sv.Git,
semverProcessor sv.SemVerCommitsProcessor,
rnProcessor sv.ReleaseNoteProcessor,
outputFormatter sv.OutputFormatter,
) func(c *cli.Context) error {
return func(c *cli.Context) error {
var commits []sv.GitCommitLog
var rnVersion *semver.Version
var tag string
var date time.Time
var err error
var (
commits []sv.GitCommitLog
rnVersion *semver.Version
tag string
date time.Time
err error
)
if tag = c.String("t"); tag != "" {
rnVersion, date, commits, err = getTagVersionInfo(git, tag)
@ -176,11 +279,14 @@ func releaseNotesHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor,
}
releasenote := rnProcessor.Create(rnVersion, tag, date, commits)
output, err := outputFormatter.FormatReleaseNote(releasenote)
if err != nil {
return fmt.Errorf("could not format release notes, message: %v", err)
return fmt.Errorf("could not format release notes, message: %w", err)
}
fmt.Println(output)
return nil
}
}
@ -190,12 +296,12 @@ func getTagVersionInfo(git sv.Git, tag string) (*semver.Version, time.Time, []sv
previousTag, currentTag, err := getTags(git, tag)
if err != nil {
return nil, time.Time{}, nil, fmt.Errorf("error listing tags, message: %v", err)
return nil, time.Time{}, nil, fmt.Errorf("error listing tags, message: %w", err)
}
commits, err := git.Log(sv.NewLogRange(sv.TagRange, previousTag, tag))
if err != nil {
return nil, time.Time{}, nil, fmt.Errorf("error getting git log from tag: %s, message: %v", tag, err)
return nil, time.Time{}, nil, fmt.Errorf("error getting git log from tag: %s, message: %w", tag, err)
}
return tagVersion, currentTag.Date, commits, nil
@ -209,13 +315,14 @@ func getTags(git sv.Git, tag string) (string, sv.GitTag, error) {
index := find(tag, tags)
if index < 0 {
return "", sv.GitTag{}, fmt.Errorf("tag: %s not found, check tag filter", tag)
return "", sv.GitTag{}, fmt.Errorf("%w: %s not found, check tag filter", errUnknownTag, tag)
}
previousTag := ""
if index > 0 {
previousTag = tags[index-1].Name
}
return previousTag, tags[index], nil
}
@ -225,15 +332,18 @@ func find(tag string, tags []sv.GitTag) int {
return i
}
}
return -1
}
func getNextVersionInfo(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) (*semver.Version, bool, time.Time, []sv.GitCommitLog, error) {
func getNextVersionInfo(
git sv.Git, semverProcessor sv.SemVerCommitsProcessor,
) (*semver.Version, bool, time.Time, []sv.GitCommitLog, error) {
lastTag := git.LastTag()
commits, err := git.Log(sv.NewLogRange(sv.TagRange, lastTag, ""))
if err != nil {
return nil, false, time.Time{}, nil, fmt.Errorf("error getting git log, message: %v", err)
return nil, false, time.Time{}, nil, fmt.Errorf("error getting git log, message: %w", err)
}
currentVer, _ := sv.ToVersion(lastTag)
@ -248,20 +358,23 @@ func tagHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) func(c *c
currentVer, err := sv.ToVersion(lastTag)
if err != nil {
return fmt.Errorf("error parsing version: %s from git tag, message: %v", lastTag, err)
return fmt.Errorf("error parsing version: %s from git tag, message: %w", lastTag, err)
}
commits, err := git.Log(sv.NewLogRange(sv.TagRange, lastTag, ""))
if err != nil {
return fmt.Errorf("error getting git log, message: %v", err)
return fmt.Errorf("error getting git log, message: %w", err)
}
nextVer, _ := semverProcessor.NextVersion(currentVer, commits)
tagname, err := git.Tag(*nextVer)
fmt.Println(tagname)
if err != nil {
return fmt.Errorf("error generating tag version: %s, message: %v", nextVer.String(), err)
return fmt.Errorf("error generating tag version: %s, message: %w", nextVer.String(), err)
}
return nil
}
}
@ -269,8 +382,10 @@ func tagHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) func(c *c
func getCommitType(cfg Config, p sv.MessageProcessor, input string) (string, error) {
if input == "" {
t, err := promptType(cfg.CommitMessage.Types)
return t.Type, err
}
return input, p.ValidateType(input)
}
@ -278,6 +393,7 @@ func getCommitScope(cfg Config, p sv.MessageProcessor, input string, noScope boo
if input == "" && !noScope {
return promptScope(cfg.CommitMessage.Scope.Values)
}
return input, p.ValidateScope(input)
}
@ -285,6 +401,7 @@ func getCommitDescription(p sv.MessageProcessor, input string) (string, error) {
if input == "" {
return promptSubject()
}
return input, p.ValidateDescription(input)
}
@ -294,17 +411,21 @@ func getCommitBody(noBody bool) (string, error) {
}
var fullBody strings.Builder
for body, err := promptBody(); body != "" || err != nil; body, err = promptBody() {
if err != nil {
return "", err
}
if fullBody.Len() > 0 {
fullBody.WriteString("\n")
}
if body != "" {
fullBody.WriteString(body)
}
}
return fullBody.String(), nil
}
@ -338,6 +459,7 @@ func getCommitBreakingChange(noBreaking bool, input string) (string, error) {
if err != nil {
return "", err
}
if !hasBreakingChanges {
return "", nil
}
@ -345,6 +467,51 @@ func getCommitBreakingChange(noBreaking bool, input string) (string, error) {
return promptBreakingChanges()
}
func commitFlags() []cli.Flag {
return []cli.Flag{
&cli.BoolFlag{
Name: "no-scope",
Aliases: []string{"nsc"},
Usage: "do not prompt for commit scope",
},
&cli.BoolFlag{
Name: "no-body",
Aliases: []string{"nbd"},
Usage: "do not prompt for commit body",
},
&cli.BoolFlag{
Name: "no-issue",
Aliases: []string{"nis"},
Usage: "do not prompt for commit issue, will try to recover from branch if enabled",
},
&cli.BoolFlag{
Name: "no-breaking",
Aliases: []string{"nbc"},
Usage: "do not prompt for breaking changes",
},
&cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Usage: "define commit type",
},
&cli.StringFlag{
Name: "scope",
Aliases: []string{"s"},
Usage: "define commit scope",
},
&cli.StringFlag{
Name: "description",
Aliases: []string{"d"},
Usage: "define commit description",
},
&cli.StringFlag{
Name: "breaking-change",
Aliases: []string{"b"},
Usage: "define commit breaking change message",
},
}
}
func commitHandler(cfg Config, git sv.Git, messageProcessor sv.MessageProcessor) func(c *cli.Context) error {
return func(c *cli.Context) error {
noBreaking := c.Bool("no-breaking")
@ -386,22 +553,54 @@ func commitHandler(cfg Config, git sv.Git, messageProcessor sv.MessageProcessor)
return err
}
header, body, footer := messageProcessor.Format(sv.NewCommitMessage(ctype, scope, subject, fullBody, issue, breakingChange))
header, body, footer := messageProcessor.Format(
sv.NewCommitMessage(ctype, scope, subject, fullBody, issue, breakingChange),
)
err = git.Commit(header, body, footer)
if err != nil {
return fmt.Errorf("error executing git commit, message: %v", err)
return fmt.Errorf("error executing git commit, message: %w", err)
}
return nil
}
}
func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnProcessor sv.ReleaseNoteProcessor, formatter sv.OutputFormatter) func(c *cli.Context) error {
func changelogFlags() []cli.Flag {
return []cli.Flag{
&cli.IntFlag{
Name: "size",
Value: 10, //nolint:gomnd
Aliases: []string{"n"},
Usage: "get changelog from last 'n' tags",
},
&cli.BoolFlag{
Name: "all",
Usage: "ignore size parameter, get changelog for every tag",
},
&cli.BoolFlag{
Name: "add-next-version",
Usage: "add next version on change log (commits since last tag, but only if there is a new version to release)",
},
&cli.BoolFlag{
Name: "semantic-version-only",
Usage: "only show tags 'SemVer-ish'",
},
}
}
func changelogHandler(
git sv.Git,
semverProcessor sv.SemVerCommitsProcessor,
rnProcessor sv.ReleaseNoteProcessor,
formatter sv.OutputFormatter,
) func(c *cli.Context) error {
return func(c *cli.Context) error {
tags, err := git.Tags()
if err != nil {
return err
}
sort.Slice(tags, func(i, j int) bool {
return tags[i].Date.After(tags[j].Date)
})
@ -418,10 +617,12 @@ func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnP
if uerr != nil {
return uerr
}
if updated {
releaseNotes = append(releaseNotes, rnProcessor.Create(rnVersion, "", date, commits))
}
}
for i, tag := range tags {
if !all && i >= size {
break
@ -438,7 +639,7 @@ func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnP
commits, err := git.Log(sv.NewLogRange(sv.TagRange, previousTag, tag.Name))
if err != nil {
return fmt.Errorf("error getting git log from tag: %s, message: %v", tag.Name, err)
return fmt.Errorf("error getting git log from tag: %s, message: %w", tag.Name, err)
}
currentVer, _ := sv.ToVersion(tag.Name)
@ -447,14 +648,35 @@ func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnP
output, err := formatter.FormatChangelog(releaseNotes)
if err != nil {
return fmt.Errorf("could not format changelog, message: %v", err)
return fmt.Errorf("could not format changelog, message: %w", err)
}
fmt.Println(output)
return nil
}
}
func validateCommitMessageFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "path",
Required: true,
Usage: "git working directory",
},
&cli.StringFlag{
Name: "file",
Required: true,
Usage: "name of the file that contains the commit log message",
},
&cli.StringFlag{
Name: "source",
Required: true,
Usage: "source of the commit message",
},
}
}
func validateCommitMessageHandler(git sv.Git, messageProcessor sv.MessageProcessor) func(c *cli.Context) error {
return func(c *cli.Context) error {
branch := git.Branch()
@ -462,11 +684,13 @@ func validateCommitMessageHandler(git sv.Git, messageProcessor sv.MessageProcess
if messageProcessor.SkipBranch(branch, derr == nil && detached) {
warnf("commit message validation skipped, branch in ignore list or detached...")
return nil
}
if source := c.String("source"); source == "merge" {
warnf("commit message validation skipped, ignoring source: %s...", source)
return nil
}
@ -474,24 +698,26 @@ func validateCommitMessageHandler(git sv.Git, messageProcessor sv.MessageProcess
commitMessage, err := readFile(filepath)
if err != nil {
return fmt.Errorf("failed to read commit message, error: %s", err.Error())
return fmt.Errorf("%w: %s", errReadCommitMessage, err.Error())
}
if err := messageProcessor.Validate(commitMessage); err != nil {
return fmt.Errorf("invalid commit message, error: %s", err.Error())
return fmt.Errorf("%w: %s", errReadCommitMessage, err.Error())
}
msg, err := messageProcessor.Enhance(branch, commitMessage)
if err != nil {
warnf("could not enhance commit message, %s", err.Error())
return nil
}
if msg == "" {
return nil
}
if err := appendOnFile(msg, filepath); err != nil {
return fmt.Errorf("failed to append meta-informations on footer, error: %s", err.Error())
return fmt.Errorf("%w: %s", errAppendFooter, err.Error())
}
return nil
@ -503,17 +729,19 @@ func readFile(filepath string) (string, error) {
if err != nil {
return "", err
}
return string(f), nil
}
func appendOnFile(message, filepath string) error {
f, err := os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY, 0644)
f, err := os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY, laxFilePerm)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(message)
return err
}
@ -521,5 +749,6 @@ func str(value, defaultValue string) string {
if value != "" {
return value
}
return defaultValue
}

View File

@ -2,161 +2,146 @@ package main
import (
"embed"
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"github.com/bvieira/sv4git/v2/sv"
"github.com/thegeeklab/git-sv/v2/sv"
"github.com/urfave/cli/v2"
)
// Version for git-sv.
var Version = "source"
//nolint:gochecknoglobals
var (
BuildVersion = "devel"
BuildDate = "00000000"
)
const (
configFilename = "config.yml"
repoConfigFilename = ".sv4git.yml"
configDir = ".sv4git"
configFilename = "config.yml"
configDir = ".gitsv"
)
var (
//go:embed resources/templates/*.tpl
defaultTemplatesFS embed.FS
)
//go:embed resources/templates/*.tpl
var defaultTemplatesFS embed.FS
func templateFS(filepath string) fs.FS {
if _, err := os.Stat(filepath); err != nil {
defaultTemplatesFS, _ := fs.Sub(defaultTemplatesFS, "resources/templates")
return defaultTemplatesFS
}
return os.DirFS(filepath)
}
func main() {
log.SetFlags(0)
repoPath, rerr := getRepoPath()
if rerr != nil {
log.Fatal("failed to discovery repository top level, error: ", rerr)
wd, err := os.Getwd()
if err != nil {
log.Fatal("error while retrieving working directory: %w", err)
}
cfg := loadCfg(repoPath)
cfg := loadCfg(wd)
messageProcessor := sv.NewMessageProcessor(cfg.CommitMessage, cfg.Branches)
git := sv.NewGit(messageProcessor, cfg.Tag)
semverProcessor := sv.NewSemVerCommitsProcessor(cfg.Versioning, cfg.CommitMessage)
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotes)
outputFormatter := sv.NewOutputFormatter(templateFS(filepath.Join(repoPath, configDir, "templates")))
outputFormatter := sv.NewOutputFormatter(templateFS(filepath.Join(wd, configDir, "templates")))
app := cli.NewApp()
app.Name = "sv"
app.Version = Version
app.Usage = "semantic version for git"
app.Commands = []*cli.Command{
{
Name: "config",
Aliases: []string{"cfg"},
Usage: "cli configuration",
Subcommands: []*cli.Command{
{
Name: "default",
Usage: "show default config",
Action: configDefaultHandler(),
},
{
Name: "show",
Usage: "show current config",
Action: configShowHandler(cfg),
cli.VersionPrinter = func(c *cli.Context) {
fmt.Printf("%s version=%s date=%s\n", c.App.Name, c.App.Version, BuildDate)
}
app := &cli.App{
Name: "git-sv",
Usage: "Semantic version for git.",
Version: BuildVersion,
Commands: []*cli.Command{
{
Name: "config",
Aliases: []string{"cfg"},
Usage: "cli configuration",
Subcommands: []*cli.Command{
{
Name: "default",
Usage: "show default config",
Action: configDefaultHandler(),
},
{
Name: "show",
Usage: "show current config",
Action: configShowHandler(cfg),
},
},
},
},
{
Name: "current-version",
Aliases: []string{"cv"},
Usage: "get last released version from git",
Action: currentVersionHandler(git),
},
{
Name: "next-version",
Aliases: []string{"nv"},
Usage: "generate the next version based on git commit messages",
Action: nextVersionHandler(git, semverProcessor),
},
{
Name: "commit-log",
Aliases: []string{"cl"},
Usage: "list all commit logs according to range as jsons",
Description: "The range filter is used based on git log filters, check https://git-scm.com/docs/git-log for more info. When flag range is \"tag\" and start is empty, last tag created will be used instead. When flag range is \"date\", if \"end\" is YYYY-MM-DD the range will be inclusive.",
Action: commitLogHandler(git),
Flags: []cli.Flag{
&cli.StringFlag{Name: "t", Aliases: []string{"tag"}, Usage: "get commit log from a specific tag"},
&cli.StringFlag{Name: "r", Aliases: []string{"range"}, Usage: "type of range of commits, use: tag, date or hash", Value: string(sv.TagRange)},
&cli.StringFlag{Name: "s", Aliases: []string{"start"}, Usage: "start range of git log revision range, if date, the value is used on since flag instead"},
&cli.StringFlag{Name: "e", Aliases: []string{"end"}, Usage: "end range of git log revision range, if date, the value is used on until flag instead"},
{
Name: "current-version",
Aliases: []string{"cv"},
Usage: "get last released version from git",
Action: currentVersionHandler(git),
},
},
{
Name: "commit-notes",
Aliases: []string{"cn"},
Usage: "generate a commit notes according to range",
Description: "The range filter is used based on git log filters, check https://git-scm.com/docs/git-log for more info. When flag range is \"tag\" and start is empty, last tag created will be used instead. When flag range is \"date\", if \"end\" is YYYY-MM-DD the range will be inclusive.",
Action: commitNotesHandler(git, releasenotesProcessor, outputFormatter),
Flags: []cli.Flag{
&cli.StringFlag{Name: "r", Aliases: []string{"range"}, Usage: "type of range of commits, use: tag, date or hash", Required: true},
&cli.StringFlag{Name: "s", Aliases: []string{"start"}, Usage: "start range of git log revision range, if date, the value is used on since flag instead"},
&cli.StringFlag{Name: "e", Aliases: []string{"end"}, Usage: "end range of git log revision range, if date, the value is used on until flag instead"},
{
Name: "next-version",
Aliases: []string{"nv"},
Usage: "generate the next version based on git commit messages",
Action: nextVersionHandler(git, semverProcessor),
},
},
{
Name: "release-notes",
Aliases: []string{"rn"},
Usage: "generate release notes",
Action: releaseNotesHandler(git, semverProcessor, releasenotesProcessor, outputFormatter),
Flags: []cli.Flag{&cli.StringFlag{Name: "t", Aliases: []string{"tag"}, Usage: "get release note from tag"}},
},
{
Name: "changelog",
Aliases: []string{"cgl"},
Usage: "generate changelog",
Action: changelogHandler(git, semverProcessor, releasenotesProcessor, outputFormatter),
Flags: []cli.Flag{
&cli.IntFlag{Name: "size", Value: 10, Aliases: []string{"n"}, Usage: "get changelog from last 'n' tags"},
&cli.BoolFlag{Name: "all", Usage: "ignore size parameter, get changelog for every tag"},
&cli.BoolFlag{Name: "add-next-version", Usage: "add next version on change log (commits since last tag, but only if there is a new version to release)"},
&cli.BoolFlag{Name: "semantic-version-only", Usage: "only show tags 'SemVer-ish'"},
{
Name: "commit-log",
Aliases: []string{"cl"},
Usage: "list all commit logs according to range as jsons",
Description: `The range filter is used based on git log filters, check https://git-scm.com/docs/git-log
for more info. When flag range is "tag" and start is empty, last tag created will be used instead.
When flag range is "date", if "end" is YYYY-MM-DD the range will be inclusive.`,
Action: commitLogHandler(git),
Flags: commitLogFlags(),
},
},
{
Name: "tag",
Aliases: []string{"tg"},
Usage: "generate tag with version based on git commit messages",
Action: tagHandler(git, semverProcessor),
},
{
Name: "commit",
Aliases: []string{"cmt"},
Usage: "execute git commit with convetional commit message helper",
Action: commitHandler(cfg, git, messageProcessor),
Flags: []cli.Flag{
&cli.BoolFlag{Name: "no-scope", Aliases: []string{"nsc"}, Usage: "do not prompt for commit scope"},
&cli.BoolFlag{Name: "no-body", Aliases: []string{"nbd"}, Usage: "do not prompt for commit body"},
&cli.BoolFlag{Name: "no-issue", Aliases: []string{"nis"}, Usage: "do not prompt for commit issue, will try to recover from branch if enabled"},
&cli.BoolFlag{Name: "no-breaking", Aliases: []string{"nbc"}, Usage: "do not prompt for breaking changes"},
&cli.StringFlag{Name: "type", Aliases: []string{"t"}, Usage: "define commit type"},
&cli.StringFlag{Name: "scope", Aliases: []string{"s"}, Usage: "define commit scope"},
&cli.StringFlag{Name: "description", Aliases: []string{"d"}, Usage: "define commit description"},
&cli.StringFlag{Name: "breaking-change", Aliases: []string{"b"}, Usage: "define commit breaking change message"},
{
Name: "commit-notes",
Aliases: []string{"cn"},
Usage: "generate a commit notes according to range",
Description: `The range filter is used based on git log filters, check https://git-scm.com/docs/git-log
for more info. When flag range is "tag" and start is empty, last tag created will be used instead.
When flag range is "date", if "end" is YYYY-MM-DD the range will be inclusive.`,
Action: commitNotesHandler(git, releasenotesProcessor, outputFormatter),
Flags: commitNotesFlags(),
},
},
{
Name: "validate-commit-message",
Aliases: []string{"vcm"},
Usage: "use as prepare-commit-message hook to validate and enhance commit message",
Action: validateCommitMessageHandler(git, messageProcessor),
Flags: []cli.Flag{
&cli.StringFlag{Name: "path", Required: true, Usage: "git working directory"},
&cli.StringFlag{Name: "file", Required: true, Usage: "name of the file that contains the commit log message"},
&cli.StringFlag{Name: "source", Required: true, Usage: "source of the commit message"},
{
Name: "release-notes",
Aliases: []string{"rn"},
Usage: "generate release notes",
Action: releaseNotesHandler(git, semverProcessor, releasenotesProcessor, outputFormatter),
Flags: releaseNotesFlags(),
},
{
Name: "changelog",
Aliases: []string{"cgl"},
Usage: "generate changelog",
Action: changelogHandler(git, semverProcessor, releasenotesProcessor, outputFormatter),
Flags: changelogFlags(),
},
{
Name: "tag",
Aliases: []string{"tg"},
Usage: "generate tag with version based on git commit messages",
Action: tagHandler(git, semverProcessor),
},
{
Name: "commit",
Aliases: []string{"cmt"},
Usage: "execute git commit with convetional commit message helper",
Action: commitHandler(cfg, git, messageProcessor),
Flags: commitFlags(),
},
{
Name: "validate-commit-message",
Aliases: []string{"vcm"},
Usage: "use as prepare-commit-message hook to validate and enhance commit message",
Action: validateCommitMessageHandler(git, messageProcessor),
Flags: validateCommitMessageFlags(),
},
},
}
@ -166,7 +151,7 @@ func main() {
}
}
func loadCfg(repoPath string) Config {
func loadCfg(wd string) Config {
cfg := defaultConfig()
envCfg := loadEnvConfig()
@ -179,11 +164,12 @@ func loadCfg(repoPath string) Config {
}
}
repoCfgFilepath := filepath.Join(repoPath, repoConfigFilename)
repoCfgFilepath := filepath.Join(wd, configDir, configFilename)
if repoCfg, err := readConfig(repoCfgFilepath); err == nil {
if merr := merge(&cfg, migrateConfig(repoCfg, repoCfgFilepath)); merr != nil {
log.Fatal("failed to merge repo config, error: ", merr)
}
if len(repoCfg.ReleaseNotes.Headers) > 0 { // mergo is merging maps, headers will be overwritten
cfg.ReleaseNotes.Headers = repoCfg.ReleaseNotes.Headers
}

View File

@ -1,6 +1,7 @@
package main
import (
"errors"
"fmt"
"reflect"
"regexp"
@ -14,22 +15,62 @@ type commitType struct {
Example string
}
var errInvalidValue = errors.New("invalid value")
func promptType(types []string) (commitType, error) {
defaultTypes := map[string]commitType{
"build": {Type: "build", Description: "changes that affect the build system or external dependencies", Example: "gradle, maven, go mod, npm"},
"ci": {Type: "ci", Description: "changes to our CI configuration files and scripts", Example: "Circle, BrowserStack, SauceLabs"},
"chore": {Type: "chore", Description: "update something without impacting the user", Example: "gitignore"},
"docs": {Type: "docs", Description: "documentation only changes"},
"feat": {Type: "feat", Description: "a new feature"},
"fix": {Type: "fix", Description: "a bug fix"},
"perf": {Type: "perf", Description: "a code change that improves performance"},
"refactor": {Type: "refactor", Description: "a code change that neither fixes a bug nor adds a feature"},
"style": {Type: "style", Description: "changes that do not affect the meaning of the code", Example: "white-space, formatting, missing semi-colons, etc"},
"test": {Type: "test", Description: "adding missing tests or correcting existing tests"},
"revert": {Type: "revert", Description: "revert a single commit"},
"build": {
Type: "build",
Description: "changes that affect the build system or external dependencies",
Example: "gradle, maven, go mod, npm",
},
"ci": {
Type: "ci",
Description: "changes to our CI configuration files and scripts",
Example: "Circle, BrowserStack, SauceLabs",
},
"chore": {
Type: "chore",
Description: "update something without impacting the user",
Example: "gitignore",
},
"docs": {
Type: "docs",
Description: "documentation only changes",
},
"feat": {
Type: "feat",
Description: "a new feature",
},
"fix": {
Type: "fix",
Description: "a bug fix",
},
"perf": {
Type: "perf",
Description: "a code change that improves performance",
},
"refactor": {
Type: "refactor",
Description: "a code change that neither fixes a bug nor adds a feature",
},
"style": {
Type: "style",
Description: "changes that do not affect the meaning of the code",
Example: "white-space, formatting, missing semi-colons, etc",
},
"test": {
Type: "test",
Description: "adding missing tests or correcting existing tests",
},
"revert": {
Type: "revert",
Description: "revert a single commit",
},
}
var items []commitType
for _, t := range types {
if v, exists := defaultTypes[t]; exists {
items = append(items, v)
@ -53,6 +94,7 @@ func promptType(types []string) (commitType, error) {
if err != nil {
return commitType{}, err
}
return items[i], nil
}
@ -62,8 +104,10 @@ func promptScope(values []string) (string, error) {
if err != nil {
return "", err
}
return values[selected], nil
}
return promptText("scope", "^[a-z0-9-]*$", "")
}
@ -85,7 +129,7 @@ func promptBreakingChanges() (string, error) {
func promptSelect(label string, items interface{}, template *promptui.SelectTemplates) (int, error) {
if items == nil || reflect.TypeOf(items).Kind() != reflect.Slice {
return 0, fmt.Errorf("items %v is not a slice", items)
return 0, fmt.Errorf("%w: %v is not a slice", errInvalidValue, items)
}
prompt := promptui.Select{
@ -96,6 +140,7 @@ func promptSelect(label string, items interface{}, template *promptui.SelectTemp
}
index, _, err := prompt.Run()
return index, err
}
@ -103,8 +148,9 @@ func promptText(label, regex, defaultValue string) (string, error) {
validate := func(input string) error {
regex := regexp.MustCompile(regex)
if !regex.MatchString(input) {
return fmt.Errorf("invalid value, expected: %s", regex)
return fmt.Errorf("%w, expected: %s", errInvalidValue, regex)
}
return nil
}
@ -122,5 +168,6 @@ func promptConfirm(label string) (bool, error) {
if err != nil {
return false, err
}
return r == "y", nil
}

View File

@ -14,9 +14,11 @@ func Test_checkTemplatesFiles(t *testing.T) {
got, err := defaultTemplatesFS.ReadFile(tt)
if err != nil {
t.Errorf("missing template error = %v", err)
return
}
if len(got) <= 0 {
if len(got) == 0 {
t.Errorf("empty template")
}
})

13
go.mod
View File

@ -1,22 +1,23 @@
module github.com/bvieira/sv4git/v2
module github.com/thegeeklab/git-sv/v2
go 1.19
require (
github.com/Masterminds/semver/v3 v3.2.0
github.com/imdario/mergo v0.3.13
dario.cat/mergo v1.0.0
github.com/Masterminds/semver/v3 v3.2.1
github.com/kelseyhightower/envconfig v1.4.0
github.com/manifoldco/promptui v0.9.0
github.com/urfave/cli/v2 v2.24.1
github.com/urfave/cli/v2 v2.25.7
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/chzyer/readline v1.5.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/sys v0.4.0 // indirect
golang.org/x/sys v0.13.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)

24
go.sum
View File

@ -1,5 +1,7 @@
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
@ -9,11 +11,9 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@ -26,21 +26,21 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/urfave/cli/v2 v2.24.1 h1:/QYYr7g0EhwXEML8jO+8OYt5trPnLHS0p3mrgExJ5NU=
github.com/urfave/cli/v2 v2.24.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

4
renovate.json Normal file
View File

@ -0,0 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>thegeeklab/renovate-presets:golang"]
}

View File

@ -16,6 +16,7 @@ func (c CommitMessageConfig) IssueFooterConfig() CommitMessageFooterConfig {
if v, exists := c.Footer[issueMetadataKey]; exists {
return v
}
return CommitMessageFooterConfig{}
}
@ -80,6 +81,7 @@ func (cfg ReleaseNotesConfig) sectionConfig(sectionType string) *ReleaseNotesSec
return &sectionCfg
}
}
return nil
}

11
sv/errors.go Normal file
View File

@ -0,0 +1,11 @@
package sv
import "errors"
var (
errUnknownGitError = errors.New("git command failed")
errInvalidCommitMessage = errors.New("commit message not valid")
errIssueIDNotFound = errors.New("could not find issue id using configured regex")
errInvalidIssueRegex = errors.New("could not compile issue regex")
errInvalidHeaderRegex = errors.New("invalid regex on header-selector")
)

View File

@ -39,6 +39,7 @@ func NewOutputFormatter(templatesFS fs.FS) *OutputFormatterImpl {
"getenv": os.Getenv,
}
tpls := template.Must(template.New("templates").Funcs(templateFNs).ParseFS(templatesFS, "*"))
return &OutputFormatterImpl{templates: tpls}
}
@ -48,6 +49,7 @@ func (p OutputFormatterImpl) FormatReleaseNote(releasenote ReleaseNote) (string,
if err := p.templates.ExecuteTemplate(&b, "releasenotes-md.tpl", releaseNoteVariables(releasenote)); err != nil {
return "", err
}
return b.String(), nil
}
@ -62,6 +64,7 @@ func (p OutputFormatterImpl) FormatChangelog(releasenotes []ReleaseNote) (string
if err := p.templates.ExecuteTemplate(&b, "changelog-md.tpl", templateVars); err != nil {
return "", err
}
return b.String(), nil
}
@ -70,6 +73,7 @@ func releaseNoteVariables(releasenote ReleaseNote) releaseNoteTemplateVariables
if releasenote.Version != nil {
release = "v" + releasenote.Version.String()
}
return releaseNoteTemplateVariables{
Release: release,
Tag: releasenote.Tag,
@ -83,10 +87,13 @@ func releaseNoteVariables(releasenote ReleaseNote) releaseNoteTemplateVariables
func toSortedArray(input map[string]struct{}) []string {
result := make([]string, len(input))
i := 0
for k := range input {
result[i] = k
i++
}
sort.Strings(result)
return result
}

View File

@ -6,14 +6,16 @@ func timeFormat(t time.Time, format string) string {
if t.IsZero() {
return ""
}
return t.Format(format)
}
func getSection(sections []ReleaseNoteSection, name string) ReleaseNoteSection {
func getSection(sections []ReleaseNoteSection, name string) ReleaseNoteSection { //nolint:ireturn
for _, section := range sections {
if section.SectionName() == name {
return section
}
}
return nil
}

View File

@ -32,8 +32,20 @@ func Test_getSection(t *testing.T) {
sectionName string
want ReleaseNoteSection
}{
{"existing section", []ReleaseNoteSection{ReleaseNoteCommitsSection{Name: "section 0"}, ReleaseNoteCommitsSection{Name: "section 1"}, ReleaseNoteCommitsSection{Name: "section 2"}}, "section 1", ReleaseNoteCommitsSection{Name: "section 1"}},
{"nonexisting section", []ReleaseNoteSection{ReleaseNoteCommitsSection{Name: "section 0"}, ReleaseNoteCommitsSection{Name: "section 1"}, ReleaseNoteCommitsSection{Name: "section 2"}}, "section 10", nil},
{
"existing section", []ReleaseNoteSection{
ReleaseNoteCommitsSection{Name: "section 0"},
ReleaseNoteCommitsSection{Name: "section 1"},
ReleaseNoteCommitsSection{Name: "section 2"},
}, "section 1", ReleaseNoteCommitsSection{Name: "section 1"},
},
{
"nonexisting section", []ReleaseNoteSection{
ReleaseNoteCommitsSection{Name: "section 0"},
ReleaseNoteCommitsSection{Name: "section 1"},
ReleaseNoteCommitsSection{Name: "section 2"},
}, "section 10", nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -73,6 +73,7 @@ func TestOutputFormatterImpl_FormatReleaseNote(t *testing.T) {
func emptyReleaseNote(tag string, date time.Time) ReleaseNote {
v, _ := semver.NewVersion(tag)
return ReleaseNote{
Version: v,
Tag: tag,
@ -83,11 +84,24 @@ func emptyReleaseNote(tag string, date time.Time) ReleaseNote {
func fullReleaseNote(tag string, date time.Time) ReleaseNote {
v, _ := semver.NewVersion(tag)
sections := []ReleaseNoteSection{
newReleaseNoteCommitsSection("Features", []string{"feat"}, []GitCommitLog{commitlog("feat", map[string]string{}, "a")}),
newReleaseNoteCommitsSection("Bug Fixes", []string{"fix"}, []GitCommitLog{commitlog("fix", map[string]string{}, "a")}),
newReleaseNoteCommitsSection("Build", []string{"build"}, []GitCommitLog{commitlog("build", map[string]string{}, "a")}),
newReleaseNoteCommitsSection(
"Features",
[]string{"feat"},
[]GitCommitLog{commitlog("feat", map[string]string{}, "a")},
),
newReleaseNoteCommitsSection(
"Bug Fixes",
[]string{"fix"},
[]GitCommitLog{commitlog("fix", map[string]string{}, "a")},
),
newReleaseNoteCommitsSection(
"Build",
[]string{"build"},
[]GitCommitLog{commitlog("build", map[string]string{}, "a")},
),
ReleaseNoteBreakingChangeSection{"Breaking Changes", []string{"break change message"}},
}
return releaseNote(v, tag, date, sections, map[string]struct{}{"a": {}})
}
@ -100,15 +114,18 @@ func Test_checkTemplatesExecution(t *testing.T) {
{"changelog-md.tpl", changelogVariables("v1.0.0", "v1.0.1")},
{"releasenotes-md.tpl", releaseNotesVariables("v1.0.0")},
}
for _, tt := range tests {
t.Run(tt.template, func(t *testing.T) {
var b bytes.Buffer
err := tpls.ExecuteTemplate(&b, tt.template, tt.variables)
if err != nil {
t.Errorf("invalid template err = %v", err)
return
}
if len(b.Bytes()) <= 0 {
if len(b.Bytes()) == 0 {
t.Errorf("empty template")
}
})
@ -118,11 +135,20 @@ func Test_checkTemplatesExecution(t *testing.T) {
func releaseNotesVariables(release string) releaseNoteTemplateVariables {
return releaseNoteTemplateVariables{
Release: release,
Date: time.Date(2006, 1, 02, 0, 0, 0, 0, time.UTC),
Date: time.Date(2006, 1, 0o2, 0, 0, 0, 0, time.UTC),
Sections: []ReleaseNoteSection{
newReleaseNoteCommitsSection("Features", []string{"feat"}, []GitCommitLog{commitlog("feat", map[string]string{}, "a")}),
newReleaseNoteCommitsSection("Bug Fixes", []string{"fix"}, []GitCommitLog{commitlog("fix", map[string]string{}, "a")}),
newReleaseNoteCommitsSection("Build", []string{"build"}, []GitCommitLog{commitlog("build", map[string]string{}, "a")}),
newReleaseNoteCommitsSection("Features",
[]string{"feat"},
[]GitCommitLog{commitlog("feat", map[string]string{}, "a")},
),
newReleaseNoteCommitsSection("Bug Fixes",
[]string{"fix"},
[]GitCommitLog{commitlog("fix", map[string]string{}, "a")},
),
newReleaseNoteCommitsSection("Build",
[]string{"build"},
[]GitCommitLog{commitlog("build", map[string]string{}, "a")},
),
ReleaseNoteBreakingChangeSection{"Breaking Changes", []string{"break change message"}},
},
}
@ -130,9 +156,10 @@ func releaseNotesVariables(release string) releaseNoteTemplateVariables {
func changelogVariables(releases ...string) []releaseNoteTemplateVariables {
var variables []releaseNoteTemplateVariables
for _, r := range releases {
variables = append(variables, releaseNotesVariables(r))
}
return variables
return variables
}

View File

@ -3,7 +3,6 @@ package sv
import (
"bufio"
"bytes"
"errors"
"fmt"
"os"
"os/exec"
@ -83,17 +82,35 @@ func NewGit(messageProcessor MessageProcessor, cfg TagConfig) *GitImpl {
// LastTag get last tag, if no tag found, return empty.
func (g GitImpl) LastTag() string {
cmd := exec.Command("git", "for-each-ref", "refs/tags/"+*g.tagCfg.Filter, "--sort", "-creatordate", "--format", "%(refname:short)", "--count", "1")
//nolint:gosec
cmd := exec.Command(
"git",
"for-each-ref",
fmt.Sprintf("refs/tags/%s", *g.tagCfg.Filter),
"--sort",
"-creatordate",
"--format",
"%(refname:short)",
"--count",
"1",
)
out, err := cmd.CombinedOutput()
if err != nil {
return ""
}
return strings.TrimSpace(strings.Trim(string(out), "\n"))
}
// Log return git log.
func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) {
format := "--pretty=format:\"%ad" + logSeparator + "%at" + logSeparator + "%cN" + logSeparator + "%h" + logSeparator + "%s" + logSeparator + "%b" + endLine + "\""
format := "--pretty=format:\"%ad" + logSeparator +
"%at" + logSeparator +
"%cN" + logSeparator +
"%h" + logSeparator +
"%s" + logSeparator +
"%b" + endLine + "\""
params := []string{"log", "--date=short", format}
if lr.start != "" || lr.end != "" {
@ -110,14 +127,17 @@ func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) {
}
cmd := exec.Command("git", params...)
out, err := cmd.CombinedOutput()
if err != nil {
return nil, combinedOutputErr(err, out)
}
logs, parseErr := parseLogOutput(g.messageProcessor, string(out))
if parseErr != nil {
return nil, parseErr
}
return logs, nil
}
@ -126,6 +146,7 @@ func (g GitImpl) Commit(header, body, footer string) error {
cmd := exec.Command("git", "commit", "-m", header, "-m", "", "-m", body, "-m", "", "-m", footer)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
@ -143,45 +164,66 @@ func (g GitImpl) Tag(version semver.Version) (string, error) {
if out, err := pushCommand.CombinedOutput(); err != nil {
return tag, combinedOutputErr(err, out)
}
return tag, nil
}
// Tags list repository tags.
func (g GitImpl) Tags() ([]GitTag, error) {
cmd := exec.Command("git", "for-each-ref", "--sort", "creatordate", "--format", "%(creatordate:iso8601)#%(refname:short)", "refs/tags/"+*g.tagCfg.Filter)
//nolint:gosec
cmd := exec.Command(
"git",
"for-each-ref",
"--sort",
"creatordate",
"--format",
"%(creatordate:iso8601)#%(refname:short)",
fmt.Sprintf("refs/tags/%s", *g.tagCfg.Filter),
)
out, err := cmd.CombinedOutput()
if err != nil {
return nil, combinedOutputErr(err, out)
}
return parseTagsOutput(string(out))
}
// Branch get git branch.
func (GitImpl) Branch() string {
cmd := exec.Command("git", "symbolic-ref", "--short", "HEAD")
out, err := cmd.CombinedOutput()
if err != nil {
return ""
}
return strings.TrimSpace(strings.Trim(string(out), "\n"))
}
// IsDetached check if is detached.
func (GitImpl) IsDetached() (bool, error) {
cmd := exec.Command("git", "symbolic-ref", "-q", "HEAD")
out, err := cmd.CombinedOutput()
if output := string(out); err != nil { //-q: do not issue an error message if the <name> is not a symbolic ref, but a detached HEAD; instead exit with non-zero status silently.
// -q: do not issue an error message if the <name> is not a symbolic ref, but a detached HEAD;
// instead exit with non-zero status silently.
if output := string(out); err != nil {
if output == "" {
return true, nil
}
return false, errors.New(output)
return false, fmt.Errorf("%w: %s", errUnknownGitError, output)
}
return false, nil
}
func parseTagsOutput(input string) ([]GitTag, error) {
scanner := bufio.NewScanner(strings.NewReader(input))
var result []GitTag
for scanner.Scan() {
if line := strings.TrimSpace(scanner.Text()); line != "" {
values := strings.Split(line, "#")
@ -189,31 +231,35 @@ func parseTagsOutput(input string) ([]GitTag, error) {
result = append(result, GitTag{Name: values[1], Date: date})
}
}
return result, nil
}
func parseLogOutput(messageProcessor MessageProcessor, log string) ([]GitCommitLog, error) {
scanner := bufio.NewScanner(strings.NewReader(log))
scanner.Split(splitAt([]byte(endLine)))
var logs []GitCommitLog
for scanner.Scan() {
if text := strings.TrimSpace(strings.Trim(scanner.Text(), "\"")); text != "" {
log, err := parseCommitLog(messageProcessor, text)
if err != nil {
return nil, err
}
logs = append(logs, log)
}
}
return logs, nil
}
func parseCommitLog(messageProcessor MessageProcessor, commit string) (GitCommitLog, error) {
content := strings.Split(strings.Trim(commit, "\""), logSeparator)
timestamp, _ := strconv.Atoi(content[1])
message, err := messageProcessor.Parse(content[4], content[5])
message, err := messageProcessor.Parse(content[4], content[5])
if err != nil {
return GitCommitLog{}, err
}
@ -228,10 +274,8 @@ func parseCommitLog(messageProcessor MessageProcessor, commit string) (GitCommit
}
func splitAt(b []byte) func(data []byte, atEOF bool) (advance int, token []byte, err error) {
return func(data []byte, atEOF bool) (advance int, token []byte, err error) {
dataLen := len(data)
if atEOF && dataLen == 0 {
return func(data []byte, atEOF bool) (advance int, token []byte, err error) { //nolint:nonamedreturns
if atEOF && len(data) == 0 {
return 0, nil, nil
}
@ -240,7 +284,7 @@ func splitAt(b []byte) func(data []byte, atEOF bool) (advance int, token []byte,
}
if atEOF {
return dataLen, data, nil
return len(data), data, nil
}
return 0, nil, nil
@ -264,10 +308,12 @@ func str(value, defaultValue string) string {
if value != "" {
return value
}
return defaultValue
}
func combinedOutputErr(err error, out []byte) error {
msg := strings.Split(string(out), "\n")
return fmt.Errorf("%v - %s", err, msg[0])
return fmt.Errorf("%w - %s", err, msg[0])
}

View File

@ -13,16 +13,28 @@ func Test_parseTagsOutput(t *testing.T) {
want []GitTag
wantErr bool
}{
{"with date", "2020-05-01 18:00:00 -0300#1.0.0", []GitTag{{Name: "1.0.0", Date: date("2020-05-01 18:00:00 -0300")}}, false},
{"without date", "#1.0.0", []GitTag{{Name: "1.0.0", Date: time.Time{}}}, false},
{
"with date",
"2020-05-01 18:00:00 -0300#1.0.0",
[]GitTag{{Name: "1.0.0", Date: date("2020-05-01 18:00:00 -0300")}},
false,
},
{
"without date",
"#1.0.0",
[]GitTag{{Name: "1.0.0", Date: time.Time{}}},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseTagsOutput(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("parseTagsOutput() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseTagsOutput() = %v, want %v", got, tt.want)
}
@ -35,5 +47,6 @@ func date(input string) time.Time {
if err != nil {
panic(err)
}
return t
}

View File

@ -8,6 +8,7 @@ import (
func version(v string) *semver.Version {
r, _ := semver.NewVersion(v)
return r
}
@ -16,6 +17,7 @@ func commitlog(ctype string, metadata map[string]string, author string) GitCommi
if _, found := metadata[breakingChangeMetadataKey]; found {
breaking = true
}
return GitCommitLog{
Message: CommitMessage{
Type: ctype,
@ -27,7 +29,13 @@ func commitlog(ctype string, metadata map[string]string, author string) GitCommi
}
}
func releaseNote(version *semver.Version, tag string, date time.Time, sections []ReleaseNoteSection, authorsNames map[string]struct{}) ReleaseNote {
func releaseNote(
version *semver.Version,
tag string,
date time.Time,
sections []ReleaseNoteSection,
authorsNames map[string]struct{},
) ReleaseNote {
return ReleaseNote{
Version: version,
Tag: tag,

View File

@ -30,10 +30,19 @@ func NewCommitMessage(ctype, scope, description, body, issue, breakingChanges st
if issue != "" {
metadata[issueMetadataKey] = issue
}
if breakingChanges != "" {
metadata[breakingChangeMetadataKey] = breakingChanges
}
return CommitMessage{Type: ctype, Scope: scope, Description: description, Body: body, IsBreakingChange: breakingChanges != "", Metadata: metadata}
return CommitMessage{
Type: ctype,
Scope: scope,
Description: description,
Body: body,
IsBreakingChange: breakingChanges != "",
Metadata: metadata,
}
}
// Issue return issue from metadata.
@ -53,7 +62,7 @@ type MessageProcessor interface {
ValidateType(ctype string) error
ValidateScope(scope string) error
ValidateDescription(description string) error
Enhance(branch string, message string) (string, error)
Enhance(branch, message string) (string, error)
IssueID(branch string) (string, error)
Format(msg CommitMessage) (string, string, string)
Parse(subject, body string) (CommitMessage, error)
@ -75,7 +84,8 @@ type MessageProcessorImpl struct {
// SkipBranch check if branch should be ignored.
func (p MessageProcessorImpl) SkipBranch(branch string, detached bool) bool {
return contains(branch, p.branchesCfg.Skip) || (p.branchesCfg.SkipDetached != nil && *p.branchesCfg.SkipDetached && detached)
return contains(branch, p.branchesCfg.Skip) ||
(p.branchesCfg.SkipDetached != nil && *p.branchesCfg.SkipDetached && detached)
}
// Validate commit message.
@ -88,7 +98,7 @@ func (p MessageProcessorImpl) Validate(message string) error {
}
if !regexp.MustCompile(`^[a-z+]+(\(.+\))?!?: .+$`).MatchString(subject) {
return fmt.Errorf("subject [%s] should be valid according with conventional commits", subject)
return fmt.Errorf("%w: subject [%s] not valid", errInvalidCommitMessage, subject)
}
if err := p.ValidateType(msg.Type); err != nil {
@ -99,40 +109,48 @@ func (p MessageProcessorImpl) Validate(message string) error {
return err
}
if err := p.ValidateDescription(msg.Description); err != nil {
return err
}
return nil
return p.ValidateDescription(msg.Description)
}
// ValidateType check if commit type is valid.
func (p MessageProcessorImpl) ValidateType(ctype string) error {
if ctype == "" || !contains(ctype, p.messageCfg.Types) {
return fmt.Errorf("message type should be one of [%v]", strings.Join(p.messageCfg.Types, ", "))
return fmt.Errorf(
"%w: type must be one of [%s]",
errInvalidCommitMessage,
strings.Join(p.messageCfg.Types, ", "),
)
}
return nil
}
// ValidateScope check if commit scope is valid.
func (p MessageProcessorImpl) ValidateScope(scope string) error {
if len(p.messageCfg.Scope.Values) > 0 && !contains(scope, p.messageCfg.Scope.Values) {
return fmt.Errorf("message scope should one of [%v]", strings.Join(p.messageCfg.Scope.Values, ", "))
return fmt.Errorf(
"%w: scope must one of [%s]",
errInvalidCommitMessage,
strings.Join(p.messageCfg.Scope.Values, ", "),
)
}
return nil
}
// ValidateDescription check if commit description is valid.
func (p MessageProcessorImpl) ValidateDescription(description string) error {
if !regexp.MustCompile("^[a-z]+.*$").MatchString(description) {
return fmt.Errorf("description [%s] should begins with lowercase letter", description)
return fmt.Errorf("%w: description [%s] must start with lowercase", errInvalidCommitMessage, description)
}
return nil
}
// Enhance add metadata on commit message.
func (p MessageProcessorImpl) Enhance(branch string, message string) (string, error) {
if p.branchesCfg.DisableIssue || p.messageCfg.IssueFooterConfig().Key == "" || hasIssueID(message, p.messageCfg.IssueFooterConfig()) {
func (p MessageProcessorImpl) Enhance(branch, message string) (string, error) {
if p.branchesCfg.DisableIssue || p.messageCfg.IssueFooterConfig().Key == "" ||
hasIssueID(message, p.messageCfg.IssueFooterConfig()) {
return "", nil // enhance disabled
}
@ -140,8 +158,9 @@ func (p MessageProcessorImpl) Enhance(branch string, message string) (string, er
if err != nil {
return "", err
}
if issue == "" {
return "", fmt.Errorf("could not find issue id using configured regex")
return "", errIssueIDNotFound
}
footer := formatIssueFooter(p.messageCfg.IssueFooterConfig(), issue)
@ -156,9 +175,11 @@ func formatIssueFooter(cfg CommitMessageFooterConfig, issue string) string {
if !strings.HasPrefix(issue, cfg.AddValuePrefix) {
issue = cfg.AddValuePrefix + issue
}
if cfg.UseHash {
return fmt.Sprintf("%s #%s", cfg.Key, strings.TrimPrefix(issue, "#"))
}
return fmt.Sprintf("%s: %s", cfg.Key, issue)
}
@ -169,25 +190,30 @@ func (p MessageProcessorImpl) IssueID(branch string) (string, error) {
}
rstr := fmt.Sprintf("^%s(%s)%s$", p.branchesCfg.Prefix, p.messageCfg.Issue.Regex, p.branchesCfg.Suffix)
r, err := regexp.Compile(rstr)
if err != nil {
return "", fmt.Errorf("could not compile issue regex: %s, error: %v", rstr, err.Error())
return "", fmt.Errorf("%w: %s: %v", errInvalidIssueRegex, rstr, err.Error())
}
groups := r.FindStringSubmatch(branch)
if len(groups) != 4 {
if len(groups) != 4 { //nolint:gomnd
return "", nil
}
return groups[2], nil
}
// Format a commit message returning header, body and footer.
func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string) {
var header strings.Builder
header.WriteString(msg.Type)
if msg.Scope != "" {
header.WriteString("(" + msg.Scope + ")")
}
header.WriteString(": ")
header.WriteString(msg.Description)
@ -195,10 +221,12 @@ func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string)
if msg.BreakingMessage() != "" {
footer.WriteString(fmt.Sprintf("%s: %s", breakingChangeFooterKey, msg.BreakingMessage()))
}
if issue, exists := msg.Metadata[issueMetadataKey]; exists && p.messageCfg.IssueFooterConfig().Key != "" {
if footer.Len() > 0 {
footer.WriteString("\n")
}
footer.WriteString(formatIssueFooter(p.messageCfg.IssueFooterConfig(), issue))
}
@ -221,17 +249,20 @@ func (p MessageProcessorImpl) Parse(subject, body string) (CommitMessage, error)
commitType, scope, description, hasBreakingChange := parseSubjectMessage(preparedSubject)
metadata := make(map[string]string)
for key, mdCfg := range p.messageCfg.Footer {
if mdCfg.Key != "" {
prefixes := append([]string{mdCfg.Key}, mdCfg.KeySynonyms...)
for _, prefix := range prefixes {
if tagValue := extractFooterMetadata(prefix, commitBody, mdCfg.UseHash); tagValue != "" {
metadata[key] = tagValue
break
}
}
}
}
if tagValue := extractFooterMetadata(breakingChangeFooterKey, commitBody, false); tagValue != "" {
metadata[breakingChangeMetadataKey] = tagValue
hasBreakingChange = true
@ -254,18 +285,23 @@ func (p MessageProcessorImpl) prepareHeader(header string) (string, error) {
regex, err := regexp.Compile(p.messageCfg.HeaderSelector)
if err != nil {
return "", fmt.Errorf("invalid regex on header-selector %s, error: %s", p.messageCfg.HeaderSelector, err.Error())
return "", fmt.Errorf("%w: %s: %s", errInvalidHeaderRegex, p.messageCfg.HeaderSelector, err.Error())
}
index := regex.SubexpIndex(messageRegexGroupName)
if index < 0 {
return "", fmt.Errorf("could not find %s regex group on header-selector regex", messageRegexGroupName)
return "", fmt.Errorf("%w: could not find group %s", errInvalidHeaderRegex, messageRegexGroupName)
}
match := regex.FindStringSubmatch(header)
if match == nil || len(match) < index {
return "", fmt.Errorf("could not find %s regex group in match result for '%s'", messageRegexGroupName, header)
return "", fmt.Errorf(
"%w: could not find group %s in match result for '%s'",
errInvalidHeaderRegex,
messageRegexGroupName,
header,
)
}
return match[index], nil
@ -273,10 +309,12 @@ func (p MessageProcessorImpl) prepareHeader(header string) (string, error) {
func parseSubjectMessage(message string) (string, string, string, bool) {
regex := regexp.MustCompile(`([a-z]+)(\((.*)\))?(!)?: (.*)`)
result := regex.FindStringSubmatch(message)
if len(result) != 6 {
if len(result) != 6 { //nolint:gomnd
return "", "", message, false
}
return result[1], result[3], strings.TrimSpace(result[5]), result[4] == "!"
}
@ -289,9 +327,10 @@ func extractFooterMetadata(key, text string, useHash bool) string {
}
result := regex.FindStringSubmatch(text)
if len(result) < 2 {
if len(result) < 2 { //nolint:gomnd
return ""
}
return result[1]
}
@ -300,6 +339,7 @@ func hasFooter(message string) bool {
scanner := bufio.NewScanner(strings.NewReader(message))
lines := 0
for scanner.Scan() {
if lines > 0 && r.MatchString(scanner.Text()) {
return true
@ -317,6 +357,7 @@ func hasIssueID(message string, issueConfig CommitMessageFooterConfig) bool {
} else {
r = regexp.MustCompile(fmt.Sprintf("(?m)^%s: .+$", issueConfig.Key))
}
return r.MatchString(message)
}
@ -326,6 +367,7 @@ func contains(value string, content []string) bool {
return true
}
}
return false
}
@ -336,12 +378,15 @@ func splitCommitMessageContent(content string) (string, string) {
subject := scanner.Text()
var body strings.Builder
first := true
for scanner.Scan() {
if !first {
body.WriteString("\n")
}
body.WriteString(scanner.Text())
first = false
}

View File

@ -146,23 +146,55 @@ func TestMessageProcessorImpl_Validate(t *testing.T) {
message string
wantErr bool
}{
{"single line valid message", ccfg, "feat: add something", false},
{"single line valid message with scope", ccfg, "feat(scope): add something", false},
{
"single line valid message",
ccfg,
"feat: add something", false,
},
{
"single line valid message with scope",
ccfg,
"feat(scope): add something", false,
},
{"single line valid scope from list", ccfgWithScope, "feat(scope): add something", false},
{"single line invalid scope from list", ccfgWithScope, "feat(invalid): add something", true},
{"single line invalid type message", ccfg, "something: add something", true},
{"single line invalid type message", ccfg, "feat?: add something", true},
{
"single line invalid type message",
ccfg,
"something: add something", true,
},
{
"single line invalid type message",
ccfg,
"feat?: add something", true,
},
{"multi line valid message", ccfg, `feat: add something
team: x`, false},
{
"multi line valid message",
ccfg,
`feat: add something
{"multi line invalid message", ccfg, `feat add something
team: x`, true},
team: x`, false,
},
{"support ! for breaking change", ccfg, "feat!: add something", false},
{"support ! with scope for breaking change", ccfg, "feat(scope)!: add something", false},
{
"multi line invalid message",
ccfg,
`feat add something
team: x`, true,
},
{
"support ! for breaking change",
ccfg,
"feat!: add something", false,
},
{
"support ! with scope for breaking change",
ccfg,
"feat(scope)!: add something", false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -181,9 +213,21 @@ func TestMessageProcessorImpl_ValidateType(t *testing.T) {
ctype string
wantErr bool
}{
{"valid type", ccfg, "feat", false},
{"invalid type", ccfg, "aaa", true},
{"empty type", ccfg, "", true},
{
"valid type",
ccfg,
"feat", false,
},
{
"invalid type",
ccfg,
"aaa", true,
},
{
"empty type",
ccfg,
"", true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -202,7 +246,11 @@ func TestMessageProcessorImpl_ValidateScope(t *testing.T) {
scope string
wantErr bool
}{
{"any scope", ccfg, "aaa", false},
{
"any scope",
ccfg,
"aaa", false,
},
{"valid scope with scope list", ccfgWithScope, "scope", false},
{"invalid scope with scope list", ccfgWithScope, "aaa", true},
}
@ -223,11 +271,31 @@ func TestMessageProcessorImpl_ValidateDescription(t *testing.T) {
description string
wantErr bool
}{
{"empty description", ccfg, "", true},
{"sigle letter description", ccfg, "a", false},
{"number description", ccfg, "1", true},
{"valid description", ccfg, "add some feature", false},
{"invalid capital letter description", ccfg, "Add some feature", true},
{
"empty description",
ccfg,
"", true,
},
{
"sigle letter description",
ccfg,
"a", false,
},
{
"number description",
ccfg,
"1", true,
},
{
"valid description",
ccfg,
"add some feature", false,
},
{
"invalid capital letter description",
ccfg,
"Add some feature", true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -248,24 +316,73 @@ func TestMessageProcessorImpl_Enhance(t *testing.T) {
want string
wantErr bool
}{
{"issue on branch name", ccfg, "JIRA-123", "fix: fix something", "\njira: JIRA-123", false},
{"issue on branch name with description", ccfg, "JIRA-123-some-description", "fix: fix something", "\njira: JIRA-123", false},
{"issue on branch name with prefix", ccfg, "feature/JIRA-123", "fix: fix something", "\njira: JIRA-123", false},
{"with footer", ccfg, "JIRA-123", fullMessage, "jira: JIRA-123", false},
{"with issue on footer", ccfg, "JIRA-123", fullMessageWithJira, "", false},
{"issue on branch name with prefix and description", ccfg, "feature/JIRA-123-some-description", "fix: fix something", "\njira: JIRA-123", false},
{"no issue on branch name", ccfg, "branch", "fix: fix something", "", true},
{"unexpected branch name", ccfg, "feature /JIRA-123", "fix: fix something", "", true},
{"issue on branch name using hash", ccfgHash, "JIRA-123-some-description", "fix: fix something", "\njira #JIRA-123", false},
{"numeric issue on branch name", ccfgGitIssue, "#13", "fix: fix something", "\nissue: #13", false},
{"numeric issue on branch name without hash", ccfgGitIssue, "13", "fix: fix something", "\nissue: #13", false},
{"numeric issue on branch name with description without hash", ccfgGitIssue, "13-some-fix", "fix: fix something", "\nissue: #13", false},
{
"issue on branch name",
ccfg,
"JIRA-123", "fix: fix something", "\njira: JIRA-123", false,
},
{
"issue on branch name with description",
ccfg,
"JIRA-123-some-description", "fix: fix something", "\njira: JIRA-123", false,
},
{
"issue on branch name with prefix",
ccfg,
"feature/JIRA-123", "fix: fix something", "\njira: JIRA-123", false,
},
{
"with footer",
ccfg,
"JIRA-123", fullMessage, "jira: JIRA-123", false,
},
{
"with issue on footer",
ccfg,
"JIRA-123", fullMessageWithJira, "", false,
},
{
"issue on branch name with prefix and description",
ccfg,
"feature/JIRA-123-some-description", "fix: fix something", "\njira: JIRA-123", false,
},
{
"no issue on branch name",
ccfg,
"branch", "fix: fix something", "", true,
},
{
"unexpected branch name",
ccfg,
"feature /JIRA-123", "fix: fix something", "", true,
},
{
"issue on branch name using hash",
ccfgHash,
"JIRA-123-some-description", "fix: fix something", "\njira #JIRA-123", false,
},
{
"numeric issue on branch name",
ccfgGitIssue,
"#13", "fix: fix something", "\nissue: #13", false,
},
{
"numeric issue on branch name without hash",
ccfgGitIssue,
"13", "fix: fix something", "\nissue: #13", false,
},
{
"numeric issue on branch name with description without hash",
ccfgGitIssue,
"13-some-fix", "fix: fix something", "\nissue: #13", false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewMessageProcessor(tt.cfg, newBranchCfg(false)).Enhance(tt.branch, tt.message)
if (err != nil) != tt.wantErr {
t.Errorf("MessageProcessorImpl.Enhance() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
@ -296,6 +413,7 @@ func TestMessageProcessorImpl_IssueID(t *testing.T) {
got, err := p.IssueID(tt.branch)
if (err != nil) != tt.wantErr {
t.Errorf("MessageProcessorImpl.IssueID() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
@ -326,19 +444,19 @@ func Test_hasIssueID(t *testing.T) {
}{
{"single line without issue", "feat: something", cfgColon, false},
{"multi line without issue", `feat: something
yay`, cfgColon, false},
{"multi line without jira issue", `feat: something
jira1: JIRA-123`, cfgColon, false},
{"multi line with issue", `feat: something
jira: JIRA-123`, cfgColon, true},
{"multi line with issue and hash", `feat: something
jira #JIRA-123`, cfgHash, true},
{"empty config", `feat: something
jira #JIRA-123`, cfgEmpty, false},
}
for _, tt := range tests {
@ -378,8 +496,10 @@ var completeBody = `some descriptions
jira: JIRA-123
BREAKING CHANGE: this change breaks everything`
var bodyWithCarriage = "some description\r\nmore description\r\n\r\njira: JIRA-123\r"
var expectedBodyWithCarriage = "some description\nmore description\n\njira: JIRA-123"
var (
bodyWithCarriage = "some description\r\nmore description\r\n\r\njira: JIRA-123\r"
expectedBodyWithCarriage = "some description\nmore description\n\njira: JIRA-123"
)
var issueOnlyBody = `some descriptions
@ -402,20 +522,145 @@ func TestMessageProcessorImpl_Parse(t *testing.T) {
body string
want CommitMessage
}{
{"simple message", ccfg, "feat: something awesome", "", CommitMessage{Type: "feat", Scope: "", Description: "something awesome", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
{"message with scope", ccfg, "feat(scope): something awesome", "", CommitMessage{Type: "feat", Scope: "scope", Description: "something awesome", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
{"unmapped type", ccfg, "unkn: something unknown", "", CommitMessage{Type: "unkn", Scope: "", Description: "something unknown", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
{"jira and breaking change metadata", ccfg, "feat: something new", completeBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: completeBody, IsBreakingChange: true, Metadata: map[string]string{issueMetadataKey: "JIRA-123", breakingChangeMetadataKey: "this change breaks everything"}}},
{"jira only metadata", ccfg, "feat: something new", issueOnlyBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: issueOnlyBody, IsBreakingChange: false, Metadata: map[string]string{issueMetadataKey: "JIRA-456"}}},
{"jira synonyms metadata", ccfg, "feat: something new", issueSynonymsBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: issueSynonymsBody, IsBreakingChange: false, Metadata: map[string]string{issueMetadataKey: "JIRA-789"}}},
{"breaking change with exclamation mark", ccfg, "feat!: something new", "", CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: "", IsBreakingChange: true, Metadata: map[string]string{}}},
{"hash metadata", ccfg, "feat: something new", hashMetadataBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: hashMetadataBody, IsBreakingChange: false, Metadata: map[string]string{issueMetadataKey: "JIRA-999", "refs": "#123"}}},
{"empty issue cfg", ccfgEmptyIssue, "feat: something new", hashMetadataBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: hashMetadataBody, IsBreakingChange: false, Metadata: map[string]string{}}},
{"carriage return on body", ccfg, "feat: something new", bodyWithCarriage, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: expectedBodyWithCarriage, IsBreakingChange: false, Metadata: map[string]string{issueMetadataKey: "JIRA-123"}}},
{
"simple message",
ccfg,
"feat: something awesome", "",
CommitMessage{
Type: "feat",
Scope: "",
Description: "something awesome",
Body: "",
IsBreakingChange: false,
Metadata: map[string]string{},
},
},
{
"message with scope",
ccfg,
"feat(scope): something awesome", "",
CommitMessage{
Type: "feat",
Scope: "scope",
Description: "something awesome",
Body: "",
IsBreakingChange: false,
Metadata: map[string]string{},
},
},
{
"unmapped type",
ccfg,
"unkn: something unknown", "",
CommitMessage{
Type: "unkn",
Scope: "",
Description: "something unknown",
Body: "",
IsBreakingChange: false,
Metadata: map[string]string{},
},
},
{
"jira and breaking change metadata",
ccfg,
"feat: something new", completeBody,
CommitMessage{
Type: "feat",
Scope: "",
Description: "something new",
Body: completeBody,
IsBreakingChange: true,
Metadata: map[string]string{
issueMetadataKey: "JIRA-123",
breakingChangeMetadataKey: "this change breaks everything",
},
},
},
{
"jira only metadata",
ccfg,
"feat: something new", issueOnlyBody,
CommitMessage{
Type: "feat",
Scope: "",
Description: "something new",
Body: issueOnlyBody,
IsBreakingChange: false,
Metadata: map[string]string{issueMetadataKey: "JIRA-456"},
},
},
{
"jira synonyms metadata",
ccfg,
"feat: something new", issueSynonymsBody,
CommitMessage{
Type: "feat",
Scope: "",
Description: "something new",
Body: issueSynonymsBody,
IsBreakingChange: false,
Metadata: map[string]string{issueMetadataKey: "JIRA-789"},
},
},
{
"breaking change with exclamation mark",
ccfg,
"feat!: something new", "",
CommitMessage{
Type: "feat",
Scope: "",
Description: "something new",
Body: "",
IsBreakingChange: true,
Metadata: map[string]string{},
},
},
{
"hash metadata",
ccfg,
"feat: something new", hashMetadataBody,
CommitMessage{
Type: "feat",
Scope: "",
Description: "something new",
Body: hashMetadataBody,
IsBreakingChange: false,
Metadata: map[string]string{issueMetadataKey: "JIRA-999", "refs": "#123"},
},
},
{
"empty issue cfg",
ccfgEmptyIssue,
"feat: something new", hashMetadataBody,
CommitMessage{
Type: "feat",
Scope: "",
Description: "something new",
Body: hashMetadataBody,
IsBreakingChange: false,
Metadata: map[string]string{},
},
},
{
"carriage return on body",
ccfg,
"feat: something new", bodyWithCarriage,
CommitMessage{
Type: "feat",
Scope: "",
Description: "something new",
Body: expectedBodyWithCarriage,
IsBreakingChange: false,
Metadata: map[string]string{issueMetadataKey: "JIRA-123"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got, err := NewMessageProcessor(tt.cfg, newBranchCfg(false)).Parse(tt.subject, tt.body); !reflect.DeepEqual(got, tt.want) && err == nil {
if got, err := NewMessageProcessor(
tt.cfg, newBranchCfg(false),
).Parse(tt.subject, tt.body); !reflect.DeepEqual(got, tt.want) && err == nil {
t.Errorf("MessageProcessorImpl.Parse() = [%+v], want [%+v]", got, tt.want)
}
})
@ -431,18 +676,102 @@ func TestMessageProcessorImpl_Format(t *testing.T) {
wantBody string
wantFooter string
}{
{"simple message", ccfg, NewCommitMessage("feat", "", "something", "", "", ""), "feat: something", "", ""},
{"with issue", ccfg, NewCommitMessage("feat", "", "something", "", "JIRA-123", ""), "feat: something", "", "jira: JIRA-123"},
{"with issue using hash", ccfgHash, NewCommitMessage("feat", "", "something", "", "JIRA-123", ""), "feat: something", "", "jira #JIRA-123"},
{"with issue using double hash", ccfgHash, NewCommitMessage("feat", "", "something", "", "#JIRA-123", ""), "feat: something", "", "jira #JIRA-123"},
{"with breaking change", ccfg, NewCommitMessage("feat", "", "something", "", "", "breaks"), "feat: something", "", "BREAKING CHANGE: breaks"},
{"with scope", ccfg, NewCommitMessage("feat", "scope", "something", "", "", ""), "feat(scope): something", "", ""},
{"with body", ccfg, NewCommitMessage("feat", "", "something", "body", "", ""), "feat: something", "body", ""},
{"with multiline body", ccfg, NewCommitMessage("feat", "", "something", multilineBody, "", ""), "feat: something", multilineBody, ""},
{"full message", ccfg, NewCommitMessage("feat", "scope", "something", multilineBody, "JIRA-123", "breaks"), "feat(scope): something", multilineBody, fullFooter},
{"config without issue key", ccfgEmptyIssue, NewCommitMessage("feat", "", "something", "", "JIRA-123", ""), "feat: something", "", ""},
{"with issue and issue prefix", ccfgGitIssue, NewCommitMessage("feat", "", "something", "", "123", ""), "feat: something", "", "issue: #123"},
{"with #issue and issue prefix", ccfgGitIssue, NewCommitMessage("feat", "", "something", "", "#123", ""), "feat: something", "", "issue: #123"},
{
"simple message",
ccfg,
NewCommitMessage("feat", "", "something", "", "", ""),
"feat: something",
"",
"",
},
{
"with issue",
ccfg,
NewCommitMessage("feat", "", "something", "", "JIRA-123", ""),
"feat: something",
"",
"jira: JIRA-123",
},
{
"with issue using hash",
ccfgHash,
NewCommitMessage("feat", "", "something", "", "JIRA-123", ""),
"feat: something",
"",
"jira #JIRA-123",
},
{
"with issue using double hash",
ccfgHash,
NewCommitMessage("feat", "", "something", "", "#JIRA-123", ""),
"feat: something",
"",
"jira #JIRA-123",
},
{
"with breaking change",
ccfg,
NewCommitMessage("feat", "", "something", "", "", "breaks"),
"feat: something",
"",
"BREAKING CHANGE: breaks",
},
{
"with scope",
ccfg,
NewCommitMessage("feat", "scope", "something", "", "", ""),
"feat(scope): something",
"",
"",
},
{
"with body",
ccfg,
NewCommitMessage("feat", "", "something", "body", "", ""),
"feat: something",
"body",
"",
},
{
"with multiline body",
ccfg,
NewCommitMessage("feat", "", "something", multilineBody, "", ""),
"feat: something",
multilineBody,
"",
},
{
"full message",
ccfg,
NewCommitMessage("feat", "scope", "something", multilineBody, "JIRA-123", "breaks"),
"feat(scope): something",
multilineBody,
fullFooter,
},
{
"config without issue key",
ccfgEmptyIssue,
NewCommitMessage("feat", "", "something", "", "JIRA-123", ""),
"feat: something",
"",
"",
},
{
"with issue and issue prefix",
ccfgGitIssue,
NewCommitMessage("feat", "", "something", "", "123", ""),
"feat: something",
"",
"issue: #123",
},
{
"with #issue and issue prefix",
ccfgGitIssue,
NewCommitMessage("feat", "", "something", "", "#123", ""),
"feat: something",
"",
"issue: #123",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -532,14 +861,61 @@ func Test_prepareHeader(t *testing.T) {
wantHeader string
wantError bool
}{
{"conventional without selector", "", "feat: something", "feat: something", false},
{"conventional with scope without selector", "", "feat(scope): something", "feat(scope): something", false},
{"non-conventional without selector", "", "something", "something", false},
{"matching conventional with selector with group", "Merged PR (\\d+): (?P<header>.*)", "Merged PR 123: feat: something", "feat: something", false},
{"matching non-conventional with selector with group", "Merged PR (\\d+): (?P<header>.*)", "Merged PR 123: something", "something", false},
{"matching non-conventional with selector without group", "Merged PR (\\d+): (.*)", "Merged PR 123: something", "", true},
{"non-matching non-conventional with selector with group", "Merged PR (\\d+): (?P<header>.*)", "something", "", true},
{"matching non-conventional with invalid regex", "Merged PR (\\d+): (?<header>.*)", "Merged PR 123: something", "", true},
{
"conventional without selector",
"",
"feat: something",
"feat: something",
false,
},
{
"conventional with scope without selector",
"",
"feat(scope): something",
"feat(scope): something",
false,
},
{
"non-conventional without selector",
"",
"something", "something",
false,
},
{
"matching conventional with selector with group",
"Merged PR (\\d+): (?P<header>.*)",
"Merged PR 123: feat: something",
"feat: something",
false,
},
{
"matching non-conventional with selector with group",
"Merged PR (\\d+): (?P<header>.*)",
"Merged PR 123: something",
"something",
false,
},
{
"matching non-conventional with selector without group",
"Merged PR (\\d+): (.*)",
"Merged PR 123: something",
"",
true,
},
{
"non-matching non-conventional with selector with group",
"Merged PR (\\d+): (?P<header>.*)",
"something",
"",
true,
},
{
"matching non-conventional with invalid regex",
"Merged PR (\\d+): (?<header>.*)",
"Merged PR 123: something",
"",
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -22,22 +22,32 @@ func NewReleaseNoteProcessor(cfg ReleaseNotesConfig) *ReleaseNoteProcessorImpl {
}
// Create create a release note based on commits.
func (p ReleaseNoteProcessorImpl) Create(version *semver.Version, tag string, date time.Time, commits []GitCommitLog) ReleaseNote {
func (p ReleaseNoteProcessorImpl) Create(
version *semver.Version,
tag string,
date time.Time,
commits []GitCommitLog,
) ReleaseNote {
mapping := commitSectionMapping(p.cfg.Sections)
sections := make(map[string]ReleaseNoteCommitsSection)
authors := make(map[string]struct{})
var breakingChanges []string
for _, commit := range commits {
authors[commit.AuthorName] = struct{}{}
if sectionCfg, exists := mapping[commit.Message.Type]; exists {
section, sexists := sections[sectionCfg.Name]
if !sexists {
section = ReleaseNoteCommitsSection{Name: sectionCfg.Name, Types: sectionCfg.CommitTypes}
}
section.Items = append(section.Items, commit)
sections[sectionCfg.Name] = section
}
if commit.Message.BreakingMessage() != "" {
// TODO: if no message found, should use description instead?
breakingChanges = append(breakingChanges, commit.Message.BreakingMessage())
@ -48,10 +58,20 @@ func (p ReleaseNoteProcessorImpl) Create(version *semver.Version, tag string, da
if bcCfg := p.cfg.sectionConfig(ReleaseNotesSectionTypeBreakingChanges); bcCfg != nil && len(breakingChanges) > 0 {
breakingChangeSection = ReleaseNoteBreakingChangeSection{Name: bcCfg.Name, Messages: breakingChanges}
}
return ReleaseNote{Version: version, Tag: tag, Date: date.Truncate(time.Minute), Sections: p.toReleaseNoteSections(sections, breakingChangeSection), AuthorsNames: authors}
return ReleaseNote{
Version: version,
Tag: tag,
Date: date.Truncate(time.Minute),
Sections: p.toReleaseNoteSections(sections, breakingChangeSection),
AuthorsNames: authors,
}
}
func (p ReleaseNoteProcessorImpl) toReleaseNoteSections(commitSections map[string]ReleaseNoteCommitsSection, breakingChange ReleaseNoteBreakingChangeSection) []ReleaseNoteSection {
func (p ReleaseNoteProcessorImpl) toReleaseNoteSections(
commitSections map[string]ReleaseNoteCommitsSection,
breakingChange ReleaseNoteBreakingChangeSection,
) []ReleaseNoteSection {
hasBreaking := 0
if breakingChange.Name != "" {
hasBreaking = 1
@ -59,11 +79,13 @@ func (p ReleaseNoteProcessorImpl) toReleaseNoteSections(commitSections map[strin
sections := make([]ReleaseNoteSection, len(commitSections)+hasBreaking)
i := 0
for _, cfg := range p.cfg.Sections {
if cfg.SectionType == ReleaseNotesSectionTypeBreakingChanges && hasBreaking > 0 {
sections[i] = breakingChange
i++
}
if s, exists := commitSections[cfg.Name]; cfg.SectionType == ReleaseNotesSectionTypeCommits && exists {
sections[i] = s
i++
@ -75,6 +97,7 @@ func (p ReleaseNoteProcessorImpl) toReleaseNoteSections(commitSections map[strin
func commitSectionMapping(sections []ReleaseNotesSectionConfig) map[string]ReleaseNotesSectionConfig {
mapping := make(map[string]ReleaseNotesSectionConfig)
for _, section := range sections {
if section.SectionType == ReleaseNotesSectionTypeCommits {
for _, commitType := range section.CommitTypes {
@ -82,6 +105,7 @@ func commitSectionMapping(sections []ReleaseNotesSectionConfig) map[string]Relea
}
}
}
return mapping
}

View File

@ -25,7 +25,15 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
tag: "v1.0.0",
date: date,
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a")},
want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, []ReleaseNoteSection{newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")})}, map[string]struct{}{"a": {}}),
want: releaseNote(
semver.MustParse("1.0.0"),
"v1.0.0",
date,
[]ReleaseNoteSection{
newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")}),
},
map[string]struct{}{"a": {}},
),
},
{
name: "unmapped tag",
@ -33,28 +41,71 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
tag: "v1.0.0",
date: date,
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a"), commitlog("unmapped", map[string]string{}, "a")},
want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, []ReleaseNoteSection{newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")})}, map[string]struct{}{"a": {}}),
want: releaseNote(
semver.MustParse("1.0.0"),
"v1.0.0",
date,
[]ReleaseNoteSection{
newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")}),
},
map[string]struct{}{"a": {}},
),
},
{
name: "breaking changes tag",
version: semver.MustParse("1.0.0"),
tag: "v1.0.0",
date: date,
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a"), commitlog("unmapped", map[string]string{"breaking-change": "breaks"}, "a")},
want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, []ReleaseNoteSection{newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")}), ReleaseNoteBreakingChangeSection{Name: "Breaking Changes", Messages: []string{"breaks"}}}, map[string]struct{}{"a": {}}),
commits: []GitCommitLog{
commitlog("t1", map[string]string{}, "a"),
commitlog("unmapped", map[string]string{"breaking-change": "breaks"}, "a"),
},
want: releaseNote(
semver.MustParse("1.0.0"),
"v1.0.0",
date,
[]ReleaseNoteSection{
newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")}),
ReleaseNoteBreakingChangeSection{Name: "Breaking Changes", Messages: []string{"breaks"}},
},
map[string]struct{}{"a": {}},
),
},
{
name: "multiple authors",
version: semver.MustParse("1.0.0"),
tag: "v1.0.0",
date: date,
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "author3"), commitlog("t1", map[string]string{}, "author2"), commitlog("t1", map[string]string{}, "author1")},
want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, []ReleaseNoteSection{newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "author3"), commitlog("t1", map[string]string{}, "author2"), commitlog("t1", map[string]string{}, "author1")})}, map[string]struct{}{"author1": {}, "author2": {}, "author3": {}}),
commits: []GitCommitLog{
commitlog("t1", map[string]string{}, "author3"),
commitlog("t1", map[string]string{}, "author2"),
commitlog("t1", map[string]string{}, "author1"),
},
want: releaseNote(
semver.MustParse("1.0.0"),
"v1.0.0",
date,
[]ReleaseNoteSection{
newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{
commitlog("t1", map[string]string{}, "author3"),
commitlog("t1", map[string]string{}, "author2"),
commitlog("t1", map[string]string{}, "author1"),
}),
},
map[string]struct{}{"author1": {}, "author2": {}, "author3": {}},
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := NewReleaseNoteProcessor(ReleaseNotesConfig{Sections: []ReleaseNotesSectionConfig{{Name: "Tag 1", SectionType: "commits", CommitTypes: []string{"t1"}}, {Name: "Tag 2", SectionType: "commits", CommitTypes: []string{"t2"}}, {Name: "Breaking Changes", SectionType: "breaking-changes"}}})
p := NewReleaseNoteProcessor(
ReleaseNotesConfig{
Sections: []ReleaseNotesSectionConfig{
{Name: "Tag 1", SectionType: "commits", CommitTypes: []string{"t1"}},
{Name: "Tag 2", SectionType: "commits", CommitTypes: []string{"t2"}},
{Name: "Breaking Changes", SectionType: "breaking-changes"},
},
})
if got := p.Create(tt.version, tt.tag, tt.date, tt.commits); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ReleaseNoteProcessorImpl.Create() = %v, want %v", got, tt.want)
}

View File

@ -14,6 +14,7 @@ const (
// IsValidVersion return true when a version is valid.
func IsValidVersion(value string) bool {
_, err := semver.NewVersion(value)
return err == nil
}
@ -23,6 +24,7 @@ func ToVersion(value string) (*semver.Version, error) {
if version == "" {
version = "0.0.0"
}
return semver.NewVersion(version)
}
@ -52,7 +54,9 @@ func NewSemVerCommitsProcessor(vcfg VersioningConfig, mcfg CommitMessageConfig)
}
// NextVersion calculates next version based on commit log.
func (p SemVerCommitsProcessorImpl) NextVersion(version *semver.Version, commits []GitCommitLog) (*semver.Version, bool) {
func (p SemVerCommitsProcessorImpl) NextVersion(
version *semver.Version, commits []GitCommitLog,
) (*semver.Version, bool) {
versionToUpdate := none
for _, commit := range commits {
if v := p.versionTypeToUpdate(commit); v > versionToUpdate {
@ -64,7 +68,9 @@ func (p SemVerCommitsProcessorImpl) NextVersion(version *semver.Version, commits
if version == nil {
return nil, updated
}
newVersion := updateVersion(*version, versionToUpdate)
return &newVersion, updated
}
@ -85,18 +91,23 @@ func (p SemVerCommitsProcessorImpl) versionTypeToUpdate(commit GitCommitLog) ver
if commit.Message.IsBreakingChange {
return major
}
if _, exists := p.MajorVersionTypes[commit.Message.Type]; exists {
return major
}
if _, exists := p.MinorVersionTypes[commit.Message.Type]; exists {
return minor
}
if _, exists := p.PatchVersionTypes[commit.Message.Type]; exists {
return patch
}
if !contains(commit.Message.Type, p.KnownTypes) && p.IncludeUnknownTypeAsPatch {
return patch
}
return none
}
@ -105,5 +116,6 @@ func toMap(values []string) map[string]struct{} {
for _, v := range values {
result[v] = struct{}{}
}
return result
}

View File

@ -16,20 +16,104 @@ func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) {
want *semver.Version
wantUpdated bool
}{
{"no update", true, version("0.0.0"), []GitCommitLog{}, version("0.0.0"), false},
{"no update without version", true, nil, []GitCommitLog{}, nil, false},
{"no update on unknown type", true, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{}, "a")}, version("0.0.0"), false},
{"no update on unmapped known type", false, version("0.0.0"), []GitCommitLog{commitlog("none", map[string]string{}, "a")}, version("0.0.0"), false},
{"update patch on unknown type", false, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{}, "a")}, version("0.0.1"), true},
{"patch update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}, "a")}, version("0.0.1"), true},
{"patch update without version", false, nil, []GitCommitLog{commitlog("patch", map[string]string{}, "a")}, nil, true},
{"minor update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}, "a"), commitlog("minor", map[string]string{}, "a")}, version("0.1.0"), true},
{"major update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}, "a"), commitlog("major", map[string]string{}, "a")}, version("1.0.0"), true},
{"breaking change update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}, "a"), commitlog("patch", map[string]string{"breaking-change": "break"}, "a")}, version("1.0.0"), true},
{
"no update",
true,
version("0.0.0"),
[]GitCommitLog{},
version("0.0.0"),
false,
},
{
"no update without version",
true,
nil,
[]GitCommitLog{},
nil,
false,
},
{
"no update on unknown type",
true,
version("0.0.0"),
[]GitCommitLog{commitlog("a", map[string]string{}, "a")},
version("0.0.0"),
false,
},
{
"no update on unmapped known type",
false,
version("0.0.0"),
[]GitCommitLog{commitlog("none", map[string]string{}, "a")},
version("0.0.0"),
false,
},
{
"update patch on unknown type",
false,
version("0.0.0"),
[]GitCommitLog{commitlog("a", map[string]string{}, "a")},
version("0.0.1"),
true,
},
{
"patch update",
false, version("0.0.0"),
[]GitCommitLog{commitlog("patch", map[string]string{}, "a")},
version("0.0.1"), true,
},
{
"patch update without version",
false,
nil,
[]GitCommitLog{commitlog("patch", map[string]string{}, "a")},
nil,
true,
},
{
"minor update",
false,
version("0.0.0"),
[]GitCommitLog{
commitlog("patch", map[string]string{}, "a"),
commitlog("minor", map[string]string{}, "a"),
},
version("0.1.0"),
true,
},
{
"major update",
false,
version("0.0.0"),
[]GitCommitLog{
commitlog("patch", map[string]string{}, "a"),
commitlog("major", map[string]string{}, "a"),
},
version("1.0.0"),
true,
},
{
"breaking change update",
false,
version("0.0.0"),
[]GitCommitLog{
commitlog("patch", map[string]string{}, "a"),
commitlog("patch", map[string]string{"breaking-change": "break"}, "a"),
},
version("1.0.0"),
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := NewSemVerCommitsProcessor(VersioningConfig{UpdateMajor: []string{"major"}, UpdateMinor: []string{"minor"}, UpdatePatch: []string{"patch"}, IgnoreUnknown: tt.ignoreUnknown}, CommitMessageConfig{Types: []string{"major", "minor", "patch", "none"}})
p := NewSemVerCommitsProcessor(
VersioningConfig{
UpdateMajor: []string{"major"},
UpdateMinor: []string{"minor"},
UpdatePatch: []string{"patch"},
IgnoreUnknown: tt.ignoreUnknown,
},
CommitMessageConfig{Types: []string{"major", "minor", "patch", "none"}})
got, gotUpdated := p.NextVersion(tt.version, tt.commits)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("SemVerCommitsProcessorImpl.NextVersion() Version = %v, want %v", got, tt.want)
@ -57,6 +141,7 @@ func TestToVersion(t *testing.T) {
got, err := ToVersion(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ToVersion() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {