timescaledb/scripts/githooks/commit_msg.py
Jacob Champion f400c75b79 Tweak commit-msg hook's line character limits
The commit-msg hook was counting the end-of-line characters in the 50-
and 72-character limit, so commit messages had to be wrapped slightly
shorter in order to avoid the hook complaint. Strip the EOL characters
during the check instead.

Since we use universal newline handling during the file read, stripping
LF ('\n') should be enough to handle both Windows and Unix development
environments.
2022-06-21 13:04:42 -05:00

200 lines
6.3 KiB
Python
Executable File

#!/usr/bin/env python3
# Check a Git commit message according to the seven rules of a good commit message:
# https://chris.beams.io/posts/git-commit/
import sys
class GitCommitMessage:
'Represents a parsed Git commit message'
rules = [ 'Separate subject from body with a blank line',
'Limit the subject line to 50 characters',
'Capitalize the subject line',
'Do not end the subject line with a period',
'Use the imperative mood in the subject line',
'Wrap the body at 72 characters',
'Use the body to explain what and why vs. how' ]
valid_rules = [ False, False, False, False, False, False, False ]
def __init__(self, filename = None):
lines = []
if filename != None:
with open(filename, 'r') as f:
for line in f:
if line.startswith('# ------------------------ >8 ------------------------'):
break
if not line.startswith('#'):
lines.append(line)
self.parse_lines(lines)
def parse_lines(self, lines):
self.body_lines = []
self.subject = []
if not lines or len(lines) == 0:
return self
self.subject = lines[0]
self.subject_words = self.subject.split()
self.has_subject_body_separator = False
if len(lines) > 1:
self.has_subject_body_separator = (len(lines[1].strip()) == 0)
if self.has_subject_body_separator:
self.body_lines = lines[2:]
else:
self.body_lines = lines[1:]
return self
def check_subject_body_separtor(self):
'Rule 1: Separate subject from body with a blank line'
if len(self.body_lines) > 0:
return self.has_subject_body_separator
return True
def check_subject_limit(self):
'Rule 2: Limit the subject line to 50 characters'
return len(self.subject.rstrip("\n")) <= 50
def check_subject_capitalized(self):
'Rule 3: Capitalize the subject line'
return len(self.subject) > 0 and self.subject[0].isupper()
def check_subject_no_period(self):
'Rule 4: Do not end the subject line with a period'
return not self.subject.endswith('.')
common_first_words = [ 'Add',
'Adjust',
'Support',
'Change',
'Remove',
'Fix',
'Print',
'Track',
'Refactor',
'Combine',
'Release',
'Set',
'Stop',
'Make',
'Mark',
'Enable',
'Check',
'Exclude',
'Format',
'Correct']
def check_subject_imperative(self):
'Rule 5: Use the imperative mood in the subject line.'
'We can only check for common mistakes here, like using'
'the -ing form of a verb or non-imperative version of '
'common verbs'
firstword = self.subject_words[0]
if firstword.endswith('ing'):
return False
for word in self.common_first_words:
if firstword.startswith(word) and firstword != word:
return False
return True
def check_body_limit(self):
'Rule 6: Wrap the body at 72 characters'
if len(self.body_lines) == 0:
return True
for line in self.body_lines:
if len(line.rstrip("\n")) > 72:
return False
return True
def check_body_uses_why(self):
'Rule 7: Use the body to explain what and why vs. how'
# Not enforcable
return True
rule_funcs = [
check_subject_body_separtor,
check_subject_limit,
check_subject_capitalized,
check_subject_no_period,
check_subject_imperative,
check_body_limit,
check_body_uses_why ]
def check_the_seven_rules(self):
'validates the commit message against the seven rules'
num_violations = 0
for i in range(len(self.rule_funcs)):
func = self.rule_funcs[i]
res = func(self)
self.valid_rules[i] = res
if not res:
num_violations += 1
if num_violations > 0:
print()
print("**** WARNING ****")
print()
print("The commit message does not seem to comply with the project's guidelines.")
print("Please try to follow the \"Seven rules of a great commit message\":")
print("https://chris.beams.io/posts/git-commit/")
print()
print("The following rules are violated:\n")
for i in range(len(self.rule_funcs)):
if not self.valid_rules[i]:
print(f"\t* Rule {i+1}: \"{self.rules[i]}\"")
# Extra sanity checks beyond the seven rules
if len(self.body_lines) == 0:
print()
print("NOTE: the commit message has no body.")
print("It is recommended to add a body with a description of your")
print("changes, even if they are small. Explain what and why instead of how:")
print("https://chris.beams.io/posts/git-commit/#why-not-how")
if len(self.subject_words) < 3:
print()
print("Warning: the subject line has less than three words.")
print("Consider using a more explanatory subject line.")
if num_violations > 0:
print()
print("Run 'git commit --amend' to change the commit message")
print()
return num_violations
def main():
if len(sys.argv) != 2:
print("Unexpected number of arguments")
exit(1)
msg = GitCommitMessage(sys.argv[1])
return msg.check_the_seven_rules()
if __name__ == "__main__":
main()
# Always exit with success. We could also fail the commit if with
# a non-zero exit code, but that might be a bit excessive and we'd
# have to save the failed commit message to a file so that it can
# be recovered.
exit(0)