jenkins-bot has submitted this change and it was merged.

Change subject: Add commit-message-validator.py tool
......................................................................


Add commit-message-validator.py tool

Add a python script that can be used to validate the format of a commit
message. When executed, the script will get the commit message of the
HEAD commit in the current working directory and validate it. Script
will exit with a non-zero status if validation fails. Validation errors
are written to stdout.

Current validation rules:
* First line <=80 characters
* Second line blank
* No line >100 characters
* "Bug:" is capitalized
* "Bug:" is followed by a space
* Exactly one task id on each Bug: line
* No "Task:", "Fixes:", "Closes:" lines
* For any "Bug: " line, next line is not blank
* For any "Bug: " line, prior line is another Bug: line or empty
* Exactly one "Change-Id: " line per commit
* For "Change-Id: " line, prior line is empty or "Bug: "
* No blank lines between any "Bug: " lines and "Change-Id: "
* Only "(cherry picked from commit" can follow "Change-Id: "
* Message has at least 3 lines (subject, blank, Change-Id)

Bug: T109119
Change-Id: Ib07957562a0f54abc669f91db9b8bddb29d369c2
---
A tests/test_commit-message-validator.py
A tests/test_commit-message-validator/check_message_errors.msg
A tests/test_commit-message-validator/check_message_errors.out
A tests/test_commit-message-validator/check_message_ok.msg
A tests/test_commit-message-validator/check_message_ok.out
A tools/commit-message-validator.py
6 files changed, 237 insertions(+), 0 deletions(-)

Approvals:
  20after4: Looks good to me, but someone else must approve
  JanZerebecki: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/tests/test_commit-message-validator.py 
