summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorNatalia <124304+nessita@users.noreply.github.com>2026-03-06 15:30:36 -0300
committernessita <124304+nessita@users.noreply.github.com>2026-03-06 18:25:45 -0300
commitd469883546e954c8b51889d8ffc21f0ff9d71d07 (patch)
treee80f24fbbf0c8e07eb1b965aaf7e3e0822507db0 /scripts
parent07c38764db1ba6a39ff0b3b224288e13c304f1c9 (diff)
Added python script suitable for using as prepare-commit-msg git hook.
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/prepare_commit_msg.py115
-rw-r--r--scripts/tests.py130
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")