mirror of
https://github.com/timescale/timescaledb.git
synced 2025-05-18 11:45:11 +08:00
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.
200 lines
6.3 KiB
Python
Executable File
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)
|