Rafidaslam has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/404155 )

Change subject: Make MessageValidator an abstract class
......................................................................

Make MessageValidator an abstract class

Make MessageValidator an abstract class so we can create
another MessageValidator for other remote origin outside
gerrit.

Change-Id: I862bf42561eb436258951947c8c4a1233a3416c5
---
M commit_message_validator/__init__.py
A commit_message_validator/message_validator.py
A commit_message_validator/tests/__init__.py
A commit_message_validator/tests/data/bug_in_header.msg
A commit_message_validator/tests/data/bug_in_header.out
A commit_message_validator/tests/data/cherry_pick_not_on_the_last_line.msg
A commit_message_validator/tests/data/cherry_pick_not_on_the_last_line.out
A commit_message_validator/tests/data/unexpected_line_in_footers.msg
A commit_message_validator/tests/data/unexpected_line_in_footers.out
A commit_message_validator/tests/test_GerritMessageValidator.py
A commit_message_validator/tests/test_message_validator.py
A commit_message_validator/validators/GerritMessageValidator.py
A commit_message_validator/validators/GlobalMessageValidator.py
A commit_message_validator/validators/__init__.py
14 files changed, 496 insertions(+), 189 deletions(-)


  git pull 
ssh://gerrit.wikimedia.org:29418/integration/commit-message-validator 
refs/changes/55/404155/1

diff --git a/commit_message_validator/__init__.py 
b/commit_message_validator/__init__.py
index cf15256..56d12bf 100644
--- a/commit_message_validator/__init__.py
+++ b/commit_message_validator/__init__.py
@@ -24,198 +24,12 @@
 from __future__ import print_function
 
 import os
-import re
 import subprocess
 import sys
 
+from .validators.GerritMessageValidator import GerritMessageValidator
+
 __version__ = '0.5.2'
