diff options
| author | Natalia <124304+nessita@users.noreply.github.com> | 2026-03-06 15:30:36 -0300 |
|---|---|---|
| committer | nessita <124304+nessita@users.noreply.github.com> | 2026-03-06 18:25:45 -0300 |
| commit | d469883546e954c8b51889d8ffc21f0ff9d71d07 (patch) | |
| tree | e80f24fbbf0c8e07eb1b965aaf7e3e0822507db0 /scripts | |
| parent | 07c38764db1ba6a39ff0b3b224288e13c304f1c9 (diff) | |
Added python script suitable for using as prepare-commit-msg git hook.
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/prepare_commit_msg.py | 115 | ||||
| -rw-r--r-- | scripts/tests.py | 130 |
2 files changed, 245 insertions, 0 deletions
diff --git a/scripts/prepare_commit_msg.py b/scripts/prepare_commit_msg.py new file mode 100755 index 0000000000..bcb5b5faf7 --- /dev/null +++ b/scripts/prepare_commit_msg.py @@ -0,0 +1,115 @@ +#! /usr/bin/env python + +""" +prepare-commit-msg hook for Django's repository. + +Adjusts commit messages on any branch: +- Ensures the summary line ends with a period. + +Additionally, on stable branches: +- Adds the [A.B.x] branch prefix if missing. +- Adds "Backport of <sha> from main." when cherry-picking. + +To install: + 1. Ensure the folder `.git/hooks` exists. + 2. Create an executable file `.git/hooks/prepare-commit-msg` with content: + +#!/bin/sh +exec python scripts/prepare_commit_msg.py "$@" + +""" + +import os +import subprocess +import sys + + +def run(cmd): + return subprocess.run(cmd, capture_output=True, text=True).stdout.strip() + + +def process_commit_message(lines, branch, cherry_sha=None): + """Adjust commit message lines for a potential backport. + + - Separates body lines from trailing git comment lines. + - Ensure all lines ends with a period. + - On stable branches, adds the [A.B.x] prefix to the first line if missing. + - If cherry_sha is provided, appends "Backport of <sha> from main." to + the body if not already present. + + Returns the modified lines (body + comments). + + """ + # Separate body lines from trailing git comment lines. + comment_start = len(lines) + for i in range(len(lines) - 1, -1, -1): + if lines[i].startswith("#"): + comment_start = i + elif lines[i].strip(): + break + + body_lines = lines[:comment_start] + comment_lines = lines[comment_start:] + + # Strip leading and trailing blank lines from the body. + while body_lines and not body_lines[0].strip(): + body_lines.pop(0) + while body_lines and not body_lines[-1].strip(): + body_lines.pop() + + # Nothing to do if the body is empty. + if not body_lines: + return lines + + summary = body_lines[0].strip() + + # Ensure summary ends with a period. + if not summary.endswith("."): + summary += "." + + # On stable branches, add the [A.B.x] prefix if missing. + prefix = None + if branch.startswith("stable/"): + version = branch[len("stable/") :] + prefix = f"[{version}] " + if not summary.startswith(prefix): + summary = prefix + summary + + # Capitalize the first character of the summary text (after any prefix). + offset = len(prefix) if prefix else 0 + summary = summary[:offset] + summary[offset].upper() + summary[offset + 1 :] + + body_lines[0] = summary + "\n" + + # Add "Backport of <sha> from main." if cherry-picking and not present. + if cherry_sha: + backport_note = f"Backport of {cherry_sha} from main." + if backport_note not in "".join(body_lines): + # Strip trailing blank lines, then append note with separator. + while body_lines and not body_lines[-1].strip(): + body_lines.pop() + body_lines.append("\n") + body_lines.append(backport_note + "\n") + + return body_lines + comment_lines + + +if __name__ == "__main__": + msg_path = sys.argv[1] + + with open(msg_path, encoding="utf-8") as f: + lines = f.readlines() + + branch = run(["git", "branch", "--show-current"]) + + cherry_sha = None + git_dir = run(["git", "rev-parse", "--git-dir"]) + cherry_pick_head_path = os.path.join(git_dir, "CHERRY_PICK_HEAD") + if os.path.exists(cherry_pick_head_path): + with open(cherry_pick_head_path, encoding="utf-8") as f: + cherry_sha = f.read().strip() + + result = process_commit_message(lines, branch, cherry_sha) + + with open(msg_path, "w", encoding="utf-8") as f: + f.writelines(result) diff --git a/scripts/tests.py b/scripts/tests.py new file mode 100644 index 0000000000..b36c293703 --- /dev/null +++ b/scripts/tests.py @@ -0,0 +1,130 @@ +#! /usr/bin/env python + +""" +Tests for scripts utilities. + +Run from within the scripts/ folder with: + +$ python -m unittest tests.py + +Or from the repo root with: + +$ PYTHONPATH=scripts/ python -m unittest scripts/tests.py +""" + +import unittest + +from prepare_commit_msg import process_commit_message + + +class ProcessCommitMessageTests(unittest.TestCase): + def test_non_stable_branch_no_prefix_added(self): + lines = ["Fixed #123 -- Added a feature.\n"] + result = process_commit_message(lines, "main") + self.assertNotIn("[", result[0].split("--")[0]) + + def test_non_stable_branch_period_added(self): + lines = ["Fixed #123 -- Added a feature\n"] + result = process_commit_message(lines, "main") + self.assertIs(result[0].rstrip("\n").endswith("."), True) + + def test_non_stable_branch_with_period_unchanged(self): + lines = ["Fixed #123 -- Added a feature.\n"] + result = process_commit_message(lines, "main") + self.assertEqual(result, lines) + + def test_empty_body_unchanged(self): + lines = ["# This is a comment.\n"] + result = process_commit_message(lines, "stable/5.2.x") + self.assertEqual(result, lines) + + def test_only_blank_lines_unchanged(self): + lines = ["\n", "\n"] + result = process_commit_message(lines, "stable/5.2.x") + self.assertEqual(result, lines) + + def test_adds_stable_prefix(self): + lines = ["Fixed #123 -- Added a feature.\n"] + result = process_commit_message(lines, "stable/5.2.x") + self.assertEqual(result[0], "[5.2.x] Fixed #123 -- Added a feature.\n") + + def test_does_not_double_add_prefix(self): + lines = ["[5.2.x] Fixed #123 -- Added a feature.\n"] + result = process_commit_message(lines, "stable/5.2.x") + self.assertEqual(result[0], "[5.2.x] Fixed #123 -- Added a feature.\n") + + def test_summary_leading_whitespace_no_double_space_before_prefix(self): + lines = [" fixed #123 -- Added a feature.\n"] + result = process_commit_message(lines, "stable/5.2.x") + self.assertEqual(result[0], "[5.2.x] Fixed #123 -- Added a feature.\n") + + def test_capitalizes_first_letter(self): + lines = ["fixed #123 -- Added a feature.\n"] + result = process_commit_message(lines, "main") + self.assertEqual(result[0], "Fixed #123 -- Added a feature.\n") + + def test_capitalizes_first_letter_after_existing_prefix(self): + lines = ["[5.2.x] fixed #123 -- Added a feature.\n"] + result = process_commit_message(lines, "stable/5.2.x") + self.assertTrue(result[0].startswith("[5.2.x] Fixed")) + + def test_adds_trailing_period(self): + lines = ["Fixed #123 -- Added a feature\n"] + result = process_commit_message(lines, "stable/5.2.x") + self.assertIs(result[0].rstrip("\n").endswith("."), True) + + def test_does_not_double_add_trailing_period(self): + lines = ["Fixed #123 -- Added a feature.\n"] + result = process_commit_message(lines, "stable/5.2.x") + self.assertIs(result[0].rstrip("\n").endswith(".."), False) + + def test_adds_backport_note(self): + sha = "abc123def456" + lines = ["Fixed #123 -- Added a feature.\n"] + result = process_commit_message(lines, "stable/5.2.x", cherry_sha=sha) + self.assertIn(f"Backport of {sha} from main.\n", result) + + def test_does_not_double_add_backport_note(self): + sha = "abc123def456" + lines = [ + "Fixed #123 -- Added a feature.\n", + "\n", + f"Backport of {sha} from main.\n", + ] + result = process_commit_message(lines, "stable/5.2.x", cherry_sha=sha) + self.assertEqual(len([line for line in result if "Backport" in line]), 1) + + def test_backport_note_separated_by_blank_line(self): + sha = "abc123def456" + lines = ["Fixed #123 -- Added a feature.\n"] + result = process_commit_message(lines, "stable/5.2.x", cherry_sha=sha) + note_idx = next(i for i, l in enumerate(result) if f"Backport of {sha}" in l) + self.assertEqual(result[note_idx - 1], "\n") + + def test_git_comments_preserved_at_end(self): + sha = "abc123def456" + lines = [ + "Fixed #123 -- Added a feature.\n", + "# Please enter the commit message.\n", + "# Changes to be committed:\n", + ] + result = process_commit_message(lines, "stable/5.2.x", cherry_sha=sha) + self.assertEqual(result[-2], "# Please enter the commit message.\n") + self.assertEqual(result[-1], "# Changes to be committed:\n") + + def test_prefix_and_period_and_backport_combined(self): + sha = "abc123def456" + lines = ["Fixed #123 -- Added a feature\n"] + result = process_commit_message(lines, "stable/5.2.x", cherry_sha=sha) + self.assertEqual(result[0], "[5.2.x] Fixed #123 -- Added a feature.\n") + self.assertIn(f"Backport of {sha} from main.\n", result) + + def test_no_cherry_sha_no_backport_note(self): + lines = ["Fixed #123 -- Added a feature.\n"] + result = process_commit_message(lines, "stable/5.2.x", cherry_sha=None) + self.assertNotIn("Backport of", "".join(result)) + + def test_leading_blank_lines_stripped(self): + lines = ["\n", "Fixed #123 -- Added a feature.\n"] + result = process_commit_message(lines, "stable/5.2.x") + self.assertEqual(result[0], "[5.2.x] Fixed #123 -- Added a feature.\n") |
