diff --git a/dist/cz/bin/cz b/dist/cz/bin/cz index 554157e..a92dd28 100755 --- a/dist/cz/bin/cz +++ b/dist/cz/bin/cz @@ -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 @@ -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" @@ -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 @@ -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() { @@ -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 @@ -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() { @@ -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 @@ -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 } @@ -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 @@ -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 @@ -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 @@ -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() { @@ -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() { @@ -663,7 +710,7 @@ cmd_lint() { message="$(cat)" [[ -z "$message" ]] && { - _err "empty commit message" + _err empty-message return 1 } @@ -673,8 +720,8 @@ cmd_lint() { local pattern='^([a-z]+)(\(([a-zA-Z0-9_@/,*-]+)\))?(!)?: (.+)$' if [[ ! "$first_line" =~ $pattern ]]; then - _err "invalid commit format" - _hint "Expected: [(scope)]: " + _err header-format + _hint "Expected: [()][!]: " return 1 fi @@ -682,13 +729,19 @@ cmd_lint() { 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 } @@ -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 } @@ -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 @@ -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 @@ -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 @@ -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 @@ -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; } @@ -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 @@ -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 @@ -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 diff --git a/dist/cz/completions/bash/cz.bash b/dist/cz/completions/bash/cz.bash index 7b955cb..727fc32 100644 --- a/dist/cz/completions/bash/cz.bash +++ b/dist/cz/completions/bash/cz.bash @@ -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 diff --git a/dist/cz/completions/zsh/_cz b/dist/cz/completions/zsh/_cz index b23af50..026482f 100644 --- a/dist/cz/completions/zsh/_cz +++ b/dist/cz/completions/zsh/_cz @@ -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' diff --git a/dist/cz/man/man1/cz.1 b/dist/cz/man/man1/cz.1 index 64db2ca..6eaf3da 100644 --- a/dist/cz/man/man1/cz.1 +++ b/dist/cz/man/man1/cz.1 @@ -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 @@ -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 @@ -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 diff --git a/dist/cz/man/man5/gitcommitizen.5 b/dist/cz/man/man5/gitcommitizen.5 index 87bf263..9f54663 100644 --- a/dist/cz/man/man5/gitcommitizen.5 +++ b/dist/cz/man/man5/gitcommitizen.5 @@ -315,6 +315,97 @@ Scopes may include letters, numbers, dashes, underscores, and \f(CR@\fP (e.g., \ .\} Glob patterns follow standard shell globbing with \f(CR**\fP for recursive matching. .RE +.SH "SPEC COMPLIANCE" +.sp +Validation rules from \c +.URL "https://www.conventionalcommits.org/en/v1.0.0/" "Conventional Commits v1.0.0" ":" +.TS +allbox tab(:); +ltB ltB ltB. +T{ +.sp +Rule +T}:T{ +.sp +Enforcement +T}:T{ +.sp +Ref +T} +.T& +lt lt lt. +T{ +.sp +Type prefix required +T}:T{ +.sp +Always (lint, create) +T}:T{ +.sp +§1 +T} +T{ +.sp +Description required +T}:T{ +.sp +Always +T}:T{ +.sp +§5 +T} +T{ +.sp +Blank line before body +T}:T{ +.sp +Always (lint) +T}:T{ +.sp +§6 +T} +T{ +.sp +BREAKING CHANGE uppercase +T}:T{ +.sp +Always (lint) +T}:T{ +.sp +§15 +T} +T{ +.sp +BREAKING\-CHANGE synonym +T}:T{ +.sp +Accepted in footers +T}:T{ +.sp +§16 +T} +T{ +.sp +Scope defined/required +T}:T{ +.sp +Configurable (\f(CR\-r\fP, \f(CR\-d\fP, \f(CR\-e\fP) +T}:T{ +.sp +— +T} +T{ +.sp +Breaking footer required +T}:T{ +.sp +Configurable (\f(CR\-b\fP, \f(CR\-\-breaking\-footer\fP) +T}:T{ +.sp +— +T} +.TE +.sp .SH "SEE ALSO" .sp git\-commit(1) diff --git a/scripts/cz/cmd_create.sh b/scripts/cz/cmd_create.sh index b852631..ed2bd2e 100644 --- a/scripts/cz/cmd_create.sh +++ b/scripts/cz/cmd_create.sh @@ -2,6 +2,8 @@ # cz create - compose a commit message interactively +# @bundle source +. ./helpers.sh # @bundle source . ./config.sh @@ -15,8 +17,8 @@ _scope_input_custom() { _gum input --header "Enter scope:" --placeholder "e.g., cmd_create() { # Check gum dependency 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 @@ -102,7 +104,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 # Get body (optional) @@ -115,7 +117,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 diff --git a/scripts/cz/cmd_hook.sh b/scripts/cz/cmd_hook.sh index 9d4bb48..e0191c8 100644 --- a/scripts/cz/cmd_hook.sh +++ b/scripts/cz/cmd_hook.sh @@ -2,13 +2,16 @@ # cz hook - manage the commit-msg git hook +# @bundle source +. ./helpers.sh + cmd_hook() { local action="${1:-status}" # Find git directory 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 } @@ -26,8 +29,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 @@ -42,8 +45,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 @@ -77,7 +80,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 diff --git a/scripts/cz/cmd_init.sh b/scripts/cz/cmd_init.sh index d2e6526..ea5893e 100644 --- a/scripts/cz/cmd_init.sh +++ b/scripts/cz/cmd_init.sh @@ -2,6 +2,8 @@ # cz init - generate a starter .gitcommitizen file +# @bundle source +. ./helpers.sh # @bundle source . ./config_defaults.sh @@ -16,7 +18,7 @@ cmd_init() { # Write to file 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 diff --git a/scripts/cz/cmd_lint.sh b/scripts/cz/cmd_lint.sh index 3a4a485..000b065 100644 --- a/scripts/cz/cmd_lint.sh +++ b/scripts/cz/cmd_lint.sh @@ -7,11 +7,8 @@ # @bundle source . ./path_validator.sh -# Output helpers - respect QUIET flag -_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() { @@ -41,7 +38,7 @@ cmd_lint() { message="$(cat)" [[ -z "$message" ]] && { - _err "empty commit message" + _err empty-message return 1 } @@ -54,8 +51,9 @@ cmd_lint() { local pattern='^([a-z]+)(\(([a-zA-Z0-9_@/,*-]+)\))?(!)?: (.+)$' if [[ ! "$first_line" =~ $pattern ]]; then - _err "invalid commit format" - _hint "Expected: [(scope)]: " + # Spec #1: commits MUST be prefixed with a type, followed by colon and space + _err header-format + _hint "Expected: [()][!]: " return 1 fi @@ -64,14 +62,22 @@ cmd_lint() { # Validate type if [[ ! -v CFG_TYPES["$type"] ]]; then - _err "unknown type '$type'" + _err type-enum "$type" _hint "Allowed types: ${!CFG_TYPES[*]}" return 1 fi - # Validate description is not empty + # Spec #5: description MUST immediately follow the colon and space [[ -z "$description" || "$description" =~ ^[[:space:]]*$ ]] && { - _err "description cannot be empty" + _err description-empty + return 1 + } + + # Validate blank line after subject when body/footer present + # Spec: "body MUST begin one blank line after the description" + local rest="${message#*$'\n'}" + [[ "$rest" != "$message" && "${rest%%$'\n'*}" != "" ]] && { + _err body-leading-blank return 1 } @@ -84,9 +90,12 @@ cmd_lint() { breaking_footer="${CFG_SETTINGS[breaking_footer]:-true}" fi - # Validate breaking change has BREAKING CHANGE footer - [[ "$breaking_footer" == "true" && -n "$breaking" && ! "$message" =~ BREAKING[[:space:]]CHANGE: ]] && { - _err "breaking change (!) requires 'BREAKING CHANGE:' footer" + # Spec #13: if included in the type/scope prefix, breaking changes MUST + # be indicated by a BREAKING CHANGE footer + # Spec #16: BREAKING-CHANGE MUST be synonymous with BREAKING CHANGE + # Spec #15: BREAKING CHANGE MUST be uppercase + [[ "$breaking_footer" == "true" && -n "$breaking" && ! "$message" =~ BREAKING[-\ ]CHANGE: ]] && { + _err breaking-footer return 1 } @@ -149,14 +158,14 @@ validate_paths_if_needed() { # -r: require scope to be present if [[ "$require_scope" == "true" && -z "$scope" ]]; then - _err "scope required" + _err scope-required return 1 fi # -d: validate scope exists in config (if scope provided) 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 @@ -178,7 +187,7 @@ validate_paths_if_needed() { # No scope provided - check if files match any pattern 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 @@ -192,14 +201,14 @@ validate_paths_if_needed() { # Multi-scope validation 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 } # Validate scopes exist _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 @@ -212,7 +221,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 diff --git a/scripts/cz/completions/_cz b/scripts/cz/completions/_cz index b23af50..026482f 100644 --- a/scripts/cz/completions/_cz +++ b/scripts/cz/completions/_cz @@ -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' diff --git a/scripts/cz/completions/cz.bash b/scripts/cz/completions/cz.bash index 7b955cb..727fc32 100644 --- a/scripts/cz/completions/cz.bash +++ b/scripts/cz/completions/cz.bash @@ -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 diff --git a/scripts/cz/config.sh b/scripts/cz/config.sh index 2436c59..3b18804 100644 --- a/scripts/cz/config.sh +++ b/scripts/cz/config.sh @@ -2,6 +2,8 @@ # Find and load .gitcommitizen configuration +# @bundle source +. ./helpers.sh # @bundle source . ./config_defaults.sh # @bundle source @@ -40,7 +42,7 @@ 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 @@ -48,7 +50,7 @@ load_config() { # If no types defined, use default types but preserve settings/scopes 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 diff --git a/scripts/cz/docs/cz.adoc b/scripts/cz/docs/cz.adoc index dd028d5..bb43039 100644 --- a/scripts/cz/docs/cz.adoc +++ b/scripts/cz/docs/cz.adoc @@ -13,7 +13,7 @@ cz - conventional commit message builder == SYNOPSIS -*cz* [*-r* | *--no-require-scope*] [*-d* | *--no-defined-scope*] [*-e* | *--no-enforce-patterns*] [*-m* | *--no-multi-scope*] [*--breaking-footer* | *--no-breaking-footer*] [*-q*] [*-c* _FILE_] [_SUBCOMMAND_] +*cz* [*-r* | *--no-require-scope*] [*-d* | *--no-defined-scope*] [*-e* | *--no-enforce-patterns*] [*-m* | *--no-multi-scope*] [*-b* | *--no-breaking-footer*] [*-q*] [*-c* _FILE_] [_SUBCOMMAND_] *cz* *create* + *cz* *lint* [*-p* _PATHS_] + @@ -106,9 +106,10 @@ in a terminal, or *lint* mode if receiving input on standard input. *--no-multi-scope*:: Disallow multiple scopes. Overrides `multi-scope` config. -*--breaking-footer*:: - Require a `BREAKING CHANGE:` footer when the commit has a breaking change - indicator (`!`). Overrides `breaking-footer` config. Default when unset: true. +*-b*, *--breaking-footer*:: + Require a `BREAKING CHANGE:` (or `BREAKING-CHANGE:`) footer when the commit + has a breaking change indicator (`!`). Overrides `breaking-footer` config. + Default when unset: true. *--no-breaking-footer*:: Allow breaking changes without a `BREAKING CHANGE:` footer. Overrides @@ -172,8 +173,8 @@ The *-r*, *-d*, and *-e* flags affect the *lint* command: |*-r -e* |Scope required and must match patterns -|*--breaking-footer* -|Breaking `!` requires `BREAKING CHANGE:` footer (default) +|*-b* / *--breaking-footer* +|Breaking `!` requires `BREAKING CHANGE:` or `BREAKING-CHANGE:` footer (default) |*--no-breaking-footer* |Breaking `!` allowed without footer diff --git a/scripts/cz/docs/gitcommitizen.adoc b/scripts/cz/docs/gitcommitizen.adoc index ad774ea..f8bd144 100644 --- a/scripts/cz/docs/gitcommitizen.adoc +++ b/scripts/cz/docs/gitcommitizen.adoc @@ -135,6 +135,43 @@ Place **.gitcommitizen** in the root of a Git repository. * Scopes may include letters, numbers, dashes, underscores, and `@` (e.g., `@types/node`). * Glob patterns follow standard shell globbing with `**` for recursive matching. +== SPEC COMPLIANCE + +Validation rules from https://www.conventionalcommits.org/en/v1.0.0/[Conventional Commits v1.0.0]: + +[cols="2,3,1", options="header"] +|=== +|Rule |Enforcement |Ref + +|Type prefix required +|Always (lint, create) +|§1 + +|Description required +|Always +|§5 + +|Blank line before body +|Always (lint) +|§6 + +|BREAKING CHANGE uppercase +|Always (lint) +|§15 + +|BREAKING-CHANGE synonym +|Accepted in footers +|§16 + +|Scope defined/required +|Configurable (`-r`, `-d`, `-e`) +|— + +|Breaking footer required +|Configurable (`-b`, `--breaking-footer`) +|— +|=== + == SEE ALSO git-commit(1):: diff --git a/scripts/cz/error_codes.sh b/scripts/cz/error_codes.sh new file mode 100644 index 0000000..9e43150 --- /dev/null +++ b/scripts/cz/error_codes.sh @@ -0,0 +1,33 @@ +# shellcheck shell=bash + +# Error code registry - centralized error messages for cz +# Each code maps to a printf format string +# Usage: _err [args...] + +# shellcheck disable=SC2034 # ERR_CODES is used by _err in helpers.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 diff --git a/scripts/cz/helpers.sh b/scripts/cz/helpers.sh index 97edc3b..41a75a8 100644 --- a/scripts/cz/helpers.sh +++ b/scripts/cz/helpers.sh @@ -2,6 +2,25 @@ # Shared helper functions for cz +# @bundle source +. ./error_codes.sh + +# Output helpers - respect QUIET flag +_err() { + [[ -n "${QUIET:-}" ]] && return + local code="$1" + shift + local msg="${ERR_CODES[$code]}" + if [[ -n "$msg" ]]; then + # shellcheck disable=SC2059 # intentional printf format from registry + 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 leading/trailing whitespace from variable # Usage: _trim varname _trim() { diff --git a/scripts/cz/main_spec.sh b/scripts/cz/main_spec.sh index c23e62d..dca6a36 100644 --- a/scripts/cz/main_spec.sh +++ b/scripts/cz/main_spec.sh @@ -97,6 +97,56 @@ Describe 'cz' End End + #─────────────────────────────────────────────────────────── + # Body blank line (spec: body MUST begin one blank line) + #─────────────────────────────────────────────────────────── + Describe 'body blank line' + It 'accepts single-line message (no body)' + Data "docs: correct spelling of CHANGELOG" + When run script "$BIN" lint + The status should be success + End + + It 'accepts message with blank line before body' + Data + #|fix: prevent racing of requests + #| + #|Introduce a request id and a reference to latest request. Dismiss + #|incoming responses other than from latest request. + End + When run script "$BIN" lint + The status should be success + End + + It 'rejects body without blank line separator' + Data + #|fix: prevent racing of requests + #|Introduce a request id and a reference to latest request. + End + When run script "$BIN" lint + The status should be failure + The stderr should include "[body-leading-blank]" + End + + It 'rejects footer without blank line separator' + Data + #|feat: allow provided config object to extend other configs + #|BREAKING CHANGE: `extends` key in config file is now used for extending other config files + End + When run script "$BIN" lint + The status should be failure + The stderr should include "[body-leading-blank]" + End + + It 'accepts message with only a trailing newline' + Data + #|docs: correct spelling of CHANGELOG + End + When run script "$BIN" lint + The status should be success + End + End + #─────────────────────────────────────────────────────────── # Invalid conventional commits #─────────────────────────────────────────────────────────── @@ -105,70 +155,70 @@ Describe 'cz' Data "" When run script "$BIN" lint The status should be failure - The stderr should include "empty commit message" + The stderr should include "[empty-message]" End It 'rejects whitespace-only message' Data " " When run script "$BIN" lint The status should be failure - The stderr should include "invalid commit format" + The stderr should include "[header-format]" End It 'rejects missing colon' Data "feat add feature" When run script "$BIN" lint The status should be failure - The stderr should include "invalid commit format" + The stderr should include "[header-format]" End It 'rejects missing description after colon' Data "feat:" When run script "$BIN" lint The status should be failure - The stderr should include "invalid commit format" + The stderr should include "[header-format]" End It 'rejects missing description after colon and space' Data "feat: " When run script "$BIN" lint The status should be failure - The stderr should include "invalid commit format" + The stderr should include "[header-format]" End It 'rejects unknown type' Data "unknown: some change" When run script "$BIN" lint The status should be failure - The stderr should include "unknown type" + The stderr should include "[type-enum]" End It 'rejects uppercase type' Data "FEAT: add feature" When run script "$BIN" lint The status should be failure - The stderr should include "invalid commit format" + The stderr should include "[header-format]" End It 'rejects breaking ! without BREAKING CHANGE footer' Data "feat!: breaking change" When run script "$BIN" lint The status should be failure - The stderr should include "BREAKING CHANGE:" + The stderr should include "[breaking-footer]" End It 'rejects empty scope' Data "feat(): add feature" When run script "$BIN" lint The status should be failure - The stderr should include "invalid commit format" + The stderr should include "[header-format]" End It 'rejects whitespace-only description' Data "feat: " When run script "$BIN" lint The status should be failure - The stderr should include "description cannot be empty" + The stderr should include "[description-empty]" End End @@ -202,7 +252,7 @@ EOF Data "feat: not in config" When run script "$BIN" lint The status should be failure - The stderr should include "unknown type" + The stderr should include "[type-enum]" End It 'uses --config-file option' @@ -229,8 +279,8 @@ EOF Data "feat: something" When run script "$BIN" --config-file nonexistent.ini lint The status should be failure - The stderr should include "config file not found" The stderr should include "nonexistent.ini" + The stderr should include "[config-not-found]" End It 'handles config with only scopes (uses default types)' @@ -341,7 +391,7 @@ EOF Data "feat(api): add endpoint" When run script "$BIN" -e lint --paths "src/ui/button.tsx" The status should be failure - The stderr should include "does not match scope" + The stderr should include "[files-scope-mismatch]" End It 'fails when some files do not match scope' @@ -394,7 +444,7 @@ EOF Data "feat(scripts): update script" When run script "$BIN" -e lint --paths "scripts/nested/main.sh" The status should be failure - The stderr should include "does not match scope" + The stderr should include "[files-scope-mismatch]" End It 'matches multi-pattern scope (first pattern)' @@ -472,7 +522,7 @@ EOF Data "feat(api,ui): cross-cutting change" When run script "$BIN" -e lint --paths "src/api/x.go" The status should be failure - The stderr should include "multi-scope not enabled" + The stderr should include "[multi-scope-disabled]" End It 'rejects multi-scope by default when validating files' @@ -487,7 +537,7 @@ EOF Data "feat(api,ui): cross-cutting change" When run script "$BIN" -e lint --paths "src/api/x.go" The status should be failure - The stderr should include "multi-scope not enabled" + The stderr should include "[multi-scope-disabled]" End It 'rejects multi-scope with unknown scope' @@ -504,7 +554,7 @@ EOF Data "feat(api,unknown): change" When run script "$BIN" -e lint --paths "src/api/x.go" The status should be failure - The stderr should include "unknown scope" + The stderr should include "[scope-enum]" End It '--multi-scope flag enables multi-scope without config' @@ -536,7 +586,7 @@ EOF Data "feat(api,ui): cross-cutting change" When run script "$BIN" --no-multi-scope -e lint --paths "src/api/x.go" The status should be failure - The stderr should include "multi-scope not enabled" + The stderr should include "[multi-scope-disabled]" End It '-m shorthand enables multi-scope' @@ -579,7 +629,7 @@ EOF Data "feat(api,ui): cross-cutting change" When run script "$BIN" -m -e lint --paths "src/api/handler.go src/other/file.txt" The status should be failure - The stderr should include "does not match" + The stderr should include "[files-scopes-mismatch]" End End @@ -598,7 +648,7 @@ EOF Data "feat(unknown): add feature" When run script "$BIN" -d lint The status should be failure - The stderr should include "unknown scope" + The stderr should include "[scope-enum]" End It 'accepts defined scope when -d is set' @@ -635,7 +685,7 @@ EOF Data "feat(anything): add feature" When run script "$BIN" -d lint The status should be failure - The stderr should include "no scopes defined" + The stderr should include "[scope-missing-config]" End It '--no-defined-scope overrides config' @@ -668,7 +718,7 @@ EOF Data "feat(unknown): add feature" When run script "$BIN" lint The status should be failure - The stderr should include "unknown scope" + The stderr should include "[scope-enum]" End End @@ -684,7 +734,7 @@ EOF Data "feat: add feature" When run script "$BIN" -r lint The status should be failure - The stderr should include "scope required" + The stderr should include "[scope-required]" End It 'accepts scope when -r is set' @@ -745,7 +795,7 @@ EOF Data "feat(api): add endpoint" When run script "$BIN" -e lint --paths "src/ui/button.tsx" The status should be failure - The stderr should include "does not match scope" + The stderr should include "[files-scope-mismatch]" End It 'passes when scope matches files with -e' @@ -758,7 +808,7 @@ EOF Data "feat: add feature" When run script "$BIN" -e lint --paths "src/api/handler.go" The status should be failure - The stderr should include "scope required" + The stderr should include "[scope-file-required]" End It 'allows no scope for unscoped files when -e is set' @@ -771,14 +821,14 @@ EOF Data "feat(unknown): add feature" When run script "$BIN" -e lint --paths "other/file.txt" The status should be failure - The stderr should include "unknown scope" + The stderr should include "[scope-enum]" End It '-e with unknown scope shows defined scopes hint' Data "feat(unknown): add feature" When run script "$BIN" -e lint --paths "src/api/x.go" The status should be failure - The stderr should include "unknown scope" + The stderr should include "[scope-enum]" The stderr should include "api" The stderr should include "ui" End @@ -813,7 +863,7 @@ EOF Data "feat(api): add endpoint" When run script "$BIN" lint --paths "src/ui/button.tsx" The status should be failure - The stderr should include "does not match scope" + The stderr should include "[files-scope-mismatch]" End It 'config enforce-patterns without defined-scope rejects unknown scope' @@ -830,7 +880,7 @@ EOF Data "feat(unknown): add feature" When run script "$BIN" lint --paths "src/api/x.go" The status should be failure - The stderr should include "unknown scope" + The stderr should include "[scope-enum]" End End @@ -839,7 +889,7 @@ EOF Data "feat!: breaking change" When run script "$BIN" lint The status should be failure - The stderr should include "breaking change (!) requires 'BREAKING CHANGE:' footer" + The stderr should include "[breaking-footer]" End It 'allows breaking change without footer when --no-breaking-footer is set' @@ -852,7 +902,7 @@ EOF Data "feat!: breaking change" When run script "$BIN" --breaking-footer lint The status should be failure - The stderr should include "breaking change (!) requires 'BREAKING CHANGE:' footer" + The stderr should include "[breaking-footer]" End It 'accepts breaking change with footer when --breaking-footer is set' @@ -899,7 +949,7 @@ EOF Data "feat!: breaking change" When run script "$BIN" --breaking-footer lint The status should be failure - The stderr should include "breaking change (!) requires 'BREAKING CHANGE:' footer" + The stderr should include "[breaking-footer]" End It 'has no effect on non-breaking commits' @@ -907,6 +957,48 @@ EOF When run script "$BIN" --no-breaking-footer lint The status should be success End + + It 'accepts BREAKING-CHANGE footer as synonym for BREAKING CHANGE' + Data + #|feat!: breaking change + #| + #|BREAKING-CHANGE: this is breaking + End + When run script "$BIN" lint + The status should be success + End + + It 'accepts BREAKING-CHANGE footer with --breaking-footer flag' + Data + #|feat!: breaking change + #| + #|BREAKING-CHANGE: this is breaking + End + When run script "$BIN" --breaking-footer lint + The status should be success + End + + It 'rejects BREAKING!CHANGE as footer (not a valid separator)' + Data + #|feat!: breaking change + #| + #|BREAKING!CHANGE: this is breaking + End + When run script "$BIN" lint + The status should be failure + The stderr should include "[breaking-footer]" + End + + It 'rejects lowercase breaking change footer' + Data + #|feat!: breaking change + #| + #|breaking change: this is breaking + End + When run script "$BIN" lint + The status should be failure + The stderr should include "[breaking-footer]" + End End #─────────────────────────────────────────────────────────── @@ -993,7 +1085,7 @@ EOF touch .gitcommitizen When run script "$BIN" init -o .gitcommitizen The status should be failure - The stderr should include "already exists" + The stderr should include "[file-exists]" End It '-f overwrites existing file' @@ -1080,7 +1172,7 @@ EOF printf '#!/bin/sh\necho "other"\n' > .git/hooks/commit-msg When run script "$BIN" hook install The status should be failure - The stderr should include "existing commit-msg hook" + The stderr should include "[hook-exists]" End End @@ -1104,7 +1196,7 @@ EOF printf '#!/bin/sh\necho "other"\n' > .git/hooks/commit-msg When run script "$BIN" hook uninstall The status should be failure - The stderr should include "not installed by cz" + The stderr should include "[hook-foreign]" End End @@ -1113,28 +1205,28 @@ EOF rm -rf .git When run script "$BIN" hook status The status should be failure - The stderr should include "not a git repository" + The stderr should include "[not-git-repo]" End It 'install errors outside git repo' rm -rf .git When run script "$BIN" hook install The status should be failure - The stderr should include "not a git repository" + The stderr should include "[not-git-repo]" End It 'uninstall errors outside git repo' rm -rf .git When run script "$BIN" hook uninstall The status should be failure - The stderr should include "not a git repository" + The stderr should include "[not-git-repo]" End End It 'errors with unknown subcommand' When run script "$BIN" hook unknown The status should be failure - The stderr should include "unknown" + The stderr should include "[hook-action-unknown]" End End @@ -1228,8 +1320,8 @@ EOF It 'fails if explicit config file does not exist' When run script "$BIN" --config-file /nonexistent/config parse The status should be failure - The stderr should include "config file not found" The stderr should include "/nonexistent/config" + The stderr should include "[config-not-found]" End End @@ -1244,7 +1336,7 @@ EOF command -v gum &>/dev/null && Skip "gum is installed system-wide" When run script "$BIN" create The status should be failure - The stderr should include "gum is required" + The stderr should include "[gum-not-found]" End End @@ -1898,7 +1990,7 @@ MOCK Data "invalid message" When run script "$BIN" The status should be failure - The stderr should include "invalid commit format" + The stderr should include "[header-format]" End End @@ -1965,7 +2057,7 @@ EOF Data "feat(unknown): add feature" When run script "$BIN" --defined-scope lint The status should be failure - The stderr should include "unknown scope" + The stderr should include "[scope-enum]" The stderr should include "api" The stderr should include "ui" End @@ -1993,7 +2085,7 @@ EOF Data "feat: " When run script "$BIN" lint The status should be failure - The stderr should include "empty" + The stderr should include "[description-empty]" End It 'handles config keys with hyphens correctly' diff --git a/scripts/cz/options.sh b/scripts/cz/options.sh index fd195d9..389d04d 100644 --- a/scripts/cz/options.sh +++ b/scripts/cz/options.sh @@ -14,7 +14,7 @@ parser_definition() { flag DEFINED_SCOPE -d --{no-}defined-scope init:@unset -- "Scope must exist in [scopes]" flag ENFORCE_PATTERNS -e --{no-}enforce-patterns init:@unset validate:'DEFINED_SCOPE=1' -- "Scope must match file patterns (implies -d)" flag MULTI_SCOPE -m --{no-}multi-scope init:@unset -- "Allow multiple scopes like feat(api,db):" - flag BREAKING_FOOTER --{no-}breaking-footer init:@unset -- "Require BREAKING CHANGE footer when ! is used" + flag BREAKING_FOOTER -b --{no-}breaking-footer init:@unset -- "Require BREAKING CHANGE footer when ! is used" flag QUIET -q --quiet -- "Suppress warnings and non-essential output" disp :usage -h --help disp VERSION -V --version