|
1 | 1 | #!/usr/bin/env bash |
2 | 2 | # |
| 3 | +# Copyright (C) 2026 Vates SAS |
| 4 | +# |
| 5 | +# This program is free software: you can redistribute it and/or modify |
| 6 | +# it under the terms of the GNU General Public License as published by |
| 7 | +# the Free Software Foundation, either version 3 of the License, or |
| 8 | +# (at your option) any later version. |
| 9 | +# This program is distributed in the hope that it will be useful, |
| 10 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | +# GNU General Public License for more details. |
| 13 | +# |
| 14 | +# You should have received a copy of the GNU General Public License |
| 15 | +# along with this program. If not, see <https://www.gnu.org/licenses/>. |
| 16 | + |
| 17 | +# ============================================================================== |
3 | 18 | # Check the commit message conventions of a pull request. |
4 | 19 | # |
5 | | -# Usage: check-commit-messages.sh <base-sha> <head-sha> <pr-number> |
| 20 | +# Usage: check-commit-messages <base-sha> <head-sha> <pr-number> |
| 21 | +# |
| 22 | +# - `<base-sha>`: base of the range to check. |
| 23 | +# - `<head-sha>`: head of the range to check. |
| 24 | +# - `<pr-number>`: number of the pull request. |
6 | 25 | # |
7 | 26 | # Rules: |
8 | 27 | # - Format: `<type>(<scope>): <subject>`, with type in `feat`, `fix`, `docs` or `chore`. |
|
15 | 34 | # - A commit that only touches files in `tests/` must have a scope starting with `tests/` |
16 | 35 | # (even for `chore`), and a scope starting with `tests/` is only allowed for such commits. |
17 | 36 | # - A `chore` commit touching files in `src/` or `tests/` only triggers a warning (never a failure). |
| 37 | +# - A subject ends with at most one pull request ID. |
18 | 38 | # - If the pull request has several commits, each one should end with ` (#<pr-number>)`. |
19 | 39 | # With a single commit nothing is checked: the ID is added when the pull request is merged. |
| 40 | +# ============================================================================== |
20 | 41 |
|
21 | 42 | set -euo pipefail |
22 | 43 |
|
23 | | -readonly SCOPES_FILE_PATH="$(dirname "$0")/../commit-scopes.txt" |
| 44 | +SCRIPT_DIR=$(dirname "$(readlink -f "$0")") |
| 45 | +readonly SCRIPT_DIR |
| 46 | + |
| 47 | +readonly SCOPES_FILE_PATH="${SCRIPT_DIR}/../commit-scopes.txt" |
24 | 48 |
|
25 | 49 | readonly MAX_SUBJECT_LENGTH=70 |
26 | 50 |
|
| 51 | +source "${SCRIPT_DIR}/lib/git.sh" |
| 52 | +source "${SCRIPT_DIR}/lib/pr-id.sh" |
| 53 | + |
27 | 54 | # ============================================================================== |
28 | 55 |
|
29 | 56 | if [ "$#" -ne 3 ]; then |
30 | 57 | echo "Usage: $0 <base-sha> <head-sha> <pr-number>" >&2 |
31 | 58 | exit 2 |
32 | 59 | fi |
33 | 60 |
|
34 | | -readonly BASE_SHA=$1 |
35 | | -readonly HEAD_SHA=$2 |
| 61 | +BASE_SHA=$(resolve_commit "$1") |
| 62 | +readonly BASE_SHA |
| 63 | + |
| 64 | +HEAD_SHA=$(resolve_commit "$2") |
| 65 | +readonly HEAD_SHA |
| 66 | + |
36 | 67 | readonly PR_NUMBER=$3 |
37 | 68 |
|
| 69 | +if ! is_pr_number "$PR_NUMBER"; then |
| 70 | + echo "Invalid pull request number: \`${PR_NUMBER}\`." >&2 |
| 71 | + exit 2 |
| 72 | +fi |
| 73 | + |
| 74 | +# Read the allowed scopes, one per line, ignoring comments and blank lines. |
| 75 | +mapfile -t scopes < <(grep -vE '^[[:space:]]*(#|$)' "$SCOPES_FILE_PATH" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g') |
| 76 | +if [ "${#scopes[@]}" -eq 0 ]; then |
| 77 | + report_error "No allowed scope found in \`${SCOPES_FILE_PATH}\`." |
| 78 | + exit 2 |
| 79 | +fi |
| 80 | + |
38 | 81 | # ------------------------------------------------------------------------------ |
39 | 82 |
|
40 | 83 | report_error() { |
@@ -64,22 +107,13 @@ get_format_error() { |
64 | 107 |
|
65 | 108 | # ------------------------------------------------------------------------------ |
66 | 109 |
|
67 | | -# Read the allowed scopes, one per line, ignoring comments and blank lines. |
68 | | -mapfile -t scopes < <(grep -vE '^[[:space:]]*(#|$)' "$SCOPES_FILE_PATH" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g') |
69 | | -if [ "${#scopes[@]}" -eq 0 ]; then |
70 | | - report_error "No allowed scope found in \`${SCOPES_FILE_PATH}\`." |
71 | | - exit 2 |
72 | | -fi |
73 | | - |
74 | 110 | # Escape the regex special characters of each scope, then join them with `|`. |
75 | 111 | ALLOWED_SCOPES=$(printf '%s\n' "${scopes[@]}" | sed -E 's/[][\\.^$*+?(){}|]/\\&/g' | paste -sd'|') |
76 | 112 | readonly ALLOWED_SCOPES |
77 | 113 |
|
78 | | -DISPLAYED_SCOPES=$(printf '`%s`, ' "${scopes[@]}") |
| 114 | +DISPLAYED_SCOPES=$(printf "\`%s\`, " "${scopes[@]}") |
79 | 115 | readonly DISPLAYED_SCOPES |
80 | 116 |
|
81 | | -readonly PR_ID_SUFFIX=" (#${PR_NUMBER})" |
82 | | - |
83 | 117 | readonly SUBJECT_REGEX='.*[^ ]' |
84 | 118 | readonly SCOPE_REGEX="\\((${ALLOWED_SCOPES})\\)" |
85 | 119 | readonly COMMIT_REGEX="^((feat|fix|docs)${SCOPE_REGEX}|chore(${SCOPE_REGEX})?): ${SUBJECT_REGEX}$" |
@@ -108,11 +142,19 @@ is_touching_src_or_tests() { |
108 | 142 |
|
109 | 143 | # ------------------------------------------------------------------------------ |
110 | 144 |
|
| 145 | +PR_ID_SUFFIX=$(get_pr_id_suffix "$PR_NUMBER") |
| 146 | +readonly PR_ID_SUFFIX |
| 147 | + |
111 | 148 | errors=0 |
112 | 149 | for hash in "${hashes[@]}"; do |
113 | 150 | subject=$(git log -1 --format=%s "$hash") |
114 | 151 | # The ID is optional here: the format is checked without it. |
115 | | - message=${subject% (#[0-9]*)} |
| 152 | + message=$(strip_pr_id "$subject") |
| 153 | + |
| 154 | + if has_pr_id "$message"; then |
| 155 | + report_error "Commit subject must not end with several pull request IDs: \"${subject}\"." |
| 156 | + continue |
| 157 | + fi |
116 | 158 |
|
117 | 159 | if ! [[ $message =~ $COMMIT_REGEX ]]; then |
118 | 160 | reason=$(get_format_error "$message") |
|
0 commit comments