devtools: Fetch and display ACKs at sign-off time in github-merge

- Fetch the ACKs only at sign-off time. This makes sure that any
  last-minute ACKs are included (fixes #16200)
- Show a list of ACKs and their author before signing off, and warn if
  there are none
This commit is contained in:
Wladimir J. van der Laan 2019-06-17 14:39:11 +02:00
parent f6f924811d
commit 0e01e4522e

View file

@ -32,10 +32,14 @@ BASH = os.getenv('BASH','bash')
# OS specific configuration for terminal attributes # OS specific configuration for terminal attributes
ATTR_RESET = '' ATTR_RESET = ''
ATTR_PR = '' ATTR_PR = ''
ATTR_NAME = ''
ATTR_WARN = ''
COMMIT_FORMAT = '%H %s (%an)%d' COMMIT_FORMAT = '%H %s (%an)%d'
if os.name == 'posix': # if posix, assume we can use basic terminal escapes if os.name == 'posix': # if posix, assume we can use basic terminal escapes
ATTR_RESET = '\033[0m' ATTR_RESET = '\033[0m'
ATTR_PR = '\033[1;36m' ATTR_PR = '\033[1;36m'
ATTR_NAME = '\033[0;36m'
ATTR_WARN = '\033[1;31m'
COMMIT_FORMAT = '%C(bold blue)%H%Creset %s %C(cyan)(%an)%Creset%C(green)%d%Creset' COMMIT_FORMAT = '%C(bold blue)%H%Creset %s %C(cyan)(%an)%Creset%C(green)%d%Creset'
def git_config_get(option, default=None): def git_config_get(option, default=None):
@ -164,18 +168,36 @@ def tree_sha512sum(commit='HEAD'):
return overall.hexdigest() return overall.hexdigest()
def get_acks_from_comments(head_commit, comments): def get_acks_from_comments(head_commit, comments):
assert len(head_commit) == 6 # Look for abbreviated commit id, because not everyone wants to type/paste
ack_str ='\n\nACKs for commit {}:\n'.format(head_commit) # the whole thing and the chance of collisions within a PR is small enough
head_abbrev = head_commit[0:6]
acks = []
for c in comments: for c in comments:
review = [l for l in c['body'].split('\r\n') if 'ACK' in l and head_commit in l] review = [l for l in c['body'].split('\r\n') if 'ACK' in l and head_abbrev in l]
if review: if review:
ack_str += ' {}:\n'.format(c['user']['login']) acks.append((c['user']['login'], review[0]))
ack_str += ' {}\n'.format(review[0]) return acks
def make_acks_message(head_commit, acks):
if acks:
ack_str ='\n\nACKs for top commit:\n'.format(head_commit)
for name, msg in acks:
ack_str += ' {}:\n'.format(name)
ack_str += ' {}\n'.format(msg)
else:
ack_str ='\n\nTop commit has no ACKs.\n'
return ack_str return ack_str
def print_merge_details(pull, title, branch, base_branch, head_branch): def print_merge_details(pull, title, branch, base_branch, head_branch, acks):
print('%s#%s%s %s %sinto %s%s' % (ATTR_RESET+ATTR_PR,pull,ATTR_RESET,title,ATTR_RESET+ATTR_PR,branch,ATTR_RESET)) print('%s#%s%s %s %sinto %s%s' % (ATTR_RESET+ATTR_PR,pull,ATTR_RESET,title,ATTR_RESET+ATTR_PR,branch,ATTR_RESET))
subprocess.check_call([GIT,'log','--graph','--topo-order','--pretty=format:'+COMMIT_FORMAT,base_branch+'..'+head_branch]) subprocess.check_call([GIT,'log','--graph','--topo-order','--pretty=format:'+COMMIT_FORMAT,base_branch+'..'+head_branch])
if acks is not None:
if acks:
print('{}ACKs:{}'.format(ATTR_PR, ATTR_RESET))
for (name, message) in acks:
print('* {} {}({}){}'.format(message, ATTR_NAME, name, ATTR_RESET))
else:
print('{}Top commit has no ACKs!{}'.format(ATTR_WARN, ATTR_RESET))
def parse_arguments(): def parse_arguments():
epilog = ''' epilog = '''
@ -225,9 +247,6 @@ def main():
info = retrieve_pr_info(repo,pull,ghtoken) info = retrieve_pr_info(repo,pull,ghtoken)
if info is None: if info is None:
sys.exit(1) sys.exit(1)
comments = retrieve_pr_comments(repo,pull,ghtoken) + retrieve_pr_reviews(repo,pull,ghtoken)
if comments is None:
sys.exit(1)
title = info['title'].strip() title = info['title'].strip()
body = info['body'].strip() body = info['body'].strip()
# precedence order for destination branch argument: # precedence order for destination branch argument:
@ -257,6 +276,8 @@ def main():
sys.exit(3) sys.exit(3)
try: try:
subprocess.check_call([GIT,'log','-q','-1','refs/heads/'+head_branch], stdout=devnull, stderr=stdout) subprocess.check_call([GIT,'log','-q','-1','refs/heads/'+head_branch], stdout=devnull, stderr=stdout)
head_commit = subprocess.check_output([GIT,'log','-1','--pretty=format:%H',head_branch]).decode('utf-8')
assert len(head_commit) == 40
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
print("ERROR: Cannot find head of pull request #%s on %s." % (pull,host_repo), file=stderr) print("ERROR: Cannot find head of pull request #%s on %s." % (pull,host_repo), file=stderr)
sys.exit(3) sys.exit(3)
@ -281,7 +302,6 @@ def main():
message = firstline + '\n\n' message = firstline + '\n\n'
message += subprocess.check_output([GIT,'log','--no-merges','--topo-order','--pretty=format:%H %s (%an)',base_branch+'..'+head_branch]).decode('utf-8') message += subprocess.check_output([GIT,'log','--no-merges','--topo-order','--pretty=format:%H %s (%an)',base_branch+'..'+head_branch]).decode('utf-8')
message += '\n\nPull request description:\n\n ' + body.replace('\n', '\n ') + '\n' message += '\n\nPull request description:\n\n ' + body.replace('\n', '\n ') + '\n'
message += get_acks_from_comments(head_commit=subprocess.check_output([GIT,'log','-1','--pretty=format:%H',head_branch]).decode('utf-8')[:6], comments=comments)
try: try:
subprocess.check_call([GIT,'merge','-q','--commit','--no-edit','--no-ff','--no-gpg-sign','-m',message.encode('utf-8'),head_branch]) subprocess.check_call([GIT,'merge','-q','--commit','--no-edit','--no-ff','--no-gpg-sign','-m',message.encode('utf-8'),head_branch])
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
@ -299,20 +319,14 @@ def main():
if len(symlink_files) > 0: if len(symlink_files) > 0:
sys.exit(4) sys.exit(4)
# Put tree SHA512 into the message # Compute SHA512 of git tree (to be able to detect changes before sign-off)
try: try:
first_sha512 = tree_sha512sum() first_sha512 = tree_sha512sum()
message += '\n\nTree-SHA512: ' + first_sha512
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
print("ERROR: Unable to compute tree hash") print("ERROR: Unable to compute tree hash")
sys.exit(4) sys.exit(4)
try:
subprocess.check_call([GIT,'commit','--amend','--no-gpg-sign','-m',message.encode('utf-8')])
except subprocess.CalledProcessError:
print("ERROR: Cannot update message.", file=stderr)
sys.exit(4)
print_merge_details(pull, title, branch, base_branch, head_branch) print_merge_details(pull, title, branch, base_branch, head_branch, None)
print() print()
# Run test command if configured. # Run test command if configured.
@ -345,8 +359,24 @@ def main():
print("ERROR: Tree hash changed unexpectedly",file=stderr) print("ERROR: Tree hash changed unexpectedly",file=stderr)
sys.exit(8) sys.exit(8)
# Retrieve PR comments and ACKs and add to commit message, store ACKs to print them with commit
# description
comments = retrieve_pr_comments(repo,pull,ghtoken) + retrieve_pr_reviews(repo,pull,ghtoken)
if comments is None:
print("ERROR: Could not fetch PR comments and reviews",file=stderr)
sys.exit(1)
acks = get_acks_from_comments(head_commit=head_commit, comments=comments)
message += make_acks_message(head_commit=head_commit, acks=acks)
# end message with SHA512 tree hash, then update message
message += '\n\nTree-SHA512: ' + first_sha512
try:
subprocess.check_call([GIT,'commit','--amend','--no-gpg-sign','-m',message.encode('utf-8')])
except subprocess.CalledProcessError:
print("ERROR: Cannot update message.", file=stderr)
sys.exit(4)
# Sign the merge commit. # Sign the merge commit.
print_merge_details(pull, title, branch, base_branch, head_branch) print_merge_details(pull, title, branch, base_branch, head_branch, acks)
while True: while True:
reply = ask_prompt("Type 's' to sign off on the above merge, or 'x' to reject and exit.").lower() reply = ask_prompt("Type 's' to sign off on the above merge, or 'x' to reject and exit.").lower()
if reply == 's': if reply == 's':