Skip to content
Merged
136 changes: 95 additions & 41 deletions dist/cz/bin/cz
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ parse() {
-[c]?*) OPTARG=$1; shift
eval 'set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}"' ${1+'"$@"'}
;;
-[rdemqhV]?*) OPTARG=$1; shift
-[rdembqhV]?*) OPTARG=$1; shift
eval 'set -- "${OPTARG%"${OPTARG#??}"}" -"${OPTARG#??}"' ${1+'"$@"'}
case $2 in --*) set -- "$1" unknown "$2" && REST=x; esac;OPTARG= ;;
esac
Expand Down Expand Up @@ -132,7 +132,7 @@ parse() {
eval '[ ${OPTARG+x} ] &&:' && OPTARG='1' || OPTARG=''
MULTI_SCOPE="$OPTARG"
;;
'--breaking-footer'|'--no-breaking-footer')
'-b'|'--breaking-footer'|'--no-breaking-footer')
[ "${OPTARG:-}" ] && OPTARG=${OPTARG#*\=} && set "noarg" "$1" && break
eval '[ ${OPTARG+x} ] &&:' && OPTARG='1' || OPTARG=''
BREAKING_FOOTER="$OPTARG"
Expand Down Expand Up @@ -191,7 +191,7 @@ Options:
-d, --{no-}defined-scope Scope must exist in [scopes]
-e, --{no-}enforce-patterns Scope must match file patterns (implies -d)
-m, --{no-}multi-scope Allow multiple scopes like feat(api,db):
--{no-}breaking-footer Require BREAKING CHANGE footer when ! is used
-b, --{no-}breaking-footer Require BREAKING CHANGE footer when ! is used
-q, --quiet Suppress warnings and non-essential output
-h, --help
-V, --version
Expand All @@ -214,6 +214,60 @@ GETOPTIONSHERE
# --- begin: scripts/cz/config.sh ---


# --- begin: scripts/cz/helpers.sh ---


# --- begin: scripts/cz/error_codes.sh ---


# @start-kcov-exclude - static data table
declare -gA ERR_CODES=(
["empty-message"]="empty commit message"
["header-format"]="commits MUST be prefixed with a type, followed by a colon and space"
["type-enum"]="unknown type '%s'"
["description-empty"]="description MUST immediately follow the colon and space"
["body-leading-blank"]="body MUST begin one blank line after the description"
["breaking-footer"]="if included in the type/scope prefix, breaking changes MUST be indicated by a BREAKING CHANGE footer"
["scope-required"]="scope required"
["scope-enum"]="unknown scope '%s'"
["scope-missing-config"]="scope '%s' used but no scopes defined in config"
["scope-file-required"]="scope required for scoped files"
["multi-scope-disabled"]="multi-scope not enabled"
["files-scope-mismatch"]="files do not match scope '%s'"
["files-scopes-mismatch"]="files do not match scopes '%s'"
["config-not-found"]="config file not found: %s"
["gum-not-found"]="gum is required for interactive mode"
["description-required"]="description MUST NOT be empty"
["breaking-explanation"]="breaking change explanation is required"
["file-exists"]="'%s' already exists (use -f to overwrite)"
["not-git-repo"]="not a git repository"
["hook-action-unknown"]="unknown hook action '%s'"
["hook-exists"]="existing commit-msg hook found"
["hook-foreign"]="commit-msg hook was not installed by cz"
)
# @end-kcov-exclude
# --- end: scripts/cz/error_codes.sh ---

_err() {
[[ -n "${QUIET:-}" ]] && return
local code="$1"
shift
local msg="${ERR_CODES[$code]}"
if [[ -n "$msg" ]]; then
printf -v msg "$msg" "$@"
echo "cz: error: $msg [$code]" >&2
else
echo "cz: error: $code $* [unknown]" >&2
fi
}
_hint() { [[ -n "${QUIET:-}" ]] || echo "$1" >&2; }

_trim() {
local v="${!1}"
v="${v#"${v%%[![:space:]]*}"}"
printf -v "$1" '%s' "${v%"${v##*[![:space:]]}"}"
}
# --- end: scripts/cz/helpers.sh ---
# --- begin: scripts/cz/config_defaults.sh ---

format_config() {
Expand Down Expand Up @@ -330,14 +384,14 @@ find_config() {
load_config() {
if [[ -n "$CONFIG_FILE" ]]; then
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "cz: error: config file not found: $CONFIG_FILE" >&2
_err config-not-found "$CONFIG_FILE"
exit 1
fi

parse_config <"$CONFIG_FILE"

if [[ ${#CFG_TYPES[@]} -eq 0 ]]; then
[[ -z "${QUIET:-}" ]] && echo "cz: warning: no [types] in $CONFIG_FILE, using defaults" >&2
_hint "cz: warning: no [types] in $CONFIG_FILE, using defaults"
_set_default_types
fi
else
Expand Down Expand Up @@ -393,6 +447,7 @@ cmd_parse() {
# --- begin: scripts/cz/cmd_init.sh ---


# --- skipped (already included): scripts/cz/helpers.sh ---
# --- skipped (already included): scripts/cz/config_defaults.sh ---

cmd_init() {
Expand All @@ -404,7 +459,7 @@ cmd_init() {
fi

if [[ -f "$OUTPUT_FILE" && -z "${FORCE:-}" ]]; then
echo "cz: error: '$OUTPUT_FILE' already exists (use -f to overwrite)" >&2
_err file-exists "$OUTPUT_FILE"
return 1
fi

Expand All @@ -414,12 +469,14 @@ cmd_init() {
# --- begin: scripts/cz/cmd_hook.sh ---


# --- skipped (already included): scripts/cz/helpers.sh ---

cmd_hook() {
local action="${1:-status}"

local git_dir
git_dir="$(git rev-parse --git-dir 2>/dev/null)" || {
echo "cz: error: not a git repository" >&2
_err not-git-repo
return 1
}

Expand All @@ -437,8 +494,8 @@ cmd_hook() {
hook_status "$hook_path" "$hook_marker"
;;
*)
echo "cz: error: unknown hook action '$action'" >&2
echo "Usage: cz hook [install|uninstall|status]" >&2
_err hook-action-unknown "$action"
_hint "Usage: cz hook [install|uninstall|status]"
return 2
;;
esac
Expand All @@ -453,8 +510,8 @@ hook_install() {
echo "cz: hook already installed"
return 0
else
echo "cz: error: existing commit-msg hook found" >&2
echo "Remove it manually or add 'cz lint < \"\$1\"' to it" >&2
_err hook-exists
_hint "Remove it manually or add 'cz lint < \"\$1\"' to it"
return 1
fi
fi
Expand Down Expand Up @@ -485,7 +542,7 @@ hook_uninstall() {
fi

if ! grep -q "$hook_marker" "$hook_path" 2>/dev/null; then
echo "cz: error: commit-msg hook was not installed by cz" >&2
_err hook-foreign
return 1
fi

Expand Down Expand Up @@ -518,15 +575,7 @@ hook_status() {
# --- begin: scripts/cz/path_validator.sh ---


# --- begin: scripts/cz/helpers.sh ---


_trim() {
local v="${!1}"
v="${v#"${v%%[![:space:]]*}"}"
printf -v "$1" '%s' "${v%"${v##*[![:space:]]}"}"
}
# --- end: scripts/cz/helpers.sh ---
# --- skipped (already included): scripts/cz/helpers.sh ---
# --- skipped (already included): scripts/cz/config_parser.sh ---

file_matches_pattern() {
Expand Down Expand Up @@ -633,10 +682,8 @@ validate_strict_no_scope() {
}
# --- end: scripts/cz/path_validator.sh ---

_err() { [[ -n "${QUIET:-}" ]] || echo "cz: error: $1" >&2; }
_hint() { [[ -n "${QUIET:-}" ]] || echo "$1" >&2; }
_scope_err() {
_err "unknown scope '$1'"
_err scope-enum "$1"
_hint "Defined scopes: ${!CFG_SCOPES[*]}"
}
_show_errors() {
Expand All @@ -663,7 +710,7 @@ cmd_lint() {
message="$(cat)"

[[ -z "$message" ]] && {
_err "empty commit message"
_err empty-message
return 1
}

Expand All @@ -673,22 +720,28 @@ cmd_lint() {
local pattern='^([a-z]+)(\(([a-zA-Z0-9_@/,*-]+)\))?(!)?: (.+)$'

if [[ ! "$first_line" =~ $pattern ]]; then
_err "invalid commit format"
_hint "Expected: <type>[(scope)]: <description>"
_err header-format
_hint "Expected: <type>[(<scope>)][!]: <description>"
return 1
fi

local type="${BASH_REMATCH[1]}" scope="${BASH_REMATCH[3]}"
local breaking="${BASH_REMATCH[4]}" description="${BASH_REMATCH[5]}"

if [[ ! -v CFG_TYPES["$type"] ]]; then
_err "unknown type '$type'"
_err type-enum "$type"
_hint "Allowed types: ${!CFG_TYPES[*]}"
return 1
fi

[[ -z "$description" || "$description" =~ ^[[:space:]]*$ ]] && {
_err "description cannot be empty"
_err description-empty
return 1
}

local rest="${message#*$'\n'}"
[[ "$rest" != "$message" && "${rest%%$'\n'*}" != "" ]] && {
_err body-leading-blank
return 1
}

Expand All @@ -700,8 +753,8 @@ cmd_lint() {
breaking_footer="${CFG_SETTINGS[breaking_footer]:-true}"
fi

[[ "$breaking_footer" == "true" && -n "$breaking" && ! "$message" =~ BREAKING[[:space:]]CHANGE: ]] && {
_err "breaking change (!) requires 'BREAKING CHANGE:' footer"
[[ "$breaking_footer" == "true" && -n "$breaking" && ! "$message" =~ BREAKING[-\ ]CHANGE: ]] && {
_err breaking-footer
return 1
}

Expand Down Expand Up @@ -752,13 +805,13 @@ validate_paths_if_needed() {
fi

if [[ "$require_scope" == "true" && -z "$scope" ]]; then
_err "scope required"
_err scope-required
return 1
fi

if [[ "$defined_scope" == "true" && -n "$scope" ]]; then
[[ ${#CFG_SCOPES[@]} -eq 0 ]] && {
_err "scope '$scope' used but no scopes defined in config"
_err scope-missing-config "$scope"
return 1
}
if is_multi_scope "$scope"; then
Expand All @@ -777,7 +830,7 @@ validate_paths_if_needed() {

if [[ -z "$scope" ]]; then
if ! validate_strict_no_scope "${files[@]}"; then
_err "scope required for scoped files"
_err scope-file-required
_show_errors "${STRICT_MATCHES[@]}"
_hint "Hint: add a scope that matches these files"
return 1
Expand All @@ -789,13 +842,13 @@ validate_paths_if_needed() {

if is_multi_scope "$scope"; then
[[ "$multi_scope" != "true" ]] && {
_err "multi-scope not enabled"
_err multi-scope-disabled
_hint "Use --multi-scope flag or set multi-scope = true in [settings]"
return 1
}
_check_scopes_exist "$scope" || return 1
if ! validate_files_against_scopes "$scope" "${files[@]}"; then
_err "files do not match scopes '$scope'"
_err files-scopes-mismatch "$scope"
_show_errors "${VALIDATION_ERRORS[@]}"
return 1
fi
Expand All @@ -807,7 +860,7 @@ validate_paths_if_needed() {
return 1
}
if ! validate_files_against_scope "$scope" "${files[@]}"; then
_err "files do not match scope '$scope'"
_err files-scope-mismatch "$scope"
_show_errors "${VALIDATION_ERRORS[@]}"
return 1
fi
Expand All @@ -816,6 +869,7 @@ validate_paths_if_needed() {
# --- begin: scripts/cz/cmd_create.sh ---


# --- skipped (already included): scripts/cz/helpers.sh ---
# --- skipped (already included): scripts/cz/config.sh ---

_gum() { gum "$@" || exit 130; }
Expand All @@ -825,8 +879,8 @@ _scope_input_custom() { _gum input --header "Enter scope:" --placeholder "e.g.,

cmd_create() {
if ! command -v gum &>/dev/null; then
echo "cz: error: gum is required for interactive mode" >&2
echo "See: https://github.com/charmbracelet/gum" >&2
_err gum-not-found
_hint "See: https://github.com/charmbracelet/gum"
exit 1
fi

Expand Down Expand Up @@ -899,7 +953,7 @@ cmd_create() {
local description=""
while [[ -z "$description" ]]; do
description=$(_gum input --header "Description (required):" --placeholder "Short summary of the change")
[[ -z "$description" ]] && echo "cz: error: description cannot be empty" >&2
[[ -z "$description" ]] && _err description-required
done

local body
Expand All @@ -910,7 +964,7 @@ cmd_create() {
local breaking_explanation=""
while [[ -z "$breaking_explanation" ]]; do
breaking_explanation=$(_gum write --header "Breaking change explanation (required):" --placeholder "Describe what breaks and how to migrate...")
[[ -z "$breaking_explanation" ]] && echo "cz: error: breaking change explanation is required" >&2
[[ -z "$breaking_explanation" ]] && _err breaking-explanation
done
footer="BREAKING CHANGE: $breaking_explanation"
else
Expand Down
2 changes: 1 addition & 1 deletion dist/cz/completions/bash/cz.bash
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ _cz() {
prev="${COMP_WORDS[COMP_CWORD-1]}"

local commands="create lint parse init hook"
local global_opts="-c --config-file -r --require-scope --no-require-scope -d --defined-scope --no-defined-scope -e --enforce-patterns --no-enforce-patterns -m --multi-scope --no-multi-scope --breaking-footer --no-breaking-footer -q --quiet -h --help -V --version"
local global_opts="-c --config-file -r --require-scope --no-require-scope -d --defined-scope --no-defined-scope -e --enforce-patterns --no-enforce-patterns -m --multi-scope --no-multi-scope -b --breaking-footer --no-breaking-footer -q --quiet -h --help -V --version"

# Complete config file path after -c/--config-file
if [[ "$prev" == "-c" || "$prev" == "--config-file" ]]; then
Expand Down
4 changes: 2 additions & 2 deletions dist/cz/completions/zsh/_cz
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ _cz() {
'(-e --enforce-patterns --no-enforce-patterns)--no-enforce-patterns[Disable pattern enforcement]' \
'(-m --multi-scope --no-multi-scope)'{-m,--multi-scope}'[Allow multiple scopes]' \
'(-m --multi-scope --no-multi-scope)--no-multi-scope[Disallow multiple scopes]' \
'(--breaking-footer --no-breaking-footer)--breaking-footer[Require BREAKING CHANGE footer]' \
'(--breaking-footer --no-breaking-footer)--no-breaking-footer[Allow breaking changes without footer]' \
'(-b --breaking-footer --no-breaking-footer)'{-b,--breaking-footer}'[Require BREAKING CHANGE footer]' \
'(-b --breaking-footer --no-breaking-footer)--no-breaking-footer[Allow breaking changes without footer]' \
'(-q --quiet)'{-q,--quiet}'[Suppress warnings and non-essential output]' \
'1:command:->commands' \
'*::arg:->args'
Expand Down
13 changes: 7 additions & 6 deletions dist/cz/man/man1/cz.1
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
cz \- conventional commit message builder
.SH "SYNOPSIS"
.sp
\fBcz\fP [\fB\-r\fP | \fB\-\-no\-require\-scope\fP] [\fB\-d\fP | \fB\-\-no\-defined\-scope\fP] [\fB\-e\fP | \fB\-\-no\-enforce\-patterns\fP] [\fB\-m\fP | \fB\-\-no\-multi\-scope\fP] [\fB\-\-breaking\-footer\fP | \fB\-\-no\-breaking\-footer\fP] [\fB\-q\fP] [\fB\-c\fP \fIFILE\fP] [\fISUBCOMMAND\fP]
\fBcz\fP [\fB\-r\fP | \fB\-\-no\-require\-scope\fP] [\fB\-d\fP | \fB\-\-no\-defined\-scope\fP] [\fB\-e\fP | \fB\-\-no\-enforce\-patterns\fP] [\fB\-m\fP | \fB\-\-no\-multi\-scope\fP] [\fB\-b\fP | \fB\-\-no\-breaking\-footer\fP] [\fB\-q\fP] [\fB\-c\fP \fIFILE\fP] [\fISUBCOMMAND\fP]
.sp
\fBcz\fP \fBcreate\fP
.br
Expand Down Expand Up @@ -155,10 +155,11 @@ Allow multiple scopes like \f(CRfeat(api,db):\fP. Overrides \f(CRmulti\-scope\fP
Disallow multiple scopes. Overrides \f(CRmulti\-scope\fP config.
.RE
.sp
\fB\-\-breaking\-footer\fP
\fB\-b\fP, \fB\-\-breaking\-footer\fP
.RS 4
Require a \f(CRBREAKING CHANGE:\fP footer when the commit has a breaking change
indicator (\f(CR!\fP). Overrides \f(CRbreaking\-footer\fP config. Default when unset: true.
Require a \f(CRBREAKING CHANGE:\fP (or \f(CRBREAKING\-CHANGE:\fP) footer when the commit
has a breaking change indicator (\f(CR!\fP). Overrides \f(CRbreaking\-footer\fP config.
Default when unset: true.
.RE
.sp
\fB\-\-no\-breaking\-footer\fP
Expand Down Expand Up @@ -285,10 +286,10 @@ Scope required and must match patterns
T}
T{
.sp
\fB\-\-breaking\-footer\fP
\fB\-b\fP / \fB\-\-breaking\-footer\fP
T}:T{
.sp
Breaking \f(CR!\fP requires \f(CRBREAKING CHANGE:\fP footer (default)
Breaking \f(CR!\fP requires \f(CRBREAKING CHANGE:\fP or \f(CRBREAKING\-CHANGE:\fP footer (default)
T}
T{
.sp
Expand Down
Loading