2020-08-18 23:00:32 +00:00
#!/usr/bin/env python3
2018-10-16 15:29:47 +00:00
# Verifies whether commit messages adhere to the standards.
# Checks the author name and email and invokes the tools/commit-msg script.
# Copy this into .git/hooks/post-commit
#
# Copyright (c) 2018 Peter Wu <peter@lekensteyn.nl>
#
# Wireshark - Network traffic analyzer
# By Gerald Combs <gerald@wireshark.org>
# Copyright 1998 Gerald Combs
#
# SPDX-License-Identifier: GPL-2.0-or-later
from __future__ import print_function
import argparse
import difflib
2020-09-02 23:31:58 +00:00
import json
2018-10-16 15:29:47 +00:00
import os
import subprocess
import sys
import tempfile
2020-09-02 23:31:58 +00:00
import urllib . request
2021-06-11 14:43:05 +00:00
import re
2018-10-16 15:29:47 +00:00
parser = argparse . ArgumentParser ( )
parser . add_argument ( ' commit ' , nargs = ' ? ' , default = ' HEAD ' ,
help = ' Commit ID to be checked (default %(default)s ) ' )
2021-03-13 19:02:08 +00:00
parser . add_argument ( ' --commitmsg ' , help = ' commit-msg check ' , action = ' store ' )
2018-10-16 15:29:47 +00:00
def print_git_user_instructions ( ) :
print ( ' To configure your name and email for git, run: ' )
print ( ' ' )
print ( ' git config --global user.name " Your Name " ' )
print ( ' git config --global user.email " you@example.com " ' )
print ( ' ' )
print ( ' After that update the author of your latest commit with: ' )
print ( ' ' )
print ( ' git commit --amend --reset-author --no-edit ' )
print ( ' ' )
def verify_name ( name ) :
name = name . lower ( ) . strip ( )
forbidden_names = ( ' unknown ' , ' root ' , ' user ' , ' your name ' )
if name in forbidden_names :
return False
# Warn about names without spaces. Sometimes it is a mistake where the
# developer accidentally committed using the system username.
if ' ' not in name :
print ( " WARNING: name ' %s ' does not contain a space. " % ( name , ) )
print_git_user_instructions ( )
return True
def verify_email ( email ) :
email = email . lower ( ) . strip ( )
try :
user , host = email . split ( ' @ ' )
except ValueError :
# Lacks a '@' (e.g. a plain domain or "foo[AT]example.com")
return False
tld = host . split ( ' . ' ) [ - 1 ]
# localhost, localhost.localdomain, my.local etc.
if ' local ' in tld :
return False
# Possibly an IP address
if tld . isdigit ( ) :
return False
# forbid code.wireshark.org. Submissions could be submitted by other
# addresses if one would like to remain anonymous.
if host . endswith ( ' .wireshark.org ' ) :
return False
# For documentation purposes only.
if host == ' example.com ' :
return False
# 'peter-ubuntu32.(none)'
if ' (none) ' in host :
return False
return True
def tools_dir ( ) :
if __file__ . endswith ( ' .py ' ) :
# Assume direct invocation from tools directory
return os . path . dirname ( __file__ )
# Otherwise it is a git hook. To support git worktrees, do not manually look
# for the .git directory, but query the actual top level instead.
cmd = [ ' git ' , ' rev-parse ' , ' --show-toplevel ' ]
srcdir = subprocess . check_output ( cmd , universal_newlines = True ) . strip ( )
return os . path . join ( srcdir , ' tools ' )
2018-12-19 15:04:01 +00:00
def extract_subject ( subject ) :
''' Extracts the original subject (ignoring the Revert prefix). '''
subject = subject . rstrip ( ' \r \n ' )
prefix = ' Revert " '
suffix = ' " '
while subject . startswith ( prefix ) and subject . endswith ( suffix ) :
subject = subject [ len ( prefix ) : - len ( suffix ) ]
return subject
2018-10-16 15:29:47 +00:00
def verify_body ( body ) :
2021-06-11 14:43:05 +00:00
bodynocomments = re . sub ( ' ^#.*$ ' , ' ' , body , flags = re . MULTILINE )
old_lines = bodynocomments . splitlines ( True )
2018-11-16 12:21:04 +00:00
is_good = True
if len ( old_lines ) > = 2 and old_lines [ 1 ] . strip ( ) :
print ( ' ERROR: missing blank line after the first subject line. ' )
is_good = False
2018-12-19 15:04:01 +00:00
cleaned_subject = extract_subject ( old_lines [ 0 ] )
if len ( cleaned_subject ) > 80 :
2021-03-13 19:02:08 +00:00
# Note that this check is also invoked by the commit-msg hook.
2018-11-16 12:21:04 +00:00
print ( ' Warning: keep lines in the commit message under 80 characters. ' )
is_good = False
if not is_good :
print ( '''
Please rewrite your commit message to our standards , matching this format :
component : a very brief summary of the change
A commit message should start with a brief summary , followed by a single
blank line and an optional longer description . If the change is specific to
a single protocol , start the summary line with the abbreviated name of the
protocol and a colon .
Use paragraphs to improve readability . Limit each line to 80 characters .
''' )
2020-08-18 23:00:32 +00:00
if any ( line . startswith ( ' Bug: ' ) or line . startswith ( ' Ping-Bug: ' ) for line in old_lines ) :
sys . stderr . write ( '''
To close an issue , use " Closes #1234 " or " Fixes #1234 " instead of " Bug: 1234 " .
To reference an issue , use " related to #1234 " instead of " Ping-Bug: 1234 " . See
https : / / docs . gitlab . com / ee / user / project / issues / managing_issues . html #closing-issues-automatically
for details .
''' )
return False
2018-10-16 15:29:47 +00:00
2020-11-04 19:03:56 +00:00
# Cherry-picking can add an extra newline, which we'll allow.
cp_line = ' \n (cherry picked from commit '
body = body . replace ( ' \n ' + cp_line , cp_line )
2020-08-18 23:00:32 +00:00
try :
cmd = [ ' git ' , ' stripspace ' ]
newbody = subprocess . check_output ( cmd , input = body , universal_newlines = True )
2018-10-16 15:29:47 +00:00
except OSError as ex :
2020-08-18 23:00:32 +00:00
print ( ' Warning: unable to invoke git stripspace: %s ' % ( ex , ) )
2018-11-16 12:21:04 +00:00
return is_good
2018-10-16 15:29:47 +00:00
if newbody != body :
new_lines = newbody . splitlines ( True )
diff = difflib . unified_diff ( old_lines , new_lines ,
fromfile = ' OLD/.git/COMMIT_EDITMSG ' ,
tofile = ' NEW/.git/COMMIT_EDITMSG ' )
# Clearly mark trailing whitespace (GNU patch supports such comments).
diff = [
' # NOTE: trailing space on the next line \n %s ' % ( line , )
if len ( line ) > 2 and line [ - 2 ] . isspace ( ) else line
for line in diff
]
print ( ' The commit message does not follow our standards. ' )
print ( ' Please rewrite it (there are likely whitespace issues): ' )
print ( ' ' )
print ( ' ' . join ( diff ) )
return False
2018-11-16 12:21:04 +00:00
return is_good
2018-10-16 15:29:47 +00:00
2021-03-13 19:02:08 +00:00
2020-09-02 23:31:58 +00:00
def verify_merge_request ( ) :
# Not needed if/when https://gitlab.com/gitlab-org/gitlab/-/issues/23308 is fixed.
gitlab_api_pfx = " https://gitlab.com/api/v4 "
# gitlab.com/wireshark/wireshark = 7898047
project_id = os . getenv ( ' CI_MERGE_REQUEST_PROJECT_ID ' )
2020-09-04 00:47:38 +00:00
ansi_csi = ' \x1b [ '
ansi_codes = {
' black_white ' : ansi_csi + ' 30;47m ' ,
' bold_red ' : ansi_csi + ' 31;1m ' , # gitlab-runner errors
' reset ' : ansi_csi + ' 0m '
}
2020-09-02 23:31:58 +00:00
m_r_iid = os . getenv ( ' CI_MERGE_REQUEST_IID ' )
if project_id is None or m_r_iid is None :
print ( " This doesn ' t appear to be a merge request. CI_MERGE_REQUEST_PROJECT_ID= {} , CI_MERGE_REQUEST_IID= {} " . format ( project_id , m_r_iid ) )
return True
m_r_url = ' {} /projects/ {} /merge_requests/ {} ' . format ( gitlab_api_pfx , project_id , m_r_iid )
req = urllib . request . Request ( m_r_url )
2020-11-04 19:03:56 +00:00
# print('req', repr(req), m_r_url)
2020-09-02 23:31:58 +00:00
with urllib . request . urlopen ( req ) as resp :
resp_json = resp . read ( ) . decode ( ' utf-8 ' )
# print('resp', resp_json)
m_r_attrs = json . loads ( resp_json )
try :
if not m_r_attrs [ ' allow_collaboration ' ] :
print ( ''' \
2020-09-04 00:47:38 +00:00
{ bold_red } ERROR : { reset } Please edit your merge request and make sure the setting
{ black_white } ✅ Allow commits from members who can merge to the target branch { reset }
is checked so that maintainers can rebase your change and make minor edits . \
''' .format(**ansi_codes))
2020-09-02 23:31:58 +00:00
return False
except KeyError :
sys . stderr . write ( ' This appears to be a merge request, but we were not able to fetch the " Allow commits " status \n ' )
return True
2018-10-16 15:29:47 +00:00
def main ( ) :
args = parser . parse_args ( )
commit = args . commit
2020-12-11 23:54:55 +00:00
2021-03-13 19:02:08 +00:00
# If called from commit-msg script, just validate that part and return.
if args . commitmsg :
try :
with open ( args . commitmsg ) as f :
return 0 if verify_body ( f . read ( ) ) else 1
except :
print ( " Couldn ' t verify body of message from file ' " , + args . commitmsg + " ' " ) ;
return 1
2020-12-11 23:54:55 +00:00
if ( os . getenv ( ' CI_MERGE_REQUEST_EVENT_TYPE ' ) == ' merge_train ' ) :
print ( " If we were on the love train, people all over the world would be joining hands for this merge request. \n Instead, we ' re on a merge train so we ' re skipping commit validation checks. " )
return 0
2018-10-16 15:29:47 +00:00
cmd = [ ' git ' , ' show ' , ' --no-patch ' ,
' --format= %h% n %a n % n %a e % n % B ' , commit , ' -- ' ]
output = subprocess . check_output ( cmd , universal_newlines = True )
# For some reason there is always an additional LF in the output, drop it.
if output . endswith ( ' \n \n ' ) :
output = output [ : - 1 ]
abbrev , author_name , author_email , body = output . split ( ' \n ' , 3 )
subject = body . split ( ' \n ' , 1 ) [ 0 ]
# If called directly (from the tools directory), print the commit that was
# being validated. If called from a git hook (without .py extension), try to
# remain silent unless there are issues.
if __file__ . endswith ( ' .py ' ) :
print ( ' Checking commit: %s %s ' % ( abbrev , subject ) )
exit_code = 0
if not verify_name ( author_name ) :
print ( ' Disallowed author name: {} ' . format ( author_name ) )
exit_code = 1
if not verify_email ( author_email ) :
print ( ' Disallowed author email address: {} ' . format ( author_email ) )
exit_code = 1
if exit_code :
print_git_user_instructions ( )
if not verify_body ( body ) :
exit_code = 1
2020-09-02 23:31:58 +00:00
if not verify_merge_request ( ) :
exit_code = 1
2018-10-16 15:29:47 +00:00
return exit_code
if __name__ == ' __main__ ' :
try :
sys . exit ( main ( ) )
except subprocess . CalledProcessError as ex :
print ( ' \n %s ' % ex )
sys . exit ( ex . returncode )
except KeyboardInterrupt :
sys . exit ( 130 )