-
-RE_BUGID = re.compile('^T[0-9]+$')
-RE_CHANGEID = re.compile('^I[a-f0-9]{40}$')
-RE_SUBJECT_BUG_OR_TASK = re.compile(r'^(bug|T?\d+)', re.IGNORECASE)
-RE_URL = re.compile(r'^<?https?://\S+>?$', re.IGNORECASE)
-RE_FOOTER = re.compile(
-    r'^(?P<name>[a-z]\S+):(?P<ws>\s*)(?P<value>.*)$', re.IGNORECASE)
-RE_CHERRYPICK = re.compile(r'^\(cherry picked from commit [0-9a-fA-F]{40}\)$')
-
-# Header-like lines that we are interested in validating
-CORRECT_FOOTERS = [
-    'Acked-by',
-    'Bug',
-    'Cc',
-    'Change-Id',
-    'Co-Authored-by',
-    'Depends-On',
-    'Requested-by',
-    'Reported-by',
-    'Reviewed-by',
-    'Signed-off-by',
-    'Suggested-by',
-    'Tested-by',
-    'Thanks',
-]
-FOOTERS = dict((footer.lower(), footer) for footer in CORRECT_FOOTERS)
-
-BEFORE_CHANGE_ID = [
-    'bug',
-    'closes',
-    'depends-on',
-    'fixes',
-    'task',
-]
-
-# Invalid footer name to expected name mapping
-BAD_FOOTERS = {
-    'closes': 'bug',
-    'fixes': 'bug',
-    'task': 'bug',
-}
-
-
-def is_valid_bug_id(s):
-    return RE_BUGID.match(s)
-
-
-def is_valid_change_id(s):
-    """A Gerrit change id is a 40 character hex string prefixed with 'I'."""
-    return RE_CHANGEID.match(s)
-
-
-class MessageValidator(object):
-
-    """Iterator to check a commit message line for errors.
-
-    Checks:
-    - First line <=80 characters
-    - Second line blank
-    - No line >100 characters (unless it is only a URL)
-    - Footer lines ("Foo: ...") are capitalized and have a space after the ':'
-    - "Bug: " is followed by one task id ("Tnnnn")
-    - "Depends-On:" is followed by one change id ("I...")
-    - "Change-Id:" is followed one change id ("I...")
-    - No "Task: ", "Fixes: ", "Closes: " lines
-    """
-
-    def __init__(self, lines):
-        self._lines = lines
-        self._first_changeid = False
-        self._in_footers = False
-
-        self._generator = self._check_generator()
-
-    def _check_line(self, lineno):
-        line = self._lines[lineno]
-        # First line <=80
-        if lineno == 0:
-            if len(line) > 80:
-                yield "First line should be <=80 characters"
-            m = RE_SUBJECT_BUG_OR_TASK.match(line)
-            if m:
-                yield "Do not define bug in the header"
-
-        # Second line blank
-        elif lineno == 1:
-            if line:
-                yield "Second line should be empty"
-
-        # No line >100 unless it is all a URL
-        elif len(line) > 100 and not RE_URL.match(line):
-            yield "Line should be <=100 characters"
-
-        if not line:
-            if self._in_footers:
-                yield "Unexpected blank line"
-            return
-
-        # Look for and validate footer lines
-        m = RE_FOOTER.match(line)
-        if m:
-            name = m.group('name')
-            normalized_name = name.lower()
-            ws = m.group('ws')
-            value = m.group('value')
-
-            if normalized_name in BAD_FOOTERS:
-                # Treat as the correct name for the rest of the rules
-                normalized_name = BAD_FOOTERS[normalized_name]
-
-            if normalized_name not in FOOTERS:
-                if self._in_footers:
-                    yield "Unexpected line in footers"
-                else:
-                    # Meh. Not a name we care about
-                    return
-            else:
-                if lineno > 0 and not self._lines[lineno - 1]:
-                    self._in_footers = True
-                elif not self._in_footers:
-                    yield "Expected '{0}:' to be in footer".format(name)
-
-            correct_name = FOOTERS[normalized_name]
-            if correct_name != name:
-                yield "Use '{0}:' not '{1}:'".format(correct_name, name)
-
-            if normalized_name == 'bug':
-                if not is_valid_bug_id(value):
-                    yield "Bug: value must be a single phabricator task ID"
-
-            elif normalized_name == 'depends-on':
-                if not is_valid_change_id(value):
-                    yield "Depends-On: value must be a single Gerrit change id"
-
-            elif normalized_name == 'change-id':
-                if not is_valid_change_id(value):
-                    yield "Change-Id: value must be a single Gerrit change id"
-                if self._first_changeid is not False:
-                    yield ("Extra Change-Id found, first at "
-                           "{0}".format(self._first_changeid))
-                else:
-                    self._first_changeid = lineno + 1
-
-            if (normalized_name in BEFORE_CHANGE_ID and
-                    self._first_changeid is not False):
-                yield ("Expected '{0}:' to come before Change-Id on line "
-                       "{1}").format(name, self._first_changeid)
-
-            if ws != ' ':
-                yield "Expected one space after '%s:'" % name
-
-        elif self._in_footers:
-            # if it wasn't a footer (not a match) but it is in the footers
-            cherry_pick = RE_CHERRYPICK.match(line)
-            if cherry_pick:
-                if lineno < len(self._lines) - 1:
-                    yield "Cherry pick line is not the last line"
-            else:
-                yield "Expected footer line to follow format of 'Name: ...'"
-
-    def _check_global(self):
-        """All checks that are done after the line checks."""
-        if len(self._lines) < 3:
-            yield "Expected at least 3 lines"
-
-        if self._first_changeid is False:
-            yield "Expected Change-Id"
-
-    def _check_generator(self):
-        """A generator returning each error and line number."""
-        for lineno in range(len(self._lines)):
-            for e in self._check_line(lineno):
-                yield lineno + 1, e
-
-        for e in self._check_global():
-            yield len(self._lines), e
-
-    def __iter__(self):
-        return self
-
-    def __next__(self):
-        """Return the next error of the generator."""
-        return next(self._generator)
-
-    def next(self):
-        # For Python 2 support
-        return self.__next__()
 
 
 def check_message(lines):
