class SpecialChars(LineRule): """ This rule will enforce that the commit message title does not contain any of the following characters: $^%@!*() """ # A rule MUST have a human friendly name name = "title-no-special-chars" # A rule MUST have a *unique* id, we recommend starting with UL (for User-defined Line-rule), but this can # really be anything. id = "UL1" # A line-rule MUST have a target (not required for CommitRules). target = CommitMessageTitle # A rule MAY have an option_spec if its behavior should be configurable. options_spec = [ ListOption( 'special-chars', ['$', '^', '%', '@', '!', '*', '(', ')'], "Comma separated list of characters that should not occur in the title" ) ] def validate(self, line, _commit): violations = [] # options can be accessed by looking them up by their name in self.options for char in self.options['special-chars'].value: if char in line: violation = RuleViolation( self.id, "Title contains the special character '{0}'".format(char), line) violations.append(violation) return violations
class ConventionalCommit(LineRule): """ This rule enforces the spec at https://www.conventionalcommits.org/. """ name = "contrib-title-conventional-commits" id = "CT1" target = CommitMessageTitle options_spec = [ ListOption( "types", ["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert"], "Comma separated list of allowed commit types.", ) ] def validate(self, line, _commit): violations = [] for commit_type in self.options["types"].value: if line.startswith(ustr(commit_type)): break else: msg = u"Title does not start with one of {0}".format(', '.join(self.options['types'].value)) violations.append(RuleViolation(self.id, msg, line)) if not RULE_REGEX.match(line): msg = u"Title does not follow ConventionalCommits.org format 'type(optional-scope): description'" violations.append(RuleViolation(self.id, msg, line)) return violations
class BranchNamingConventions(CommitRule): """ This rule will enforce that a commit is part of a branch that meets certain naming conventions. See GitFlow for real-world example of this: https://nvie.com/posts/a-successful-git-branching-model/ """ # A rule MUST have a human friendly name name = "branch-naming-conventions" # A rule MUST have a *unique* id, we recommend starting with UC (for User-defined Commit-rule). id = "UC3" # A rule MAY have an option_spec if its behavior should be configurable. options_spec = [ ListOption('branch-prefixes', ["feature/", "hotfix/", "release/"], "Allowed branch prefixes") ] def validate(self, commit): violations = [] allowed_branch_prefixes = self.options['branch-prefixes'].value for branch in commit.branches: valid_branch_name = False for allowed_prefix in allowed_branch_prefixes: if branch.startswith(allowed_prefix): valid_branch_name = True break if not valid_branch_name: msg = "Branch name '{0}' does not start with one of {1}".format( branch, utils.sstr(allowed_branch_prefixes)) violations.append(RuleViolation(self.id, msg, line_nr=1)) return violations
class AreaFormatting(CommitRule): name = "area-formatting" id = "ZT2" options_spec = [ ListOption("exclusions", ["WIP"], "Exclusions to area lower-case rule") ] def validate(self, commit: Any) -> Optional[List[RuleViolation]]: title_components = commit.message.title.split(": ") violations = [] # Return just this violation, since latter checks assume an area error = ("Title should start with at least one area, " "followed by a colon and space") if len(title_components) < 2: return [RuleViolation(self.id, error, line_nr=1)] exclusions = self.options['exclusions'].value exclusions_text = ", or ".join(exclusions) if exclusions_text: exclusions_text = " (or {})".format(exclusions_text) error = ("Areas at start of title should be lower case{}, " "followed by ': '".format(exclusions_text)) for area in title_components[:-1]: if not (area.islower() or area in exclusions) or ' ' in area: violations += [RuleViolation(self.id, error, line_nr=1)] error = "Summary of change, after area(s), should be capitalized" if not title_components[-1][0].isupper(): violations += [RuleViolation(self.id, error, line_nr=1)] return violations
class ConfigurableCommitRule(CommitRule): """ Rule that tests that we can add configuration to user-defined rules """ name = "configürable" id = "UC4" options_spec = [ IntOption("int-öption", 2, "int-öption description"), StrOption("str-öption", "föo", "int-öption description"), ListOption("list-öption", ["foo", "bar"], "list-öption description") ] def validate(self, _): violations = [ RuleViolation(self.id, f"int-öption: {self.options[u'int-öption'].value}", line_nr=1), RuleViolation(self.id, f"str-öption: {self.options[u'str-öption'].value}", line_nr=1), RuleViolation(self.id, f"list-öption: {self.options[u'list-öption'].value}", line_nr=1), ] return violations
def test_list_option(self): # normal behavior option = ListOption("test-name", "bar", "Test Description") option.set("a,b,c,d") self.assertListEqual(option.value, ["a", "b", "c", "d"]) # trailing comma option.set("e,f,g,") self.assertListEqual(option.value, ["e", "f", "g"]) # spaces should be trimmed option.set(" abc , def , ghi \t ") self.assertListEqual(option.value, ["abc", "def", "ghi"]) # conversion to string before split option.set(123) self.assertListEqual(option.value, ["123"])
class BodyChangedFileMention(CommitRule): name = "body-changed-file-mention" id = "B7" options_spec = [ListOption('files', [], "Files that need to be mentioned")] def validate(self, commit): violations = [] for needs_mentioned_file in self.options['files'].value: # if a file that we need to look out for is actually changed, then check whether it occurs # in the commit msg body if needs_mentioned_file in commit.changed_files: if needs_mentioned_file not in " ".join(commit.message.body): violation_message = u"Body does not mention changed file '{0}'".format(needs_mentioned_file) violations.append(RuleViolation(self.id, violation_message, None, len(commit.message.body) + 1)) return violations if violations else None
class SpecialChars(LineRule): """ This rule will enforce that the commit message title does not contain any banned characters. """ # A rule MUST have a human friendly name name = "title-no-special-chars" # A rule MUST have a *unique* id id = "UL1" # A line-rule MUST have a target (not required for CommitRules). target = CommitMessageTitle # A rule MAY have an option_spec if its behavior should be configurable. options_spec = [ ListOption( "special-chars", list(DEFAULT_BANNED_CHARS), "Comma separated list of characters that should not occur in the title", ) ] def validate(self, line, _commit): """ Validate each line Args: line (str): line _commit (???): ??? Returns: list: List of violations """ violations = [] # options can be accessed by looking them up by their name in self.options for char in self.options["special-chars"].value: if char in line: violation = RuleViolation( self.id, "Title contains the special character '{0}'".format(char), line, ) violations.append(violation) return violations
class LineMustNotContainWord(LineRule): """ Violation if a line contains one of a list of words (NOTE: using a word in the list inside another word is not a violation, e.g: WIPING is not a violation if 'WIP' is a word that is not allowed.) """ name = "line-must-not-contain" id = "R5" options_spec = [ListOption('words', [], "Comma separated list of words that should not be found")] violation_message = u"Line contains {0}" def validate(self, line, _commit): strings = self.options['words'].value violations = [] for string in strings: regex = re.compile(r"\b%s\b" % string.lower(), re.IGNORECASE | re.UNICODE) match = regex.search(line.lower()) if match: violations.append(RuleViolation(self.id, self.violation_message.format(string), line)) return violations if violations else None
class ConfigurableCommitRule(CommitRule): """ Rule that tests that we can add configuration to user-defined rules """ name = u"configürable" id = "UC4" options_spec = [IntOption(u"int-öption", 2, u"int-öption description"), StrOption(u"str-öption", u"föo", u"int-öption description"), ListOption(u"list-öption", [u"foo", u"bar"], u"list-öption description")] def validate(self, _): violations = [ RuleViolation(self.id, u"int-öption: {0}".format(self.options[u'int-öption'].value), line_nr=1), RuleViolation(self.id, u"str-öption: {0}".format(self.options[u'str-öption'].value), line_nr=1), RuleViolation(self.id, u"list-öption: {0}".format(sstr(self.options[u'list-öption'].value)), line_nr=1), ] return violations
class CommitType(LineRule): """ This rule will enforce that the commit message title contains special word indicating the type of commit, as recommended by PyCharm Git Commit Template plugin (see also https://udacity.github.io/git-styleguide/). """ name = "title-commit-type" id = "UL1" target = CommitMessageTitle options_spec = [ListOption('special-words', ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert'], "Comma separated list of words that should occur in the title")] def validate(self, line, _commit): violations = [] first_word = line.split('(')[0].split(':')[0] if first_word not in self.options['special-words'].value: violation = RuleViolation(self.id, "Title begins with '{0}'; only words from {1} are allowed. In commit". format(first_word, self.options['special-words'].value), line) violations.append(violation) return violations
class TitleMustNotContainWord(LineMustNotContainWord): name = "title-must-not-contain-word" id = "T5" target = CommitMessageTitle options_spec = [ListOption('words', ["WIP"], "Must not contain word")] violation_message = "Title contains the word '{0}' (case-insensitive)"
def test_list_option(self): # normal behavior option = ListOption("test-name", u"å,b,c,d", "Test Description") self.assertListEqual(option.value, [u"å", u"b", u"c", u"d"]) # re-set value option.set(u"1,2,3,4") self.assertListEqual(option.value, [u"1", u"2", u"3", u"4"]) # set list option.set([u"foo", u"bår", u"test"]) self.assertListEqual(option.value, [u"foo", u"bår", u"test"]) # empty string option.set("") self.assertListEqual(option.value, []) # whitespace string option.set(" \t ") self.assertListEqual(option.value, []) # empty list option.set([]) self.assertListEqual(option.value, []) # trailing comma option.set(u"ë,f,g,") self.assertListEqual(option.value, [u"ë", u"f", u"g"]) # leading and trailing whitespace should be trimmed, but only deduped within text option.set(" abc , def , ghi \t , jkl mno ") self.assertListEqual(option.value, ["abc", "def", "ghi", "jkl mno"]) # Also strip whitespace within a list option.set(["\t foo", "bar \t ", " test 123 "]) self.assertListEqual(option.value, ["foo", "bar", "test 123"]) # conversion to string before split option.set(123) self.assertListEqual(option.value, ["123"])
def test_list_option(self): # normal behavior option = ListOption("test-name", "a,b,c,d", "Test Description") self.assertListEqual(option.value, ["a", "b", "c", "d"]) # re-set value option.set("1,2,3,4") self.assertListEqual(option.value, ["1", "2", "3", "4"]) # set list option.set(["foo", "bar", "test"]) self.assertListEqual(option.value, ["foo", "bar", "test"]) # trailing comma option.set("e,f,g,") self.assertListEqual(option.value, ["e", "f", "g"]) # leading and trailing whitespace should be trimmed, but only deduped within text option.set(" abc , def , ghi \t , jkl mno ") self.assertListEqual(option.value, ["abc", "def", "ghi", "jkl mno"]) # Also strip whitespace within a list option.set(["\t foo", "bar \t ", " test 123 "]) self.assertListEqual(option.value, ["foo", "bar", "test 123"]) # conversion to string before split option.set(123) self.assertListEqual(option.value, ["123"])