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 /dist
bin/ /release
*.exe /git-sv*
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c` coverage.out
*.test CHANGELOG.md
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
*.sample
todo
# Additional generated artifacts
artifacts/
# Mac metadata
.DS_Store

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: linters:
enable-all: false
disable-all: true
enable: 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 - tagliatelle
- tenv
- testableexamples
- thelper
- tparallel
- unconvert
- unparam
- usestdlibvars
# - wastedassign
- whitespace
- wsl
fast: false
run: run:
skip-dirs: timeout: 3m
- build
- artifacts
linters-settings: linters-settings:
tagliatelle: tagliatelle:
case: case:
use-field-name: true
rules: rules:
json: camel
yaml: kebab yaml: kebab
xml: camel gofumpt:
bson: camel extra-rules: true
avro: snake lang-version: "1.21"
mapstructure: kebab
issues: issues:
exclude-rules: exclude-rules:
- path: _test\.go - path: (.+)_test.go
linters: linters:
- gocyclo
- errcheck
- dupl
- gosec - gosec
- gochecknoglobals - gochecknoglobals
- testpackage - prealloc
- path: cmd/git-sv/main.go
linters:
- gochecknoglobals
- funlen

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 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is copies of the Software, and to permit persons to whom the Software is furnished
furnished to do so, subject to the following conditions: to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all The above copyright notice and this permission notice (including the next
copies or substantial portions of the Software. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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 EXECUTABLE := git-sv
NO_COLOR=\033[0m
ERROR_COLOR=\033[31;01m
WARN_COLOR=\033[33;01m
PKGS = $(shell go list ./...) DIST := dist
BIN = git-sv 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") GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@$(GOFUMPT_PACKAGE_VERSION)
VERSION ?= dev-$(BUILD_TIME) GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_PACKAGE_VERSION)
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
BUILDOS ?= linux GENERATE ?=
BUILDARCH ?= amd64 XGO_VERSION := go-1.21.x
BUILDENVS ?= CGO_ENABLED=0 GOOS=$(BUILDOS) GOARCH=$(BUILDARCH) XGO_TARGETS ?= linux/amd64,linux/arm-6,linux/arm-7,linux/arm64
BUILDFLAGS ?= -a -installsuffix cgo --ldflags '-X main.Version=$(VERSION) -extldflags "-lm -lstdc++ -static"'
COMPRESS_TYPE ?= targz TARGETOS ?= linux
TARGETARCH ?= amd64
ifneq ("$(TARGETVARIANT)","")
GOARM ?= $(subst v,,$(TARGETVARIANT))
endif
TAGS ?= netgo,osusergo
usage: Makefile ifndef VERSION
@echo $(ECHOFLAGS) "to use make call:" ifneq ($(CI_COMMIT_TAG),)
@echo $(ECHOFLAGS) " make <action>" VERSION ?= $(subst v,,$(CI_COMMIT_TAG))
@echo $(ECHOFLAGS) "" else
@echo $(ECHOFLAGS) "list of available actions:" VERSION ?= $(shell git rev-parse --short HEAD)
@sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /' endif
## 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)
endif endif
## release-all: prepare linux, darwin and windows binary for release (requires sv4git) ifndef DATE
release-all: DATE := $(shell date -u +"%Y-%m-%dT%H:%M:%S%z")
@rm -rf bin endif
VERSION=$(shell git sv nv) BUILDOS=linux BUILDARCH=amd64 make release LDFLAGS += -s -w -X "main.BuildVersion=$(VERSION)" -X "main.BuildDate=$(DATE)"
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
VERSION=$(shell git sv nv) BUILDOS=linux BUILDARCH=arm64 make release .PHONY: all
VERSION=$(shell git sv nv) BUILDOS=darwin BUILDARCH=arm64 make release all: clean build
VERSION=$(shell git sv nv) COMPRESS_TYPE=zip BUILDOS=windows BUILDARCH=arm64 make release
.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"> # git-sv
<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> A command line tool (CLI) to validate commit messages, bump version, create tags and generate changelogs.
<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>
## Getting Started ## Getting Started
@ -23,23 +11,23 @@
### Installing ### Installing
- Download the latest release and add the binary to your path. - 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: If you want to install from source using `go install`, just run:
```bash ```bash
# keep in mind that with this, it will compile from source and won't show the version on cli -h. # 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. # 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 ### Config
#### YAML #### 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: To see the current config, run:
@ -59,22 +47,22 @@ git sv cfg default
###### User ###### 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 ```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.: And create a `config.yml` file inside it, eg.:
```bash ```bash
.sv4git .gitsv
└── config.yml └── config.yml
``` ```
###### Repository ###### 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 ##### 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 version: "1.1" #config version
versioning: # versioning bump versioning: # versioning bump
update-major: [] # Commit types used to bump major. update-major: [] # Commit types used to bump major.
update-minor: [feat] # Commit types used to bump minor. update-minor: [feat] # Commit types used to bump minor.
update-patch: [build, ci, chore, fix, perf, refactor, test] # Commit types used to bump patch. 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); # 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 # if ignore-unknown=false bump patch, if ignore-unknown=true do not bump version
ignore-unknown: false ignore-unknown: false
tag: tag:
pattern: '%d.%d.%d' # Pattern used to create git tag. pattern: "%d.%d.%d" # Pattern used to create git tag.
filter: '' # Enables you to filter for considerable tags using git pattern syntax filter: "" # Enables you to filter for considerable tags using git pattern syntax
release-notes: release-notes:
# Deprecated!!! please use 'sections' instead! # Deprecated!!! please use 'sections' instead!
# Headers names for release notes markdown. To disable a section just remove the header # 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 # 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. # respecting the following order: feat, fix, refactor, perf, test, build, ci, chore, docs, style, breaking-change.
headers: headers:
breaking-change: Breaking Changes breaking-change: Breaking Changes
feat: Features feat: Features
fix: Bug Fixes fix: Bug Fixes
sections: # Array with each section of release note. Check template section for more information. sections: # Array with each section of release note. Check template section for more information.
- name: Features # Name used on section. - name: Features # Name used on section.
section-type: commits # Type of the section, supported types: commits, breaking-changes. 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. commit-types: [feat] # Commit types for commit section-type, one commit type cannot be in more than one section.
- name: Bug Fixes - name: Bug Fixes
section-type: commits section-type: commits
commit-types: [fix] commit-types: [fix]
- name: Breaking Changes - name: Breaking Changes
section-type: breaking-changes section-type: breaking-changes
branches: # Git branches config. branches: # Git branches config.
prefix: ([a-z]+\/)? # Prefix used on branch name, it should be a regex group. 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. 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. 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: [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. skip-detached: false # Set true if a detached branch should be ignored on commit message validation.
commit-message: commit-message:
types: [build, ci, chore, docs, feat, fix, perf, refactor, revert, style, test] # Supported commit types. 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'. build,
scope: ci,
# Define supported scopes, if blank, scope will not be validated, if not, only scope listed will be valid. chore,
# Don't forget to add "" on your list if you need to define scopes and keep it optional. docs,
values: [] feat,
footer: fix,
issue: # Use "issue: {}" if you wish to disable issue footer. perf,
key: jira # Name used to define an issue on footer metadata. refactor,
key-synonyms: [Jira, JIRA] # Supported variations for footer metadata. revert,
use-hash: false # If false, use :<space> separator. If true, use <space># separator. style,
add-value-prefix: '' # Add a prefix to issue value. test,
issue: ] # Supported commit types.
regex: '[A-Z]+-[0-9]+' # Regex for issue id. 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 #### 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 ```bash
.sv4git .gitsv
└── templates └── templates
├── changelog-md.tpl ├── changelog-md.tpl
└── releasenotes-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 ##### 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. 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 | | section-type | ReleaseNoteSection |
| -- | -- | | ---------------- | -------------------------------- |
| commits | ReleaseNoteCommitsSection | | commits | ReleaseNoteCommitsSection |
| breaking-changes | ReleaseNoteBreakingChangeSection | | 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. > :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" "fmt"
"log" "log"
"os" "os"
"os/exec"
"reflect" "reflect"
"strings"
"github.com/bvieira/sv4git/v2/sv" "dario.cat/mergo"
"github.com/imdario/mergo"
"github.com/kelseyhightower/envconfig" "github.com/kelseyhightower/envconfig"
"github.com/thegeeklab/git-sv/v2/sv"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// EnvConfig env vars for cli configuration. // EnvConfig env vars for cli configuration.
type EnvConfig struct { type EnvConfig struct {
Home string `envconfig:"SV4GIT_HOME" default:""` Home string `envconfig:"GITSV_HOME" default:""`
} }
func loadEnvConfig() EnvConfig { func loadEnvConfig() EnvConfig {
var c EnvConfig var c EnvConfig
err := envconfig.Process("", &c) err := envconfig.Process("", &c)
if err != nil { if err != nil {
log.Fatal("failed to load env config, error: ", err.Error()) log.Fatal("failed to load env config, error: ", err.Error())
} }
return c return c
} }
@ -38,20 +38,6 @@ type Config struct {
CommitMessage sv.CommitMessageConfig `yaml:"commit-message"` 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) { func readConfig(filepath string) (Config, error) {
content, rerr := os.ReadFile(filepath) content, rerr := os.ReadFile(filepath)
if rerr != nil { if rerr != nil {
@ -59,9 +45,10 @@ func readConfig(filepath string) (Config, error) {
} }
var cfg Config var cfg Config
cerr := yaml.Unmarshal(content, &cfg) cerr := yaml.Unmarshal(content, &cfg)
if cerr != nil { 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 return cfg, nil
@ -71,6 +58,7 @@ func defaultConfig() Config {
skipDetached := false skipDetached := false
pattern := "%d.%d.%d" pattern := "%d.%d.%d"
filter := "" filter := ""
return Config{ return Config{
Version: "1.1", Version: "1.1",
Versioning: sv.VersioningConfig{ Versioning: sv.VersioningConfig{
@ -116,6 +104,7 @@ func merge(dst *Config, src Config) error {
dst.ReleaseNotes.Headers = src.ReleaseNotes.Headers dst.ReleaseNotes.Headers = src.ReleaseNotes.Headers
} }
} }
return err return err
} }
@ -127,6 +116,7 @@ func (t *mergeTransformer) Transformer(typ reflect.Type) func(dst, src reflect.V
if dst.CanSet() && !src.IsNil() { if dst.CanSet() && !src.IsNil() {
dst.Set(src) dst.Set(src)
} }
return nil return nil
} }
} }
@ -136,9 +126,11 @@ func (t *mergeTransformer) Transformer(typ reflect.Type) func(dst, src reflect.V
if dst.CanSet() && !src.IsNil() { if dst.CanSet() && !src.IsNil() {
dst.Set(src) dst.Set(src)
} }
return nil return nil
} }
} }
return nil return nil
} }
@ -146,6 +138,7 @@ func migrateConfig(cfg Config, filename string) Config {
if cfg.ReleaseNotes.Headers == nil { if cfg.ReleaseNotes.Headers == nil {
return cfg return cfg
} }
warnf("config 'release-notes.headers' on %s is deprecated, please use 'sections' instead!", filename) warnf("config 'release-notes.headers' on %s is deprecated, please use 'sections' instead!", filename)
return Config{ return Config{
@ -162,14 +155,29 @@ func migrateConfig(cfg Config, filename string) Config {
func migrateReleaseNotesConfig(headers map[string]string) []sv.ReleaseNotesSectionConfig { func migrateReleaseNotesConfig(headers map[string]string) []sv.ReleaseNotesSectionConfig {
order := []string{"feat", "fix", "refactor", "perf", "test", "build", "ci", "chore", "docs", "style"} order := []string{"feat", "fix", "refactor", "perf", "test", "build", "ci", "chore", "docs", "style"}
var sections []sv.ReleaseNotesSectionConfig var sections []sv.ReleaseNotesSectionConfig
for _, key := range order { for _, key := range order {
if name, exists := headers[key]; exists { 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 { 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 return sections
} }

View File

@ -4,7 +4,7 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/bvieira/sv4git/v2/sv" "github.com/thegeeklab/git-sv/v2/sv"
) )
func Test_merge(t *testing.T) { func Test_merge(t *testing.T) {
@ -20,24 +20,135 @@ func Test_merge(t *testing.T) {
want Config want Config
wantErr bool 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}, "overwrite pointer bool 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}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}},
Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}},
{"overwrite pointer bool false", Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}}, false}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}},
{"overwrite pointer bool true", Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}}, false}, 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}, "overwrite pointer bool true",
{"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}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}},
{"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}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}},
Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}},
{"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}, 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}, {
"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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View File

@ -2,6 +2,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -10,19 +11,32 @@ import (
"time" "time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/bvieira/sv4git/v2/sv" "github.com/thegeeklab/git-sv/v2/sv"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"gopkg.in/yaml.v3" "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 { func configDefaultHandler() func(c *cli.Context) error {
cfg := defaultConfig() cfg := defaultConfig()
return func(c *cli.Context) error { return func(c *cli.Context) error {
content, err := yaml.Marshal(&cfg) content, err := yaml.Marshal(&cfg)
if err != nil { if err != nil {
return err return err
} }
fmt.Println(string(content)) fmt.Println(string(content))
return nil return nil
} }
} }
@ -33,7 +47,9 @@ func configShowHandler(cfg Config) func(c *cli.Context) error {
if err != nil { if err != nil {
return err return err
} }
fmt.Println(string(content)) fmt.Println(string(content))
return nil return nil
} }
} }
@ -44,9 +60,11 @@ func currentVersionHandler(git sv.Git) func(c *cli.Context) error {
currentVer, err := sv.ToVersion(lastTag) currentVer, err := sv.ToVersion(lastTag)
if err != nil { 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()) fmt.Printf("%d.%d.%d\n", currentVer.Major(), currentVer.Minor(), currentVer.Patch())
return nil return nil
} }
} }
@ -57,30 +75,62 @@ func nextVersionHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) f
currentVer, err := sv.ToVersion(lastTag) currentVer, err := sv.ToVersion(lastTag)
if err != nil { 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, "")) commits, err := git.Log(sv.NewLogRange(sv.TagRange, lastTag, ""))
if err != nil { 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) nextVer, _ := semverProcessor.NextVersion(currentVer, commits)
fmt.Printf("%d.%d.%d\n", nextVer.Major(), nextVer.Minor(), nextVer.Patch()) fmt.Printf("%d.%d.%d\n", nextVer.Major(), nextVer.Minor(), nextVer.Patch())
return nil 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 { func commitLogHandler(git sv.Git) func(c *cli.Context) error {
return func(c *cli.Context) error { return func(c *cli.Context) error {
var commits []sv.GitCommitLog var (
var err error commits []sv.GitCommitLog
err error
)
tagFlag := c.String("t") tagFlag := c.String("t")
rangeFlag := c.String("r") rangeFlag := c.String("r")
startFlag := c.String("s") startFlag := c.String("s")
endFlag := c.String("e") endFlag := c.String("e")
if tagFlag != "" && (rangeFlag != string(sv.TagRange) || startFlag != "" || endFlag != "") { 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 != "" { if tagFlag != "" {
@ -92,8 +142,9 @@ func commitLogHandler(git sv.Git) func(c *cli.Context) error {
} }
commits, err = git.Log(r) commits, err = git.Log(r)
} }
if err != nil { 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 { for _, commit := range commits {
@ -101,8 +152,10 @@ func commitLogHandler(git sv.Git) func(c *cli.Context) error {
if err != nil { if err != nil {
return err return err
} }
fmt.Println(string(content)) fmt.Println(string(content))
} }
return nil return nil
} }
} }
@ -112,6 +165,7 @@ func getTagCommits(git sv.Git, tag string) ([]sv.GitCommitLog, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return git.Log(sv.NewLogRange(sv.TagRange, prev, tag)) 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): case string(sv.HashRange):
return sv.NewLogRange(sv.HashRange, startFlag, endFlag), nil return sv.NewLogRange(sv.HashRange, startFlag, endFlag), nil
default: 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 { return func(c *cli.Context) error {
var date time.Time var date time.Time
rangeFlag := c.String("r") rangeFlag := c.String("r")
lr, err := logRange(git, rangeFlag, c.String("s"), c.String("e")) lr, err := logRange(git, rangeFlag, c.String("s"), c.String("e"))
if err != nil { if err != nil {
return err return err
@ -140,7 +224,7 @@ func commitNotesHandler(git sv.Git, rnProcessor sv.ReleaseNoteProcessor, outputF
commits, err := git.Log(lr) commits, err := git.Log(lr)
if err != nil { 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 { 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)) output, err := outputFormatter.FormatReleaseNote(rnProcessor.Create(nil, "", date, commits))
if err != nil { 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) fmt.Println(output)
return nil 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 { return func(c *cli.Context) error {
var commits []sv.GitCommitLog var (
var rnVersion *semver.Version commits []sv.GitCommitLog
var tag string rnVersion *semver.Version
var date time.Time tag string
var err error date time.Time
err error
)
if tag = c.String("t"); tag != "" { if tag = c.String("t"); tag != "" {
rnVersion, date, commits, err = getTagVersionInfo(git, 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) releasenote := rnProcessor.Create(rnVersion, tag, date, commits)
output, err := outputFormatter.FormatReleaseNote(releasenote) output, err := outputFormatter.FormatReleaseNote(releasenote)
if err != nil { 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) fmt.Println(output)
return nil return nil
} }
} }
@ -190,12 +296,12 @@ func getTagVersionInfo(git sv.Git, tag string) (*semver.Version, time.Time, []sv
previousTag, currentTag, err := getTags(git, tag) previousTag, currentTag, err := getTags(git, tag)
if err != nil { 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)) commits, err := git.Log(sv.NewLogRange(sv.TagRange, previousTag, tag))
if err != nil { 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 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) index := find(tag, tags)
if index < 0 { 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 := "" previousTag := ""
if index > 0 { if index > 0 {
previousTag = tags[index-1].Name previousTag = tags[index-1].Name
} }
return previousTag, tags[index], nil return previousTag, tags[index], nil
} }
@ -225,15 +332,18 @@ func find(tag string, tags []sv.GitTag) int {
return i return i
} }
} }
return -1 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() lastTag := git.LastTag()
commits, err := git.Log(sv.NewLogRange(sv.TagRange, lastTag, "")) commits, err := git.Log(sv.NewLogRange(sv.TagRange, lastTag, ""))
if err != nil { 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) currentVer, _ := sv.ToVersion(lastTag)
@ -248,20 +358,23 @@ func tagHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) func(c *c
currentVer, err := sv.ToVersion(lastTag) currentVer, err := sv.ToVersion(lastTag)
if err != nil { 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, "")) commits, err := git.Log(sv.NewLogRange(sv.TagRange, lastTag, ""))
if err != nil { 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) nextVer, _ := semverProcessor.NextVersion(currentVer, commits)
tagname, err := git.Tag(*nextVer) tagname, err := git.Tag(*nextVer)
fmt.Println(tagname) fmt.Println(tagname)
if err != nil { 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 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) { func getCommitType(cfg Config, p sv.MessageProcessor, input string) (string, error) {
if input == "" { if input == "" {
t, err := promptType(cfg.CommitMessage.Types) t, err := promptType(cfg.CommitMessage.Types)
return t.Type, err return t.Type, err
} }
return input, p.ValidateType(input) return input, p.ValidateType(input)
} }
@ -278,6 +393,7 @@ func getCommitScope(cfg Config, p sv.MessageProcessor, input string, noScope boo
if input == "" && !noScope { if input == "" && !noScope {
return promptScope(cfg.CommitMessage.Scope.Values) return promptScope(cfg.CommitMessage.Scope.Values)
} }
return input, p.ValidateScope(input) return input, p.ValidateScope(input)
} }
@ -285,6 +401,7 @@ func getCommitDescription(p sv.MessageProcessor, input string) (string, error) {
if input == "" { if input == "" {
return promptSubject() return promptSubject()
} }
return input, p.ValidateDescription(input) return input, p.ValidateDescription(input)
} }
@ -294,17 +411,21 @@ func getCommitBody(noBody bool) (string, error) {
} }
var fullBody strings.Builder var fullBody strings.Builder
for body, err := promptBody(); body != "" || err != nil; body, err = promptBody() { for body, err := promptBody(); body != "" || err != nil; body, err = promptBody() {
if err != nil { if err != nil {
return "", err return "", err
} }
if fullBody.Len() > 0 { if fullBody.Len() > 0 {
fullBody.WriteString("\n") fullBody.WriteString("\n")
} }
if body != "" { if body != "" {
fullBody.WriteString(body) fullBody.WriteString(body)
} }
} }
return fullBody.String(), nil return fullBody.String(), nil
} }
@ -338,6 +459,7 @@ func getCommitBreakingChange(noBreaking bool, input string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
if !hasBreakingChanges { if !hasBreakingChanges {
return "", nil return "", nil
} }
@ -345,6 +467,51 @@ func getCommitBreakingChange(noBreaking bool, input string) (string, error) {
return promptBreakingChanges() 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 { func commitHandler(cfg Config, git sv.Git, messageProcessor sv.MessageProcessor) func(c *cli.Context) error {
return func(c *cli.Context) error { return func(c *cli.Context) error {
noBreaking := c.Bool("no-breaking") noBreaking := c.Bool("no-breaking")
@ -386,22 +553,54 @@ func commitHandler(cfg Config, git sv.Git, messageProcessor sv.MessageProcessor)
return err 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) err = git.Commit(header, body, footer)
if err != nil { 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 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 { return func(c *cli.Context) error {
tags, err := git.Tags() tags, err := git.Tags()
if err != nil { if err != nil {
return err return err
} }
sort.Slice(tags, func(i, j int) bool { sort.Slice(tags, func(i, j int) bool {
return tags[i].Date.After(tags[j].Date) return tags[i].Date.After(tags[j].Date)
}) })
@ -418,10 +617,12 @@ func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnP
if uerr != nil { if uerr != nil {
return uerr return uerr
} }
if updated { if updated {
releaseNotes = append(releaseNotes, rnProcessor.Create(rnVersion, "", date, commits)) releaseNotes = append(releaseNotes, rnProcessor.Create(rnVersion, "", date, commits))
} }
} }
for i, tag := range tags { for i, tag := range tags {
if !all && i >= size { if !all && i >= size {
break 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)) commits, err := git.Log(sv.NewLogRange(sv.TagRange, previousTag, tag.Name))
if err != nil { 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) currentVer, _ := sv.ToVersion(tag.Name)
@ -447,14 +648,35 @@ func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnP
output, err := formatter.FormatChangelog(releaseNotes) output, err := formatter.FormatChangelog(releaseNotes)
if err != nil { 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) fmt.Println(output)
return nil 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 { func validateCommitMessageHandler(git sv.Git, messageProcessor sv.MessageProcessor) func(c *cli.Context) error {
return func(c *cli.Context) error { return func(c *cli.Context) error {
branch := git.Branch() branch := git.Branch()
@ -462,11 +684,13 @@ func validateCommitMessageHandler(git sv.Git, messageProcessor sv.MessageProcess
if messageProcessor.SkipBranch(branch, derr == nil && detached) { if messageProcessor.SkipBranch(branch, derr == nil && detached) {
warnf("commit message validation skipped, branch in ignore list or detached...") warnf("commit message validation skipped, branch in ignore list or detached...")
return nil return nil
} }
if source := c.String("source"); source == "merge" { if source := c.String("source"); source == "merge" {
warnf("commit message validation skipped, ignoring source: %s...", source) warnf("commit message validation skipped, ignoring source: %s...", source)
return nil return nil
} }
@ -474,24 +698,26 @@ func validateCommitMessageHandler(git sv.Git, messageProcessor sv.MessageProcess
commitMessage, err := readFile(filepath) commitMessage, err := readFile(filepath)
if err != nil { 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 { 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) msg, err := messageProcessor.Enhance(branch, commitMessage)
if err != nil { if err != nil {
warnf("could not enhance commit message, %s", err.Error()) warnf("could not enhance commit message, %s", err.Error())
return nil return nil
} }
if msg == "" { if msg == "" {
return nil return nil
} }
if err := appendOnFile(msg, filepath); err != 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 return nil
@ -503,17 +729,19 @@ func readFile(filepath string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
return string(f), nil return string(f), nil
} }
func appendOnFile(message, filepath string) error { 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 { if err != nil {
return err return err
} }
defer f.Close() defer f.Close()
_, err = f.WriteString(message) _, err = f.WriteString(message)
return err return err
} }
@ -521,5 +749,6 @@ func str(value, defaultValue string) string {
if value != "" { if value != "" {
return value return value
} }
return defaultValue return defaultValue
} }

View File

@ -2,161 +2,146 @@ package main
import ( import (
"embed" "embed"
"fmt"
"io/fs" "io/fs"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"github.com/bvieira/sv4git/v2/sv" "github.com/thegeeklab/git-sv/v2/sv"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
// Version for git-sv. //nolint:gochecknoglobals
var Version = "source" var (
BuildVersion = "devel"
BuildDate = "00000000"
)
const ( const (
configFilename = "config.yml" configFilename = "config.yml"
repoConfigFilename = ".sv4git.yml" configDir = ".gitsv"
configDir = ".sv4git"
) )
var ( //go:embed resources/templates/*.tpl
//go:embed resources/templates/*.tpl var defaultTemplatesFS embed.FS
defaultTemplatesFS embed.FS
)
func templateFS(filepath string) fs.FS { func templateFS(filepath string) fs.FS {
if _, err := os.Stat(filepath); err != nil { if _, err := os.Stat(filepath); err != nil {
defaultTemplatesFS, _ := fs.Sub(defaultTemplatesFS, "resources/templates") defaultTemplatesFS, _ := fs.Sub(defaultTemplatesFS, "resources/templates")
return defaultTemplatesFS return defaultTemplatesFS
} }
return os.DirFS(filepath) return os.DirFS(filepath)
} }
func main() { func main() {
log.SetFlags(0) log.SetFlags(0)
repoPath, rerr := getRepoPath() wd, err := os.Getwd()
if rerr != nil { if err != nil {
log.Fatal("failed to discovery repository top level, error: ", rerr) log.Fatal("error while retrieving working directory: %w", err)
} }
cfg := loadCfg(repoPath) cfg := loadCfg(wd)
messageProcessor := sv.NewMessageProcessor(cfg.CommitMessage, cfg.Branches) messageProcessor := sv.NewMessageProcessor(cfg.CommitMessage, cfg.Branches)
git := sv.NewGit(messageProcessor, cfg.Tag) git := sv.NewGit(messageProcessor, cfg.Tag)
semverProcessor := sv.NewSemVerCommitsProcessor(cfg.Versioning, cfg.CommitMessage) semverProcessor := sv.NewSemVerCommitsProcessor(cfg.Versioning, cfg.CommitMessage)
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotes) 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() cli.VersionPrinter = func(c *cli.Context) {
app.Name = "sv" fmt.Printf("%s version=%s date=%s\n", c.App.Name, c.App.Version, BuildDate)
app.Version = Version }
app.Usage = "semantic version for git"
app.Commands = []*cli.Command{ app := &cli.App{
{ Name: "git-sv",
Name: "config", Usage: "Semantic version for git.",
Aliases: []string{"cfg"}, Version: BuildVersion,
Usage: "cli configuration", Commands: []*cli.Command{
Subcommands: []*cli.Command{ {
{ Name: "config",
Name: "default", Aliases: []string{"cfg"},
Usage: "show default config", Usage: "cli configuration",
Action: configDefaultHandler(), Subcommands: []*cli.Command{
}, {
{ Name: "default",
Name: "show", Usage: "show default config",
Usage: "show current config", Action: configDefaultHandler(),
Action: configShowHandler(cfg), },
{
Name: "show",
Usage: "show current config",
Action: configShowHandler(cfg),
},
}, },
}, },
}, {
{ Name: "current-version",
Name: "current-version", Aliases: []string{"cv"},
Aliases: []string{"cv"}, Usage: "get last released version from git",
Usage: "get last released version from git", Action: currentVersionHandler(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: "next-version",
Name: "commit-notes", Aliases: []string{"nv"},
Aliases: []string{"cn"}, Usage: "generate the next version based on git commit messages",
Usage: "generate a commit notes according to range", Action: nextVersionHandler(git, semverProcessor),
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: "commit-log",
Name: "release-notes", Aliases: []string{"cl"},
Aliases: []string{"rn"}, Usage: "list all commit logs according to range as jsons",
Usage: "generate release notes", Description: `The range filter is used based on git log filters, check https://git-scm.com/docs/git-log
Action: releaseNotesHandler(git, semverProcessor, releasenotesProcessor, outputFormatter), for more info. When flag range is "tag" and start is empty, last tag created will be used instead.
Flags: []cli.Flag{&cli.StringFlag{Name: "t", Aliases: []string{"tag"}, Usage: "get release note from tag"}}, When flag range is "date", if "end" is YYYY-MM-DD the range will be inclusive.`,
}, Action: commitLogHandler(git),
{ Flags: commitLogFlags(),
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-notes",
Name: "tag", Aliases: []string{"cn"},
Aliases: []string{"tg"}, Usage: "generate a commit notes according to range",
Usage: "generate tag with version based on git commit messages", Description: `The range filter is used based on git log filters, check https://git-scm.com/docs/git-log
Action: tagHandler(git, semverProcessor), 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),
Name: "commit", Flags: commitNotesFlags(),
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: "release-notes",
Name: "validate-commit-message", Aliases: []string{"rn"},
Aliases: []string{"vcm"}, Usage: "generate release notes",
Usage: "use as prepare-commit-message hook to validate and enhance commit message", Action: releaseNotesHandler(git, semverProcessor, releasenotesProcessor, outputFormatter),
Action: validateCommitMessageHandler(git, messageProcessor), Flags: releaseNotesFlags(),
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"}, Name: "changelog",
&cli.StringFlag{Name: "source", Required: true, Usage: "source of the commit message"}, 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() cfg := defaultConfig()
envCfg := loadEnvConfig() 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 repoCfg, err := readConfig(repoCfgFilepath); err == nil {
if merr := merge(&cfg, migrateConfig(repoCfg, repoCfgFilepath)); merr != nil { if merr := merge(&cfg, migrateConfig(repoCfg, repoCfgFilepath)); merr != nil {
log.Fatal("failed to merge repo config, error: ", merr) log.Fatal("failed to merge repo config, error: ", merr)
} }
if len(repoCfg.ReleaseNotes.Headers) > 0 { // mergo is merging maps, headers will be overwritten if len(repoCfg.ReleaseNotes.Headers) > 0 { // mergo is merging maps, headers will be overwritten
cfg.ReleaseNotes.Headers = repoCfg.ReleaseNotes.Headers cfg.ReleaseNotes.Headers = repoCfg.ReleaseNotes.Headers
} }

View File

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

View File

@ -14,9 +14,11 @@ func Test_checkTemplatesFiles(t *testing.T) {
got, err := defaultTemplatesFS.ReadFile(tt) got, err := defaultTemplatesFS.ReadFile(tt)
if err != nil { if err != nil {
t.Errorf("missing template error = %v", err) t.Errorf("missing template error = %v", err)
return return
} }
if len(got) <= 0 {
if len(got) == 0 {
t.Errorf("empty template") 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 go 1.19
require ( require (
github.com/Masterminds/semver/v3 v3.2.0 dario.cat/mergo v1.0.0
github.com/imdario/mergo v0.3.13 github.com/Masterminds/semver/v3 v3.2.1
github.com/kelseyhightower/envconfig v1.4.0 github.com/kelseyhightower/envconfig v1.4.0
github.com/manifoldco/promptui v0.9.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 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/chzyer/readline v1.5.1 // indirect 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/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/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // 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 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= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= 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.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 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 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 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 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.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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/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 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 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= 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 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 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/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.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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.24.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= 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 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 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-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.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 { if v, exists := c.Footer[issueMetadataKey]; exists {
return v return v
} }
return CommitMessageFooterConfig{} return CommitMessageFooterConfig{}
} }
@ -80,6 +81,7 @@ func (cfg ReleaseNotesConfig) sectionConfig(sectionType string) *ReleaseNotesSec
return &sectionCfg return &sectionCfg
} }
} }
return nil 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, "getenv": os.Getenv,
} }
tpls := template.Must(template.New("templates").Funcs(templateFNs).ParseFS(templatesFS, "*")) tpls := template.Must(template.New("templates").Funcs(templateFNs).ParseFS(templatesFS, "*"))
return &OutputFormatterImpl{templates: tpls} 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 { if err := p.templates.ExecuteTemplate(&b, "releasenotes-md.tpl", releaseNoteVariables(releasenote)); err != nil {
return "", err return "", err
} }
return b.String(), nil 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 { if err := p.templates.ExecuteTemplate(&b, "changelog-md.tpl", templateVars); err != nil {
return "", err return "", err
} }
return b.String(), nil return b.String(), nil
} }
@ -70,6 +73,7 @@ func releaseNoteVariables(releasenote ReleaseNote) releaseNoteTemplateVariables
if releasenote.Version != nil { if releasenote.Version != nil {
release = "v" + releasenote.Version.String() release = "v" + releasenote.Version.String()
} }
return releaseNoteTemplateVariables{ return releaseNoteTemplateVariables{
Release: release, Release: release,
Tag: releasenote.Tag, Tag: releasenote.Tag,
@ -83,10 +87,13 @@ func releaseNoteVariables(releasenote ReleaseNote) releaseNoteTemplateVariables
func toSortedArray(input map[string]struct{}) []string { func toSortedArray(input map[string]struct{}) []string {
result := make([]string, len(input)) result := make([]string, len(input))
i := 0 i := 0
for k := range input { for k := range input {
result[i] = k result[i] = k
i++ i++
} }
sort.Strings(result) sort.Strings(result)
return result return result
} }

View File

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

View File

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

View File

@ -3,7 +3,6 @@ package sv
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@ -83,17 +82,35 @@ func NewGit(messageProcessor MessageProcessor, cfg TagConfig) *GitImpl {
// LastTag get last tag, if no tag found, return empty. // LastTag get last tag, if no tag found, return empty.
func (g GitImpl) LastTag() string { 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() out, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return "" return ""
} }
return strings.TrimSpace(strings.Trim(string(out), "\n")) return strings.TrimSpace(strings.Trim(string(out), "\n"))
} }
// Log return git log. // Log return git log.
func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) { 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} params := []string{"log", "--date=short", format}
if lr.start != "" || lr.end != "" { if lr.start != "" || lr.end != "" {
@ -110,14 +127,17 @@ func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) {
} }
cmd := exec.Command("git", params...) cmd := exec.Command("git", params...)
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return nil, combinedOutputErr(err, out) return nil, combinedOutputErr(err, out)
} }
logs, parseErr := parseLogOutput(g.messageProcessor, string(out)) logs, parseErr := parseLogOutput(g.messageProcessor, string(out))
if parseErr != nil { if parseErr != nil {
return nil, parseErr return nil, parseErr
} }
return logs, nil 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 := exec.Command("git", "commit", "-m", header, "-m", "", "-m", body, "-m", "", "-m", footer)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
return cmd.Run() return cmd.Run()
} }
@ -143,45 +164,66 @@ func (g GitImpl) Tag(version semver.Version) (string, error) {
if out, err := pushCommand.CombinedOutput(); err != nil { if out, err := pushCommand.CombinedOutput(); err != nil {
return tag, combinedOutputErr(err, out) return tag, combinedOutputErr(err, out)
} }
return tag, nil return tag, nil
} }
// Tags list repository tags. // Tags list repository tags.
func (g GitImpl) Tags() ([]GitTag, error) { 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() out, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return nil, combinedOutputErr(err, out) return nil, combinedOutputErr(err, out)
} }
return parseTagsOutput(string(out)) return parseTagsOutput(string(out))
} }
// Branch get git branch. // Branch get git branch.
func (GitImpl) Branch() string { func (GitImpl) Branch() string {
cmd := exec.Command("git", "symbolic-ref", "--short", "HEAD") cmd := exec.Command("git", "symbolic-ref", "--short", "HEAD")
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return "" return ""
} }
return strings.TrimSpace(strings.Trim(string(out), "\n")) return strings.TrimSpace(strings.Trim(string(out), "\n"))
} }
// IsDetached check if is detached. // IsDetached check if is detached.
func (GitImpl) IsDetached() (bool, error) { func (GitImpl) IsDetached() (bool, error) {
cmd := exec.Command("git", "symbolic-ref", "-q", "HEAD") cmd := exec.Command("git", "symbolic-ref", "-q", "HEAD")
out, err := cmd.CombinedOutput() 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 == "" { if output == "" {
return true, nil return true, nil
} }
return false, errors.New(output)
return false, fmt.Errorf("%w: %s", errUnknownGitError, output)
} }
return false, nil return false, nil
} }
func parseTagsOutput(input string) ([]GitTag, error) { func parseTagsOutput(input string) ([]GitTag, error) {
scanner := bufio.NewScanner(strings.NewReader(input)) scanner := bufio.NewScanner(strings.NewReader(input))
var result []GitTag var result []GitTag
for scanner.Scan() { for scanner.Scan() {
if line := strings.TrimSpace(scanner.Text()); line != "" { if line := strings.TrimSpace(scanner.Text()); line != "" {
values := strings.Split(line, "#") values := strings.Split(line, "#")
@ -189,31 +231,35 @@ func parseTagsOutput(input string) ([]GitTag, error) {
result = append(result, GitTag{Name: values[1], Date: date}) result = append(result, GitTag{Name: values[1], Date: date})
} }
} }
return result, nil return result, nil
} }
func parseLogOutput(messageProcessor MessageProcessor, log string) ([]GitCommitLog, error) { func parseLogOutput(messageProcessor MessageProcessor, log string) ([]GitCommitLog, error) {
scanner := bufio.NewScanner(strings.NewReader(log)) scanner := bufio.NewScanner(strings.NewReader(log))
scanner.Split(splitAt([]byte(endLine))) scanner.Split(splitAt([]byte(endLine)))
var logs []GitCommitLog var logs []GitCommitLog
for scanner.Scan() { for scanner.Scan() {
if text := strings.TrimSpace(strings.Trim(scanner.Text(), "\"")); text != "" { if text := strings.TrimSpace(strings.Trim(scanner.Text(), "\"")); text != "" {
log, err := parseCommitLog(messageProcessor, text) log, err := parseCommitLog(messageProcessor, text)
if err != nil { if err != nil {
return nil, err return nil, err
} }
logs = append(logs, log) logs = append(logs, log)
} }
} }
return logs, nil return logs, nil
} }
func parseCommitLog(messageProcessor MessageProcessor, commit string) (GitCommitLog, error) { func parseCommitLog(messageProcessor MessageProcessor, commit string) (GitCommitLog, error) {
content := strings.Split(strings.Trim(commit, "\""), logSeparator) content := strings.Split(strings.Trim(commit, "\""), logSeparator)
timestamp, _ := strconv.Atoi(content[1]) timestamp, _ := strconv.Atoi(content[1])
message, err := messageProcessor.Parse(content[4], content[5])
message, err := messageProcessor.Parse(content[4], content[5])
if err != nil { if err != nil {
return GitCommitLog{}, err 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) { 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) { return func(data []byte, atEOF bool) (advance int, token []byte, err error) { //nolint:nonamedreturns
dataLen := len(data) if atEOF && len(data) == 0 {
if atEOF && dataLen == 0 {
return 0, nil, nil return 0, nil, nil
} }
@ -240,7 +284,7 @@ func splitAt(b []byte) func(data []byte, atEOF bool) (advance int, token []byte,
} }
if atEOF { if atEOF {
return dataLen, data, nil return len(data), data, nil
} }
return 0, nil, nil return 0, nil, nil
@ -264,10 +308,12 @@ func str(value, defaultValue string) string {
if value != "" { if value != "" {
return value return value
} }
return defaultValue return defaultValue
} }
func combinedOutputErr(err error, out []byte) error { func combinedOutputErr(err error, out []byte) error {
msg := strings.Split(string(out), "\n") 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 want []GitTag
wantErr bool 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := parseTagsOutput(tt.input) got, err := parseTagsOutput(tt.input)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("parseTagsOutput() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("parseTagsOutput() error = %v, wantErr %v", err, tt.wantErr)
return return
} }
if !reflect.DeepEqual(got, tt.want) { if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseTagsOutput() = %v, want %v", 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 { if err != nil {
panic(err) panic(err)
} }
return t return t
} }

View File

@ -8,6 +8,7 @@ import (
func version(v string) *semver.Version { func version(v string) *semver.Version {
r, _ := semver.NewVersion(v) r, _ := semver.NewVersion(v)
return r return r
} }
@ -16,6 +17,7 @@ func commitlog(ctype string, metadata map[string]string, author string) GitCommi
if _, found := metadata[breakingChangeMetadataKey]; found { if _, found := metadata[breakingChangeMetadataKey]; found {
breaking = true breaking = true
} }
return GitCommitLog{ return GitCommitLog{
Message: CommitMessage{ Message: CommitMessage{
Type: ctype, 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{ return ReleaseNote{
Version: version, Version: version,
Tag: tag, Tag: tag,

View File

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

View File

@ -146,23 +146,55 @@ func TestMessageProcessorImpl_Validate(t *testing.T) {
message string message string
wantErr bool 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 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 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 {
"multi line valid message",
team: x`, false}, ccfg,
`feat: add something
{"multi line invalid message", ccfg, `feat add something team: x`, false,
},
team: x`, true},
{"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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -181,9 +213,21 @@ func TestMessageProcessorImpl_ValidateType(t *testing.T) {
ctype string ctype string
wantErr bool wantErr bool
}{ }{
{"valid type", ccfg, "feat", false}, {
{"invalid type", ccfg, "aaa", true}, "valid type",
{"empty type", ccfg, "", true}, ccfg,
"feat", false,
},
{
"invalid type",
ccfg,
"aaa", true,
},
{
"empty type",
ccfg,
"", true,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -202,7 +246,11 @@ func TestMessageProcessorImpl_ValidateScope(t *testing.T) {
scope string scope string
wantErr bool wantErr bool
}{ }{
{"any scope", ccfg, "aaa", false}, {
"any scope",
ccfg,
"aaa", false,
},
{"valid scope with scope list", ccfgWithScope, "scope", false}, {"valid scope with scope list", ccfgWithScope, "scope", false},
{"invalid scope with scope list", ccfgWithScope, "aaa", true}, {"invalid scope with scope list", ccfgWithScope, "aaa", true},
} }
@ -223,11 +271,31 @@ func TestMessageProcessorImpl_ValidateDescription(t *testing.T) {
description string description string
wantErr bool wantErr bool
}{ }{
{"empty description", ccfg, "", true}, {
{"sigle letter description", ccfg, "a", false}, "empty description",
{"number description", ccfg, "1", true}, ccfg,
{"valid description", ccfg, "add some feature", false}, "", true,
{"invalid capital letter description", ccfg, "Add some feature", 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -248,24 +316,73 @@ func TestMessageProcessorImpl_Enhance(t *testing.T) {
want string want string
wantErr bool 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",
{"issue on branch name with prefix", ccfg, "feature/JIRA-123", "fix: fix something", "\njira: JIRA-123", false}, ccfg,
{"with footer", ccfg, "JIRA-123", fullMessage, "jira: JIRA-123", false}, "JIRA-123", "fix: fix something", "\njira: 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}, "issue on branch name with description",
{"unexpected branch name", ccfg, "feature /JIRA-123", "fix: fix something", "", true}, ccfg,
{"issue on branch name using hash", ccfgHash, "JIRA-123-some-description", "fix: fix something", "\njira #JIRA-123", false}, "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 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := NewMessageProcessor(tt.cfg, newBranchCfg(false)).Enhance(tt.branch, tt.message) got, err := NewMessageProcessor(tt.cfg, newBranchCfg(false)).Enhance(tt.branch, tt.message)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("MessageProcessorImpl.Enhance() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("MessageProcessorImpl.Enhance() error = %v, wantErr %v", err, tt.wantErr)
return return
} }
if got != tt.want { if got != tt.want {
@ -296,6 +413,7 @@ func TestMessageProcessorImpl_IssueID(t *testing.T) {
got, err := p.IssueID(tt.branch) got, err := p.IssueID(tt.branch)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("MessageProcessorImpl.IssueID() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("MessageProcessorImpl.IssueID() error = %v, wantErr %v", err, tt.wantErr)
return return
} }
if got != tt.want { if got != tt.want {
@ -326,19 +444,19 @@ func Test_hasIssueID(t *testing.T) {
}{ }{
{"single line without issue", "feat: something", cfgColon, false}, {"single line without issue", "feat: something", cfgColon, false},
{"multi line without issue", `feat: something {"multi line without issue", `feat: something
yay`, cfgColon, false}, yay`, cfgColon, false},
{"multi line without jira issue", `feat: something {"multi line without jira issue", `feat: something
jira1: JIRA-123`, cfgColon, false}, jira1: JIRA-123`, cfgColon, false},
{"multi line with issue", `feat: something {"multi line with issue", `feat: something
jira: JIRA-123`, cfgColon, true}, jira: JIRA-123`, cfgColon, true},
{"multi line with issue and hash", `feat: something {"multi line with issue and hash", `feat: something
jira #JIRA-123`, cfgHash, true}, jira #JIRA-123`, cfgHash, true},
{"empty config", `feat: something {"empty config", `feat: something
jira #JIRA-123`, cfgEmpty, false}, jira #JIRA-123`, cfgEmpty, false},
} }
for _, tt := range tests { for _, tt := range tests {
@ -378,8 +496,10 @@ var completeBody = `some descriptions
jira: JIRA-123 jira: JIRA-123
BREAKING CHANGE: this change breaks everything` BREAKING CHANGE: this change breaks everything`
var bodyWithCarriage = "some description\r\nmore description\r\n\r\njira: JIRA-123\r" var (
var expectedBodyWithCarriage = "some description\nmore description\n\njira: JIRA-123" 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 var issueOnlyBody = `some descriptions
@ -402,20 +522,145 @@ func TestMessageProcessorImpl_Parse(t *testing.T) {
body string body string
want CommitMessage 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{}}}, "simple message",
{"unmapped type", ccfg, "unkn: something unknown", "", CommitMessage{Type: "unkn", Scope: "", Description: "something unknown", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}}, ccfg,
{"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"}}}, "feat: something awesome", "",
{"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"}}}, CommitMessage{
{"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"}}}, Type: "feat",
{"breaking change with exclamation mark", ccfg, "feat!: something new", "", CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: "", IsBreakingChange: true, Metadata: map[string]string{}}}, Scope: "",
{"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"}}}, Description: "something awesome",
{"empty issue cfg", ccfgEmptyIssue, "feat: something new", hashMetadataBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: hashMetadataBody, IsBreakingChange: false, Metadata: map[string]string{}}}, Body: "",
{"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"}}}, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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) t.Errorf("MessageProcessorImpl.Parse() = [%+v], want [%+v]", got, tt.want)
} }
}) })
@ -431,18 +676,102 @@ func TestMessageProcessorImpl_Format(t *testing.T) {
wantBody string wantBody string
wantFooter string wantFooter string
}{ }{
{"simple message", ccfg, NewCommitMessage("feat", "", "something", "", "", ""), "feat: something", "", ""}, {
{"with issue", ccfg, NewCommitMessage("feat", "", "something", "", "JIRA-123", ""), "feat: something", "", "jira: JIRA-123"}, "simple message",
{"with issue using hash", ccfgHash, NewCommitMessage("feat", "", "something", "", "JIRA-123", ""), "feat: something", "", "jira #JIRA-123"}, ccfg,
{"with issue using double hash", ccfgHash, NewCommitMessage("feat", "", "something", "", "#JIRA-123", ""), "feat: something", "", "jira #JIRA-123"}, NewCommitMessage("feat", "", "something", "", "", ""),
{"with breaking change", ccfg, NewCommitMessage("feat", "", "something", "", "", "breaks"), "feat: something", "", "BREAKING CHANGE: breaks"}, "feat: something",
{"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",
{"with issue and issue prefix", ccfgGitIssue, NewCommitMessage("feat", "", "something", "", "123", ""), "feat: something", "", "issue: #123"}, ccfg,
{"with #issue and issue prefix", ccfgGitIssue, NewCommitMessage("feat", "", "something", "", "#123", ""), "feat: something", "", "issue: #123"}, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -532,14 +861,61 @@ func Test_prepareHeader(t *testing.T) {
wantHeader string wantHeader string
wantError bool wantError bool
}{ }{
{"conventional without selector", "", "feat: something", "feat: something", false}, {
{"conventional with scope without selector", "", "feat(scope): something", "feat(scope): something", false}, "conventional without selector",
{"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}, "feat: something",
{"matching non-conventional with selector with group", "Merged PR (\\d+): (?P<header>.*)", "Merged PR 123: something", "something", false}, "feat: something",
{"matching non-conventional with selector without group", "Merged PR (\\d+): (.*)", "Merged PR 123: something", "", true}, false,
{"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 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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. // 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) mapping := commitSectionMapping(p.cfg.Sections)
sections := make(map[string]ReleaseNoteCommitsSection) sections := make(map[string]ReleaseNoteCommitsSection)
authors := make(map[string]struct{}) authors := make(map[string]struct{})
var breakingChanges []string var breakingChanges []string
for _, commit := range commits { for _, commit := range commits {
authors[commit.AuthorName] = struct{}{} authors[commit.AuthorName] = struct{}{}
if sectionCfg, exists := mapping[commit.Message.Type]; exists { if sectionCfg, exists := mapping[commit.Message.Type]; exists {
section, sexists := sections[sectionCfg.Name] section, sexists := sections[sectionCfg.Name]
if !sexists { if !sexists {
section = ReleaseNoteCommitsSection{Name: sectionCfg.Name, Types: sectionCfg.CommitTypes} section = ReleaseNoteCommitsSection{Name: sectionCfg.Name, Types: sectionCfg.CommitTypes}
} }
section.Items = append(section.Items, commit) section.Items = append(section.Items, commit)
sections[sectionCfg.Name] = section sections[sectionCfg.Name] = section
} }
if commit.Message.BreakingMessage() != "" { if commit.Message.BreakingMessage() != "" {
// TODO: if no message found, should use description instead? // TODO: if no message found, should use description instead?
breakingChanges = append(breakingChanges, commit.Message.BreakingMessage()) 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 { if bcCfg := p.cfg.sectionConfig(ReleaseNotesSectionTypeBreakingChanges); bcCfg != nil && len(breakingChanges) > 0 {
breakingChangeSection = ReleaseNoteBreakingChangeSection{Name: bcCfg.Name, Messages: breakingChanges} 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 hasBreaking := 0
if breakingChange.Name != "" { if breakingChange.Name != "" {
hasBreaking = 1 hasBreaking = 1
@ -59,11 +79,13 @@ func (p ReleaseNoteProcessorImpl) toReleaseNoteSections(commitSections map[strin
sections := make([]ReleaseNoteSection, len(commitSections)+hasBreaking) sections := make([]ReleaseNoteSection, len(commitSections)+hasBreaking)
i := 0 i := 0
for _, cfg := range p.cfg.Sections { for _, cfg := range p.cfg.Sections {
if cfg.SectionType == ReleaseNotesSectionTypeBreakingChanges && hasBreaking > 0 { if cfg.SectionType == ReleaseNotesSectionTypeBreakingChanges && hasBreaking > 0 {
sections[i] = breakingChange sections[i] = breakingChange
i++ i++
} }
if s, exists := commitSections[cfg.Name]; cfg.SectionType == ReleaseNotesSectionTypeCommits && exists { if s, exists := commitSections[cfg.Name]; cfg.SectionType == ReleaseNotesSectionTypeCommits && exists {
sections[i] = s sections[i] = s
i++ i++
@ -75,6 +97,7 @@ func (p ReleaseNoteProcessorImpl) toReleaseNoteSections(commitSections map[strin
func commitSectionMapping(sections []ReleaseNotesSectionConfig) map[string]ReleaseNotesSectionConfig { func commitSectionMapping(sections []ReleaseNotesSectionConfig) map[string]ReleaseNotesSectionConfig {
mapping := make(map[string]ReleaseNotesSectionConfig) mapping := make(map[string]ReleaseNotesSectionConfig)
for _, section := range sections { for _, section := range sections {
if section.SectionType == ReleaseNotesSectionTypeCommits { if section.SectionType == ReleaseNotesSectionTypeCommits {
for _, commitType := range section.CommitTypes { for _, commitType := range section.CommitTypes {
@ -82,6 +105,7 @@ func commitSectionMapping(sections []ReleaseNotesSectionConfig) map[string]Relea
} }
} }
} }
return mapping return mapping
} }

View File

@ -25,7 +25,15 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
tag: "v1.0.0", tag: "v1.0.0",
date: date, date: date,
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a")}, 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", name: "unmapped tag",
@ -33,28 +41,71 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
tag: "v1.0.0", tag: "v1.0.0",
date: date, date: date,
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a"), commitlog("unmapped", map[string]string{}, "a")}, 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", name: "breaking changes tag",
version: semver.MustParse("1.0.0"), version: semver.MustParse("1.0.0"),
tag: "v1.0.0", tag: "v1.0.0",
date: date, date: date,
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a"), commitlog("unmapped", map[string]string{"breaking-change": "breaks"}, "a")}, commits: []GitCommitLog{
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": {}}), 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", name: "multiple authors",
version: semver.MustParse("1.0.0"), version: semver.MustParse("1.0.0"),
tag: "v1.0.0", tag: "v1.0.0",
date: date, date: date,
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "author3"), commitlog("t1", map[string]string{}, "author2"), commitlog("t1", map[string]string{}, "author1")}, commits: []GitCommitLog{
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": {}}), 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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) { 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) 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. // IsValidVersion return true when a version is valid.
func IsValidVersion(value string) bool { func IsValidVersion(value string) bool {
_, err := semver.NewVersion(value) _, err := semver.NewVersion(value)
return err == nil return err == nil
} }
@ -23,6 +24,7 @@ func ToVersion(value string) (*semver.Version, error) {
if version == "" { if version == "" {
version = "0.0.0" version = "0.0.0"
} }
return semver.NewVersion(version) return semver.NewVersion(version)
} }
@ -52,7 +54,9 @@ func NewSemVerCommitsProcessor(vcfg VersioningConfig, mcfg CommitMessageConfig)
} }
// NextVersion calculates next version based on commit log. // 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 versionToUpdate := none
for _, commit := range commits { for _, commit := range commits {
if v := p.versionTypeToUpdate(commit); v > versionToUpdate { if v := p.versionTypeToUpdate(commit); v > versionToUpdate {
@ -64,7 +68,9 @@ func (p SemVerCommitsProcessorImpl) NextVersion(version *semver.Version, commits
if version == nil { if version == nil {
return nil, updated return nil, updated
} }
newVersion := updateVersion(*version, versionToUpdate) newVersion := updateVersion(*version, versionToUpdate)
return &newVersion, updated return &newVersion, updated
} }
@ -85,18 +91,23 @@ func (p SemVerCommitsProcessorImpl) versionTypeToUpdate(commit GitCommitLog) ver
if commit.Message.IsBreakingChange { if commit.Message.IsBreakingChange {
return major return major
} }
if _, exists := p.MajorVersionTypes[commit.Message.Type]; exists { if _, exists := p.MajorVersionTypes[commit.Message.Type]; exists {
return major return major
} }
if _, exists := p.MinorVersionTypes[commit.Message.Type]; exists { if _, exists := p.MinorVersionTypes[commit.Message.Type]; exists {
return minor return minor
} }
if _, exists := p.PatchVersionTypes[commit.Message.Type]; exists { if _, exists := p.PatchVersionTypes[commit.Message.Type]; exists {
return patch return patch
} }
if !contains(commit.Message.Type, p.KnownTypes) && p.IncludeUnknownTypeAsPatch { if !contains(commit.Message.Type, p.KnownTypes) && p.IncludeUnknownTypeAsPatch {
return patch return patch
} }
return none return none
} }
@ -105,5 +116,6 @@ func toMap(values []string) map[string]struct{} {
for _, v := range values { for _, v := range values {
result[v] = struct{}{} result[v] = struct{}{}
} }
return result return result
} }

View File

@ -16,20 +16,104 @@ func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) {
want *semver.Version want *semver.Version
wantUpdated bool 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",
{"no update on unknown type", true, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{}, "a")}, version("0.0.0"), false}, true,
{"no update on unmapped known type", false, version("0.0.0"), []GitCommitLog{commitlog("none", map[string]string{}, "a")}, version("0.0.0"), false}, version("0.0.0"),
{"update patch on unknown type", false, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{}, "a")}, version("0.0.1"), true}, []GitCommitLog{},
{"patch update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}, "a")}, version("0.0.1"), true}, version("0.0.0"),
{"patch update without version", false, nil, []GitCommitLog{commitlog("patch", map[string]string{}, "a")}, nil, true}, false,
{"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 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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) got, gotUpdated := p.NextVersion(tt.version, tt.commits)
if !reflect.DeepEqual(got, tt.want) { if !reflect.DeepEqual(got, tt.want) {
t.Errorf("SemVerCommitsProcessorImpl.NextVersion() Version = %v, want %v", 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) got, err := ToVersion(tt.input)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("ToVersion() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("ToVersion() error = %v, wantErr %v", err, tt.wantErr)
return return
} }
if !reflect.DeepEqual(got, tt.want) { if !reflect.DeepEqual(got, tt.want) {