@@ -230,7 +44,7 @@
     - Any "Bug:" and "Depends-On:" lines come before "Change-Id:"
     - "(cherry picked from commit ...)" is last line in footer if present
     """
-    validator = MessageValidator(lines)
+    validator = GerritMessageValidator(lines)
     errors = ["Line {0}: {1}".format(lineno, error)
               for lineno, error in validator]
 
diff --git a/commit_message_validator/message_validator.py 
b/commit_message_validator/message_validator.py
new file mode 100644
index 0000000..f31218d
--- /dev/null
+++ b/commit_message_validator/message_validator.py
@@ -0,0 +1,83 @@
+
+
+class MessageValidator(object):
+    """
+    IterableMessageValidator is supposed to make a MessageValidator that
+    yields a line number and the error message.
+
+    A class that implements this class, should implement:
+    - `check_line()`, that yields the error message of the checked line,
+    and optional (can be implemented or not):
+    - `check_global()` that yields the line number and the error message.
+
+    See `HelperMessageValdator` and `GerritMessageValidator` for the
+    implementation of `check_line()`, and `check_global()`
+    methods.
+
+    Example usage:
+    >>> lines = ['Title', 'This should be empty', 'Body']
+    >>> for lineno, msg in GerritMessageValidator(lines):
+    ...     print('{0} {1}'.format(lineno, msg))
+    ...
+    2 Second line should be empty
+    3 Expected Change-Id
+
+    """
+
+    def __init__(self, lines):
+        """
+        Constructor for MessageValidator.
+
+        :param lines: list of lines from a commit message that will be checked.
+        """
+        self._lines = lines
+        self._generator = self._check_generator()
+
+    def check_line(self, lineno):
+        """
+        A function that will be called to check the commit message.
+        This function should yields a string that contain description
+        of what error that occured on `lineno`.
+
+        :param lineno:       int, line number that's being checked right now.
+        """
+        raise NotImplementedError(
+            '`check_line()` should be implemented in {0}'.format(
+                type(self).__name__))
+
+    def check_global(self):
+        """
+        All checks that are done after the line checks.
+        This function should yields a string that contain description
+        of what error that is occured.
+        """
+        raise NotImplementedError(
+            '`check_global()` isn\'t implemented in {0} but called'.format(
+                type(self).__name__))
+
+    def _check_generator(self):
+        """
+        A generator returning each error and line number
+        """
+        for lineno in range(len(self._lines)):
+            for e in self.check_line(lineno):
+                yield lineno + 1, e
+
+        try:
+            for e in self.check_global():
+                yield len(self._lines), e
+        except NotImplementedError:
+            pass
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        """
+        Return the next error of the generator
+        """
+        return next(self._generator)
+
+    def next(self):
+        # For Python 2 support
+        return self.__next__()
diff --git a/commit_message_validator/tests/__init__.py 
b/commit_message_validator/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/commit_message_validator/tests/__init__.py
diff --git a/commit_message_validator/tests/data/bug_in_header.msg 
b/commit_message_validator/tests/data/bug_in_header.msg
new file mode 100644
index 0000000..c551bda
--- /dev/null
+++ b/commit_message_validator/tests/data/bug_in_header.msg
@@ -0,0 +1,5 @@
+T1234 Fix
+
+Should fail because a bug id is written in the header
+
+Change-Id: I395fd614bfa8bcfc46a35f955fb77ec6f03f7a01
diff --git a/commit_message_validator/tests/data/bug_in_header.out 
b/commit_message_validator/tests/data/bug_in_header.out
new file mode 100644
index 0000000..d0f1d67
--- /dev/null
+++ b/commit_message_validator/tests/data/bug_in_header.out
@@ -0,0 +1,5 @@
+commit-message-validator v%version%
+The following errors were found:
+Line 1: Do not define bug in the header
+Please review <https://www.mediawiki.org/wiki/Gerrit/Commit_message_guidelines>
+and update your commit message accordingly
diff --git 
a/commit_message_validator/tests/data/cherry_pick_not_on_the_last_line.msg 
b/commit_message_validator/tests/data/cherry_pick_not_on_the_last_line.msg
new file mode 100644
index 0000000..4bd1e4a
--- /dev/null
+++ b/commit_message_validator/tests/data/cherry_pick_not_on_the_last_line.msg
@@ -0,0 +1,6 @@
+This is a commit header
+
+This is a commit body
+
+(cherry picked from commit a24ca11e277afd8b5259d44b2d645d4dbb99502f)
+Change-Id: If89d24838e326fe25fe867d02181eebcfbb0e196
diff --git 
a/commit_message_validator/tests/data/cherry_pick_not_on_the_last_line.out 
b/commit_message_validator/tests/data/cherry_pick_not_on_the_last_line.out
new file mode 100644
index 0000000..ecbfa6a
--- /dev/null
+++ b/commit_message_validator/tests/data/cherry_pick_not_on_the_last_line.out
@@ -0,0 +1,5 @@
+commit-message-validator v%version%
+The following errors were found:
+Line 5: Cherry pick line is not the last line
+Please review <https://www.mediawiki.org/wiki/Gerrit/Commit_message_guidelines>
+and update your commit message accordingly
diff --git a/commit_message_validator/tests/data/unexpected_line_in_footers.msg 
b/commit_message_validator/tests/data/unexpected_line_in_footers.msg
new file mode 100644
index 0000000..fd7e694
--- /dev/null
+++ b/commit_message_validator/tests/data/unexpected_line_in_footers.msg
@@ -0,0 +1,7 @@
+This is a commit header
+
+Commit body
+
+Bug: T1234
+Err: Unexpected line
+Change-Id: If89d24838e326fe25fe867d02181eebcfbb0e196
diff --git a/commit_message_validator/tests/data/unexpected_line_in_footers.out 
b/commit_message_validator/tests/data/unexpected_line_in_footers.out
new file mode 100644
index 0000000..60f94c3
--- /dev/null
+++ b/commit_message_validator/tests/data/unexpected_line_in_footers.out
@@ -0,0 +1,5 @@
+commit-message-validator v%version%
+The following errors were found:
+Line 6: Unexpected line in footers
+Please review <https://www.mediawiki.org/wiki/Gerrit/Commit_message_guidelines>
+and update your commit message accordingly
diff --git a/commit_message_validator/tests/test_GerritMessageValidator.py 
b/commit_message_validator/tests/test_GerritMessageValidator.py
new file mode 100644
index 0000000..df376a4
--- /dev/null
+++ b/commit_message_validator/tests/test_GerritMessageValidator.py
@@ -0,0 +1,31 @@
+import unittest
+
+from commit_message_validator.validators.GerritMessageValidator import (
+    GerritMessageValidator, CommitMessageContext
+)
+
+
+class GerritMessageValidatorTest(unittest.TestCase):
+
+    def test_context_handler(self):
+        lines = [
+            'Commit header',
+            '',
+            'Commit body message',
+            '',
+            'Change-Id: I00d0f7c3b294c3ddc656f9a5447df89c63142203'
+        ]
+
+        gerrit_mv = GerritMessageValidator(lines)
+
+        expected_result = [
+            CommitMessageContext.HEADER,
+            CommitMessageContext.BODY,
+            CommitMessageContext.BODY,
+            CommitMessageContext.BODY,
+            CommitMessageContext.FOOTER
+        ]
+
+        result = [gerrit_mv.get_context(lineno)
+                  for lineno in range(len(lines))]
+        self.assertEqual(expected_result, result)
diff --git a/commit_message_validator/tests/test_message_validator.py 
b/commit_message_validator/tests/test_message_validator.py
new file mode 100644
index 0000000..5defaa9
--- /dev/null
+++ b/commit_message_validator/tests/test_message_validator.py
@@ -0,0 +1,98 @@
+import re
+import unittest
+
+
+from commit_message_validator.message_validator import MessageValidator
+
+
+class NoCheckLineMessageValidator(MessageValidator):
+
+    def __init__(self, lines):
+        super(NoCheckLineMessageValidator, self).__init__(lines)
+
+
+class JustTestMessageValidator(MessageValidator):
+
+    def __init__(self, lines):
+        super(JustTestMessageValidator, self).__init__(lines)
+
+    def check_line(self, lineno):
+        if lineno == 1:
+            yield 'Error on line 2'
+        elif lineno == 3:
+            yield 'Error on line 4'
+
+    def check_global(self):
+        yield 'From global check'
+
+
+class NoCheckGlobalMessageValidator(MessageValidator):
+
+    def __init__(self, lines):
+        super(NoCheckGlobalMessageValidator, self).__init__(lines)
+
+    def check_line(self, lineno):
+        if lineno == 1:
+            yield 'Error on line 2'
+        elif lineno == 3:
+            yield 'Error on line 4'
+
+
+class MessageValidatorTest(unittest.TestCase):
+
+    def test_check_line_not_implemented(self):
+        # If 'check_line()` is not implemented, the method should raise
+        # NotImplementedError.
+        lines = ['This is a line']
+        no_check_line_mv = NoCheckLineMessageValidator(lines)
+
+        with self.assertRaisesRegex(
+            NotImplementedError,
+            re.escape('`check_line()` should be '
+                      'implemented in NoCheckLineMessageValidator')):
+            no_check_line_mv.check_line(0)
+
+    def test_check_global_not_implemented_but_called(self):
+        # If 'check_global()` is not implemented, but called,
+        # the method should raise NotImplementedError.
+        lines = ['This is a line']
+        no_check_line_mv = NoCheckLineMessageValidator(lines)
+
+        with self.assertRaisesRegex(
+            NotImplementedError,
+            re.escape('`check_global()` isn\'t '
+                      'implemented in NoCheckLineMessageValidator but '
+                      'called'
+                      )):
+            no_check_line_mv.check_global()
+
+    def test_iterate_iterable_message_validator(self):
+        lines = ['This is a line', '2nd line', '3rd line', '4th line', '5th']
+
+        expected_result = [(lineno, msg)
+                           for lineno, msg in
+                           JustTestMessageValidator(lines)]
+        self.assertEqual(
+            expected_result,
+            [(2, 'Error on line 2'),
+             (4, 'Error on line 4'),
+             (5, 'From global check')])
+
+    def test_iterate_iterable_message_validator_no_check_global(self):
+        lines = ['This is a line', '2nd line', '3rd line', '4th line', '5th']
+
+        expected_result = [(lineno, msg)
+                           for lineno, msg in
+                           NoCheckGlobalMessageValidator(lines)]
+        self.assertEqual(
+            expected_result,
+            [(2, 'Error on line 2'),
+             (4, 'Error on line 4')])
+
+    def test_iterate_with_next_method(self):
+        lines = ['This is a line', '2nd line', '3rd line', '4th line', '5th']
+
+        just_test_mv = JustTestMessageValidator(lines)
+        self.assertEqual(
+            (2, 'Error on line 2'),
+            just_test_mv.next())
diff --git a/commit_message_validator/validators/GerritMessageValidator.py 
b/commit_message_validator/validators/GerritMessageValidator.py
new file mode 100644
index 0000000..63672dc
--- /dev/null
+++ b/commit_message_validator/validators/GerritMessageValidator.py
@@ -0,0 +1,200 @@
+import re
+
+from enum import Enum
+
+from commit_message_validator.message_validator import MessageValidator
+from .GlobalMessageValidator import GlobalMessageValidator
+
+RE_CHERRYPICK = re.compile(r'^\(cherry picked from commit [0-9a-fA-F]{40}\)$')
+RE_GERRIT_CHANGEID = re.compile('^I[a-f0-9]{40}$')
+RE_GERRIT_FOOTER = re.compile(
+    r'^(?P<name>[a-z]\S+):(?P<ws>\s*)(?P<value>.*)$', re.IGNORECASE)
+RE_PHABRICATOR_BUGID = re.compile('^T[0-9]+$')
+RE_SUBJECT_BUG_OR_TASK = re.compile(r'^(bug|T?\d+)', re.IGNORECASE)
+
+# Header-like lines that we are interested in validating
+CORRECT_FOOTERS = [
+    'Acked-by',
+    'Bug',
+    'Cc',
+    'Change-Id',
+    'Co-Authored-by',
+    'Depends-On',
+    'Requested-by',
+    'Reported-by',
+    'Reviewed-by',
+    'Signed-off-by',
+    'Suggested-by',
+    'Tested-by',
+    'Thanks',
+]
+FOOTERS = dict((footer.lower(), footer) for footer in CORRECT_FOOTERS)
+
+BEFORE_CHANGE_ID = [
+    'bug',
+    'closes',
+    'depends-on',
+    'fixes',
+    'task',
+]
+
+# Invalid footer name to expected name mapping
+BAD_FOOTERS = {
+    'closes': 'bug',
+    'fixes': 'bug',
+    'task': 'bug',
+}
+
+
+def is_valid_bug_id(s):
+    return RE_PHABRICATOR_BUGID.match(s)
+
+
+def is_valid_change_id(s):
+    """A Gerrit change id is a 40 character hex string prefixed with 'I'."""
+    return RE_GERRIT_CHANGEID.match(s)
+
+
+class CommitMessageContext(Enum):
+    HEADER = 1
+    BODY = 2
+    FOOTER = 3
+
+
+class GerritMessageValidator(MessageValidator):
+    """
+    An iterator to validate Gerrit remote repo commit message.
+
+    Checks:
+    - First line <=80 characters
+    - Second line blank
+    - No line >100 characters (unless it is only a URL)
+    - Footer lines ("Foo: ...") are capitalized and have a space after the ':'
+    - "Bug: " is followed by one task id ("Tnnnn")
+    - "Depends-On:" is followed by one change id ("I...")
+    - "Change-Id:" is followed one change id ("I...")
+    - No "Task: ", "Fixes: ", "Closes: " lines
+    """
+
+    def __init__(self, lines):
+        """
+        MessageValidator for Gerrit remote origin.
+
+        :param lines: list of lines from the commit message that will be
+                      checked.
+        """
+        super(GerritMessageValidator, self).__init__(lines)
+        self._global_mv = GlobalMessageValidator(lines)
+
+        self._commit_message_context = None
+        self._first_changeid = False
+
+    def check_line(self, lineno):
+        yield from self._global_mv.check_line(lineno)
+
+        line_context = self.get_context(lineno)
+        line = self._lines[lineno]
+
+        if (line_context is CommitMessageContext.HEADER and
+                RE_SUBJECT_BUG_OR_TASK.match(line)):
+            yield "Do not define bug in the header"
+
+        if (not line and
+                line_context is CommitMessageContext.FOOTER):
+            yield "Unexpected blank line"
+
+        gerrit_footer_match = RE_GERRIT_FOOTER.match(line)
+        if gerrit_footer_match:
+            name = gerrit_footer_match.group('name')
+            normalized_name = name.lower()
+            ws = gerrit_footer_match.group('ws')
+            value = gerrit_footer_match.group('value')
+
+            if normalized_name in BAD_FOOTERS:
+                # Treat as the correct name for the rest of the rules
+                normalized_name = BAD_FOOTERS[normalized_name]
+
+            if normalized_name not in FOOTERS:
+                if line_context is CommitMessageContext.FOOTER:
+                    yield "Unexpected line in footers"
+                else:
+                    # Meh. Not a name we care about
+                    return
+            elif (line_context is not
+                    CommitMessageContext.FOOTER):
+                yield "Expected '{0}:' to be in footer".format(name)
+
+            correct_name = FOOTERS.get(normalized_name)
+            if correct_name and correct_name != name:
+                yield "Use '{0}:' not '{1}:'".format(correct_name, name)
+
+            if normalized_name == 'bug':
+                if not is_valid_bug_id(value):
+                    yield "Bug: value must be a single phabricator task ID"
+
+            elif normalized_name == 'depends-on':
+                if not is_valid_change_id(value):
+                    yield "Depends-On: value must be a single Gerrit change id"
+
+            elif normalized_name == 'change-id':
+                if not is_valid_change_id(value):
+                    yield "Change-Id: value must be a single Gerrit change id"
+                if self._first_changeid is not False:
+                    yield ("Extra Change-Id found, first at "
+                           "{0}".format(self._first_changeid))
+                else:
+                    self._first_changeid = lineno + 1
+
+            if (normalized_name in BEFORE_CHANGE_ID and
+                    self._first_changeid is not False):
+                yield ("Expected '{0}:' to come before Change-Id on line "
+                       "{1}").format(name, self._first_changeid)
+
+            if ws != ' ':
+                yield "Expected one space after '%s:'" % name
+
+        elif (line and
+              line_context is CommitMessageContext.FOOTER):
+            # if it wasn't a footer (not a match) but it is in the footers
+            cherry_pick = RE_CHERRYPICK.match(line)
+            if cherry_pick:
+                if lineno < len(self._lines) - 1:
+                    yield "Cherry pick line is not the last line"
+            else:
+                yield "Expected footer line to follow format of 'Name: ...'"
+
+    def check_global(self):
+        yield from self._global_mv.check_global()
+
+        if self._first_changeid is False:
+            yield "Expected Change-Id"
+
+    def get_context(self, lineno):
+        """
+        Get the context of the current line.
+
+        :param lineno: Line number that the context will be checked.
+        :return:       A `CommitMessageContext` enum.
+        """
+        if lineno == 0:
+            # First line in the commit message is HEADER.
+            self._commit_message_context = CommitMessageContext.HEADER
+        elif self._commit_message_context is not CommitMessageContext.FOOTER:
+            line = self._lines[lineno]
+            footer_match = RE_GERRIT_FOOTER.match(line)
+            cherrypick_match = RE_CHERRYPICK.match(line)
+
+            if (((footer_match and
+                  footer_match.group('name').lower() in FOOTERS)
+                 or cherrypick_match) and
+                    not self._lines[lineno - 1]):
+                # If the current line is a footer ("Name: ..." formatted)
+                # and it's indeed a footer (the "Name" listed in the FOOTERS)
+                # or it's a cherry pick
+                # and the previous line is a blank line.
+                # Mark the current line until the end as FOOTER.
+                self._commit_message_context = CommitMessageContext.FOOTER
+            else:
+                self._commit_message_context = CommitMessageContext.BODY
+
+        return self._commit_message_context
diff --git a/commit_message_validator/validators/GlobalMessageValidator.py 
b/commit_message_validator/validators/GlobalMessageValidator.py
new file mode 100644
index 0000000..8bcb0b6
--- /dev/null
+++ b/commit_message_validator/validators/GlobalMessageValidator.py
@@ -0,0 +1,48 @@
+import re
+
+from commit_message_validator.message_validator import MessageValidator
+
+RE_URL = re.compile(r'^<?https?://\S+>?$', re.IGNORECASE)
+
+
+class GlobalMessageValidator(MessageValidator):
+    """
+    An iterator to validate all remote repo commit message.
+
+    Checks:
+    - First line <=80 characters
+    - Second line blank
+    - No line >100 characters (unless it is only a URL)
+
+    Global checks:
+    - At least 3 lines in a commit message
+    """
+
+    def __init__(self, lines):
+        """
+        MessageValidator for all remote origin.
+
+        :param lines: list of lines from the commit message that will be
+                      checked.
+        """
+        super(GlobalMessageValidator, self).__init__(lines)
+
+    def check_line(self, lineno):
+        line = self._lines[lineno]
+
+        # First line <=80
+        if lineno == 0 and len(line) > 80:
+            yield "First line should be <=80 characters"
+
+        # Second line blank
+        elif lineno == 1 and line:
+            yield "Second line should be empty"
+
+        # No line >100 characters (unless it is only a URL)
+        elif len(line) > 100 and not RE_URL.match(line):
+            yield "Line should be <=100 characters"
+
+    def check_global(self):
+        # At least 3 lines in a commit message
+        if len(self._lines) < 3:
+            yield "Expected at least 3 lines"
diff --git a/commit_message_validator/validators/__init__.py 
b/commit_message_validator/validators/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/commit_message_validator/validators/__init__.py

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I862bf42561eb436258951947c8c4a1233a3416c5
Gerrit-PatchSet: 1
Gerrit-Project: integration/commit-message-validator
Gerrit-Branch: master
Gerrit-Owner: Rafidaslam <rafidt...@gmail.com>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to