Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/commit-scopes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Allowed commit scopes, one per line (blank lines and lines starting with `#` are ignored).
# Used by `.github/scripts/check-commit-messages`.
core
tests/core
159 changes: 159 additions & 0 deletions .github/scripts/add-commit-pr-id
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#!/usr/bin/env bash
#
# Copyright (C) 2026 Vates SAS
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

# ==============================================================================
# Make the title of every commit of the current branch end with ` (#<pr-number>)`.
# Commits whose title already ends with this ID only are left untouched, so the script can be run
# several times.
#
# Usage: add-commit-pr-id [--force] <base-sha> <head-sha> <pr-number>
#
# - `<base-sha>`: base of the pull request to check and modify.
# - `<head-sha>`: head of the pull request to check and modify.
# - `<pr-number>`: number of the pull request.
# - `--force`: replace the existing pull request IDs of the titles.
#
# Every trailing ` (#<digits>)` of a title is considered to be a pull request ID. Without `--force`,
# the commits that already end with another ID are ignored (and listed), the others are rewritten:
# an ignored commit is probably in the range because the base is wrong, or the range contains
# commits of other pull requests.
# ==============================================================================

set -euo pipefail

SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
readonly SCRIPT_DIR

readonly AMEND_SCRIPT_PATH="${SCRIPT_DIR}/amend-commit-pr-id"

source "${SCRIPT_DIR}/lib/git.sh"
source "${SCRIPT_DIR}/lib/pr-id.sh"

# ==============================================================================

readonly FORCE_OPTION="--force"

FORCE=false
ARGS=()
for arg in "$@"; do
if [ "$arg" = "$FORCE_OPTION" ]; then
FORCE=true
else
ARGS+=("$arg")
fi
done
readonly FORCE
readonly ARGS

if [ "${#ARGS[@]}" -ne 3 ]; then
echo "Usage: $0 [${FORCE_OPTION}] <base-sha> <head-sha> <pr-number>" >&2
exit 2
fi

BASE_SHA=$(resolve_commit "${ARGS[0]}")
readonly BASE_SHA

HEAD_SHA=$(resolve_commit "${ARGS[1]}")
readonly HEAD_SHA

readonly PR_NUMBER=${ARGS[2]}
if ! is_pr_number "$PR_NUMBER"; then
echo "Invalid pull request number: \`${PR_NUMBER}\`." >&2
exit 2
fi

# ------------------------------------------------------------------------------

if ! git diff --quiet || ! git diff --cached --quiet; then
echo "The working tree has uncommitted changes: commit or stash them first." >&2
exit 1
fi

if [ "$(git rev-parse HEAD)" != "$HEAD_SHA" ]; then
echo "\`HEAD\` is not \`${HEAD_SHA}\`: checkout the head of the pull request first." >&2
exit 1
fi

# ------------------------------------------------------------------------------

if ! MERGE_BASE_SHA=$(git merge-base "$BASE_SHA" "$HEAD_SHA"); then
echo "No common ancestor between \`${BASE_SHA}\` and \`${HEAD_SHA}\`." >&2
exit 2
fi
readonly MERGE_BASE_SHA

readonly COMMIT_RANGE="${MERGE_BASE_SHA}..${HEAD_SHA}"
if [ -n "$(git rev-list --merges "${MERGE_BASE_SHA}..${HEAD_SHA}")" ]; then
echo "Merge commits found in \`${COMMIT_RANGE}\`: this script only supports linear history." >&2
exit 1
fi

mapfile -t hashes < <(git log --format=%H "${MERGE_BASE_SHA}..${HEAD_SHA}")

# ------------------------------------------------------------------------------

PR_ID_SUFFIX=$(get_pr_id_suffix "$PR_NUMBER")
readonly PR_ID_SUFFIX

to_rewrite=0
to_ignore=0
for hash in "${hashes[@]}"; do
subject=$(git log -1 --format=%s "$hash")
if [ "$(with_pr_id "$subject" "$PR_ID_SUFFIX")" = "$subject" ]; then
continue
fi

# The title already ends with a pull request ID other than the requested one.
if has_pr_id "$subject" && ! $FORCE; then
to_ignore=$((to_ignore + 1))
echo "Commit ${hash} ignored, it already has another pull request ID: \"${subject}\"." >&2
continue
fi

to_rewrite=$((to_rewrite + 1))
done

if [ "$to_rewrite" -eq 0 ]; then
if [ "$to_ignore" -eq 0 ]; then
echo "Nothing to do: every commit of \`${COMMIT_RANGE}\` already ends with \"${PR_ID_SUFFIX}\"."
else
echo "Nothing to do: ${to_ignore} commit(s) of \`${COMMIT_RANGE}\` ignored with another pull request ID, the others already end with \"${PR_ID_SUFFIX}\"."
fi
exit 0
fi

# ------------------------------------------------------------------------------

exec_options=()
if $FORCE; then
exec_options+=("$FORCE_OPTION")
fi

readonly PREVIOUS_HEAD=$HEAD_SHA
printf -v EXEC_COMMAND '%q ' "$AMEND_SCRIPT_PATH" "${exec_options[@]}" "$PR_ID_SUFFIX"
readonly EXEC_COMMAND

if ! rebase_output=$(GIT_SEQUENCE_EDITOR=: git rebase --interactive --no-autosquash --exec "$EXEC_COMMAND" "$MERGE_BASE_SHA" 2>&1); then
echo "$rebase_output" >&2
git rebase --abort || true
echo "Rewrite failed, branch restored (previous HEAD: ${PREVIOUS_HEAD})." >&2
exit 1
fi

echo "${to_rewrite} commit title(s) updated with \"${PR_ID_SUFFIX}\" (previous HEAD: ${PREVIOUS_HEAD})."
if [ "$to_ignore" -ne 0 ]; then
echo "${to_ignore} commit(s) ignored because they already have another pull request ID."
fi
63 changes: 63 additions & 0 deletions .github/scripts/amend-commit-pr-id
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
#
# Copyright (C) 2026 Vates SAS
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

# ==============================================================================
# Amend the title of `HEAD` so that it ends with the given pull request ID suffix, and only with it.
# The title is left untouched if it already ends like that.
#
# Usage: amend-commit-pr-id [--force] <pr-id-suffix>
#
# - `--force`: replace the pull request IDs already at the end of the title. Without it, a title
# that already ends with a pull request ID is left untouched.
#
# Run by `git rebase --exec` from `add-commit-pr-id`, once for each commit it replays. It does
# none of the checks of `add-commit-pr-id`: do not use it directly.
# ==============================================================================

set -euo pipefail

source "$(dirname "$0")/lib/pr-id.sh"

# ==============================================================================

readonly FORCE_OPTION="--force"

FORCE=false
if [ "${1:-}" = "$FORCE_OPTION" ]; then
FORCE=true
shift
fi
readonly FORCE

if [ "$#" -ne 1 ]; then
echo "Usage: $0 [${FORCE_OPTION}] <pr-id-suffix>" >&2
exit 2
fi

readonly PR_ID_SUFFIX=$1

# ------------------------------------------------------------------------------

subject=$(git log -1 --format=%s)
if has_pr_id "$subject" && ! $FORCE; then
exit 0
fi

new_subject=$(with_pr_id "$subject" "$PR_ID_SUFFIX")
if [ "$new_subject" != "$subject" ]; then
# Only the first line of the message (the title) changes, the body is kept as is.
{ echo "$new_subject"; git log -1 --format=%B | tail -n +2; } | git commit --amend --allow-empty --no-verify -q -F -
fi
Loading
Loading