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