mirror of
https://github.com/thegeeklab/git-sv.git
synced 2024-11-21 01:50:40 +00:00
initial commit after fork
This commit is contained in:
parent
20e64f8fcf
commit
00e2fe31e5
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@ -1,6 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
71
.github/settings.yml
vendored
Normal file
71
.github/settings.yml
vendored
Normal 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
|
102
.github/workflows/ci.yml
vendored
102
.github/workflows/ci.yml
vendored
@ -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_*
|
34
.github/workflows/pull-request.yml
vendored
34
.github/workflows/pull-request.yml
vendored
@ -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
27
.gitignore
vendored
@ -1,23 +1,6 @@
|
||||
# Binaries for programs and plugins
|
||||
bin/
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
/dist
|
||||
/release
|
||||
/git-sv*
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
*.sample
|
||||
todo
|
||||
|
||||
# Additional generated artifacts
|
||||
artifacts/
|
||||
|
||||
# Mac metadata
|
||||
|
||||
.DS_Store
|
||||
coverage.out
|
||||
CHANGELOG.md
|
||||
|
47
.gitsv/config.yml
Normal file
47
.gitsv/config.yml
Normal 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]+"
|
110
.golangci.yml
110
.golangci.yml
@ -1,35 +1,109 @@
|
||||
linters:
|
||||
enable-all: false
|
||||
disable-all: true
|
||||
enable:
|
||||
- errcheck
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- typecheck
|
||||
- unused
|
||||
- asasalint
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- containedctx
|
||||
- contextcheck
|
||||
- decorder
|
||||
- dogsled
|
||||
- dupl
|
||||
- dupword
|
||||
- durationcheck
|
||||
- errchkjson
|
||||
- errname
|
||||
- errorlint
|
||||
- execinquery
|
||||
# - exhaustive
|
||||
- exportloopref
|
||||
- forcetypeassert
|
||||
- ginkgolinter
|
||||
- gocheckcompilerdirectives
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
# - godox
|
||||
- goerr113
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goheader
|
||||
- goimports
|
||||
- gomnd
|
||||
- gomoddirectives
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- grouper
|
||||
- importas
|
||||
- interfacebloat
|
||||
- ireturn
|
||||
- lll
|
||||
- loggercheck
|
||||
- maintidx
|
||||
- makezero
|
||||
- misspell
|
||||
- musttag
|
||||
- nakedret
|
||||
- nestif
|
||||
- nilerr
|
||||
- nilnil
|
||||
- nlreturn
|
||||
- noctx
|
||||
- nolintlint
|
||||
- nonamedreturns
|
||||
- nosprintfhostport
|
||||
- prealloc
|
||||
- predeclared
|
||||
- promlinter
|
||||
- reassign
|
||||
- revive
|
||||
# - rowserrcheck
|
||||
# - sqlclosecheck
|
||||
# - structcheck
|
||||
- stylecheck
|
||||
- tagliatelle
|
||||
- tenv
|
||||
- testableexamples
|
||||
- thelper
|
||||
- tparallel
|
||||
- unconvert
|
||||
- unparam
|
||||
- usestdlibvars
|
||||
# - wastedassign
|
||||
- whitespace
|
||||
- wsl
|
||||
fast: false
|
||||
|
||||
run:
|
||||
skip-dirs:
|
||||
- build
|
||||
- artifacts
|
||||
timeout: 3m
|
||||
|
||||
linters-settings:
|
||||
tagliatelle:
|
||||
case:
|
||||
use-field-name: true
|
||||
rules:
|
||||
json: camel
|
||||
yaml: kebab
|
||||
xml: camel
|
||||
bson: camel
|
||||
avro: snake
|
||||
mapstructure: kebab
|
||||
gofumpt:
|
||||
extra-rules: true
|
||||
lang-version: "1.21"
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
- path: (.+)_test.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- errcheck
|
||||
- dupl
|
||||
- gosec
|
||||
- gochecknoglobals
|
||||
- testpackage
|
||||
- path: cmd/git-sv/main.go
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- funlen
|
||||
- prealloc
|
||||
|
6
.markdownlint.yml
Normal file
6
.markdownlint.yml
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
default: True
|
||||
MD013: False
|
||||
MD041: False
|
||||
MD004:
|
||||
style: dash
|
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@ -0,0 +1,2 @@
|
||||
*.tpl.md
|
||||
LICENSE
|
31
.sv4git.yml
31
.sv4git.yml
@ -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]+"
|
66
.woodpecker/build-container.yml
Normal file
66
.woodpecker/build-container.yml
Normal 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
|
44
.woodpecker/build-package.yml
Normal file
44
.woodpecker/build-package.yml
Normal 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
72
.woodpecker/docs.yml
Normal 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
26
.woodpecker/notify.yml
Normal 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
17
.woodpecker/test.yml
Normal 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
25
Containerfile.multiarch
Normal 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
22
LICENSE
@ -1,21 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Beatriz Vieira
|
||||
Copyright (c) 2022 Robert Kaussow <mail@thegeeklab.de>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The above copyright notice and this permission notice (including the next
|
||||
paragraph) shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
168
Makefile
168
Makefile
@ -1,89 +1,101 @@
|
||||
.PHONY: usage build lint lint-autofix test test-coverage test-show-coverage run tidy release release-all
|
||||
# renovate: datasource=github-releases depName=mvdan/gofumpt
|
||||
GOFUMPT_PACKAGE_VERSION := v0.5.0
|
||||
# renovate: datasource=github-releases depName=golangci/golangci-lint
|
||||
GOLANGCI_LINT_PACKAGE_VERSION := v1.54.2
|
||||
|
||||
OK_COLOR=\033[32;01m
|
||||
NO_COLOR=\033[0m
|
||||
ERROR_COLOR=\033[31;01m
|
||||
WARN_COLOR=\033[33;01m
|
||||
EXECUTABLE := git-sv
|
||||
|
||||
PKGS = $(shell go list ./...)
|
||||
BIN = git-sv
|
||||
DIST := dist
|
||||
DIST_DIRS := $(DIST)
|
||||
IMPORT := github.com/thegeeklab/$(EXECUTABLE)
|
||||
|
||||
ECHOFLAGS ?=
|
||||
GO ?= go
|
||||
CWD ?= $(shell pwd)
|
||||
PACKAGES ?= $(shell go list ./...)
|
||||
SOURCES ?= $(shell find . -name "*.go" -type f)
|
||||
|
||||
BUILD_TIME = $(shell date +"%Y%m%d%H%M")
|
||||
VERSION ?= dev-$(BUILD_TIME)
|
||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@$(GOFUMPT_PACKAGE_VERSION)
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_PACKAGE_VERSION)
|
||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||
|
||||
BUILDOS ?= linux
|
||||
BUILDARCH ?= amd64
|
||||
BUILDENVS ?= CGO_ENABLED=0 GOOS=$(BUILDOS) GOARCH=$(BUILDARCH)
|
||||
BUILDFLAGS ?= -a -installsuffix cgo --ldflags '-X main.Version=$(VERSION) -extldflags "-lm -lstdc++ -static"'
|
||||
GENERATE ?=
|
||||
XGO_VERSION := go-1.21.x
|
||||
XGO_TARGETS ?= linux/amd64,linux/arm-6,linux/arm-7,linux/arm64
|
||||
|
||||
COMPRESS_TYPE ?= targz
|
||||
TARGETOS ?= linux
|
||||
TARGETARCH ?= amd64
|
||||
ifneq ("$(TARGETVARIANT)","")
|
||||
GOARM ?= $(subst v,,$(TARGETVARIANT))
|
||||
endif
|
||||
TAGS ?= netgo,osusergo
|
||||
|
||||
usage: Makefile
|
||||
@echo $(ECHOFLAGS) "to use make call:"
|
||||
@echo $(ECHOFLAGS) " make <action>"
|
||||
@echo $(ECHOFLAGS) ""
|
||||
@echo $(ECHOFLAGS) "list of available actions:"
|
||||
@sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'
|
||||
|
||||
## build: build git-sv
|
||||
build: test
|
||||
@echo $(ECHOFLAGS) "$(OK_COLOR)==> Building binary ($(BUILDOS)/$(BUILDARCH)/$(BIN))...$(NO_COLOR)"
|
||||
@$(BUILDENVS) go build -v $(BUILDFLAGS) -o bin/$(BUILDOS)_$(BUILDARCH)/$(BIN) ./cmd/git-sv
|
||||
|
||||
## lint: run golangci-lint without autofix
|
||||
lint:
|
||||
@echo $(ECHOFLAGS) "$(OK_COLOR)==> Running golangci-lint...$(NO_COLOR)"
|
||||
@golangci-lint run ./... --config .golangci.yml
|
||||
|
||||
## lint-autofix: run golangci-lint with autofix enabled
|
||||
lint-autofix:
|
||||
@echo $(ECHOFLAGS) "$(OK_COLOR)==> Running golangci-lint...$(NO_COLOR)"
|
||||
@golangci-lint run ./... --config .golangci.yml --fix
|
||||
|
||||
## test: run unit tests
|
||||
test:
|
||||
@echo $(ECHOFLAGS) "$(OK_COLOR)==> Running tests...$(NO_COLOR)"
|
||||
@go test $(PKGS)
|
||||
|
||||
## test-coverage: run tests with coverage
|
||||
test-coverage:
|
||||
@echo $(ECHOFLAGS) "$(OK_COLOR)==> Running tests with coverage...$(NO_COLOR)"
|
||||
@go test -race -covermode=atomic -coverprofile coverage.out ./...
|
||||
|
||||
## test-show-coverage: show coverage
|
||||
test-show-coverage: test-coverage
|
||||
@echo $(ECHOFLAGS) "$(OK_COLOR)==> Show test coverage...$(NO_COLOR)"
|
||||
@go tool cover -html coverage.out
|
||||
|
||||
## run: run git-sv
|
||||
run:
|
||||
@echo $(ECHOFLAGS) "$(OK_COLOR)==> Running bin/$(BUILDOS)_$(BUILDARCH)/$(BIN)...$(NO_COLOR)"
|
||||
@./bin/$(BUILDOS)_$(BUILDARCH)/$(BIN) $(args)
|
||||
|
||||
## tidy: execute go mod tidy
|
||||
tidy:
|
||||
@echo $(ECHOFLAGS) "$(OK_COLOR)==> runing tidy"
|
||||
@go mod tidy
|
||||
|
||||
## release: prepare binary for release
|
||||
release:
|
||||
make build
|
||||
ifeq ($(COMPRESS_TYPE), zip)
|
||||
@zip -j bin/git-sv_$(VERSION)_$(BUILDOS)_$(BUILDARCH).zip bin/$(BUILDOS)_$(BUILDARCH)/$(BIN)
|
||||
else
|
||||
@tar -czf bin/git-sv_$(VERSION)_$(BUILDOS)_$(BUILDARCH).tar.gz -C bin/$(BUILDOS)_$(BUILDARCH)/ $(BIN)
|
||||
ifndef VERSION
|
||||
ifneq ($(CI_COMMIT_TAG),)
|
||||
VERSION ?= $(subst v,,$(CI_COMMIT_TAG))
|
||||
else
|
||||
VERSION ?= $(shell git rev-parse --short HEAD)
|
||||
endif
|
||||
endif
|
||||
|
||||
## release-all: prepare linux, darwin and windows binary for release (requires sv4git)
|
||||
release-all:
|
||||
@rm -rf bin
|
||||
ifndef DATE
|
||||
DATE := $(shell date -u +"%Y-%m-%dT%H:%M:%S%z")
|
||||
endif
|
||||
|
||||
VERSION=$(shell git sv nv) BUILDOS=linux BUILDARCH=amd64 make release
|
||||
VERSION=$(shell git sv nv) BUILDOS=darwin BUILDARCH=amd64 make release
|
||||
VERSION=$(shell git sv nv) COMPRESS_TYPE=zip BUILDOS=windows BUILDARCH=amd64 make release
|
||||
LDFLAGS += -s -w -X "main.BuildVersion=$(VERSION)" -X "main.BuildDate=$(DATE)"
|
||||
|
||||
VERSION=$(shell git sv nv) BUILDOS=linux BUILDARCH=arm64 make release
|
||||
VERSION=$(shell git sv nv) BUILDOS=darwin BUILDARCH=arm64 make release
|
||||
VERSION=$(shell git sv nv) COMPRESS_TYPE=zip BUILDOS=windows BUILDARCH=arm64 make release
|
||||
.PHONY: all
|
||||
all: clean build
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
$(GO) clean -i ./...
|
||||
rm -rf $(DIST_DIRS)
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
$(GO) run $(GOFUMPT_PACKAGE) -extra -w $(SOURCES)
|
||||
|
||||
.PHONY: golangci-lint
|
||||
golangci-lint:
|
||||
$(GO) run $(GOLANGCI_LINT_PACKAGE) run
|
||||
|
||||
.PHONY: lint
|
||||
lint: golangci-lint
|
||||
|
||||
.PHONY: generate
|
||||
generate:
|
||||
$(GO) generate $(GENERATE)
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
$(GO) test -v -coverprofile coverage.out $(PACKAGES)
|
||||
|
||||
.PHONY: build
|
||||
build: $(DIST)/$(EXECUTABLE)
|
||||
|
||||
$(DIST)/$(EXECUTABLE): $(SOURCES)
|
||||
GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) GOARM=$(GOARM) $(GO) build -v -tags '$(TAGS)' -ldflags '-extldflags "-static" $(LDFLAGS)' -o $@ ./cmd/$(EXECUTABLE)
|
||||
|
||||
$(DIST_DIRS):
|
||||
mkdir -p $(DIST_DIRS)
|
||||
|
||||
.PHONY: xgo
|
||||
xgo: | $(DIST_DIRS)
|
||||
$(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -v -ldflags '-extldflags "-static" $(LDFLAGS)' -tags '$(TAGS)' -targets '$(XGO_TARGETS)' -out $(EXECUTABLE) --pkg cmd/$(EXECUTABLE) .
|
||||
cp /build/* $(CWD)/$(DIST)
|
||||
ls -l $(CWD)/$(DIST)
|
||||
|
||||
.PHONY: checksum
|
||||
checksum:
|
||||
cd $(DIST); $(foreach file,$(wildcard $(DIST)/$(EXECUTABLE)-*),sha256sum $(notdir $(file)) > $(notdir $(file)).sha256;)
|
||||
ls -l $(CWD)/$(DIST)
|
||||
|
||||
.PHONY: release
|
||||
release: xgo checksum
|
||||
|
||||
.PHONY: deps
|
||||
deps:
|
||||
$(GO) mod download
|
||||
$(GO) install $(GOFUMPT_PACKAGE)
|
||||
$(GO) install $(GOLANGCI_LINT_PACKAGE)
|
||||
$(GO) install $(XGO_PACKAGE)
|
||||
|
148
README.md
148
README.md
@ -1,18 +1,6 @@
|
||||
<p align="center">
|
||||
<h1 align="center">sv4git</h1>
|
||||
<p align="center">A command line tool (CLI) to validate commit messages, bump version, create tags and generate changelogs!</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/bvieira/sv4git/releases/latest"><img alt="Release" src="https://img.shields.io/github/release/bvieira/sv4git.svg?style=for-the-badge"></a>
|
||||
<a href="https://pkg.go.dev/github.com/bvieira/sv4git/v2"><img alt="Go Reference" src="https://img.shields.io/badge/-Reference-blue?style=for-the-badge&logo=go&labelColor=gray"></a>
|
||||
<a href="https://github.com/bvieira/sv4git/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/bvieira/sv4git?style=for-the-badge"></a>
|
||||
<a href="https://github.com/bvieira/sv4git/releases/latest"><img alt="GitHub release (latest by date)" src="https://img.shields.io/github/downloads/bvieira/sv4git/latest/total?color=blue&style=for-the-badge"></a>
|
||||
<a href="https://github.com/bvieira/sv4git/releases/latest"><img alt="GitHub all releases" src="https://img.shields.io/github/downloads/bvieira/sv4git/total?color=blue&style=for-the-badge"></a>
|
||||
<a href="/LICENSE"><img alt="Software License" src="https://img.shields.io/badge/license-MIT-informational.svg?style=for-the-badge"></a>
|
||||
<a href="https://github.com/bvieira/sv4git/actions?workflow=ci"><img alt="GitHub Actions Status" src="https://img.shields.io/github/actions/workflow/status/bvieira/sv4git/ci.yml?style=for-the-badge"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/bvieira/sv4git"><img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/bvieira/sv4git?style=for-the-badge"></a>
|
||||
<a href="https://conventionalcommits.org"><img alt="Conventional Commits" src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-informational.svg?style=for-the-badge"></a>
|
||||
</p>
|
||||
</p>
|
||||
# git-sv
|
||||
|
||||
A command line tool (CLI) to validate commit messages, bump version, create tags and generate changelogs.
|
||||
|
||||
## Getting Started
|
||||
|
||||
@ -23,23 +11,23 @@
|
||||
### Installing
|
||||
|
||||
- Download the latest release and add the binary to your path.
|
||||
- Optional: Set `SV4GIT_HOME` to define user configs. Check the [Config](#config) topic for more information.
|
||||
- Optional: Set `GITSV_HOME` to define user configs. Check the [Config](#config) topic for more information.
|
||||
|
||||
If you want to install from source using `go install`, just run:
|
||||
|
||||
```bash
|
||||
# keep in mind that with this, it will compile from source and won't show the version on cli -h.
|
||||
go install github.com/bvieira/sv4git/v2/cmd/git-sv@latest
|
||||
go install github.com/thegeeklab/git-sv/v2/cmd/git-sv@latest
|
||||
|
||||
# if you want to add the version on the binary, run this command instead.
|
||||
SV4GIT_VERSION=$(go list -f '{{ .Version }}' -m github.com/bvieira/sv4git/v2@latest | sed 's/v//') && go install --ldflags "-X main.Version=$SV4GIT_VERSION" github.com/bvieira/sv4git/v2/cmd/git-sv@v$SV4GIT_VERSION
|
||||
GITSV_VERSION=$(go list -f '{{ .Version }}' -m github.com/thegeeklab/git-sv/v2@latest | sed 's/v//') && go install --ldflags "-X main.Version=$SGITSV_VERSION" github.com/thegeeklab/git-sv/v2/cmd/git-sv@v$GITSV_VERSION
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
#### YAML
|
||||
|
||||
There are 3 config levels when using sv4git: [default](#default), [user](#user), [repository](#repository). All of them are merged considering the follow priority: **repository > user > default**.
|
||||
There are 3 config levels when using git-sv: [default](#default), [user](#user), [repository](#repository). All of them are merged considering the follow priority: **repository > user > default**.
|
||||
|
||||
To see the current config, run:
|
||||
|
||||
@ -59,22 +47,22 @@ git sv cfg default
|
||||
|
||||
###### User
|
||||
|
||||
For user config, it is necessary to define the `SV4GIT_HOME` environment variable, eg.:
|
||||
For user config, it is necessary to define the `GITSV_HOME` environment variable, eg.:
|
||||
|
||||
```bash
|
||||
SV4GIT_HOME=/home/myuser/.sv4git # myuser is just an example.
|
||||
GITSV_HOME=/home/myuser/.gitsv # myuser is just an example.
|
||||
```
|
||||
|
||||
And create a `config.yml` file inside it, eg.:
|
||||
|
||||
```bash
|
||||
.sv4git
|
||||
.gitsv
|
||||
└── config.yml
|
||||
```
|
||||
|
||||
###### Repository
|
||||
|
||||
Create a `.sv4git.yml` file on the root of your repository, eg.: [.sv4git.yml](.sv4git.yml).
|
||||
Create a `.gitsv/config.yml` file on the root of your repository, eg. [.gitsv/config.yml](.gitsv/config.yml).
|
||||
|
||||
##### Configuration format
|
||||
|
||||
@ -82,73 +70,85 @@ Create a `.sv4git.yml` file on the root of your repository, eg.: [.sv4git.yml](.
|
||||
version: "1.1" #config version
|
||||
|
||||
versioning: # versioning bump
|
||||
update-major: [] # Commit types used to bump major.
|
||||
update-minor: [feat] # Commit types used to bump minor.
|
||||
update-patch: [build, ci, chore, fix, perf, refactor, test] # Commit types used to bump patch.
|
||||
# When type is not present on update rules and is unknown (not mapped on commit message types);
|
||||
# if ignore-unknown=false bump patch, if ignore-unknown=true do not bump version
|
||||
ignore-unknown: false
|
||||
update-major: [] # Commit types used to bump major.
|
||||
update-minor: [feat] # Commit types used to bump minor.
|
||||
update-patch: [build, ci, chore, fix, perf, refactor, test] # Commit types used to bump patch.
|
||||
# When type is not present on update rules and is unknown (not mapped on commit message types);
|
||||
# if ignore-unknown=false bump patch, if ignore-unknown=true do not bump version
|
||||
ignore-unknown: false
|
||||
|
||||
tag:
|
||||
pattern: '%d.%d.%d' # Pattern used to create git tag.
|
||||
filter: '' # Enables you to filter for considerable tags using git pattern syntax
|
||||
pattern: "%d.%d.%d" # Pattern used to create git tag.
|
||||
filter: "" # Enables you to filter for considerable tags using git pattern syntax
|
||||
|
||||
release-notes:
|
||||
# Deprecated!!! please use 'sections' instead!
|
||||
# Headers names for release notes markdown. To disable a section just remove the header
|
||||
# line. It's possible to add other commit types, the release note will be created
|
||||
# respecting the following order: feat, fix, refactor, perf, test, build, ci, chore, docs, style, breaking-change.
|
||||
headers:
|
||||
breaking-change: Breaking Changes
|
||||
feat: Features
|
||||
fix: Bug Fixes
|
||||
|
||||
sections: # Array with each section of release note. Check template section for more information.
|
||||
- name: Features # Name used on section.
|
||||
section-type: commits # Type of the section, supported types: commits, breaking-changes.
|
||||
commit-types: [feat] # Commit types for commit section-type, one commit type cannot be in more than one section.
|
||||
- name: Bug Fixes
|
||||
section-type: commits
|
||||
commit-types: [fix]
|
||||
- name: Breaking Changes
|
||||
section-type: breaking-changes
|
||||
# Deprecated!!! please use 'sections' instead!
|
||||
# Headers names for release notes markdown. To disable a section just remove the header
|
||||
# line. It's possible to add other commit types, the release note will be created
|
||||
# respecting the following order: feat, fix, refactor, perf, test, build, ci, chore, docs, style, breaking-change.
|
||||
headers:
|
||||
breaking-change: Breaking Changes
|
||||
feat: Features
|
||||
fix: Bug Fixes
|
||||
|
||||
sections: # Array with each section of release note. Check template section for more information.
|
||||
- name: Features # Name used on section.
|
||||
section-type: commits # Type of the section, supported types: commits, breaking-changes.
|
||||
commit-types: [feat] # Commit types for commit section-type, one commit type cannot be in more than one section.
|
||||
- name: Bug Fixes
|
||||
section-type: commits
|
||||
commit-types: [fix]
|
||||
- name: Breaking Changes
|
||||
section-type: breaking-changes
|
||||
|
||||
branches: # Git branches config.
|
||||
prefix: ([a-z]+\/)? # Prefix used on branch name, it should be a regex group.
|
||||
suffix: (-.*)? # Suffix used on branch name, it should be a regex group.
|
||||
disable-issue: false # Set true if there is no need to recover issue id from branch name.
|
||||
skip: [master, main, developer] # List of branch names ignored on commit message validation.
|
||||
skip-detached: false # Set true if a detached branch should be ignored on commit message validation.
|
||||
prefix: ([a-z]+\/)? # Prefix used on branch name, it should be a regex group.
|
||||
suffix: (-.*)? # Suffix used on branch name, it should be a regex group.
|
||||
disable-issue: false # Set true if there is no need to recover issue id from branch name.
|
||||
skip: [master, main, developer] # List of branch names ignored on commit message validation.
|
||||
skip-detached: false # Set true if a detached branch should be ignored on commit message validation.
|
||||
|
||||
commit-message:
|
||||
types: [build, ci, chore, docs, feat, fix, perf, refactor, revert, style, test] # Supported commit types.
|
||||
header-selector: '' # You can put in a regex here to select only a certain part of the commit message. Please define a regex group 'header'.
|
||||
scope:
|
||||
# Define supported scopes, if blank, scope will not be validated, if not, only scope listed will be valid.
|
||||
# Don't forget to add "" on your list if you need to define scopes and keep it optional.
|
||||
values: []
|
||||
footer:
|
||||
issue: # Use "issue: {}" if you wish to disable issue footer.
|
||||
key: jira # Name used to define an issue on footer metadata.
|
||||
key-synonyms: [Jira, JIRA] # Supported variations for footer metadata.
|
||||
use-hash: false # If false, use :<space> separator. If true, use <space># separator.
|
||||
add-value-prefix: '' # Add a prefix to issue value.
|
||||
issue:
|
||||
regex: '[A-Z]+-[0-9]+' # Regex for issue id.
|
||||
types: [
|
||||
build,
|
||||
ci,
|
||||
chore,
|
||||
docs,
|
||||
feat,
|
||||
fix,
|
||||
perf,
|
||||
refactor,
|
||||
revert,
|
||||
style,
|
||||
test,
|
||||
] # Supported commit types.
|
||||
header-selector: "" # You can put in a regex here to select only a certain part of the commit message. Please define a regex group 'header'.
|
||||
scope:
|
||||
# Define supported scopes, if blank, scope will not be validated, if not, only scope listed will be valid.
|
||||
# Don't forget to add "" on your list if you need to define scopes and keep it optional.
|
||||
values: []
|
||||
footer:
|
||||
issue: # Use "issue: {}" if you wish to disable issue footer.
|
||||
key: jira # Name used to define an issue on footer metadata.
|
||||
key-synonyms: [Jira, JIRA] # Supported variations for footer metadata.
|
||||
use-hash: false # If false, use :<space> separator. If true, use <space># separator.
|
||||
add-value-prefix: "" # Add a prefix to issue value.
|
||||
issue:
|
||||
regex: "[A-Z]+-[0-9]+" # Regex for issue id.
|
||||
```
|
||||
|
||||
#### Templates
|
||||
|
||||
**sv4git** uses *go templates* to format the output for `release-notes` and `changelog`, to see how the default template is configured check [template directory](cmd/git-sv/resources/templates). On v2.7.0+, its possible to overwrite the default configuration by adding `.sv4git/templates` on your repository. The cli expects that at least 2 files exists on your directory: `changelog-md.tpl` and `releasenotes-md.tpl`.
|
||||
**git-sv** uses _go templates_ to format the output for `release-notes` and `changelog`, to see how the default template is configured check [template directory](cmd/git-sv/resources/templates). It's possible to overwrite the default configuration by adding `.gitsv/templates` on your repository. The cli expects that at least 2 files exists on your directory: `changelog-md.tpl` and `releasenotes-md.tpl`.
|
||||
|
||||
```bash
|
||||
.sv4git
|
||||
.gitsv
|
||||
└── templates
|
||||
├── changelog-md.tpl
|
||||
└── releasenotes-md.tpl
|
||||
```
|
||||
|
||||
Everything inside `.sv4git/templates` will be loaded, so it's possible to add more files to be used as needed.
|
||||
Everything inside `.gitsv/templates` will be loaded, so it's possible to add more files to be used as needed.
|
||||
|
||||
##### Variables
|
||||
|
||||
@ -156,9 +156,9 @@ To execute the template the `releasenotes-md.tpl` will receive a single **Releas
|
||||
|
||||
Each **ReleaseNoteSection** will be configured according with `release-notes.section` from config file. The order for each section will be maintained and the **SectionType** is defined according with `section-type` attribute as described on the table below.
|
||||
|
||||
| section-type | ReleaseNoteSection |
|
||||
| -- | -- |
|
||||
| commits | ReleaseNoteCommitsSection |
|
||||
| section-type | ReleaseNoteSection |
|
||||
| ---------------- | -------------------------------- |
|
||||
| commits | ReleaseNoteCommitsSection |
|
||||
| breaking-changes | ReleaseNoteBreakingChangeSection |
|
||||
|
||||
> :warning: currently only `commits` and `breaking-changes` are supported as `section-types`, using a different value for this field will make the section to be removed from the template variables.
|
||||
|
@ -4,27 +4,27 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/bvieira/sv4git/v2/sv"
|
||||
"github.com/imdario/mergo"
|
||||
"dario.cat/mergo"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"github.com/thegeeklab/git-sv/v2/sv"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// EnvConfig env vars for cli configuration.
|
||||
type EnvConfig struct {
|
||||
Home string `envconfig:"SV4GIT_HOME" default:""`
|
||||
Home string `envconfig:"GITSV_HOME" default:""`
|
||||
}
|
||||
|
||||
func loadEnvConfig() EnvConfig {
|
||||
var c EnvConfig
|
||||
|
||||
err := envconfig.Process("", &c)
|
||||
if err != nil {
|
||||
log.Fatal("failed to load env config, error: ", err.Error())
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
@ -38,20 +38,6 @@ type Config struct {
|
||||
CommitMessage sv.CommitMessageConfig `yaml:"commit-message"`
|
||||
}
|
||||
|
||||
func getRepoPath() (string, error) {
|
||||
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", combinedOutputErr(err, out)
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
func combinedOutputErr(err error, out []byte) error {
|
||||
msg := strings.Split(string(out), "\n")
|
||||
return fmt.Errorf("%v - %s", err, msg[0])
|
||||
}
|
||||
|
||||
func readConfig(filepath string) (Config, error) {
|
||||
content, rerr := os.ReadFile(filepath)
|
||||
if rerr != nil {
|
||||
@ -59,9 +45,10 @@ func readConfig(filepath string) (Config, error) {
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
|
||||
cerr := yaml.Unmarshal(content, &cfg)
|
||||
if cerr != nil {
|
||||
return Config{}, fmt.Errorf("could not parse config from path: %s, error: %v", filepath, cerr)
|
||||
return Config{}, fmt.Errorf("could not parse config from path: %s, error: %w", filepath, cerr)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
@ -71,6 +58,7 @@ func defaultConfig() Config {
|
||||
skipDetached := false
|
||||
pattern := "%d.%d.%d"
|
||||
filter := ""
|
||||
|
||||
return Config{
|
||||
Version: "1.1",
|
||||
Versioning: sv.VersioningConfig{
|
||||
@ -116,6 +104,7 @@ func merge(dst *Config, src Config) error {
|
||||
dst.ReleaseNotes.Headers = src.ReleaseNotes.Headers
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@ -127,6 +116,7 @@ func (t *mergeTransformer) Transformer(typ reflect.Type) func(dst, src reflect.V
|
||||
if dst.CanSet() && !src.IsNil() {
|
||||
dst.Set(src)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -136,9 +126,11 @@ func (t *mergeTransformer) Transformer(typ reflect.Type) func(dst, src reflect.V
|
||||
if dst.CanSet() && !src.IsNil() {
|
||||
dst.Set(src)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -146,6 +138,7 @@ func migrateConfig(cfg Config, filename string) Config {
|
||||
if cfg.ReleaseNotes.Headers == nil {
|
||||
return cfg
|
||||
}
|
||||
|
||||
warnf("config 'release-notes.headers' on %s is deprecated, please use 'sections' instead!", filename)
|
||||
|
||||
return Config{
|
||||
@ -162,14 +155,29 @@ func migrateConfig(cfg Config, filename string) Config {
|
||||
|
||||
func migrateReleaseNotesConfig(headers map[string]string) []sv.ReleaseNotesSectionConfig {
|
||||
order := []string{"feat", "fix", "refactor", "perf", "test", "build", "ci", "chore", "docs", "style"}
|
||||
|
||||
var sections []sv.ReleaseNotesSectionConfig
|
||||
|
||||
for _, key := range order {
|
||||
if name, exists := headers[key]; exists {
|
||||
sections = append(sections, sv.ReleaseNotesSectionConfig{Name: name, SectionType: sv.ReleaseNotesSectionTypeCommits, CommitTypes: []string{key}})
|
||||
sections = append(
|
||||
sections,
|
||||
sv.ReleaseNotesSectionConfig{
|
||||
Name: name,
|
||||
SectionType: sv.ReleaseNotesSectionTypeCommits,
|
||||
CommitTypes: []string{key},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if name, exists := headers["breaking-change"]; exists {
|
||||
sections = append(sections, sv.ReleaseNotesSectionConfig{Name: name, SectionType: sv.ReleaseNotesSectionTypeBreakingChanges})
|
||||
sections = append(
|
||||
sections,
|
||||
sv.ReleaseNotesSectionConfig{
|
||||
Name: name,
|
||||
SectionType: sv.ReleaseNotesSectionTypeBreakingChanges,
|
||||
})
|
||||
}
|
||||
|
||||
return sections
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/bvieira/sv4git/v2/sv"
|
||||
"github.com/thegeeklab/git-sv/v2/sv"
|
||||
)
|
||||
|
||||
func Test_merge(t *testing.T) {
|
||||
@ -20,24 +20,135 @@ func Test_merge(t *testing.T) {
|
||||
want Config
|
||||
wantErr bool
|
||||
}{
|
||||
{"overwrite string", Config{Version: "a"}, Config{Version: "b"}, Config{Version: "b"}, false},
|
||||
{"default string", Config{Version: "a"}, Config{Version: ""}, Config{Version: "a"}, false},
|
||||
{
|
||||
"overwrite string",
|
||||
Config{Version: "a"},
|
||||
Config{Version: "b"},
|
||||
Config{Version: "b"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"default string",
|
||||
Config{Version: "a"},
|
||||
Config{Version: ""},
|
||||
Config{Version: "a"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"overwrite list",
|
||||
Config{Branches: sv.BranchesConfig{Skip: []string{"a", "b"}}},
|
||||
Config{Branches: sv.BranchesConfig{Skip: []string{"c", "d"}}},
|
||||
Config{Branches: sv.BranchesConfig{Skip: []string{"c", "d"}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"overwrite list with empty",
|
||||
Config{Branches: sv.BranchesConfig{Skip: []string{"a", "b"}}},
|
||||
Config{Branches: sv.BranchesConfig{Skip: make([]string, 0)}},
|
||||
Config{Branches: sv.BranchesConfig{Skip: make([]string, 0)}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"default list",
|
||||
Config{Branches: sv.BranchesConfig{Skip: []string{"a", "b"}}},
|
||||
Config{Branches: sv.BranchesConfig{Skip: nil}},
|
||||
Config{Branches: sv.BranchesConfig{Skip: []string{"a", "b"}}},
|
||||
false,
|
||||
},
|
||||
|
||||
{"overwrite list", Config{Branches: sv.BranchesConfig{Skip: []string{"a", "b"}}}, Config{Branches: sv.BranchesConfig{Skip: []string{"c", "d"}}}, Config{Branches: sv.BranchesConfig{Skip: []string{"c", "d"}}}, false},
|
||||
{"overwrite list with empty", Config{Branches: sv.BranchesConfig{Skip: []string{"a", "b"}}}, Config{Branches: sv.BranchesConfig{Skip: make([]string, 0)}}, Config{Branches: sv.BranchesConfig{Skip: make([]string, 0)}}, false},
|
||||
{"default list", Config{Branches: sv.BranchesConfig{Skip: []string{"a", "b"}}}, Config{Branches: sv.BranchesConfig{Skip: nil}}, Config{Branches: sv.BranchesConfig{Skip: []string{"a", "b"}}}, false},
|
||||
|
||||
{"overwrite pointer bool false", Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}}, false},
|
||||
{"overwrite pointer bool true", Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}}, false},
|
||||
{"default pointer bool", Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}}, Config{Branches: sv.BranchesConfig{SkipDetached: nil}}, Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}}, false},
|
||||
|
||||
{"merge maps", Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}}}}, Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{"issue2": {Key: "jira2"}}}}, Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}, "issue2": {Key: "jira2"}}}}, false},
|
||||
{"default maps", Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}}}}, Config{CommitMessage: sv.CommitMessageConfig{Footer: nil}}, Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}}}}, false},
|
||||
{"merge empty maps", Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}}}}, Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{}}}, Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}}}}, false},
|
||||
|
||||
{"overwrite release notes header", Config{ReleaseNotes: sv.ReleaseNotesConfig{Headers: map[string]string{"a": "aa"}}}, Config{ReleaseNotes: sv.ReleaseNotesConfig{Headers: map[string]string{"b": "bb"}}}, Config{ReleaseNotes: sv.ReleaseNotesConfig{Headers: map[string]string{"b": "bb"}}}, false},
|
||||
|
||||
{"overwrite tag config", Config{Version: "a", Tag: sv.TagConfig{Pattern: &nonEmptyStr, Filter: &nonEmptyStr}}, Config{Version: "", Tag: sv.TagConfig{Pattern: &emptyStr, Filter: &emptyStr}}, Config{Version: "a", Tag: sv.TagConfig{Pattern: &emptyStr, Filter: &emptyStr}}, false},
|
||||
{
|
||||
"overwrite pointer bool false",
|
||||
Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}},
|
||||
Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}},
|
||||
Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"overwrite pointer bool true",
|
||||
Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}},
|
||||
Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}},
|
||||
Config{Branches: sv.BranchesConfig{SkipDetached: &boolFalse}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"default pointer bool",
|
||||
Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}},
|
||||
Config{Branches: sv.BranchesConfig{SkipDetached: nil}},
|
||||
Config{Branches: sv.BranchesConfig{SkipDetached: &boolTrue}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"merge maps",
|
||||
Config{CommitMessage: sv.CommitMessageConfig{
|
||||
Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}},
|
||||
}},
|
||||
Config{CommitMessage: sv.CommitMessageConfig{
|
||||
Footer: map[string]sv.CommitMessageFooterConfig{"issue2": {Key: "jira2"}},
|
||||
}},
|
||||
Config{CommitMessage: sv.CommitMessageConfig{Footer: map[string]sv.CommitMessageFooterConfig{
|
||||
"issue": {Key: "jira"},
|
||||
"issue2": {Key: "jira2"},
|
||||
}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"default maps",
|
||||
Config{CommitMessage: sv.CommitMessageConfig{
|
||||
Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}},
|
||||
}},
|
||||
Config{CommitMessage: sv.CommitMessageConfig{
|
||||
Footer: nil,
|
||||
}},
|
||||
Config{CommitMessage: sv.CommitMessageConfig{
|
||||
Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}},
|
||||
}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"merge empty maps",
|
||||
Config{CommitMessage: sv.CommitMessageConfig{
|
||||
Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}},
|
||||
}},
|
||||
Config{CommitMessage: sv.CommitMessageConfig{
|
||||
Footer: map[string]sv.CommitMessageFooterConfig{},
|
||||
}},
|
||||
Config{CommitMessage: sv.CommitMessageConfig{
|
||||
Footer: map[string]sv.CommitMessageFooterConfig{"issue": {Key: "jira"}},
|
||||
}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"overwrite release notes header",
|
||||
Config{ReleaseNotes: sv.ReleaseNotesConfig{Headers: map[string]string{"a": "aa"}}},
|
||||
Config{ReleaseNotes: sv.ReleaseNotesConfig{Headers: map[string]string{"b": "bb"}}},
|
||||
Config{ReleaseNotes: sv.ReleaseNotesConfig{Headers: map[string]string{"b": "bb"}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"overwrite tag config",
|
||||
Config{
|
||||
Version: "a",
|
||||
Tag: sv.TagConfig{
|
||||
Pattern: &nonEmptyStr,
|
||||
Filter: &nonEmptyStr,
|
||||
},
|
||||
},
|
||||
Config{
|
||||
Version: "",
|
||||
Tag: sv.TagConfig{
|
||||
Pattern: &emptyStr,
|
||||
Filter: &emptyStr,
|
||||
},
|
||||
},
|
||||
Config{
|
||||
Version: "a",
|
||||
Tag: sv.TagConfig{
|
||||
Pattern: &emptyStr,
|
||||
Filter: &emptyStr,
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -10,19 +11,32 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/bvieira/sv4git/v2/sv"
|
||||
"github.com/thegeeklab/git-sv/v2/sv"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const laxFilePerm = 0o644
|
||||
|
||||
var (
|
||||
errCanNotCreateTagFlag = errors.New("cannot define tag flag with range, start or end flags")
|
||||
errUnknownTag = errors.New("unknown tag")
|
||||
errReadCommitMessage = errors.New("failed to read commit message")
|
||||
errAppendFooter = errors.New("failed to append meta-informations on footer")
|
||||
errInvalidRange = errors.New("invalid log range")
|
||||
)
|
||||
|
||||
func configDefaultHandler() func(c *cli.Context) error {
|
||||
cfg := defaultConfig()
|
||||
|
||||
return func(c *cli.Context) error {
|
||||
content, err := yaml.Marshal(&cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(content))
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -33,7 +47,9 @@ func configShowHandler(cfg Config) func(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(content))
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -44,9 +60,11 @@ func currentVersionHandler(git sv.Git) func(c *cli.Context) error {
|
||||
|
||||
currentVer, err := sv.ToVersion(lastTag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing version: %s from git tag, message: %v", lastTag, err)
|
||||
return fmt.Errorf("error parsing version: %s from git tag, message: %w", lastTag, err)
|
||||
}
|
||||
|
||||
fmt.Printf("%d.%d.%d\n", currentVer.Major(), currentVer.Minor(), currentVer.Patch())
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -57,30 +75,62 @@ func nextVersionHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) f
|
||||
|
||||
currentVer, err := sv.ToVersion(lastTag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing version: %s from git tag, message: %v", lastTag, err)
|
||||
return fmt.Errorf("error parsing version: %s from git tag, message: %w", lastTag, err)
|
||||
}
|
||||
|
||||
commits, err := git.Log(sv.NewLogRange(sv.TagRange, lastTag, ""))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting git log, message: %v", err)
|
||||
return fmt.Errorf("error getting git log, message: %w", err)
|
||||
}
|
||||
|
||||
nextVer, _ := semverProcessor.NextVersion(currentVer, commits)
|
||||
|
||||
fmt.Printf("%d.%d.%d\n", nextVer.Major(), nextVer.Minor(), nextVer.Patch())
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func commitLogFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "t",
|
||||
Aliases: []string{"tag"},
|
||||
Usage: "get commit log from a specific tag",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "r",
|
||||
Aliases: []string{"range"},
|
||||
Usage: "type of range of commits, use: tag, date or hash",
|
||||
Value: string(sv.TagRange),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "s",
|
||||
Aliases: []string{"start"},
|
||||
Usage: "start range of git log revision range, if date, the value is used on since flag instead",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "e",
|
||||
Aliases: []string{"end"},
|
||||
Usage: "end range of git log revision range, if date, the value is used on until flag instead",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func commitLogHandler(git sv.Git) func(c *cli.Context) error {
|
||||
return func(c *cli.Context) error {
|
||||
var commits []sv.GitCommitLog
|
||||
var err error
|
||||
var (
|
||||
commits []sv.GitCommitLog
|
||||
err error
|
||||
)
|
||||
|
||||
tagFlag := c.String("t")
|
||||
rangeFlag := c.String("r")
|
||||
startFlag := c.String("s")
|
||||
endFlag := c.String("e")
|
||||
|
||||
if tagFlag != "" && (rangeFlag != string(sv.TagRange) || startFlag != "" || endFlag != "") {
|
||||
return fmt.Errorf("cannot define tag flag with range, start or end flags")
|
||||
return errCanNotCreateTagFlag
|
||||
}
|
||||
|
||||
if tagFlag != "" {
|
||||
@ -92,8 +142,9 @@ func commitLogHandler(git sv.Git) func(c *cli.Context) error {
|
||||
}
|
||||
commits, err = git.Log(r)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting git log, message: %v", err)
|
||||
return fmt.Errorf("error getting git log, message: %w", err)
|
||||
}
|
||||
|
||||
for _, commit := range commits {
|
||||
@ -101,8 +152,10 @@ func commitLogHandler(git sv.Git) func(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(content))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -112,6 +165,7 @@ func getTagCommits(git sv.Git, tag string) ([]sv.GitCommitLog, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return git.Log(sv.NewLogRange(sv.TagRange, prev, tag))
|
||||
}
|
||||
|
||||
@ -124,15 +178,45 @@ func logRange(git sv.Git, rangeFlag, startFlag, endFlag string) (sv.LogRange, er
|
||||
case string(sv.HashRange):
|
||||
return sv.NewLogRange(sv.HashRange, startFlag, endFlag), nil
|
||||
default:
|
||||
return sv.LogRange{}, fmt.Errorf("invalid range: %s, expected: %s, %s or %s", rangeFlag, sv.TagRange, sv.DateRange, sv.HashRange)
|
||||
return sv.LogRange{}, fmt.Errorf(
|
||||
"%w: %s, expected: %s, %s or %s",
|
||||
errInvalidRange,
|
||||
rangeFlag,
|
||||
sv.TagRange,
|
||||
sv.DateRange,
|
||||
sv.HashRange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func commitNotesHandler(git sv.Git, rnProcessor sv.ReleaseNoteProcessor, outputFormatter sv.OutputFormatter) func(c *cli.Context) error {
|
||||
func commitNotesFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "r", Aliases: []string{"range"},
|
||||
Usage: "type of range of commits, use: tag, date or hash",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "s",
|
||||
Aliases: []string{"start"},
|
||||
Usage: "start range of git log revision range, if date, the value is used on since flag instead",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "e",
|
||||
Aliases: []string{"end"},
|
||||
Usage: "end range of git log revision range, if date, the value is used on until flag instead",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func commitNotesHandler(
|
||||
git sv.Git, rnProcessor sv.ReleaseNoteProcessor, outputFormatter sv.OutputFormatter,
|
||||
) func(c *cli.Context) error {
|
||||
return func(c *cli.Context) error {
|
||||
var date time.Time
|
||||
|
||||
rangeFlag := c.String("r")
|
||||
|
||||
lr, err := logRange(git, rangeFlag, c.String("s"), c.String("e"))
|
||||
if err != nil {
|
||||
return err
|
||||
@ -140,7 +224,7 @@ func commitNotesHandler(git sv.Git, rnProcessor sv.ReleaseNoteProcessor, outputF
|
||||
|
||||
commits, err := git.Log(lr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting git log from range: %s, message: %v", rangeFlag, err)
|
||||
return fmt.Errorf("error getting git log from range: %s, message: %w", rangeFlag, err)
|
||||
}
|
||||
|
||||
if len(commits) > 0 {
|
||||
@ -149,20 +233,39 @@ func commitNotesHandler(git sv.Git, rnProcessor sv.ReleaseNoteProcessor, outputF
|
||||
|
||||
output, err := outputFormatter.FormatReleaseNote(rnProcessor.Create(nil, "", date, commits))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not format release notes, message: %v", err)
|
||||
return fmt.Errorf("could not format release notes, message: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(output)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func releaseNotesHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnProcessor sv.ReleaseNoteProcessor, outputFormatter sv.OutputFormatter) func(c *cli.Context) error {
|
||||
func releaseNotesFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "t",
|
||||
Aliases: []string{"tag"},
|
||||
Usage: "get release note from tag",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func releaseNotesHandler(
|
||||
git sv.Git,
|
||||
semverProcessor sv.SemVerCommitsProcessor,
|
||||
rnProcessor sv.ReleaseNoteProcessor,
|
||||
outputFormatter sv.OutputFormatter,
|
||||
) func(c *cli.Context) error {
|
||||
return func(c *cli.Context) error {
|
||||
var commits []sv.GitCommitLog
|
||||
var rnVersion *semver.Version
|
||||
var tag string
|
||||
var date time.Time
|
||||
var err error
|
||||
var (
|
||||
commits []sv.GitCommitLog
|
||||
rnVersion *semver.Version
|
||||
tag string
|
||||
date time.Time
|
||||
err error
|
||||
)
|
||||
|
||||
if tag = c.String("t"); tag != "" {
|
||||
rnVersion, date, commits, err = getTagVersionInfo(git, tag)
|
||||
@ -176,11 +279,14 @@ func releaseNotesHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor,
|
||||
}
|
||||
|
||||
releasenote := rnProcessor.Create(rnVersion, tag, date, commits)
|
||||
|
||||
output, err := outputFormatter.FormatReleaseNote(releasenote)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not format release notes, message: %v", err)
|
||||
return fmt.Errorf("could not format release notes, message: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(output)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -190,12 +296,12 @@ func getTagVersionInfo(git sv.Git, tag string) (*semver.Version, time.Time, []sv
|
||||
|
||||
previousTag, currentTag, err := getTags(git, tag)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, nil, fmt.Errorf("error listing tags, message: %v", err)
|
||||
return nil, time.Time{}, nil, fmt.Errorf("error listing tags, message: %w", err)
|
||||
}
|
||||
|
||||
commits, err := git.Log(sv.NewLogRange(sv.TagRange, previousTag, tag))
|
||||
if err != nil {
|
||||
return nil, time.Time{}, nil, fmt.Errorf("error getting git log from tag: %s, message: %v", tag, err)
|
||||
return nil, time.Time{}, nil, fmt.Errorf("error getting git log from tag: %s, message: %w", tag, err)
|
||||
}
|
||||
|
||||
return tagVersion, currentTag.Date, commits, nil
|
||||
@ -209,13 +315,14 @@ func getTags(git sv.Git, tag string) (string, sv.GitTag, error) {
|
||||
|
||||
index := find(tag, tags)
|
||||
if index < 0 {
|
||||
return "", sv.GitTag{}, fmt.Errorf("tag: %s not found, check tag filter", tag)
|
||||
return "", sv.GitTag{}, fmt.Errorf("%w: %s not found, check tag filter", errUnknownTag, tag)
|
||||
}
|
||||
|
||||
previousTag := ""
|
||||
if index > 0 {
|
||||
previousTag = tags[index-1].Name
|
||||
}
|
||||
|
||||
return previousTag, tags[index], nil
|
||||
}
|
||||
|
||||
@ -225,15 +332,18 @@ func find(tag string, tags []sv.GitTag) int {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func getNextVersionInfo(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) (*semver.Version, bool, time.Time, []sv.GitCommitLog, error) {
|
||||
func getNextVersionInfo(
|
||||
git sv.Git, semverProcessor sv.SemVerCommitsProcessor,
|
||||
) (*semver.Version, bool, time.Time, []sv.GitCommitLog, error) {
|
||||
lastTag := git.LastTag()
|
||||
|
||||
commits, err := git.Log(sv.NewLogRange(sv.TagRange, lastTag, ""))
|
||||
if err != nil {
|
||||
return nil, false, time.Time{}, nil, fmt.Errorf("error getting git log, message: %v", err)
|
||||
return nil, false, time.Time{}, nil, fmt.Errorf("error getting git log, message: %w", err)
|
||||
}
|
||||
|
||||
currentVer, _ := sv.ToVersion(lastTag)
|
||||
@ -248,20 +358,23 @@ func tagHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) func(c *c
|
||||
|
||||
currentVer, err := sv.ToVersion(lastTag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing version: %s from git tag, message: %v", lastTag, err)
|
||||
return fmt.Errorf("error parsing version: %s from git tag, message: %w", lastTag, err)
|
||||
}
|
||||
|
||||
commits, err := git.Log(sv.NewLogRange(sv.TagRange, lastTag, ""))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting git log, message: %v", err)
|
||||
return fmt.Errorf("error getting git log, message: %w", err)
|
||||
}
|
||||
|
||||
nextVer, _ := semverProcessor.NextVersion(currentVer, commits)
|
||||
tagname, err := git.Tag(*nextVer)
|
||||
|
||||
fmt.Println(tagname)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating tag version: %s, message: %v", nextVer.String(), err)
|
||||
return fmt.Errorf("error generating tag version: %s, message: %w", nextVer.String(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -269,8 +382,10 @@ func tagHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) func(c *c
|
||||
func getCommitType(cfg Config, p sv.MessageProcessor, input string) (string, error) {
|
||||
if input == "" {
|
||||
t, err := promptType(cfg.CommitMessage.Types)
|
||||
|
||||
return t.Type, err
|
||||
}
|
||||
|
||||
return input, p.ValidateType(input)
|
||||
}
|
||||
|
||||
@ -278,6 +393,7 @@ func getCommitScope(cfg Config, p sv.MessageProcessor, input string, noScope boo
|
||||
if input == "" && !noScope {
|
||||
return promptScope(cfg.CommitMessage.Scope.Values)
|
||||
}
|
||||
|
||||
return input, p.ValidateScope(input)
|
||||
}
|
||||
|
||||
@ -285,6 +401,7 @@ func getCommitDescription(p sv.MessageProcessor, input string) (string, error) {
|
||||
if input == "" {
|
||||
return promptSubject()
|
||||
}
|
||||
|
||||
return input, p.ValidateDescription(input)
|
||||
}
|
||||
|
||||
@ -294,17 +411,21 @@ func getCommitBody(noBody bool) (string, error) {
|
||||
}
|
||||
|
||||
var fullBody strings.Builder
|
||||
|
||||
for body, err := promptBody(); body != "" || err != nil; body, err = promptBody() {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if fullBody.Len() > 0 {
|
||||
fullBody.WriteString("\n")
|
||||
}
|
||||
|
||||
if body != "" {
|
||||
fullBody.WriteString(body)
|
||||
}
|
||||
}
|
||||
|
||||
return fullBody.String(), nil
|
||||
}
|
||||
|
||||
@ -338,6 +459,7 @@ func getCommitBreakingChange(noBreaking bool, input string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !hasBreakingChanges {
|
||||
return "", nil
|
||||
}
|
||||
@ -345,6 +467,51 @@ func getCommitBreakingChange(noBreaking bool, input string) (string, error) {
|
||||
return promptBreakingChanges()
|
||||
}
|
||||
|
||||
func commitFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "no-scope",
|
||||
Aliases: []string{"nsc"},
|
||||
Usage: "do not prompt for commit scope",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-body",
|
||||
Aliases: []string{"nbd"},
|
||||
Usage: "do not prompt for commit body",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-issue",
|
||||
Aliases: []string{"nis"},
|
||||
Usage: "do not prompt for commit issue, will try to recover from branch if enabled",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-breaking",
|
||||
Aliases: []string{"nbc"},
|
||||
Usage: "do not prompt for breaking changes",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "define commit type",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "scope",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "define commit scope",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "description",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "define commit description",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "breaking-change",
|
||||
Aliases: []string{"b"},
|
||||
Usage: "define commit breaking change message",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func commitHandler(cfg Config, git sv.Git, messageProcessor sv.MessageProcessor) func(c *cli.Context) error {
|
||||
return func(c *cli.Context) error {
|
||||
noBreaking := c.Bool("no-breaking")
|
||||
@ -386,22 +553,54 @@ func commitHandler(cfg Config, git sv.Git, messageProcessor sv.MessageProcessor)
|
||||
return err
|
||||
}
|
||||
|
||||
header, body, footer := messageProcessor.Format(sv.NewCommitMessage(ctype, scope, subject, fullBody, issue, breakingChange))
|
||||
header, body, footer := messageProcessor.Format(
|
||||
sv.NewCommitMessage(ctype, scope, subject, fullBody, issue, breakingChange),
|
||||
)
|
||||
|
||||
err = git.Commit(header, body, footer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error executing git commit, message: %v", err)
|
||||
return fmt.Errorf("error executing git commit, message: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnProcessor sv.ReleaseNoteProcessor, formatter sv.OutputFormatter) func(c *cli.Context) error {
|
||||
func changelogFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.IntFlag{
|
||||
Name: "size",
|
||||
Value: 10, //nolint:gomnd
|
||||
Aliases: []string{"n"},
|
||||
Usage: "get changelog from last 'n' tags",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "all",
|
||||
Usage: "ignore size parameter, get changelog for every tag",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "add-next-version",
|
||||
Usage: "add next version on change log (commits since last tag, but only if there is a new version to release)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "semantic-version-only",
|
||||
Usage: "only show tags 'SemVer-ish'",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func changelogHandler(
|
||||
git sv.Git,
|
||||
semverProcessor sv.SemVerCommitsProcessor,
|
||||
rnProcessor sv.ReleaseNoteProcessor,
|
||||
formatter sv.OutputFormatter,
|
||||
) func(c *cli.Context) error {
|
||||
return func(c *cli.Context) error {
|
||||
tags, err := git.Tags()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Slice(tags, func(i, j int) bool {
|
||||
return tags[i].Date.After(tags[j].Date)
|
||||
})
|
||||
@ -418,10 +617,12 @@ func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnP
|
||||
if uerr != nil {
|
||||
return uerr
|
||||
}
|
||||
|
||||
if updated {
|
||||
releaseNotes = append(releaseNotes, rnProcessor.Create(rnVersion, "", date, commits))
|
||||
}
|
||||
}
|
||||
|
||||
for i, tag := range tags {
|
||||
if !all && i >= size {
|
||||
break
|
||||
@ -438,7 +639,7 @@ func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnP
|
||||
|
||||
commits, err := git.Log(sv.NewLogRange(sv.TagRange, previousTag, tag.Name))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting git log from tag: %s, message: %v", tag.Name, err)
|
||||
return fmt.Errorf("error getting git log from tag: %s, message: %w", tag.Name, err)
|
||||
}
|
||||
|
||||
currentVer, _ := sv.ToVersion(tag.Name)
|
||||
@ -447,14 +648,35 @@ func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnP
|
||||
|
||||
output, err := formatter.FormatChangelog(releaseNotes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not format changelog, message: %v", err)
|
||||
return fmt.Errorf("could not format changelog, message: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(output)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func validateCommitMessageFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Required: true,
|
||||
Usage: "git working directory",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "file",
|
||||
Required: true,
|
||||
Usage: "name of the file that contains the commit log message",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "source",
|
||||
Required: true,
|
||||
Usage: "source of the commit message",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func validateCommitMessageHandler(git sv.Git, messageProcessor sv.MessageProcessor) func(c *cli.Context) error {
|
||||
return func(c *cli.Context) error {
|
||||
branch := git.Branch()
|
||||
@ -462,11 +684,13 @@ func validateCommitMessageHandler(git sv.Git, messageProcessor sv.MessageProcess
|
||||
|
||||
if messageProcessor.SkipBranch(branch, derr == nil && detached) {
|
||||
warnf("commit message validation skipped, branch in ignore list or detached...")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if source := c.String("source"); source == "merge" {
|
||||
warnf("commit message validation skipped, ignoring source: %s...", source)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -474,24 +698,26 @@ func validateCommitMessageHandler(git sv.Git, messageProcessor sv.MessageProcess
|
||||
|
||||
commitMessage, err := readFile(filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read commit message, error: %s", err.Error())
|
||||
return fmt.Errorf("%w: %s", errReadCommitMessage, err.Error())
|
||||
}
|
||||
|
||||
if err := messageProcessor.Validate(commitMessage); err != nil {
|
||||
return fmt.Errorf("invalid commit message, error: %s", err.Error())
|
||||
return fmt.Errorf("%w: %s", errReadCommitMessage, err.Error())
|
||||
}
|
||||
|
||||
msg, err := messageProcessor.Enhance(branch, commitMessage)
|
||||
if err != nil {
|
||||
warnf("could not enhance commit message, %s", err.Error())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if msg == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := appendOnFile(msg, filepath); err != nil {
|
||||
return fmt.Errorf("failed to append meta-informations on footer, error: %s", err.Error())
|
||||
return fmt.Errorf("%w: %s", errAppendFooter, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -503,17 +729,19 @@ func readFile(filepath string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(f), nil
|
||||
}
|
||||
|
||||
func appendOnFile(message, filepath string) error {
|
||||
f, err := os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY, 0644)
|
||||
f, err := os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY, laxFilePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.WriteString(message)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@ -521,5 +749,6 @@ func str(value, defaultValue string) string {
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
@ -2,161 +2,146 @@ package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bvieira/sv4git/v2/sv"
|
||||
"github.com/thegeeklab/git-sv/v2/sv"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Version for git-sv.
|
||||
var Version = "source"
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
BuildVersion = "devel"
|
||||
BuildDate = "00000000"
|
||||
)
|
||||
|
||||
const (
|
||||
configFilename = "config.yml"
|
||||
repoConfigFilename = ".sv4git.yml"
|
||||
configDir = ".sv4git"
|
||||
configFilename = "config.yml"
|
||||
configDir = ".gitsv"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed resources/templates/*.tpl
|
||||
defaultTemplatesFS embed.FS
|
||||
)
|
||||
//go:embed resources/templates/*.tpl
|
||||
var defaultTemplatesFS embed.FS
|
||||
|
||||
func templateFS(filepath string) fs.FS {
|
||||
if _, err := os.Stat(filepath); err != nil {
|
||||
defaultTemplatesFS, _ := fs.Sub(defaultTemplatesFS, "resources/templates")
|
||||
|
||||
return defaultTemplatesFS
|
||||
}
|
||||
|
||||
return os.DirFS(filepath)
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
|
||||
repoPath, rerr := getRepoPath()
|
||||
if rerr != nil {
|
||||
log.Fatal("failed to discovery repository top level, error: ", rerr)
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatal("error while retrieving working directory: %w", err)
|
||||
}
|
||||
|
||||
cfg := loadCfg(repoPath)
|
||||
cfg := loadCfg(wd)
|
||||
messageProcessor := sv.NewMessageProcessor(cfg.CommitMessage, cfg.Branches)
|
||||
git := sv.NewGit(messageProcessor, cfg.Tag)
|
||||
semverProcessor := sv.NewSemVerCommitsProcessor(cfg.Versioning, cfg.CommitMessage)
|
||||
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotes)
|
||||
outputFormatter := sv.NewOutputFormatter(templateFS(filepath.Join(repoPath, configDir, "templates")))
|
||||
outputFormatter := sv.NewOutputFormatter(templateFS(filepath.Join(wd, configDir, "templates")))
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "sv"
|
||||
app.Version = Version
|
||||
app.Usage = "semantic version for git"
|
||||
app.Commands = []*cli.Command{
|
||||
{
|
||||
Name: "config",
|
||||
Aliases: []string{"cfg"},
|
||||
Usage: "cli configuration",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "default",
|
||||
Usage: "show default config",
|
||||
Action: configDefaultHandler(),
|
||||
},
|
||||
{
|
||||
Name: "show",
|
||||
Usage: "show current config",
|
||||
Action: configShowHandler(cfg),
|
||||
cli.VersionPrinter = func(c *cli.Context) {
|
||||
fmt.Printf("%s version=%s date=%s\n", c.App.Name, c.App.Version, BuildDate)
|
||||
}
|
||||
|
||||
app := &cli.App{
|
||||
Name: "git-sv",
|
||||
Usage: "Semantic version for git.",
|
||||
Version: BuildVersion,
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "config",
|
||||
Aliases: []string{"cfg"},
|
||||
Usage: "cli configuration",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "default",
|
||||
Usage: "show default config",
|
||||
Action: configDefaultHandler(),
|
||||
},
|
||||
{
|
||||
Name: "show",
|
||||
Usage: "show current config",
|
||||
Action: configShowHandler(cfg),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "current-version",
|
||||
Aliases: []string{"cv"},
|
||||
Usage: "get last released version from git",
|
||||
Action: currentVersionHandler(git),
|
||||
},
|
||||
{
|
||||
Name: "next-version",
|
||||
Aliases: []string{"nv"},
|
||||
Usage: "generate the next version based on git commit messages",
|
||||
Action: nextVersionHandler(git, semverProcessor),
|
||||
},
|
||||
{
|
||||
Name: "commit-log",
|
||||
Aliases: []string{"cl"},
|
||||
Usage: "list all commit logs according to range as jsons",
|
||||
Description: "The range filter is used based on git log filters, check https://git-scm.com/docs/git-log for more info. When flag range is \"tag\" and start is empty, last tag created will be used instead. When flag range is \"date\", if \"end\" is YYYY-MM-DD the range will be inclusive.",
|
||||
Action: commitLogHandler(git),
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{Name: "t", Aliases: []string{"tag"}, Usage: "get commit log from a specific tag"},
|
||||
&cli.StringFlag{Name: "r", Aliases: []string{"range"}, Usage: "type of range of commits, use: tag, date or hash", Value: string(sv.TagRange)},
|
||||
&cli.StringFlag{Name: "s", Aliases: []string{"start"}, Usage: "start range of git log revision range, if date, the value is used on since flag instead"},
|
||||
&cli.StringFlag{Name: "e", Aliases: []string{"end"}, Usage: "end range of git log revision range, if date, the value is used on until flag instead"},
|
||||
{
|
||||
Name: "current-version",
|
||||
Aliases: []string{"cv"},
|
||||
Usage: "get last released version from git",
|
||||
Action: currentVersionHandler(git),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "commit-notes",
|
||||
Aliases: []string{"cn"},
|
||||
Usage: "generate a commit notes according to range",
|
||||
Description: "The range filter is used based on git log filters, check https://git-scm.com/docs/git-log for more info. When flag range is \"tag\" and start is empty, last tag created will be used instead. When flag range is \"date\", if \"end\" is YYYY-MM-DD the range will be inclusive.",
|
||||
Action: commitNotesHandler(git, releasenotesProcessor, outputFormatter),
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{Name: "r", Aliases: []string{"range"}, Usage: "type of range of commits, use: tag, date or hash", Required: true},
|
||||
&cli.StringFlag{Name: "s", Aliases: []string{"start"}, Usage: "start range of git log revision range, if date, the value is used on since flag instead"},
|
||||
&cli.StringFlag{Name: "e", Aliases: []string{"end"}, Usage: "end range of git log revision range, if date, the value is used on until flag instead"},
|
||||
{
|
||||
Name: "next-version",
|
||||
Aliases: []string{"nv"},
|
||||
Usage: "generate the next version based on git commit messages",
|
||||
Action: nextVersionHandler(git, semverProcessor),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "release-notes",
|
||||
Aliases: []string{"rn"},
|
||||
Usage: "generate release notes",
|
||||
Action: releaseNotesHandler(git, semverProcessor, releasenotesProcessor, outputFormatter),
|
||||
Flags: []cli.Flag{&cli.StringFlag{Name: "t", Aliases: []string{"tag"}, Usage: "get release note from tag"}},
|
||||
},
|
||||
{
|
||||
Name: "changelog",
|
||||
Aliases: []string{"cgl"},
|
||||
Usage: "generate changelog",
|
||||
Action: changelogHandler(git, semverProcessor, releasenotesProcessor, outputFormatter),
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{Name: "size", Value: 10, Aliases: []string{"n"}, Usage: "get changelog from last 'n' tags"},
|
||||
&cli.BoolFlag{Name: "all", Usage: "ignore size parameter, get changelog for every tag"},
|
||||
&cli.BoolFlag{Name: "add-next-version", Usage: "add next version on change log (commits since last tag, but only if there is a new version to release)"},
|
||||
&cli.BoolFlag{Name: "semantic-version-only", Usage: "only show tags 'SemVer-ish'"},
|
||||
{
|
||||
Name: "commit-log",
|
||||
Aliases: []string{"cl"},
|
||||
Usage: "list all commit logs according to range as jsons",
|
||||
Description: `The range filter is used based on git log filters, check https://git-scm.com/docs/git-log
|
||||
for more info. When flag range is "tag" and start is empty, last tag created will be used instead.
|
||||
When flag range is "date", if "end" is YYYY-MM-DD the range will be inclusive.`,
|
||||
Action: commitLogHandler(git),
|
||||
Flags: commitLogFlags(),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tag",
|
||||
Aliases: []string{"tg"},
|
||||
Usage: "generate tag with version based on git commit messages",
|
||||
Action: tagHandler(git, semverProcessor),
|
||||
},
|
||||
{
|
||||
Name: "commit",
|
||||
Aliases: []string{"cmt"},
|
||||
Usage: "execute git commit with convetional commit message helper",
|
||||
Action: commitHandler(cfg, git, messageProcessor),
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{Name: "no-scope", Aliases: []string{"nsc"}, Usage: "do not prompt for commit scope"},
|
||||
&cli.BoolFlag{Name: "no-body", Aliases: []string{"nbd"}, Usage: "do not prompt for commit body"},
|
||||
&cli.BoolFlag{Name: "no-issue", Aliases: []string{"nis"}, Usage: "do not prompt for commit issue, will try to recover from branch if enabled"},
|
||||
&cli.BoolFlag{Name: "no-breaking", Aliases: []string{"nbc"}, Usage: "do not prompt for breaking changes"},
|
||||
&cli.StringFlag{Name: "type", Aliases: []string{"t"}, Usage: "define commit type"},
|
||||
&cli.StringFlag{Name: "scope", Aliases: []string{"s"}, Usage: "define commit scope"},
|
||||
&cli.StringFlag{Name: "description", Aliases: []string{"d"}, Usage: "define commit description"},
|
||||
&cli.StringFlag{Name: "breaking-change", Aliases: []string{"b"}, Usage: "define commit breaking change message"},
|
||||
{
|
||||
Name: "commit-notes",
|
||||
Aliases: []string{"cn"},
|
||||
Usage: "generate a commit notes according to range",
|
||||
Description: `The range filter is used based on git log filters, check https://git-scm.com/docs/git-log
|
||||
for more info. When flag range is "tag" and start is empty, last tag created will be used instead.
|
||||
When flag range is "date", if "end" is YYYY-MM-DD the range will be inclusive.`,
|
||||
Action: commitNotesHandler(git, releasenotesProcessor, outputFormatter),
|
||||
Flags: commitNotesFlags(),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "validate-commit-message",
|
||||
Aliases: []string{"vcm"},
|
||||
Usage: "use as prepare-commit-message hook to validate and enhance commit message",
|
||||
Action: validateCommitMessageHandler(git, messageProcessor),
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{Name: "path", Required: true, Usage: "git working directory"},
|
||||
&cli.StringFlag{Name: "file", Required: true, Usage: "name of the file that contains the commit log message"},
|
||||
&cli.StringFlag{Name: "source", Required: true, Usage: "source of the commit message"},
|
||||
{
|
||||
Name: "release-notes",
|
||||
Aliases: []string{"rn"},
|
||||
Usage: "generate release notes",
|
||||
Action: releaseNotesHandler(git, semverProcessor, releasenotesProcessor, outputFormatter),
|
||||
Flags: releaseNotesFlags(),
|
||||
},
|
||||
{
|
||||
Name: "changelog",
|
||||
Aliases: []string{"cgl"},
|
||||
Usage: "generate changelog",
|
||||
Action: changelogHandler(git, semverProcessor, releasenotesProcessor, outputFormatter),
|
||||
Flags: changelogFlags(),
|
||||
},
|
||||
{
|
||||
Name: "tag",
|
||||
Aliases: []string{"tg"},
|
||||
Usage: "generate tag with version based on git commit messages",
|
||||
Action: tagHandler(git, semverProcessor),
|
||||
},
|
||||
{
|
||||
Name: "commit",
|
||||
Aliases: []string{"cmt"},
|
||||
Usage: "execute git commit with convetional commit message helper",
|
||||
Action: commitHandler(cfg, git, messageProcessor),
|
||||
Flags: commitFlags(),
|
||||
},
|
||||
{
|
||||
Name: "validate-commit-message",
|
||||
Aliases: []string{"vcm"},
|
||||
Usage: "use as prepare-commit-message hook to validate and enhance commit message",
|
||||
Action: validateCommitMessageHandler(git, messageProcessor),
|
||||
Flags: validateCommitMessageFlags(),
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -166,7 +151,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func loadCfg(repoPath string) Config {
|
||||
func loadCfg(wd string) Config {
|
||||
cfg := defaultConfig()
|
||||
|
||||
envCfg := loadEnvConfig()
|
||||
@ -179,11 +164,12 @@ func loadCfg(repoPath string) Config {
|
||||
}
|
||||
}
|
||||
|
||||
repoCfgFilepath := filepath.Join(repoPath, repoConfigFilename)
|
||||
repoCfgFilepath := filepath.Join(wd, configDir, configFilename)
|
||||
if repoCfg, err := readConfig(repoCfgFilepath); err == nil {
|
||||
if merr := merge(&cfg, migrateConfig(repoCfg, repoCfgFilepath)); merr != nil {
|
||||
log.Fatal("failed to merge repo config, error: ", merr)
|
||||
}
|
||||
|
||||
if len(repoCfg.ReleaseNotes.Headers) > 0 { // mergo is merging maps, headers will be overwritten
|
||||
cfg.ReleaseNotes.Headers = repoCfg.ReleaseNotes.Headers
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
@ -14,22 +15,62 @@ type commitType struct {
|
||||
Example string
|
||||
}
|
||||
|
||||
var errInvalidValue = errors.New("invalid value")
|
||||
|
||||
func promptType(types []string) (commitType, error) {
|
||||
defaultTypes := map[string]commitType{
|
||||
"build": {Type: "build", Description: "changes that affect the build system or external dependencies", Example: "gradle, maven, go mod, npm"},
|
||||
"ci": {Type: "ci", Description: "changes to our CI configuration files and scripts", Example: "Circle, BrowserStack, SauceLabs"},
|
||||
"chore": {Type: "chore", Description: "update something without impacting the user", Example: "gitignore"},
|
||||
"docs": {Type: "docs", Description: "documentation only changes"},
|
||||
"feat": {Type: "feat", Description: "a new feature"},
|
||||
"fix": {Type: "fix", Description: "a bug fix"},
|
||||
"perf": {Type: "perf", Description: "a code change that improves performance"},
|
||||
"refactor": {Type: "refactor", Description: "a code change that neither fixes a bug nor adds a feature"},
|
||||
"style": {Type: "style", Description: "changes that do not affect the meaning of the code", Example: "white-space, formatting, missing semi-colons, etc"},
|
||||
"test": {Type: "test", Description: "adding missing tests or correcting existing tests"},
|
||||
"revert": {Type: "revert", Description: "revert a single commit"},
|
||||
"build": {
|
||||
Type: "build",
|
||||
Description: "changes that affect the build system or external dependencies",
|
||||
Example: "gradle, maven, go mod, npm",
|
||||
},
|
||||
"ci": {
|
||||
Type: "ci",
|
||||
Description: "changes to our CI configuration files and scripts",
|
||||
Example: "Circle, BrowserStack, SauceLabs",
|
||||
},
|
||||
"chore": {
|
||||
Type: "chore",
|
||||
Description: "update something without impacting the user",
|
||||
Example: "gitignore",
|
||||
},
|
||||
"docs": {
|
||||
Type: "docs",
|
||||
Description: "documentation only changes",
|
||||
},
|
||||
"feat": {
|
||||
Type: "feat",
|
||||
Description: "a new feature",
|
||||
},
|
||||
"fix": {
|
||||
Type: "fix",
|
||||
Description: "a bug fix",
|
||||
},
|
||||
"perf": {
|
||||
Type: "perf",
|
||||
Description: "a code change that improves performance",
|
||||
},
|
||||
"refactor": {
|
||||
Type: "refactor",
|
||||
Description: "a code change that neither fixes a bug nor adds a feature",
|
||||
},
|
||||
"style": {
|
||||
Type: "style",
|
||||
Description: "changes that do not affect the meaning of the code",
|
||||
Example: "white-space, formatting, missing semi-colons, etc",
|
||||
},
|
||||
"test": {
|
||||
Type: "test",
|
||||
Description: "adding missing tests or correcting existing tests",
|
||||
},
|
||||
"revert": {
|
||||
Type: "revert",
|
||||
Description: "revert a single commit",
|
||||
},
|
||||
}
|
||||
|
||||
var items []commitType
|
||||
|
||||
for _, t := range types {
|
||||
if v, exists := defaultTypes[t]; exists {
|
||||
items = append(items, v)
|
||||
@ -53,6 +94,7 @@ func promptType(types []string) (commitType, error) {
|
||||
if err != nil {
|
||||
return commitType{}, err
|
||||
}
|
||||
|
||||
return items[i], nil
|
||||
}
|
||||
|
||||
@ -62,8 +104,10 @@ func promptScope(values []string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return values[selected], nil
|
||||
}
|
||||
|
||||
return promptText("scope", "^[a-z0-9-]*$", "")
|
||||
}
|
||||
|
||||
@ -85,7 +129,7 @@ func promptBreakingChanges() (string, error) {
|
||||
|
||||
func promptSelect(label string, items interface{}, template *promptui.SelectTemplates) (int, error) {
|
||||
if items == nil || reflect.TypeOf(items).Kind() != reflect.Slice {
|
||||
return 0, fmt.Errorf("items %v is not a slice", items)
|
||||
return 0, fmt.Errorf("%w: %v is not a slice", errInvalidValue, items)
|
||||
}
|
||||
|
||||
prompt := promptui.Select{
|
||||
@ -96,6 +140,7 @@ func promptSelect(label string, items interface{}, template *promptui.SelectTemp
|
||||
}
|
||||
|
||||
index, _, err := prompt.Run()
|
||||
|
||||
return index, err
|
||||
}
|
||||
|
||||
@ -103,8 +148,9 @@ func promptText(label, regex, defaultValue string) (string, error) {
|
||||
validate := func(input string) error {
|
||||
regex := regexp.MustCompile(regex)
|
||||
if !regex.MatchString(input) {
|
||||
return fmt.Errorf("invalid value, expected: %s", regex)
|
||||
return fmt.Errorf("%w, expected: %s", errInvalidValue, regex)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -122,5 +168,6 @@ func promptConfirm(label string) (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return r == "y", nil
|
||||
}
|
||||
|
@ -14,9 +14,11 @@ func Test_checkTemplatesFiles(t *testing.T) {
|
||||
got, err := defaultTemplatesFS.ReadFile(tt)
|
||||
if err != nil {
|
||||
t.Errorf("missing template error = %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
if len(got) <= 0 {
|
||||
|
||||
if len(got) == 0 {
|
||||
t.Errorf("empty template")
|
||||
}
|
||||
})
|
||||
|
13
go.mod
13
go.mod
@ -1,22 +1,23 @@
|
||||
module github.com/bvieira/sv4git/v2
|
||||
module github.com/thegeeklab/git-sv/v2
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.2.0
|
||||
github.com/imdario/mergo v0.3.13
|
||||
dario.cat/mergo v1.0.0
|
||||
github.com/Masterminds/semver/v3 v3.2.1
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/urfave/cli/v2 v2.24.1
|
||||
github.com/urfave/cli/v2 v2.25.7
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
)
|
||||
|
24
go.sum
24
go.sum
@ -1,5 +1,7 @@
|
||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
@ -9,11 +11,9 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
||||
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
||||
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
@ -26,21 +26,21 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/urfave/cli/v2 v2.24.1 h1:/QYYr7g0EhwXEML8jO+8OYt5trPnLHS0p3mrgExJ5NU=
|
||||
github.com/urfave/cli/v2 v2.24.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
4
renovate.json
Normal file
4
renovate.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["github>thegeeklab/renovate-presets:golang"]
|
||||
}
|
@ -16,6 +16,7 @@ func (c CommitMessageConfig) IssueFooterConfig() CommitMessageFooterConfig {
|
||||
if v, exists := c.Footer[issueMetadataKey]; exists {
|
||||
return v
|
||||
}
|
||||
|
||||
return CommitMessageFooterConfig{}
|
||||
}
|
||||
|
||||
@ -80,6 +81,7 @@ func (cfg ReleaseNotesConfig) sectionConfig(sectionType string) *ReleaseNotesSec
|
||||
return §ionCfg
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
11
sv/errors.go
Normal file
11
sv/errors.go
Normal 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")
|
||||
)
|
@ -39,6 +39,7 @@ func NewOutputFormatter(templatesFS fs.FS) *OutputFormatterImpl {
|
||||
"getenv": os.Getenv,
|
||||
}
|
||||
tpls := template.Must(template.New("templates").Funcs(templateFNs).ParseFS(templatesFS, "*"))
|
||||
|
||||
return &OutputFormatterImpl{templates: tpls}
|
||||
}
|
||||
|
||||
@ -48,6 +49,7 @@ func (p OutputFormatterImpl) FormatReleaseNote(releasenote ReleaseNote) (string,
|
||||
if err := p.templates.ExecuteTemplate(&b, "releasenotes-md.tpl", releaseNoteVariables(releasenote)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
@ -62,6 +64,7 @@ func (p OutputFormatterImpl) FormatChangelog(releasenotes []ReleaseNote) (string
|
||||
if err := p.templates.ExecuteTemplate(&b, "changelog-md.tpl", templateVars); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
@ -70,6 +73,7 @@ func releaseNoteVariables(releasenote ReleaseNote) releaseNoteTemplateVariables
|
||||
if releasenote.Version != nil {
|
||||
release = "v" + releasenote.Version.String()
|
||||
}
|
||||
|
||||
return releaseNoteTemplateVariables{
|
||||
Release: release,
|
||||
Tag: releasenote.Tag,
|
||||
@ -83,10 +87,13 @@ func releaseNoteVariables(releasenote ReleaseNote) releaseNoteTemplateVariables
|
||||
func toSortedArray(input map[string]struct{}) []string {
|
||||
result := make([]string, len(input))
|
||||
i := 0
|
||||
|
||||
for k := range input {
|
||||
result[i] = k
|
||||
i++
|
||||
}
|
||||
|
||||
sort.Strings(result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
@ -6,14 +6,16 @@ func timeFormat(t time.Time, format string) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
|
||||
return t.Format(format)
|
||||
}
|
||||
|
||||
func getSection(sections []ReleaseNoteSection, name string) ReleaseNoteSection {
|
||||
func getSection(sections []ReleaseNoteSection, name string) ReleaseNoteSection { //nolint:ireturn
|
||||
for _, section := range sections {
|
||||
if section.SectionName() == name {
|
||||
return section
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -32,8 +32,20 @@ func Test_getSection(t *testing.T) {
|
||||
sectionName string
|
||||
want ReleaseNoteSection
|
||||
}{
|
||||
{"existing section", []ReleaseNoteSection{ReleaseNoteCommitsSection{Name: "section 0"}, ReleaseNoteCommitsSection{Name: "section 1"}, ReleaseNoteCommitsSection{Name: "section 2"}}, "section 1", ReleaseNoteCommitsSection{Name: "section 1"}},
|
||||
{"nonexisting section", []ReleaseNoteSection{ReleaseNoteCommitsSection{Name: "section 0"}, ReleaseNoteCommitsSection{Name: "section 1"}, ReleaseNoteCommitsSection{Name: "section 2"}}, "section 10", nil},
|
||||
{
|
||||
"existing section", []ReleaseNoteSection{
|
||||
ReleaseNoteCommitsSection{Name: "section 0"},
|
||||
ReleaseNoteCommitsSection{Name: "section 1"},
|
||||
ReleaseNoteCommitsSection{Name: "section 2"},
|
||||
}, "section 1", ReleaseNoteCommitsSection{Name: "section 1"},
|
||||
},
|
||||
{
|
||||
"nonexisting section", []ReleaseNoteSection{
|
||||
ReleaseNoteCommitsSection{Name: "section 0"},
|
||||
ReleaseNoteCommitsSection{Name: "section 1"},
|
||||
ReleaseNoteCommitsSection{Name: "section 2"},
|
||||
}, "section 10", nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -73,6 +73,7 @@ func TestOutputFormatterImpl_FormatReleaseNote(t *testing.T) {
|
||||
|
||||
func emptyReleaseNote(tag string, date time.Time) ReleaseNote {
|
||||
v, _ := semver.NewVersion(tag)
|
||||
|
||||
return ReleaseNote{
|
||||
Version: v,
|
||||
Tag: tag,
|
||||
@ -83,11 +84,24 @@ func emptyReleaseNote(tag string, date time.Time) ReleaseNote {
|
||||
func fullReleaseNote(tag string, date time.Time) ReleaseNote {
|
||||
v, _ := semver.NewVersion(tag)
|
||||
sections := []ReleaseNoteSection{
|
||||
newReleaseNoteCommitsSection("Features", []string{"feat"}, []GitCommitLog{commitlog("feat", map[string]string{}, "a")}),
|
||||
newReleaseNoteCommitsSection("Bug Fixes", []string{"fix"}, []GitCommitLog{commitlog("fix", map[string]string{}, "a")}),
|
||||
newReleaseNoteCommitsSection("Build", []string{"build"}, []GitCommitLog{commitlog("build", map[string]string{}, "a")}),
|
||||
newReleaseNoteCommitsSection(
|
||||
"Features",
|
||||
[]string{"feat"},
|
||||
[]GitCommitLog{commitlog("feat", map[string]string{}, "a")},
|
||||
),
|
||||
newReleaseNoteCommitsSection(
|
||||
"Bug Fixes",
|
||||
[]string{"fix"},
|
||||
[]GitCommitLog{commitlog("fix", map[string]string{}, "a")},
|
||||
),
|
||||
newReleaseNoteCommitsSection(
|
||||
"Build",
|
||||
[]string{"build"},
|
||||
[]GitCommitLog{commitlog("build", map[string]string{}, "a")},
|
||||
),
|
||||
ReleaseNoteBreakingChangeSection{"Breaking Changes", []string{"break change message"}},
|
||||
}
|
||||
|
||||
return releaseNote(v, tag, date, sections, map[string]struct{}{"a": {}})
|
||||
}
|
||||
|
||||
@ -100,15 +114,18 @@ func Test_checkTemplatesExecution(t *testing.T) {
|
||||
{"changelog-md.tpl", changelogVariables("v1.0.0", "v1.0.1")},
|
||||
{"releasenotes-md.tpl", releaseNotesVariables("v1.0.0")},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.template, func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
err := tpls.ExecuteTemplate(&b, tt.template, tt.variables)
|
||||
if err != nil {
|
||||
t.Errorf("invalid template err = %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
if len(b.Bytes()) <= 0 {
|
||||
|
||||
if len(b.Bytes()) == 0 {
|
||||
t.Errorf("empty template")
|
||||
}
|
||||
})
|
||||
@ -118,11 +135,20 @@ func Test_checkTemplatesExecution(t *testing.T) {
|
||||
func releaseNotesVariables(release string) releaseNoteTemplateVariables {
|
||||
return releaseNoteTemplateVariables{
|
||||
Release: release,
|
||||
Date: time.Date(2006, 1, 02, 0, 0, 0, 0, time.UTC),
|
||||
Date: time.Date(2006, 1, 0o2, 0, 0, 0, 0, time.UTC),
|
||||
Sections: []ReleaseNoteSection{
|
||||
newReleaseNoteCommitsSection("Features", []string{"feat"}, []GitCommitLog{commitlog("feat", map[string]string{}, "a")}),
|
||||
newReleaseNoteCommitsSection("Bug Fixes", []string{"fix"}, []GitCommitLog{commitlog("fix", map[string]string{}, "a")}),
|
||||
newReleaseNoteCommitsSection("Build", []string{"build"}, []GitCommitLog{commitlog("build", map[string]string{}, "a")}),
|
||||
newReleaseNoteCommitsSection("Features",
|
||||
[]string{"feat"},
|
||||
[]GitCommitLog{commitlog("feat", map[string]string{}, "a")},
|
||||
),
|
||||
newReleaseNoteCommitsSection("Bug Fixes",
|
||||
[]string{"fix"},
|
||||
[]GitCommitLog{commitlog("fix", map[string]string{}, "a")},
|
||||
),
|
||||
newReleaseNoteCommitsSection("Build",
|
||||
[]string{"build"},
|
||||
[]GitCommitLog{commitlog("build", map[string]string{}, "a")},
|
||||
),
|
||||
ReleaseNoteBreakingChangeSection{"Breaking Changes", []string{"break change message"}},
|
||||
},
|
||||
}
|
||||
@ -130,9 +156,10 @@ func releaseNotesVariables(release string) releaseNoteTemplateVariables {
|
||||
|
||||
func changelogVariables(releases ...string) []releaseNoteTemplateVariables {
|
||||
var variables []releaseNoteTemplateVariables
|
||||
|
||||
for _, r := range releases {
|
||||
variables = append(variables, releaseNotesVariables(r))
|
||||
}
|
||||
return variables
|
||||
|
||||
return variables
|
||||
}
|
||||
|
74
sv/git.go
74
sv/git.go
@ -3,7 +3,6 @@ package sv
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@ -83,17 +82,35 @@ func NewGit(messageProcessor MessageProcessor, cfg TagConfig) *GitImpl {
|
||||
|
||||
// LastTag get last tag, if no tag found, return empty.
|
||||
func (g GitImpl) LastTag() string {
|
||||
cmd := exec.Command("git", "for-each-ref", "refs/tags/"+*g.tagCfg.Filter, "--sort", "-creatordate", "--format", "%(refname:short)", "--count", "1")
|
||||
//nolint:gosec
|
||||
cmd := exec.Command(
|
||||
"git",
|
||||
"for-each-ref",
|
||||
fmt.Sprintf("refs/tags/%s", *g.tagCfg.Filter),
|
||||
"--sort",
|
||||
"-creatordate",
|
||||
"--format",
|
||||
"%(refname:short)",
|
||||
"--count",
|
||||
"1",
|
||||
)
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(strings.Trim(string(out), "\n"))
|
||||
}
|
||||
|
||||
// Log return git log.
|
||||
func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) {
|
||||
format := "--pretty=format:\"%ad" + logSeparator + "%at" + logSeparator + "%cN" + logSeparator + "%h" + logSeparator + "%s" + logSeparator + "%b" + endLine + "\""
|
||||
format := "--pretty=format:\"%ad" + logSeparator +
|
||||
"%at" + logSeparator +
|
||||
"%cN" + logSeparator +
|
||||
"%h" + logSeparator +
|
||||
"%s" + logSeparator +
|
||||
"%b" + endLine + "\""
|
||||
params := []string{"log", "--date=short", format}
|
||||
|
||||
if lr.start != "" || lr.end != "" {
|
||||
@ -110,14 +127,17 @@ func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) {
|
||||
}
|
||||
|
||||
cmd := exec.Command("git", params...)
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, combinedOutputErr(err, out)
|
||||
}
|
||||
|
||||
logs, parseErr := parseLogOutput(g.messageProcessor, string(out))
|
||||
if parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
@ -126,6 +146,7 @@ func (g GitImpl) Commit(header, body, footer string) error {
|
||||
cmd := exec.Command("git", "commit", "-m", header, "-m", "", "-m", body, "-m", "", "-m", footer)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
@ -143,45 +164,66 @@ func (g GitImpl) Tag(version semver.Version) (string, error) {
|
||||
if out, err := pushCommand.CombinedOutput(); err != nil {
|
||||
return tag, combinedOutputErr(err, out)
|
||||
}
|
||||
|
||||
return tag, nil
|
||||
}
|
||||
|
||||
// Tags list repository tags.
|
||||
func (g GitImpl) Tags() ([]GitTag, error) {
|
||||
cmd := exec.Command("git", "for-each-ref", "--sort", "creatordate", "--format", "%(creatordate:iso8601)#%(refname:short)", "refs/tags/"+*g.tagCfg.Filter)
|
||||
//nolint:gosec
|
||||
cmd := exec.Command(
|
||||
"git",
|
||||
"for-each-ref",
|
||||
"--sort",
|
||||
"creatordate",
|
||||
"--format",
|
||||
"%(creatordate:iso8601)#%(refname:short)",
|
||||
fmt.Sprintf("refs/tags/%s", *g.tagCfg.Filter),
|
||||
)
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, combinedOutputErr(err, out)
|
||||
}
|
||||
|
||||
return parseTagsOutput(string(out))
|
||||
}
|
||||
|
||||
// Branch get git branch.
|
||||
func (GitImpl) Branch() string {
|
||||
cmd := exec.Command("git", "symbolic-ref", "--short", "HEAD")
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(strings.Trim(string(out), "\n"))
|
||||
}
|
||||
|
||||
// IsDetached check if is detached.
|
||||
func (GitImpl) IsDetached() (bool, error) {
|
||||
cmd := exec.Command("git", "symbolic-ref", "-q", "HEAD")
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if output := string(out); err != nil { //-q: do not issue an error message if the <name> is not a symbolic ref, but a detached HEAD; instead exit with non-zero status silently.
|
||||
// -q: do not issue an error message if the <name> is not a symbolic ref, but a detached HEAD;
|
||||
// instead exit with non-zero status silently.
|
||||
if output := string(out); err != nil {
|
||||
if output == "" {
|
||||
return true, nil
|
||||
}
|
||||
return false, errors.New(output)
|
||||
|
||||
return false, fmt.Errorf("%w: %s", errUnknownGitError, output)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func parseTagsOutput(input string) ([]GitTag, error) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(input))
|
||||
|
||||
var result []GitTag
|
||||
|
||||
for scanner.Scan() {
|
||||
if line := strings.TrimSpace(scanner.Text()); line != "" {
|
||||
values := strings.Split(line, "#")
|
||||
@ -189,31 +231,35 @@ func parseTagsOutput(input string) ([]GitTag, error) {
|
||||
result = append(result, GitTag{Name: values[1], Date: date})
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseLogOutput(messageProcessor MessageProcessor, log string) ([]GitCommitLog, error) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(log))
|
||||
scanner.Split(splitAt([]byte(endLine)))
|
||||
|
||||
var logs []GitCommitLog
|
||||
|
||||
for scanner.Scan() {
|
||||
if text := strings.TrimSpace(strings.Trim(scanner.Text(), "\"")); text != "" {
|
||||
log, err := parseCommitLog(messageProcessor, text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logs = append(logs, log)
|
||||
}
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func parseCommitLog(messageProcessor MessageProcessor, commit string) (GitCommitLog, error) {
|
||||
content := strings.Split(strings.Trim(commit, "\""), logSeparator)
|
||||
|
||||
timestamp, _ := strconv.Atoi(content[1])
|
||||
message, err := messageProcessor.Parse(content[4], content[5])
|
||||
|
||||
message, err := messageProcessor.Parse(content[4], content[5])
|
||||
if err != nil {
|
||||
return GitCommitLog{}, err
|
||||
}
|
||||
@ -228,10 +274,8 @@ func parseCommitLog(messageProcessor MessageProcessor, commit string) (GitCommit
|
||||
}
|
||||
|
||||
func splitAt(b []byte) func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
return func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
dataLen := len(data)
|
||||
|
||||
if atEOF && dataLen == 0 {
|
||||
return func(data []byte, atEOF bool) (advance int, token []byte, err error) { //nolint:nonamedreturns
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
@ -240,7 +284,7 @@ func splitAt(b []byte) func(data []byte, atEOF bool) (advance int, token []byte,
|
||||
}
|
||||
|
||||
if atEOF {
|
||||
return dataLen, data, nil
|
||||
return len(data), data, nil
|
||||
}
|
||||
|
||||
return 0, nil, nil
|
||||
@ -264,10 +308,12 @@ func str(value, defaultValue string) string {
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func combinedOutputErr(err error, out []byte) error {
|
||||
msg := strings.Split(string(out), "\n")
|
||||
return fmt.Errorf("%v - %s", err, msg[0])
|
||||
|
||||
return fmt.Errorf("%w - %s", err, msg[0])
|
||||
}
|
||||
|
@ -13,16 +13,28 @@ func Test_parseTagsOutput(t *testing.T) {
|
||||
want []GitTag
|
||||
wantErr bool
|
||||
}{
|
||||
{"with date", "2020-05-01 18:00:00 -0300#1.0.0", []GitTag{{Name: "1.0.0", Date: date("2020-05-01 18:00:00 -0300")}}, false},
|
||||
{"without date", "#1.0.0", []GitTag{{Name: "1.0.0", Date: time.Time{}}}, false},
|
||||
{
|
||||
"with date",
|
||||
"2020-05-01 18:00:00 -0300#1.0.0",
|
||||
[]GitTag{{Name: "1.0.0", Date: date("2020-05-01 18:00:00 -0300")}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"without date",
|
||||
"#1.0.0",
|
||||
[]GitTag{{Name: "1.0.0", Date: time.Time{}}},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseTagsOutput(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseTagsOutput() error = %v, wantErr %v", err, tt.wantErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("parseTagsOutput() = %v, want %v", got, tt.want)
|
||||
}
|
||||
@ -35,5 +47,6 @@ func date(input string) time.Time {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
|
||||
func version(v string) *semver.Version {
|
||||
r, _ := semver.NewVersion(v)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@ -16,6 +17,7 @@ func commitlog(ctype string, metadata map[string]string, author string) GitCommi
|
||||
if _, found := metadata[breakingChangeMetadataKey]; found {
|
||||
breaking = true
|
||||
}
|
||||
|
||||
return GitCommitLog{
|
||||
Message: CommitMessage{
|
||||
Type: ctype,
|
||||
@ -27,7 +29,13 @@ func commitlog(ctype string, metadata map[string]string, author string) GitCommi
|
||||
}
|
||||
}
|
||||
|
||||
func releaseNote(version *semver.Version, tag string, date time.Time, sections []ReleaseNoteSection, authorsNames map[string]struct{}) ReleaseNote {
|
||||
func releaseNote(
|
||||
version *semver.Version,
|
||||
tag string,
|
||||
date time.Time,
|
||||
sections []ReleaseNoteSection,
|
||||
authorsNames map[string]struct{},
|
||||
) ReleaseNote {
|
||||
return ReleaseNote{
|
||||
Version: version,
|
||||
Tag: tag,
|
||||
|
@ -30,10 +30,19 @@ func NewCommitMessage(ctype, scope, description, body, issue, breakingChanges st
|
||||
if issue != "" {
|
||||
metadata[issueMetadataKey] = issue
|
||||
}
|
||||
|
||||
if breakingChanges != "" {
|
||||
metadata[breakingChangeMetadataKey] = breakingChanges
|
||||
}
|
||||
return CommitMessage{Type: ctype, Scope: scope, Description: description, Body: body, IsBreakingChange: breakingChanges != "", Metadata: metadata}
|
||||
|
||||
return CommitMessage{
|
||||
Type: ctype,
|
||||
Scope: scope,
|
||||
Description: description,
|
||||
Body: body,
|
||||
IsBreakingChange: breakingChanges != "",
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
// Issue return issue from metadata.
|
||||
@ -53,7 +62,7 @@ type MessageProcessor interface {
|
||||
ValidateType(ctype string) error
|
||||
ValidateScope(scope string) error
|
||||
ValidateDescription(description string) error
|
||||
Enhance(branch string, message string) (string, error)
|
||||
Enhance(branch, message string) (string, error)
|
||||
IssueID(branch string) (string, error)
|
||||
Format(msg CommitMessage) (string, string, string)
|
||||
Parse(subject, body string) (CommitMessage, error)
|
||||
@ -75,7 +84,8 @@ type MessageProcessorImpl struct {
|
||||
|
||||
// SkipBranch check if branch should be ignored.
|
||||
func (p MessageProcessorImpl) SkipBranch(branch string, detached bool) bool {
|
||||
return contains(branch, p.branchesCfg.Skip) || (p.branchesCfg.SkipDetached != nil && *p.branchesCfg.SkipDetached && detached)
|
||||
return contains(branch, p.branchesCfg.Skip) ||
|
||||
(p.branchesCfg.SkipDetached != nil && *p.branchesCfg.SkipDetached && detached)
|
||||
}
|
||||
|
||||
// Validate commit message.
|
||||
@ -88,7 +98,7 @@ func (p MessageProcessorImpl) Validate(message string) error {
|
||||
}
|
||||
|
||||
if !regexp.MustCompile(`^[a-z+]+(\(.+\))?!?: .+$`).MatchString(subject) {
|
||||
return fmt.Errorf("subject [%s] should be valid according with conventional commits", subject)
|
||||
return fmt.Errorf("%w: subject [%s] not valid", errInvalidCommitMessage, subject)
|
||||
}
|
||||
|
||||
if err := p.ValidateType(msg.Type); err != nil {
|
||||
@ -99,40 +109,48 @@ func (p MessageProcessorImpl) Validate(message string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.ValidateDescription(msg.Description); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return p.ValidateDescription(msg.Description)
|
||||
}
|
||||
|
||||
// ValidateType check if commit type is valid.
|
||||
func (p MessageProcessorImpl) ValidateType(ctype string) error {
|
||||
if ctype == "" || !contains(ctype, p.messageCfg.Types) {
|
||||
return fmt.Errorf("message type should be one of [%v]", strings.Join(p.messageCfg.Types, ", "))
|
||||
return fmt.Errorf(
|
||||
"%w: type must be one of [%s]",
|
||||
errInvalidCommitMessage,
|
||||
strings.Join(p.messageCfg.Types, ", "),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateScope check if commit scope is valid.
|
||||
func (p MessageProcessorImpl) ValidateScope(scope string) error {
|
||||
if len(p.messageCfg.Scope.Values) > 0 && !contains(scope, p.messageCfg.Scope.Values) {
|
||||
return fmt.Errorf("message scope should one of [%v]", strings.Join(p.messageCfg.Scope.Values, ", "))
|
||||
return fmt.Errorf(
|
||||
"%w: scope must one of [%s]",
|
||||
errInvalidCommitMessage,
|
||||
strings.Join(p.messageCfg.Scope.Values, ", "),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateDescription check if commit description is valid.
|
||||
func (p MessageProcessorImpl) ValidateDescription(description string) error {
|
||||
if !regexp.MustCompile("^[a-z]+.*$").MatchString(description) {
|
||||
return fmt.Errorf("description [%s] should begins with lowercase letter", description)
|
||||
return fmt.Errorf("%w: description [%s] must start with lowercase", errInvalidCommitMessage, description)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enhance add metadata on commit message.
|
||||
func (p MessageProcessorImpl) Enhance(branch string, message string) (string, error) {
|
||||
if p.branchesCfg.DisableIssue || p.messageCfg.IssueFooterConfig().Key == "" || hasIssueID(message, p.messageCfg.IssueFooterConfig()) {
|
||||
func (p MessageProcessorImpl) Enhance(branch, message string) (string, error) {
|
||||
if p.branchesCfg.DisableIssue || p.messageCfg.IssueFooterConfig().Key == "" ||
|
||||
hasIssueID(message, p.messageCfg.IssueFooterConfig()) {
|
||||
return "", nil // enhance disabled
|
||||
}
|
||||
|
||||
@ -140,8 +158,9 @@ func (p MessageProcessorImpl) Enhance(branch string, message string) (string, er
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if issue == "" {
|
||||
return "", fmt.Errorf("could not find issue id using configured regex")
|
||||
return "", errIssueIDNotFound
|
||||
}
|
||||
|
||||
footer := formatIssueFooter(p.messageCfg.IssueFooterConfig(), issue)
|
||||
@ -156,9 +175,11 @@ func formatIssueFooter(cfg CommitMessageFooterConfig, issue string) string {
|
||||
if !strings.HasPrefix(issue, cfg.AddValuePrefix) {
|
||||
issue = cfg.AddValuePrefix + issue
|
||||
}
|
||||
|
||||
if cfg.UseHash {
|
||||
return fmt.Sprintf("%s #%s", cfg.Key, strings.TrimPrefix(issue, "#"))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s: %s", cfg.Key, issue)
|
||||
}
|
||||
|
||||
@ -169,25 +190,30 @@ func (p MessageProcessorImpl) IssueID(branch string) (string, error) {
|
||||
}
|
||||
|
||||
rstr := fmt.Sprintf("^%s(%s)%s$", p.branchesCfg.Prefix, p.messageCfg.Issue.Regex, p.branchesCfg.Suffix)
|
||||
|
||||
r, err := regexp.Compile(rstr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not compile issue regex: %s, error: %v", rstr, err.Error())
|
||||
return "", fmt.Errorf("%w: %s: %v", errInvalidIssueRegex, rstr, err.Error())
|
||||
}
|
||||
|
||||
groups := r.FindStringSubmatch(branch)
|
||||
if len(groups) != 4 {
|
||||
if len(groups) != 4 { //nolint:gomnd
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return groups[2], nil
|
||||
}
|
||||
|
||||
// Format a commit message returning header, body and footer.
|
||||
func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string) {
|
||||
var header strings.Builder
|
||||
|
||||
header.WriteString(msg.Type)
|
||||
|
||||
if msg.Scope != "" {
|
||||
header.WriteString("(" + msg.Scope + ")")
|
||||
}
|
||||
|
||||
header.WriteString(": ")
|
||||
header.WriteString(msg.Description)
|
||||
|
||||
@ -195,10 +221,12 @@ func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string)
|
||||
if msg.BreakingMessage() != "" {
|
||||
footer.WriteString(fmt.Sprintf("%s: %s", breakingChangeFooterKey, msg.BreakingMessage()))
|
||||
}
|
||||
|
||||
if issue, exists := msg.Metadata[issueMetadataKey]; exists && p.messageCfg.IssueFooterConfig().Key != "" {
|
||||
if footer.Len() > 0 {
|
||||
footer.WriteString("\n")
|
||||
}
|
||||
|
||||
footer.WriteString(formatIssueFooter(p.messageCfg.IssueFooterConfig(), issue))
|
||||
}
|
||||
|
||||
@ -221,17 +249,20 @@ func (p MessageProcessorImpl) Parse(subject, body string) (CommitMessage, error)
|
||||
commitType, scope, description, hasBreakingChange := parseSubjectMessage(preparedSubject)
|
||||
|
||||
metadata := make(map[string]string)
|
||||
|
||||
for key, mdCfg := range p.messageCfg.Footer {
|
||||
if mdCfg.Key != "" {
|
||||
prefixes := append([]string{mdCfg.Key}, mdCfg.KeySynonyms...)
|
||||
for _, prefix := range prefixes {
|
||||
if tagValue := extractFooterMetadata(prefix, commitBody, mdCfg.UseHash); tagValue != "" {
|
||||
metadata[key] = tagValue
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tagValue := extractFooterMetadata(breakingChangeFooterKey, commitBody, false); tagValue != "" {
|
||||
metadata[breakingChangeMetadataKey] = tagValue
|
||||
hasBreakingChange = true
|
||||
@ -254,18 +285,23 @@ func (p MessageProcessorImpl) prepareHeader(header string) (string, error) {
|
||||
|
||||
regex, err := regexp.Compile(p.messageCfg.HeaderSelector)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid regex on header-selector %s, error: %s", p.messageCfg.HeaderSelector, err.Error())
|
||||
return "", fmt.Errorf("%w: %s: %s", errInvalidHeaderRegex, p.messageCfg.HeaderSelector, err.Error())
|
||||
}
|
||||
|
||||
index := regex.SubexpIndex(messageRegexGroupName)
|
||||
if index < 0 {
|
||||
return "", fmt.Errorf("could not find %s regex group on header-selector regex", messageRegexGroupName)
|
||||
return "", fmt.Errorf("%w: could not find group %s", errInvalidHeaderRegex, messageRegexGroupName)
|
||||
}
|
||||
|
||||
match := regex.FindStringSubmatch(header)
|
||||
|
||||
if match == nil || len(match) < index {
|
||||
return "", fmt.Errorf("could not find %s regex group in match result for '%s'", messageRegexGroupName, header)
|
||||
return "", fmt.Errorf(
|
||||
"%w: could not find group %s in match result for '%s'",
|
||||
errInvalidHeaderRegex,
|
||||
messageRegexGroupName,
|
||||
header,
|
||||
)
|
||||
}
|
||||
|
||||
return match[index], nil
|
||||
@ -273,10 +309,12 @@ func (p MessageProcessorImpl) prepareHeader(header string) (string, error) {
|
||||
|
||||
func parseSubjectMessage(message string) (string, string, string, bool) {
|
||||
regex := regexp.MustCompile(`([a-z]+)(\((.*)\))?(!)?: (.*)`)
|
||||
|
||||
result := regex.FindStringSubmatch(message)
|
||||
if len(result) != 6 {
|
||||
if len(result) != 6 { //nolint:gomnd
|
||||
return "", "", message, false
|
||||
}
|
||||
|
||||
return result[1], result[3], strings.TrimSpace(result[5]), result[4] == "!"
|
||||
}
|
||||
|
||||
@ -289,9 +327,10 @@ func extractFooterMetadata(key, text string, useHash bool) string {
|
||||
}
|
||||
|
||||
result := regex.FindStringSubmatch(text)
|
||||
if len(result) < 2 {
|
||||
if len(result) < 2 { //nolint:gomnd
|
||||
return ""
|
||||
}
|
||||
|
||||
return result[1]
|
||||
}
|
||||
|
||||
@ -300,6 +339,7 @@ func hasFooter(message string) bool {
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(message))
|
||||
lines := 0
|
||||
|
||||
for scanner.Scan() {
|
||||
if lines > 0 && r.MatchString(scanner.Text()) {
|
||||
return true
|
||||
@ -317,6 +357,7 @@ func hasIssueID(message string, issueConfig CommitMessageFooterConfig) bool {
|
||||
} else {
|
||||
r = regexp.MustCompile(fmt.Sprintf("(?m)^%s: .+$", issueConfig.Key))
|
||||
}
|
||||
|
||||
return r.MatchString(message)
|
||||
}
|
||||
|
||||
@ -326,6 +367,7 @@ func contains(value string, content []string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@ -336,12 +378,15 @@ func splitCommitMessageContent(content string) (string, string) {
|
||||
subject := scanner.Text()
|
||||
|
||||
var body strings.Builder
|
||||
|
||||
first := true
|
||||
for scanner.Scan() {
|
||||
if !first {
|
||||
body.WriteString("\n")
|
||||
}
|
||||
|
||||
body.WriteString(scanner.Text())
|
||||
|
||||
first = false
|
||||
}
|
||||
|
||||
|
@ -146,23 +146,55 @@ func TestMessageProcessorImpl_Validate(t *testing.T) {
|
||||
message string
|
||||
wantErr bool
|
||||
}{
|
||||
{"single line valid message", ccfg, "feat: add something", false},
|
||||
{"single line valid message with scope", ccfg, "feat(scope): add something", false},
|
||||
{
|
||||
"single line valid message",
|
||||
ccfg,
|
||||
"feat: add something", false,
|
||||
},
|
||||
{
|
||||
"single line valid message with scope",
|
||||
ccfg,
|
||||
"feat(scope): add something", false,
|
||||
},
|
||||
{"single line valid scope from list", ccfgWithScope, "feat(scope): add something", false},
|
||||
{"single line invalid scope from list", ccfgWithScope, "feat(invalid): add something", true},
|
||||
{"single line invalid type message", ccfg, "something: add something", true},
|
||||
{"single line invalid type message", ccfg, "feat?: add something", true},
|
||||
{
|
||||
"single line invalid type message",
|
||||
ccfg,
|
||||
"something: add something", true,
|
||||
},
|
||||
{
|
||||
"single line invalid type message",
|
||||
ccfg,
|
||||
"feat?: add something", true,
|
||||
},
|
||||
|
||||
{"multi line valid message", ccfg, `feat: add something
|
||||
|
||||
team: x`, false},
|
||||
{
|
||||
"multi line valid message",
|
||||
ccfg,
|
||||
`feat: add something
|
||||
|
||||
{"multi line invalid message", ccfg, `feat add something
|
||||
|
||||
team: x`, true},
|
||||
team: x`, false,
|
||||
},
|
||||
|
||||
{"support ! for breaking change", ccfg, "feat!: add something", false},
|
||||
{"support ! with scope for breaking change", ccfg, "feat(scope)!: add something", false},
|
||||
{
|
||||
"multi line invalid message",
|
||||
ccfg,
|
||||
`feat add something
|
||||
|
||||
team: x`, true,
|
||||
},
|
||||
|
||||
{
|
||||
"support ! for breaking change",
|
||||
ccfg,
|
||||
"feat!: add something", false,
|
||||
},
|
||||
{
|
||||
"support ! with scope for breaking change",
|
||||
ccfg,
|
||||
"feat(scope)!: add something", false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@ -181,9 +213,21 @@ func TestMessageProcessorImpl_ValidateType(t *testing.T) {
|
||||
ctype string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid type", ccfg, "feat", false},
|
||||
{"invalid type", ccfg, "aaa", true},
|
||||
{"empty type", ccfg, "", true},
|
||||
{
|
||||
"valid type",
|
||||
ccfg,
|
||||
"feat", false,
|
||||
},
|
||||
{
|
||||
"invalid type",
|
||||
ccfg,
|
||||
"aaa", true,
|
||||
},
|
||||
{
|
||||
"empty type",
|
||||
ccfg,
|
||||
"", true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@ -202,7 +246,11 @@ func TestMessageProcessorImpl_ValidateScope(t *testing.T) {
|
||||
scope string
|
||||
wantErr bool
|
||||
}{
|
||||
{"any scope", ccfg, "aaa", false},
|
||||
{
|
||||
"any scope",
|
||||
ccfg,
|
||||
"aaa", false,
|
||||
},
|
||||
{"valid scope with scope list", ccfgWithScope, "scope", false},
|
||||
{"invalid scope with scope list", ccfgWithScope, "aaa", true},
|
||||
}
|
||||
@ -223,11 +271,31 @@ func TestMessageProcessorImpl_ValidateDescription(t *testing.T) {
|
||||
description string
|
||||
wantErr bool
|
||||
}{
|
||||
{"empty description", ccfg, "", true},
|
||||
{"sigle letter description", ccfg, "a", false},
|
||||
{"number description", ccfg, "1", true},
|
||||
{"valid description", ccfg, "add some feature", false},
|
||||
{"invalid capital letter description", ccfg, "Add some feature", true},
|
||||
{
|
||||
"empty description",
|
||||
ccfg,
|
||||
"", true,
|
||||
},
|
||||
{
|
||||
"sigle letter description",
|
||||
ccfg,
|
||||
"a", false,
|
||||
},
|
||||
{
|
||||
"number description",
|
||||
ccfg,
|
||||
"1", true,
|
||||
},
|
||||
{
|
||||
"valid description",
|
||||
ccfg,
|
||||
"add some feature", false,
|
||||
},
|
||||
{
|
||||
"invalid capital letter description",
|
||||
ccfg,
|
||||
"Add some feature", true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@ -248,24 +316,73 @@ func TestMessageProcessorImpl_Enhance(t *testing.T) {
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{"issue on branch name", ccfg, "JIRA-123", "fix: fix something", "\njira: JIRA-123", false},
|
||||
{"issue on branch name with description", ccfg, "JIRA-123-some-description", "fix: fix something", "\njira: JIRA-123", false},
|
||||
{"issue on branch name with prefix", ccfg, "feature/JIRA-123", "fix: fix something", "\njira: JIRA-123", false},
|
||||
{"with footer", ccfg, "JIRA-123", fullMessage, "jira: JIRA-123", false},
|
||||
{"with issue on footer", ccfg, "JIRA-123", fullMessageWithJira, "", false},
|
||||
{"issue on branch name with prefix and description", ccfg, "feature/JIRA-123-some-description", "fix: fix something", "\njira: JIRA-123", false},
|
||||
{"no issue on branch name", ccfg, "branch", "fix: fix something", "", true},
|
||||
{"unexpected branch name", ccfg, "feature /JIRA-123", "fix: fix something", "", true},
|
||||
{"issue on branch name using hash", ccfgHash, "JIRA-123-some-description", "fix: fix something", "\njira #JIRA-123", false},
|
||||
{"numeric issue on branch name", ccfgGitIssue, "#13", "fix: fix something", "\nissue: #13", false},
|
||||
{"numeric issue on branch name without hash", ccfgGitIssue, "13", "fix: fix something", "\nissue: #13", false},
|
||||
{"numeric issue on branch name with description without hash", ccfgGitIssue, "13-some-fix", "fix: fix something", "\nissue: #13", false},
|
||||
{
|
||||
"issue on branch name",
|
||||
ccfg,
|
||||
"JIRA-123", "fix: fix something", "\njira: JIRA-123", false,
|
||||
},
|
||||
{
|
||||
"issue on branch name with description",
|
||||
ccfg,
|
||||
"JIRA-123-some-description", "fix: fix something", "\njira: JIRA-123", false,
|
||||
},
|
||||
{
|
||||
"issue on branch name with prefix",
|
||||
ccfg,
|
||||
"feature/JIRA-123", "fix: fix something", "\njira: JIRA-123", false,
|
||||
},
|
||||
{
|
||||
"with footer",
|
||||
ccfg,
|
||||
"JIRA-123", fullMessage, "jira: JIRA-123", false,
|
||||
},
|
||||
{
|
||||
"with issue on footer",
|
||||
ccfg,
|
||||
"JIRA-123", fullMessageWithJira, "", false,
|
||||
},
|
||||
{
|
||||
"issue on branch name with prefix and description",
|
||||
ccfg,
|
||||
"feature/JIRA-123-some-description", "fix: fix something", "\njira: JIRA-123", false,
|
||||
},
|
||||
{
|
||||
"no issue on branch name",
|
||||
ccfg,
|
||||
"branch", "fix: fix something", "", true,
|
||||
},
|
||||
{
|
||||
"unexpected branch name",
|
||||
ccfg,
|
||||
"feature /JIRA-123", "fix: fix something", "", true,
|
||||
},
|
||||
{
|
||||
"issue on branch name using hash",
|
||||
ccfgHash,
|
||||
"JIRA-123-some-description", "fix: fix something", "\njira #JIRA-123", false,
|
||||
},
|
||||
{
|
||||
"numeric issue on branch name",
|
||||
ccfgGitIssue,
|
||||
"#13", "fix: fix something", "\nissue: #13", false,
|
||||
},
|
||||
{
|
||||
"numeric issue on branch name without hash",
|
||||
ccfgGitIssue,
|
||||
"13", "fix: fix something", "\nissue: #13", false,
|
||||
},
|
||||
{
|
||||
"numeric issue on branch name with description without hash",
|
||||
ccfgGitIssue,
|
||||
"13-some-fix", "fix: fix something", "\nissue: #13", false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := NewMessageProcessor(tt.cfg, newBranchCfg(false)).Enhance(tt.branch, tt.message)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("MessageProcessorImpl.Enhance() error = %v, wantErr %v", err, tt.wantErr)
|
||||
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
@ -296,6 +413,7 @@ func TestMessageProcessorImpl_IssueID(t *testing.T) {
|
||||
got, err := p.IssueID(tt.branch)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("MessageProcessorImpl.IssueID() error = %v, wantErr %v", err, tt.wantErr)
|
||||
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
@ -326,19 +444,19 @@ func Test_hasIssueID(t *testing.T) {
|
||||
}{
|
||||
{"single line without issue", "feat: something", cfgColon, false},
|
||||
{"multi line without issue", `feat: something
|
||||
|
||||
|
||||
yay`, cfgColon, false},
|
||||
{"multi line without jira issue", `feat: something
|
||||
|
||||
|
||||
jira1: JIRA-123`, cfgColon, false},
|
||||
{"multi line with issue", `feat: something
|
||||
|
||||
|
||||
jira: JIRA-123`, cfgColon, true},
|
||||
{"multi line with issue and hash", `feat: something
|
||||
|
||||
|
||||
jira #JIRA-123`, cfgHash, true},
|
||||
{"empty config", `feat: something
|
||||
|
||||
|
||||
jira #JIRA-123`, cfgEmpty, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@ -378,8 +496,10 @@ var completeBody = `some descriptions
|
||||
jira: JIRA-123
|
||||
BREAKING CHANGE: this change breaks everything`
|
||||
|
||||
var bodyWithCarriage = "some description\r\nmore description\r\n\r\njira: JIRA-123\r"
|
||||
var expectedBodyWithCarriage = "some description\nmore description\n\njira: JIRA-123"
|
||||
var (
|
||||
bodyWithCarriage = "some description\r\nmore description\r\n\r\njira: JIRA-123\r"
|
||||
expectedBodyWithCarriage = "some description\nmore description\n\njira: JIRA-123"
|
||||
)
|
||||
|
||||
var issueOnlyBody = `some descriptions
|
||||
|
||||
@ -402,20 +522,145 @@ func TestMessageProcessorImpl_Parse(t *testing.T) {
|
||||
body string
|
||||
want CommitMessage
|
||||
}{
|
||||
{"simple message", ccfg, "feat: something awesome", "", CommitMessage{Type: "feat", Scope: "", Description: "something awesome", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
|
||||
{"message with scope", ccfg, "feat(scope): something awesome", "", CommitMessage{Type: "feat", Scope: "scope", Description: "something awesome", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
|
||||
{"unmapped type", ccfg, "unkn: something unknown", "", CommitMessage{Type: "unkn", Scope: "", Description: "something unknown", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
|
||||
{"jira and breaking change metadata", ccfg, "feat: something new", completeBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: completeBody, IsBreakingChange: true, Metadata: map[string]string{issueMetadataKey: "JIRA-123", breakingChangeMetadataKey: "this change breaks everything"}}},
|
||||
{"jira only metadata", ccfg, "feat: something new", issueOnlyBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: issueOnlyBody, IsBreakingChange: false, Metadata: map[string]string{issueMetadataKey: "JIRA-456"}}},
|
||||
{"jira synonyms metadata", ccfg, "feat: something new", issueSynonymsBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: issueSynonymsBody, IsBreakingChange: false, Metadata: map[string]string{issueMetadataKey: "JIRA-789"}}},
|
||||
{"breaking change with exclamation mark", ccfg, "feat!: something new", "", CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: "", IsBreakingChange: true, Metadata: map[string]string{}}},
|
||||
{"hash metadata", ccfg, "feat: something new", hashMetadataBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: hashMetadataBody, IsBreakingChange: false, Metadata: map[string]string{issueMetadataKey: "JIRA-999", "refs": "#123"}}},
|
||||
{"empty issue cfg", ccfgEmptyIssue, "feat: something new", hashMetadataBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: hashMetadataBody, IsBreakingChange: false, Metadata: map[string]string{}}},
|
||||
{"carriage return on body", ccfg, "feat: something new", bodyWithCarriage, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: expectedBodyWithCarriage, IsBreakingChange: false, Metadata: map[string]string{issueMetadataKey: "JIRA-123"}}},
|
||||
{
|
||||
"simple message",
|
||||
ccfg,
|
||||
"feat: something awesome", "",
|
||||
CommitMessage{
|
||||
Type: "feat",
|
||||
Scope: "",
|
||||
Description: "something awesome",
|
||||
Body: "",
|
||||
IsBreakingChange: false,
|
||||
Metadata: map[string]string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"message with scope",
|
||||
ccfg,
|
||||
"feat(scope): something awesome", "",
|
||||
CommitMessage{
|
||||
Type: "feat",
|
||||
Scope: "scope",
|
||||
Description: "something awesome",
|
||||
Body: "",
|
||||
IsBreakingChange: false,
|
||||
Metadata: map[string]string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"unmapped type",
|
||||
ccfg,
|
||||
"unkn: something unknown", "",
|
||||
CommitMessage{
|
||||
Type: "unkn",
|
||||
Scope: "",
|
||||
Description: "something unknown",
|
||||
Body: "",
|
||||
IsBreakingChange: false,
|
||||
Metadata: map[string]string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"jira and breaking change metadata",
|
||||
ccfg,
|
||||
"feat: something new", completeBody,
|
||||
CommitMessage{
|
||||
Type: "feat",
|
||||
Scope: "",
|
||||
Description: "something new",
|
||||
Body: completeBody,
|
||||
IsBreakingChange: true,
|
||||
Metadata: map[string]string{
|
||||
issueMetadataKey: "JIRA-123",
|
||||
breakingChangeMetadataKey: "this change breaks everything",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"jira only metadata",
|
||||
ccfg,
|
||||
"feat: something new", issueOnlyBody,
|
||||
CommitMessage{
|
||||
Type: "feat",
|
||||
Scope: "",
|
||||
Description: "something new",
|
||||
Body: issueOnlyBody,
|
||||
IsBreakingChange: false,
|
||||
Metadata: map[string]string{issueMetadataKey: "JIRA-456"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"jira synonyms metadata",
|
||||
ccfg,
|
||||
"feat: something new", issueSynonymsBody,
|
||||
CommitMessage{
|
||||
Type: "feat",
|
||||
Scope: "",
|
||||
Description: "something new",
|
||||
Body: issueSynonymsBody,
|
||||
IsBreakingChange: false,
|
||||
Metadata: map[string]string{issueMetadataKey: "JIRA-789"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"breaking change with exclamation mark",
|
||||
ccfg,
|
||||
"feat!: something new", "",
|
||||
CommitMessage{
|
||||
Type: "feat",
|
||||
Scope: "",
|
||||
Description: "something new",
|
||||
Body: "",
|
||||
IsBreakingChange: true,
|
||||
Metadata: map[string]string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"hash metadata",
|
||||
ccfg,
|
||||
"feat: something new", hashMetadataBody,
|
||||
CommitMessage{
|
||||
Type: "feat",
|
||||
Scope: "",
|
||||
Description: "something new",
|
||||
Body: hashMetadataBody,
|
||||
IsBreakingChange: false,
|
||||
Metadata: map[string]string{issueMetadataKey: "JIRA-999", "refs": "#123"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"empty issue cfg",
|
||||
ccfgEmptyIssue,
|
||||
"feat: something new", hashMetadataBody,
|
||||
CommitMessage{
|
||||
Type: "feat",
|
||||
Scope: "",
|
||||
Description: "something new",
|
||||
Body: hashMetadataBody,
|
||||
IsBreakingChange: false,
|
||||
Metadata: map[string]string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"carriage return on body",
|
||||
ccfg,
|
||||
"feat: something new", bodyWithCarriage,
|
||||
CommitMessage{
|
||||
Type: "feat",
|
||||
Scope: "",
|
||||
Description: "something new",
|
||||
Body: expectedBodyWithCarriage,
|
||||
IsBreakingChange: false,
|
||||
Metadata: map[string]string{issueMetadataKey: "JIRA-123"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got, err := NewMessageProcessor(tt.cfg, newBranchCfg(false)).Parse(tt.subject, tt.body); !reflect.DeepEqual(got, tt.want) && err == nil {
|
||||
if got, err := NewMessageProcessor(
|
||||
tt.cfg, newBranchCfg(false),
|
||||
).Parse(tt.subject, tt.body); !reflect.DeepEqual(got, tt.want) && err == nil {
|
||||
t.Errorf("MessageProcessorImpl.Parse() = [%+v], want [%+v]", got, tt.want)
|
||||
}
|
||||
})
|
||||
@ -431,18 +676,102 @@ func TestMessageProcessorImpl_Format(t *testing.T) {
|
||||
wantBody string
|
||||
wantFooter string
|
||||
}{
|
||||
{"simple message", ccfg, NewCommitMessage("feat", "", "something", "", "", ""), "feat: something", "", ""},
|
||||
{"with issue", ccfg, NewCommitMessage("feat", "", "something", "", "JIRA-123", ""), "feat: something", "", "jira: JIRA-123"},
|
||||
{"with issue using hash", ccfgHash, NewCommitMessage("feat", "", "something", "", "JIRA-123", ""), "feat: something", "", "jira #JIRA-123"},
|
||||
{"with issue using double hash", ccfgHash, NewCommitMessage("feat", "", "something", "", "#JIRA-123", ""), "feat: something", "", "jira #JIRA-123"},
|
||||
{"with breaking change", ccfg, NewCommitMessage("feat", "", "something", "", "", "breaks"), "feat: something", "", "BREAKING CHANGE: breaks"},
|
||||
{"with scope", ccfg, NewCommitMessage("feat", "scope", "something", "", "", ""), "feat(scope): something", "", ""},
|
||||
{"with body", ccfg, NewCommitMessage("feat", "", "something", "body", "", ""), "feat: something", "body", ""},
|
||||
{"with multiline body", ccfg, NewCommitMessage("feat", "", "something", multilineBody, "", ""), "feat: something", multilineBody, ""},
|
||||
{"full message", ccfg, NewCommitMessage("feat", "scope", "something", multilineBody, "JIRA-123", "breaks"), "feat(scope): something", multilineBody, fullFooter},
|
||||
{"config without issue key", ccfgEmptyIssue, NewCommitMessage("feat", "", "something", "", "JIRA-123", ""), "feat: something", "", ""},
|
||||
{"with issue and issue prefix", ccfgGitIssue, NewCommitMessage("feat", "", "something", "", "123", ""), "feat: something", "", "issue: #123"},
|
||||
{"with #issue and issue prefix", ccfgGitIssue, NewCommitMessage("feat", "", "something", "", "#123", ""), "feat: something", "", "issue: #123"},
|
||||
{
|
||||
"simple message",
|
||||
ccfg,
|
||||
NewCommitMessage("feat", "", "something", "", "", ""),
|
||||
"feat: something",
|
||||
"",
|
||||
"",
|
||||
},
|
||||
{
|
||||
"with issue",
|
||||
ccfg,
|
||||
NewCommitMessage("feat", "", "something", "", "JIRA-123", ""),
|
||||
"feat: something",
|
||||
"",
|
||||
"jira: JIRA-123",
|
||||
},
|
||||
{
|
||||
"with issue using hash",
|
||||
ccfgHash,
|
||||
NewCommitMessage("feat", "", "something", "", "JIRA-123", ""),
|
||||
"feat: something",
|
||||
"",
|
||||
"jira #JIRA-123",
|
||||
},
|
||||
{
|
||||
"with issue using double hash",
|
||||
ccfgHash,
|
||||
NewCommitMessage("feat", "", "something", "", "#JIRA-123", ""),
|
||||
"feat: something",
|
||||
"",
|
||||
"jira #JIRA-123",
|
||||
},
|
||||
{
|
||||
"with breaking change",
|
||||
ccfg,
|
||||
NewCommitMessage("feat", "", "something", "", "", "breaks"),
|
||||
"feat: something",
|
||||
"",
|
||||
"BREAKING CHANGE: breaks",
|
||||
},
|
||||
{
|
||||
"with scope",
|
||||
ccfg,
|
||||
NewCommitMessage("feat", "scope", "something", "", "", ""),
|
||||
"feat(scope): something",
|
||||
"",
|
||||
"",
|
||||
},
|
||||
{
|
||||
"with body",
|
||||
ccfg,
|
||||
NewCommitMessage("feat", "", "something", "body", "", ""),
|
||||
"feat: something",
|
||||
"body",
|
||||
"",
|
||||
},
|
||||
{
|
||||
"with multiline body",
|
||||
ccfg,
|
||||
NewCommitMessage("feat", "", "something", multilineBody, "", ""),
|
||||
"feat: something",
|
||||
multilineBody,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"full message",
|
||||
ccfg,
|
||||
NewCommitMessage("feat", "scope", "something", multilineBody, "JIRA-123", "breaks"),
|
||||
"feat(scope): something",
|
||||
multilineBody,
|
||||
fullFooter,
|
||||
},
|
||||
{
|
||||
"config without issue key",
|
||||
ccfgEmptyIssue,
|
||||
NewCommitMessage("feat", "", "something", "", "JIRA-123", ""),
|
||||
"feat: something",
|
||||
"",
|
||||
"",
|
||||
},
|
||||
{
|
||||
"with issue and issue prefix",
|
||||
ccfgGitIssue,
|
||||
NewCommitMessage("feat", "", "something", "", "123", ""),
|
||||
"feat: something",
|
||||
"",
|
||||
"issue: #123",
|
||||
},
|
||||
{
|
||||
"with #issue and issue prefix",
|
||||
ccfgGitIssue,
|
||||
NewCommitMessage("feat", "", "something", "", "#123", ""),
|
||||
"feat: something",
|
||||
"",
|
||||
"issue: #123",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@ -532,14 +861,61 @@ func Test_prepareHeader(t *testing.T) {
|
||||
wantHeader string
|
||||
wantError bool
|
||||
}{
|
||||
{"conventional without selector", "", "feat: something", "feat: something", false},
|
||||
{"conventional with scope without selector", "", "feat(scope): something", "feat(scope): something", false},
|
||||
{"non-conventional without selector", "", "something", "something", false},
|
||||
{"matching conventional with selector with group", "Merged PR (\\d+): (?P<header>.*)", "Merged PR 123: feat: something", "feat: something", false},
|
||||
{"matching non-conventional with selector with group", "Merged PR (\\d+): (?P<header>.*)", "Merged PR 123: something", "something", false},
|
||||
{"matching non-conventional with selector without group", "Merged PR (\\d+): (.*)", "Merged PR 123: something", "", true},
|
||||
{"non-matching non-conventional with selector with group", "Merged PR (\\d+): (?P<header>.*)", "something", "", true},
|
||||
{"matching non-conventional with invalid regex", "Merged PR (\\d+): (?<header>.*)", "Merged PR 123: something", "", true},
|
||||
{
|
||||
"conventional without selector",
|
||||
"",
|
||||
"feat: something",
|
||||
"feat: something",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"conventional with scope without selector",
|
||||
"",
|
||||
"feat(scope): something",
|
||||
"feat(scope): something",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"non-conventional without selector",
|
||||
"",
|
||||
"something", "something",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"matching conventional with selector with group",
|
||||
"Merged PR (\\d+): (?P<header>.*)",
|
||||
"Merged PR 123: feat: something",
|
||||
"feat: something",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"matching non-conventional with selector with group",
|
||||
"Merged PR (\\d+): (?P<header>.*)",
|
||||
"Merged PR 123: something",
|
||||
"something",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"matching non-conventional with selector without group",
|
||||
"Merged PR (\\d+): (.*)",
|
||||
"Merged PR 123: something",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"non-matching non-conventional with selector with group",
|
||||
"Merged PR (\\d+): (?P<header>.*)",
|
||||
"something",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"matching non-conventional with invalid regex",
|
||||
"Merged PR (\\d+): (?<header>.*)",
|
||||
"Merged PR 123: something",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -22,22 +22,32 @@ func NewReleaseNoteProcessor(cfg ReleaseNotesConfig) *ReleaseNoteProcessorImpl {
|
||||
}
|
||||
|
||||
// Create create a release note based on commits.
|
||||
func (p ReleaseNoteProcessorImpl) Create(version *semver.Version, tag string, date time.Time, commits []GitCommitLog) ReleaseNote {
|
||||
func (p ReleaseNoteProcessorImpl) Create(
|
||||
version *semver.Version,
|
||||
tag string,
|
||||
date time.Time,
|
||||
commits []GitCommitLog,
|
||||
) ReleaseNote {
|
||||
mapping := commitSectionMapping(p.cfg.Sections)
|
||||
|
||||
sections := make(map[string]ReleaseNoteCommitsSection)
|
||||
authors := make(map[string]struct{})
|
||||
|
||||
var breakingChanges []string
|
||||
|
||||
for _, commit := range commits {
|
||||
authors[commit.AuthorName] = struct{}{}
|
||||
|
||||
if sectionCfg, exists := mapping[commit.Message.Type]; exists {
|
||||
section, sexists := sections[sectionCfg.Name]
|
||||
if !sexists {
|
||||
section = ReleaseNoteCommitsSection{Name: sectionCfg.Name, Types: sectionCfg.CommitTypes}
|
||||
}
|
||||
|
||||
section.Items = append(section.Items, commit)
|
||||
sections[sectionCfg.Name] = section
|
||||
}
|
||||
|
||||
if commit.Message.BreakingMessage() != "" {
|
||||
// TODO: if no message found, should use description instead?
|
||||
breakingChanges = append(breakingChanges, commit.Message.BreakingMessage())
|
||||
@ -48,10 +58,20 @@ func (p ReleaseNoteProcessorImpl) Create(version *semver.Version, tag string, da
|
||||
if bcCfg := p.cfg.sectionConfig(ReleaseNotesSectionTypeBreakingChanges); bcCfg != nil && len(breakingChanges) > 0 {
|
||||
breakingChangeSection = ReleaseNoteBreakingChangeSection{Name: bcCfg.Name, Messages: breakingChanges}
|
||||
}
|
||||
return ReleaseNote{Version: version, Tag: tag, Date: date.Truncate(time.Minute), Sections: p.toReleaseNoteSections(sections, breakingChangeSection), AuthorsNames: authors}
|
||||
|
||||
return ReleaseNote{
|
||||
Version: version,
|
||||
Tag: tag,
|
||||
Date: date.Truncate(time.Minute),
|
||||
Sections: p.toReleaseNoteSections(sections, breakingChangeSection),
|
||||
AuthorsNames: authors,
|
||||
}
|
||||
}
|
||||
|
||||
func (p ReleaseNoteProcessorImpl) toReleaseNoteSections(commitSections map[string]ReleaseNoteCommitsSection, breakingChange ReleaseNoteBreakingChangeSection) []ReleaseNoteSection {
|
||||
func (p ReleaseNoteProcessorImpl) toReleaseNoteSections(
|
||||
commitSections map[string]ReleaseNoteCommitsSection,
|
||||
breakingChange ReleaseNoteBreakingChangeSection,
|
||||
) []ReleaseNoteSection {
|
||||
hasBreaking := 0
|
||||
if breakingChange.Name != "" {
|
||||
hasBreaking = 1
|
||||
@ -59,11 +79,13 @@ func (p ReleaseNoteProcessorImpl) toReleaseNoteSections(commitSections map[strin
|
||||
|
||||
sections := make([]ReleaseNoteSection, len(commitSections)+hasBreaking)
|
||||
i := 0
|
||||
|
||||
for _, cfg := range p.cfg.Sections {
|
||||
if cfg.SectionType == ReleaseNotesSectionTypeBreakingChanges && hasBreaking > 0 {
|
||||
sections[i] = breakingChange
|
||||
i++
|
||||
}
|
||||
|
||||
if s, exists := commitSections[cfg.Name]; cfg.SectionType == ReleaseNotesSectionTypeCommits && exists {
|
||||
sections[i] = s
|
||||
i++
|
||||
@ -75,6 +97,7 @@ func (p ReleaseNoteProcessorImpl) toReleaseNoteSections(commitSections map[strin
|
||||
|
||||
func commitSectionMapping(sections []ReleaseNotesSectionConfig) map[string]ReleaseNotesSectionConfig {
|
||||
mapping := make(map[string]ReleaseNotesSectionConfig)
|
||||
|
||||
for _, section := range sections {
|
||||
if section.SectionType == ReleaseNotesSectionTypeCommits {
|
||||
for _, commitType := range section.CommitTypes {
|
||||
@ -82,6 +105,7 @@ func commitSectionMapping(sections []ReleaseNotesSectionConfig) map[string]Relea
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mapping
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,15 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
|
||||
tag: "v1.0.0",
|
||||
date: date,
|
||||
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a")},
|
||||
want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, []ReleaseNoteSection{newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")})}, map[string]struct{}{"a": {}}),
|
||||
want: releaseNote(
|
||||
semver.MustParse("1.0.0"),
|
||||
"v1.0.0",
|
||||
date,
|
||||
[]ReleaseNoteSection{
|
||||
newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")}),
|
||||
},
|
||||
map[string]struct{}{"a": {}},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "unmapped tag",
|
||||
@ -33,28 +41,71 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
|
||||
tag: "v1.0.0",
|
||||
date: date,
|
||||
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a"), commitlog("unmapped", map[string]string{}, "a")},
|
||||
want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, []ReleaseNoteSection{newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")})}, map[string]struct{}{"a": {}}),
|
||||
want: releaseNote(
|
||||
semver.MustParse("1.0.0"),
|
||||
"v1.0.0",
|
||||
date,
|
||||
[]ReleaseNoteSection{
|
||||
newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")}),
|
||||
},
|
||||
map[string]struct{}{"a": {}},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "breaking changes tag",
|
||||
version: semver.MustParse("1.0.0"),
|
||||
tag: "v1.0.0",
|
||||
date: date,
|
||||
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a"), commitlog("unmapped", map[string]string{"breaking-change": "breaks"}, "a")},
|
||||
want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, []ReleaseNoteSection{newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")}), ReleaseNoteBreakingChangeSection{Name: "Breaking Changes", Messages: []string{"breaks"}}}, map[string]struct{}{"a": {}}),
|
||||
commits: []GitCommitLog{
|
||||
commitlog("t1", map[string]string{}, "a"),
|
||||
commitlog("unmapped", map[string]string{"breaking-change": "breaks"}, "a"),
|
||||
},
|
||||
want: releaseNote(
|
||||
semver.MustParse("1.0.0"),
|
||||
"v1.0.0",
|
||||
date,
|
||||
[]ReleaseNoteSection{
|
||||
newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")}),
|
||||
ReleaseNoteBreakingChangeSection{Name: "Breaking Changes", Messages: []string{"breaks"}},
|
||||
},
|
||||
map[string]struct{}{"a": {}},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "multiple authors",
|
||||
version: semver.MustParse("1.0.0"),
|
||||
tag: "v1.0.0",
|
||||
date: date,
|
||||
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "author3"), commitlog("t1", map[string]string{}, "author2"), commitlog("t1", map[string]string{}, "author1")},
|
||||
want: releaseNote(semver.MustParse("1.0.0"), "v1.0.0", date, []ReleaseNoteSection{newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "author3"), commitlog("t1", map[string]string{}, "author2"), commitlog("t1", map[string]string{}, "author1")})}, map[string]struct{}{"author1": {}, "author2": {}, "author3": {}}),
|
||||
commits: []GitCommitLog{
|
||||
commitlog("t1", map[string]string{}, "author3"),
|
||||
commitlog("t1", map[string]string{}, "author2"),
|
||||
commitlog("t1", map[string]string{}, "author1"),
|
||||
},
|
||||
want: releaseNote(
|
||||
semver.MustParse("1.0.0"),
|
||||
"v1.0.0",
|
||||
date,
|
||||
[]ReleaseNoteSection{
|
||||
newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{
|
||||
commitlog("t1", map[string]string{}, "author3"),
|
||||
commitlog("t1", map[string]string{}, "author2"),
|
||||
commitlog("t1", map[string]string{}, "author1"),
|
||||
}),
|
||||
},
|
||||
map[string]struct{}{"author1": {}, "author2": {}, "author3": {}},
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := NewReleaseNoteProcessor(ReleaseNotesConfig{Sections: []ReleaseNotesSectionConfig{{Name: "Tag 1", SectionType: "commits", CommitTypes: []string{"t1"}}, {Name: "Tag 2", SectionType: "commits", CommitTypes: []string{"t2"}}, {Name: "Breaking Changes", SectionType: "breaking-changes"}}})
|
||||
p := NewReleaseNoteProcessor(
|
||||
ReleaseNotesConfig{
|
||||
Sections: []ReleaseNotesSectionConfig{
|
||||
{Name: "Tag 1", SectionType: "commits", CommitTypes: []string{"t1"}},
|
||||
{Name: "Tag 2", SectionType: "commits", CommitTypes: []string{"t2"}},
|
||||
{Name: "Breaking Changes", SectionType: "breaking-changes"},
|
||||
},
|
||||
})
|
||||
if got := p.Create(tt.version, tt.tag, tt.date, tt.commits); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ReleaseNoteProcessorImpl.Create() = %v, want %v", got, tt.want)
|
||||
}
|
||||
|
14
sv/semver.go
14
sv/semver.go
@ -14,6 +14,7 @@ const (
|
||||
// IsValidVersion return true when a version is valid.
|
||||
func IsValidVersion(value string) bool {
|
||||
_, err := semver.NewVersion(value)
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@ -23,6 +24,7 @@ func ToVersion(value string) (*semver.Version, error) {
|
||||
if version == "" {
|
||||
version = "0.0.0"
|
||||
}
|
||||
|
||||
return semver.NewVersion(version)
|
||||
}
|
||||
|
||||
@ -52,7 +54,9 @@ func NewSemVerCommitsProcessor(vcfg VersioningConfig, mcfg CommitMessageConfig)
|
||||
}
|
||||
|
||||
// NextVersion calculates next version based on commit log.
|
||||
func (p SemVerCommitsProcessorImpl) NextVersion(version *semver.Version, commits []GitCommitLog) (*semver.Version, bool) {
|
||||
func (p SemVerCommitsProcessorImpl) NextVersion(
|
||||
version *semver.Version, commits []GitCommitLog,
|
||||
) (*semver.Version, bool) {
|
||||
versionToUpdate := none
|
||||
for _, commit := range commits {
|
||||
if v := p.versionTypeToUpdate(commit); v > versionToUpdate {
|
||||
@ -64,7 +68,9 @@ func (p SemVerCommitsProcessorImpl) NextVersion(version *semver.Version, commits
|
||||
if version == nil {
|
||||
return nil, updated
|
||||
}
|
||||
|
||||
newVersion := updateVersion(*version, versionToUpdate)
|
||||
|
||||
return &newVersion, updated
|
||||
}
|
||||
|
||||
@ -85,18 +91,23 @@ func (p SemVerCommitsProcessorImpl) versionTypeToUpdate(commit GitCommitLog) ver
|
||||
if commit.Message.IsBreakingChange {
|
||||
return major
|
||||
}
|
||||
|
||||
if _, exists := p.MajorVersionTypes[commit.Message.Type]; exists {
|
||||
return major
|
||||
}
|
||||
|
||||
if _, exists := p.MinorVersionTypes[commit.Message.Type]; exists {
|
||||
return minor
|
||||
}
|
||||
|
||||
if _, exists := p.PatchVersionTypes[commit.Message.Type]; exists {
|
||||
return patch
|
||||
}
|
||||
|
||||
if !contains(commit.Message.Type, p.KnownTypes) && p.IncludeUnknownTypeAsPatch {
|
||||
return patch
|
||||
}
|
||||
|
||||
return none
|
||||
}
|
||||
|
||||
@ -105,5 +116,6 @@ func toMap(values []string) map[string]struct{} {
|
||||
for _, v := range values {
|
||||
result[v] = struct{}{}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
@ -16,20 +16,104 @@ func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) {
|
||||
want *semver.Version
|
||||
wantUpdated bool
|
||||
}{
|
||||
{"no update", true, version("0.0.0"), []GitCommitLog{}, version("0.0.0"), false},
|
||||
{"no update without version", true, nil, []GitCommitLog{}, nil, false},
|
||||
{"no update on unknown type", true, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{}, "a")}, version("0.0.0"), false},
|
||||
{"no update on unmapped known type", false, version("0.0.0"), []GitCommitLog{commitlog("none", map[string]string{}, "a")}, version("0.0.0"), false},
|
||||
{"update patch on unknown type", false, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{}, "a")}, version("0.0.1"), true},
|
||||
{"patch update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}, "a")}, version("0.0.1"), true},
|
||||
{"patch update without version", false, nil, []GitCommitLog{commitlog("patch", map[string]string{}, "a")}, nil, true},
|
||||
{"minor update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}, "a"), commitlog("minor", map[string]string{}, "a")}, version("0.1.0"), true},
|
||||
{"major update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}, "a"), commitlog("major", map[string]string{}, "a")}, version("1.0.0"), true},
|
||||
{"breaking change update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}, "a"), commitlog("patch", map[string]string{"breaking-change": "break"}, "a")}, version("1.0.0"), true},
|
||||
{
|
||||
"no update",
|
||||
true,
|
||||
version("0.0.0"),
|
||||
[]GitCommitLog{},
|
||||
version("0.0.0"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"no update without version",
|
||||
true,
|
||||
nil,
|
||||
[]GitCommitLog{},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"no update on unknown type",
|
||||
true,
|
||||
version("0.0.0"),
|
||||
[]GitCommitLog{commitlog("a", map[string]string{}, "a")},
|
||||
version("0.0.0"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"no update on unmapped known type",
|
||||
false,
|
||||
version("0.0.0"),
|
||||
[]GitCommitLog{commitlog("none", map[string]string{}, "a")},
|
||||
version("0.0.0"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"update patch on unknown type",
|
||||
false,
|
||||
version("0.0.0"),
|
||||
[]GitCommitLog{commitlog("a", map[string]string{}, "a")},
|
||||
version("0.0.1"),
|
||||
true,
|
||||
},
|
||||
{
|
||||
"patch update",
|
||||
false, version("0.0.0"),
|
||||
[]GitCommitLog{commitlog("patch", map[string]string{}, "a")},
|
||||
version("0.0.1"), true,
|
||||
},
|
||||
{
|
||||
"patch update without version",
|
||||
false,
|
||||
nil,
|
||||
[]GitCommitLog{commitlog("patch", map[string]string{}, "a")},
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"minor update",
|
||||
false,
|
||||
version("0.0.0"),
|
||||
[]GitCommitLog{
|
||||
commitlog("patch", map[string]string{}, "a"),
|
||||
commitlog("minor", map[string]string{}, "a"),
|
||||
},
|
||||
version("0.1.0"),
|
||||
true,
|
||||
},
|
||||
{
|
||||
"major update",
|
||||
false,
|
||||
version("0.0.0"),
|
||||
[]GitCommitLog{
|
||||
commitlog("patch", map[string]string{}, "a"),
|
||||
commitlog("major", map[string]string{}, "a"),
|
||||
},
|
||||
version("1.0.0"),
|
||||
true,
|
||||
},
|
||||
{
|
||||
"breaking change update",
|
||||
false,
|
||||
version("0.0.0"),
|
||||
[]GitCommitLog{
|
||||
commitlog("patch", map[string]string{}, "a"),
|
||||
commitlog("patch", map[string]string{"breaking-change": "break"}, "a"),
|
||||
},
|
||||
version("1.0.0"),
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := NewSemVerCommitsProcessor(VersioningConfig{UpdateMajor: []string{"major"}, UpdateMinor: []string{"minor"}, UpdatePatch: []string{"patch"}, IgnoreUnknown: tt.ignoreUnknown}, CommitMessageConfig{Types: []string{"major", "minor", "patch", "none"}})
|
||||
p := NewSemVerCommitsProcessor(
|
||||
VersioningConfig{
|
||||
UpdateMajor: []string{"major"},
|
||||
UpdateMinor: []string{"minor"},
|
||||
UpdatePatch: []string{"patch"},
|
||||
IgnoreUnknown: tt.ignoreUnknown,
|
||||
},
|
||||
CommitMessageConfig{Types: []string{"major", "minor", "patch", "none"}})
|
||||
got, gotUpdated := p.NextVersion(tt.version, tt.commits)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("SemVerCommitsProcessorImpl.NextVersion() Version = %v, want %v", got, tt.want)
|
||||
@ -57,6 +141,7 @@ func TestToVersion(t *testing.T) {
|
||||
got, err := ToVersion(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ToVersion() error = %v, wantErr %v", err, tt.wantErr)
|
||||
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
|
Loading…
Reference in New Issue
Block a user