b/tests/test_commit-message-validator.py
new file mode 100644
index 0000000..c45935c
--- /dev/null
+++ b/tests/test_commit-message-validator.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+
+import imp
+import os
+import unittest
+
+cmv = imp.load_source(
+    'cmv',
+    os.path.dirname(os.path.dirname(__file__)) +
+    '/tools/commit-message-validator.py')
+
+
+class TestCommitMessageValidator(unittest.TestCase):
+    def test_check_message_errors(self):
+        path = os.path.dirname(__file__) + '/test_commit-message-validator/'
+        with open(path + 'check_message_errors.msg') as msg:
+            with open(path + 'check_message_errors.out') as out:
+                self._test_check_message(msg.read(), 1, out.read())
+
+    def test_check_message_ok(self):
+        path = os.path.dirname(__file__) + '/test_commit-message-validator/'
+        with open(path + 'check_message_ok.msg') as msg:
+            with open(path + 'check_message_ok.out') as out:
+                self._test_check_message(msg.read(), 0, out.read())
+
+    def _test_check_message(self, msg, expect_status, expect_out):
+        import sys
+        import StringIO
+        saved_stdout = sys.stdout
+        try:
+            out = StringIO.StringIO()
+            sys.stdout = out
+            self.assertEqual(
+                expect_status,
+                cmv.check_message(msg.splitlines()))
+            self.assertEqual(expect_out, out.getvalue())
+        finally:
+            sys.stdout = saved_stdout
diff --git a/tests/test_commit-message-validator/check_message_errors.msg 
b/tests/test_commit-message-validator/check_message_errors.msg
new file mode 100644
index 0000000..ac0fc3c
--- /dev/null
+++ b/tests/test_commit-message-validator/check_message_errors.msg
@@ -0,0 +1,15 @@
+XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+YYYYYYYYYYY
+YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
+Fixes:foo
+Closes:bar
+Task: T11
+BUG:13
+Bug: T11 T12
+
+Bug: T11
+Bug: T12
+
+Change-Id: I1234567890abcdef123456
+Foo bar
+(cherry picked from commit 7ad8cc24d0672111cff5beddcfc9d778a1a3478e)
diff --git a/tests/test_commit-message-validator/check_message_errors.out 
b/tests/test_commit-message-validator/check_message_errors.out
new file mode 100644
index 0000000..d2611ec
--- /dev/null
+++ b/tests/test_commit-message-validator/check_message_errors.out
@@ -0,0 +1,13 @@
+Line 1: First line should be <=80 characters
+Line 2: Second line should be empty
+Line 3: Line should be <=100 characters
+Line 4: Use 'Bug: ' not 'Fixes:'
+Line 5: Use 'Bug: ' not 'Closes:'
+Line 6: Use 'Bug: ' not 'Task:'
+Line 7: Expected 'Bug:' not 'BUG:'
+Line 8: Each Bug: should list exactly one task
+Line 8: Expected blank line before Bug:
+Line 9: Unexpected blank line after Bug:
+Line 12: Unexpected blank line after Bug:
+Line 12: Unexpected line between Bug: and Change-Id:
+Line 14: Unexpected line after Change-Id
diff --git a/tests/test_commit-message-validator/check_message_ok.msg 
b/tests/test_commit-message-validator/check_message_ok.msg
new file mode 100644
index 0000000..018adc6
--- /dev/null
+++ b/tests/test_commit-message-validator/check_message_ok.msg
@@ -0,0 +1,8 @@
+Add mediawiki::iwlink define and use it with role::mobilefrontend
+
+Add a new Puppet define that can be used to easily add rows to the
+interwiki links table. The define is used to add a few interwiki
+language links to mobilewiki provisioned by role:mobilefrontend.
+
+Bug: T106633
+Change-Id: Ifcd397165df1cbf9fa04f2044e1bb33ad7414d8d
diff --git a/tests/test_commit-message-validator/check_message_ok.out 
b/tests/test_commit-message-validator/check_message_ok.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_commit-message-validator/check_message_ok.out
diff --git a/tools/commit-message-validator.py 
b/tools/commit-message-validator.py
new file mode 100755
index 0000000..4852c8e
--- /dev/null
+++ b/tools/commit-message-validator.py
@@ -0,0 +1,163 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# License: GPLv2+
+# Copyright (c) 2015 Bryan Davis and Wikimedia Foundation.
+"""
+Validate the format of a commit message to WMF Gerrit standards.
+
+https://www.mediawiki.org/wiki/Gerrit/Commit_message_guidelines
+"""
+
+import re
+import subprocess
+
+
+def line_has_errors(lineno, line):
+    """Check a commit message line to see if it has errors.
+
+    Checks:
+    - First line <=80 characters
+    - Second line blank
+    - No line >100 characters
+    - "Bug:" is capitalized
+    - "Bug:" is followed by a space
+    - Exactly one task id on each Bug: line
+    - No "Task: ", "Fixes: ", "Closes: " lines
+    """
+    rline = lineno + 1
+
+    # First line <=80
+    if lineno == 0 and len(line) > 80:
+        return "Line %d: First line should be <=80 characters" % rline
+
+    # Second line blank
+    if lineno == 1 and line:
+        return "Line %d: Second line should be empty" % rline
+
+    # No line >100
+    if len(line) > 100:
+        return "Line %d: Line should be <=100 characters" % rline
+
+    m = re.match(r'^(bug:)(.)', line, re.IGNORECASE)
+    if m:
+        if m.group(1) != 'Bug:':
+            return "Line %d: Expected 'Bug:' not '%s'" % (rline, m.group(1))
+
+        if m.group(2) != ' ':
+            return "Line %d: Expected space after 'Bug:'" % rline
+
+        # Exactly one task id on each Bug: line
+        m = re.match(r'^Bug: \w+$', line)
+        if not m:
+            return "Line %d: Each Bug: should list exactly one task" % rline
+
+    # No "Task: ", "Fixes: ", "Closes: " lines
+    m = re.match(r'^(closes|fixes|task):', line, re.IGNORECASE)
+    if m:
+        return "Line %d: Use 'Bug: ' not '%s'" % (rline, m.group(0))
+
+    return False
+
+
+def check_message(lines):
+    """Check a commit message to see if it has errors.
+
+    Checks:
+    - All lines ok as checked by line_has_errors()
+    - For any "^Bug: " line, next line is not blank
+    - For any "^Bug: " line, prior line is another Bug: line or empty
+    - Exactly one "Change-Id: " line per commit
+    - For "Change-Id: " line, prior line is empty or "^Bug: "
+    - No blank lines between any "Bug: " lines and "Change-Id: "
+    - Only "(cherry picked from commit" can follow "Change-Id: "
+    - Message has at least 3 lines (subject, blank, Change-Id)
+    """
+    errors = []
+    last_lineno = 0
+    last_line = ''
+    changeid_line = False
+    last_bug = False
+    for lineno, line in enumerate(lines):
+        # Strip leading spaces to remove `git log` indenting
+        stripped = line.lstrip()
+        rline = lineno + 1
+        e = line_has_errors(lineno, stripped)
+        if e:
+            errors.append(e)
+
+        # For any "Bug: " line, next line is not blank
+        if last_bug == last_lineno:
+            if not line:
+                errors.append(
+                    "Line %d: Unexpected blank line after Bug:" % rline)
+
+        # For any "Bug: " line, prior line is another Bug: line or empty
+        if line.startswith('Bug: '):
+            last_bug = rline
+            if last_line and not last_line.startswith('Bug: '):
+                errors.append(
+                    "Line %d: Expected blank line before Bug:" % rline)
+
+        if line.startswith('Change-Id: I'):
+            # Only expect one "Change-Id: " line
+            if changeid_line is not False:
+                errors.append(
+                    "Line %d: Extra Change-Id found, next at %d" %
+                    (changeid_line, rline))
+
+            # For "Change-Id: " line, prior line is empty or Bug:
+            elif last_line and not last_line.startswith('Bug: '):
+                errors.append(
+                    "Line %d: Expected blank line or Bug: before Change-Id:" %
+                    rline)
+
+            # If we have Bug: lines, Change-Id follows immediately
+            elif last_bug and last_bug != rline - 1:
+                for lno in range(last_bug + 1, rline):
+                    errors.append(
+                        "Line %d: Unexpected line between Bug: and Change-Id:"
+                        % lno)
+
+            changeid_line = rline
+
+        last_lineno = rline
+        last_line = stripped
+
+    if last_lineno < 2:
+        errors.append("Line %d: Expected at least 3 lines" % last_lineno)
+
+    if changeid_line is False:
+        errors.append("Line %d: Expected Change-Id" % last_lineno)
+
+    elif changeid_line != last_lineno:
+        if last_lineno != changeid_line + 1:
+            for lno in range(changeid_line + 1, last_lineno):
+                errors.append(
+                    "Line %d: Unexpected line after Change-Id" % lno)
+        if not last_line.startswith("(cherry picked from commit"):
+            errors.append(
+                "Line %d: Unexpected line after Change-Id" % last_lineno)
+
+    if errors:
+        for e in errors:
+            print e
+        return 1
+    return 0
+
+
+def main():
+    """Validate the current HEAD commit message."""
+    commit = subprocess.check_output('git log --pretty=raw -1', shell=True)
+    lines = commit.splitlines()
+
+    # Discard until the first blank line
+    line = lines.pop(0)
+    while line:
+        line = lines.pop(0)
+
+    return check_message(lines)
+
+if __name__ == '__main__':
+    import sys
+    sys.exit(main())

-- 
To view, visit https://gerrit.wikimedia.org/r/237719
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ib07957562a0f54abc669f91db9b8bddb29d369c2
Gerrit-PatchSet: 3
Gerrit-Project: integration/jenkins
Gerrit-Branch: master
Gerrit-Owner: BryanDavis <[email protected]>
Gerrit-Reviewer: 20after4 <[email protected]>
Gerrit-Reviewer: Alex Monk <[email protected]>
Gerrit-Reviewer: BryanDavis <[email protected]>
Gerrit-Reviewer: Dduvall <[email protected]>
Gerrit-Reviewer: Hashar <[email protected]>
Gerrit-Reviewer: JanZerebecki <[email protected]>
Gerrit-Reviewer: Jforrester <[email protected]>
Gerrit-Reviewer: Krinkle <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: Polybuildr <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to