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

Change subject: [FEAT] Rewrite commit message validator
......................................................................

[FEAT] Rewrite commit message validator

It converts the module wide functions into a class which acts as an iterator
which can internally store information. This allows each line check to
actually know "outside information" without the need of module wide variables,
like whether it is inside the footer or whether a change id already occurred.

This also converts all of the checks into yields so that it is almost nowhere
necessary to actually provide the line number it acts on. This changes a bit
the logic but also makes sure that the line numbers are now sequential.

Change-Id: If77a0abc8c46334e6ebbf415140c12375a4870f8
---
M commit_message_validator/__init__.py
M commit_message_validator/tests/data/check_message_errors.out
2 files changed, 108 insertions(+), 119 deletions(-)


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

diff --git a/commit_message_validator/__init__.py 
b/commit_message_validator/__init__.py
index 848d61b..175b795 100644
--- a/commit_message_validator/__init__.py
+++ b/commit_message_validator/__init__.py
@@ -81,8 +81,10 @@
     return RE_CHANGEID.match(s)
 
 
-def line_errors(lineno, line):
-    """Check a commit message line to see if it has errors.
+class CheckMessage(object):
+
+    """
+    Iterator to check a commit message line for errors.
 
     Checks:
     - First line <=80 characters
@@ -94,62 +96,113 @@
     - "Change-Id:" is followed one change id ("I...")
     - No "Task: ", "Fixes: ", "Closes: " lines
     """
-    # 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"
+    def __init__(self, lines):
+        self._lines = lines
+        self._first_changeid = False
+        self._in_footers = False
 
-    # No line >100 unless it is all a URL
-    if len(line) > 100 and not RE_URL.match(line):
-        yield "Line should be <=100 characters"
+    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"
 
-    # 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')
+        # Second line blank
+        elif lineno == 1:
+            if line:
+                yield "Second line should be empty"
 
-        if normalized_name not in FOOTERS:
-            # Meh. Not a name we care about
+        # 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
 
-        if normalized_name in BAD_FOOTERS:
-            # Treat as the correct name for the rest of the rules
-            normalized_name = BAD_FOOTERS[normalized_name]
+        # 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 == 'bug':
-            if name != 'Bug':
-                yield "Use 'Bug:' not '{0}:'".format(name)
-            if not is_valid_bug_id(value):
-                yield "Bug: value must be a single phabricator task ID"
+            if normalized_name in FOOTERS:
+                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)
+            else:
+                if self._in_footers:
+                    yield "Unexpected line in footers"
+                else:
+                    # Meh. Not a name we care about
+                    return
 
-        elif normalized_name == 'depends-on':
-            if name != 'Depends-On':
-                yield "Use 'Depends-On:' not '%s:'" % name
-            if not is_valid_change_id(value):
-                yield "Depends-On: value must be a single Gerrit change id"
+            if normalized_name in BAD_FOOTERS:
+                # Treat as the correct name for the rest of the rules
+                normalized_name = BAD_FOOTERS[normalized_name]
 
-        elif normalized_name == 'change-id':
-            if name != 'Change-Id':
-                yield "Use 'Change-Id:' not '%s:'" % name
-            if not is_valid_change_id(value):
-                yield "Change-Id: value must be a single Gerrit change id"
+            if normalized_name == 'bug':
+                if name != 'Bug':
+                    yield "Use 'Bug:' not '{0}:'".format(name)
+                if not is_valid_bug_id(value):
+                    yield "Bug: value must be a single phabricator task ID"
 
-        elif name[0].upper() != name[0]:
-            yield "'%s:' must start with a capital letter" % name
+            elif normalized_name == 'depends-on':
+                if name != 'Depends-On':
+                    yield "Use 'Depends-On:' not '%s:'" % name
+                if not is_valid_change_id(value):
+                    yield "Depends-On: value must be a single Gerrit change id"
 
-        if ws != ' ':
-            yield "Expected one space after '%s:'" % name
+            elif normalized_name == 'change-id':
+                if name != 'Change-Id':
+                    yield "Use 'Change-Id:' not '%s:'" % name
+                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
+
+            elif name[0].upper() != name[0]:
+                yield "'%s:' must start with a capital letter" % name
+
+            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:
+            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 __iter__(self):
+        for lineno in range(len(self._lines)):
+            for e in self.check_line(lineno):
+                yield 'Line {0}: {1}'.format(lineno + 1, e)
+
+        if len(self._lines) < 3:
+            yield ("Line {0}: Expected at least 3 "
+                   "lines".format(len(self._lines)))
+
+        if self._first_changeid is False:
+            yield "Line {0}: Expected Change-Id".format(len(self._lines))
 
 
 def check_message(lines):
@@ -164,72 +217,8 @@
     - Any "Bug:" and "Depends-On:" lines come before "Change-Id:"
     - "(cherry picked from commit ...)" is last line in footer if present
     """
-    errors = []
-    last_lineno = 0
-    last_line = ''
-    changeid_line = False
-    cherrypick_line = False
-    in_footer = False
-
-    for lineno, line in enumerate(lines):
-        rline = lineno + 1
-        errors.extend('Line {0}: {1}'.format(rline, e)
-                      for e in line_errors(lineno, line))
-
-        if in_footer:
-            if line == '':
-                errors.append(
-                    "Line {0}: Unexpected blank line".format(rline))
-            elif not (RE_FOOTER.match(line) or RE_CHERRYPICK.match(line)):
-                errors.append((
-                    "Line {0}: Expected footer line to follow format of "
-                    "'Name: ...'").format(rline))
-
-        m = RE_FOOTER.match(line)
-        if m:
-            name = m.group('name')
-            normalized_name = name.lower()
-
-            if last_line == '' and normalized_name in FOOTERS:
-                # The first footer after a blank line starts the footer
-                in_footer = True
-
-            if normalized_name in FOOTERS and not in_footer:
-                errors.append(
-                    "Line {0}: Expected '{1}:' to be in footer".format(
-                        rline, name))
-
-            if normalized_name == 'change-id':
-                # Only expect one "Change-Id: " line
-                if changeid_line is not False:
-                    errors.append(
-                        "Line {0}: Extra Change-Id found, first at {1}".format(
-                            rline, changeid_line))
-                changeid_line = rline
-
-            elif normalized_name in BEFORE_CHANGE_ID:
-                if changeid_line is not False:
-                    errors.append((
-                        "Line {0}: Expected '{1}:' to come before "
-                        "Change-Id on line {2}").format(
-                            rline, name, changeid_line))
-
-        elif RE_CHERRYPICK.match(line):
-            cherrypick_line = rline
-
-        last_lineno = rline
-        last_line = line
-
-    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)
-
-    if cherrypick_line and cherrypick_line != last_lineno:
-        errors.append(
-            "Line {0}: Cherry pick expected to be last line".format(
-                cherrypick_line))
+    checker = CheckMessage(lines)
+    errors = list(checker)
 
     print('commit-message-validator v%s' % __version__)
     if errors:
diff --git a/commit_message_validator/tests/data/check_message_errors.out 
b/commit_message_validator/tests/data/check_message_errors.out
index 9cea8be..4609a24 100644
--- a/commit_message_validator/tests/data/check_message_errors.out
+++ b/commit_message_validator/tests/data/check_message_errors.out
@@ -3,29 +3,29 @@
 Line 1: First line should be <=80 characters
 Line 2: Second line should be empty
 Line 3: Line should be <=100 characters
+Line 4: Expected 'Fixes:' to be in footer
 Line 4: Use 'Bug:' not 'Fixes:'
 Line 4: Bug: value must be a single phabricator task ID
 Line 4: Expected one space after 'Fixes:'
-Line 4: Expected 'Fixes:' to be in footer
+Line 5: Expected 'Closes:' to be in footer
 Line 5: Use 'Bug:' not 'Closes:'
 Line 5: Bug: value must be a single phabricator task ID
 Line 5: Expected one space after 'Closes:'
-Line 5: Expected 'Closes:' to be in footer
-Line 6: Use 'Bug:' not 'Task:'
 Line 6: Expected 'Task:' to be in footer
+Line 6: Use 'Bug:' not 'Task:'
+Line 7: Expected 'BUG:' to be in footer
 Line 7: Use 'Bug:' not 'BUG:'
 Line 7: Bug: value must be a single phabricator task ID
 Line 7: Expected one space after 'BUG:'
-Line 7: Expected 'BUG:' to be in footer
-Line 8: Bug: value must be a single phabricator task ID
 Line 8: Expected 'Bug:' to be in footer
+Line 8: Bug: value must be a single phabricator task ID
 Line 12: Bug: value must be a single phabricator task ID
 Line 13: Unexpected blank line
 Line 15: Unexpected blank line
 Line 16: Bug: value must be a single phabricator task ID
 Line 19: Extra Change-Id found, first at 18
 Line 20: Depends-On: value must be a single Gerrit change id
-Line 20: Expected 'Depends-On:' to come before Change-Id on line 19
+Line 20: Expected 'Depends-On:' to come before Change-Id on line 18
 Line 21: Unexpected blank line
 Line 22: Expected footer line to follow format of 'Name: ...'
 Please review <https://www.mediawiki.org/wiki/Gerrit/Commit_message_guidelines>

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: If77a0abc8c46334e6ebbf415140c12375a4870f8
Gerrit-PatchSet: 1
Gerrit-Project: integration/commit-message-validator
Gerrit-Branch: master
Gerrit-Owner: XZise <commodorefabia...@gmx.de>

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

Reply via email to