mirror of
git://nv-tegra.nvidia.com/linux-nvgpu.git
synced 2025-12-22 09:12:24 +03:00
Add regex rule to match patch number, e.g ./rfr 2279114 ./rfr https://git-master.nvidia.com/r/c/linux-nvgpu/+/2279114 Would be equivalent. Change-Id: I3ffa5f233951d5e4d404517362e984c7a04b9849 Signed-off-by: Thomas Fleury <tfleury@nvidia.com> Reviewed-on: https://git-master.nvidia.com/r/c/linux-nvgpu/+/2283726 Reviewed-by: Automatic_Commit_Validation_User Reviewed-by: Alex Waterman <alexw@nvidia.com> Reviewed-by: Vinod Gopalakrishnakurup <vinodg@nvidia.com> Reviewed-by: mobile promotions <svcmobile_promotions@nvidia.com> Tested-by: mobile promotions <svcmobile_promotions@nvidia.com> GVS: Gerrit_Virtual_Submit
510 lines
17 KiB
Python
Executable File
510 lines
17 KiB
Python
Executable File
#!/usr/bin/python
|
|
#
|
|
# Copyright (c) 2018-2020, NVIDIA CORPORATION. All Rights Reserved.
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
# copy of this software and associated documentation files (the "Software"),
|
|
# to deal in the Software without restriction, including without limitation
|
|
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
# and/or sell copies of the Software, and to permit persons to whom the
|
|
# Software is furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
# DEALINGS IN THE SOFTWARE.
|
|
#
|
|
# Simple script for formatting RFR messages for nvgpu changes.
|
|
#
|
|
|
|
import os
|
|
import re
|
|
import json
|
|
import argparse
|
|
import tempfile
|
|
import subprocess
|
|
import smtplib
|
|
|
|
from email.mime.text import MIMEText
|
|
|
|
from rfr_addrbook import rfr_ab_load
|
|
from rfr_addrbook import rfr_ab_query
|
|
from rfr_addrbook import rfr_ab_lookup
|
|
|
|
VERSION = '1.1.0'
|
|
|
|
# Default email address.
|
|
to_addr = 'sw-mobile-nvgpu-core <sw-mobile-nvgpu-core@exchange.nvidia.com>'
|
|
|
|
# Gerrit commit URL formats. These are regular expressions to match the
|
|
# incoming URLs against.
|
|
gr_fmts = [ r'(\d+)',
|
|
r'http://git-master/r/(\d+)',
|
|
r'http://git-master/r/#/c/(\d+)/',
|
|
r'http://git-master.nvidia.com/r/(\d+)',
|
|
r'http://git-master.nvidia.com/r/#/c/(\d+)/',
|
|
r'https://git-master/r/(\d+)',
|
|
r'https://git-master/r/#/c/(\d+)/',
|
|
r'https://git-master.nvidia.com/r/(\d+)',
|
|
r'https://git-master.nvidia.com/r/#/c/(\d+)',
|
|
r'https://git-master.nvidia.com/r/#/c/(\d+)/',
|
|
r'https://git-master.nvidia.com/r/c/(?:[\w\-_\/]+?)\+/(\d+)' ]
|
|
|
|
# The user to use. May be overridden but the default comes from the environment.
|
|
user = os.environ['USER']
|
|
|
|
# Gerrit query command to obtain the patch URL. The substitution will be the
|
|
# gerrit Change-ID parsed from the git commit message.
|
|
gr_query_cmd = 'ssh %s@git-master -p 29418 gerrit query --format json --current-patch-set --files %s'
|
|
|
|
def parse_args():
|
|
"""
|
|
Parse arguments to rfr.
|
|
"""
|
|
|
|
ep="""This program will format commit messages into something that can be
|
|
sent to the nvgpu mailing list for review.
|
|
"""
|
|
help_msg="""Git or gerrit commits to describe. Can be either a git or gerrit
|
|
commit ID. If the ID starts with a 'I' then it will be treated as a gerrit ID.
|
|
If the commit ID looks like a gerrit URL then it is treated as a gerrit URL.
|
|
Otherwise it's treated as a git commit ID.
|
|
"""
|
|
parser = argparse.ArgumentParser(description='RFR formatting tool',
|
|
epilog=ep)
|
|
|
|
parser.add_argument('-V', '--version', action='store_true', default=False,
|
|
help='print the program version')
|
|
parser.add_argument('-m', '--msg', action='store', default=None,
|
|
help='Custom message to add to the RFR email')
|
|
parser.add_argument('-N', '--no-msg', action='store_true', default=False,
|
|
help='Force no message in output.')
|
|
parser.add_argument('-e', '--email', action='store_true', default=False,
|
|
help='Use direct email to post the RFR.')
|
|
parser.add_argument('-s', '--subject', action='store', default=None,
|
|
help='Specify the email\'s subject.')
|
|
parser.add_argument('-R', '--sender', action='store', default=None,
|
|
help='Specify a special from: email address.')
|
|
parser.add_argument('-t', '--to', action='append', default=[],
|
|
help='Specify an additional To: email address.'
|
|
'Can be repeated (-t a -t b).')
|
|
parser.add_argument('-c', '--cc', action='append', default=[],
|
|
help='Specify an additional Cc: email address.'
|
|
'Can be repeated (-c a -c b).')
|
|
parser.add_argument('-F', '--file', action='store', default=None,
|
|
help='File with commits, one per line')
|
|
parser.add_argument('-q', '--query-addrbook', action='store', default=None,
|
|
nargs='?', const='.', metavar='regex',
|
|
help='Regex query for address book')
|
|
parser.add_argument('-a', '--addrbook', action='store', default=None,
|
|
metavar='addrbook',
|
|
help='Specify location to look for address book')
|
|
parser.add_argument('-I', '--ignore-addrbook', action='store_true',
|
|
default=False,
|
|
help='Ignore address book lookup. Default is false.')
|
|
parser.add_argument('--no-default-addr', action='store_true',
|
|
default=False,
|
|
help='Do not use the default address.')
|
|
|
|
# Positionals: the gerrit URLs.
|
|
parser.add_argument('commits', metavar='Commit-IDs',
|
|
nargs='*',
|
|
help=help_msg)
|
|
|
|
arg_parser = parser.parse_args()
|
|
|
|
return arg_parser
|
|
|
|
def get_gerrit_url_id(cmt):
|
|
"""
|
|
Determines if the passed cmt is a gerrit commit URL. If it is then this
|
|
returns the URL ID; otherwise it returns None.
|
|
"""
|
|
|
|
for fmt in gr_fmts:
|
|
p = re.compile(fmt)
|
|
m = p.search(cmt)
|
|
if m:
|
|
return m.group(1)
|
|
|
|
return None
|
|
|
|
def read_commit_file(filp):
|
|
"""
|
|
Read a file full of commits and return a list of those commits.
|
|
"""
|
|
|
|
commits = [ ]
|
|
|
|
for line in filp:
|
|
|
|
line = line.strip()
|
|
|
|
# Skip empty lines and lines that start with a '#'.
|
|
if line.find('#') == 0 or line == '':
|
|
continue
|
|
|
|
# Otherwise append the line to the list of commits. This will append
|
|
# all the text, so in cases like:
|
|
#
|
|
# http://git-master/r/1540705 - my commit
|
|
#
|
|
# Anything after the first space in the line is ignored. This lets you
|
|
# add some descriptive text after the commit.
|
|
commits.append(line.split()[0])
|
|
|
|
return commits
|
|
|
|
def gerrit_query(change):
|
|
"""
|
|
Query gerrit for the JSON change information. Return a python object
|
|
describing the JSON data.
|
|
|
|
change can either be a Change-Id or the numeric change number from a URL.
|
|
|
|
Note there is an interesting limitation with this: gerrit can have multiple
|
|
changes with the same Change-Id (./sigh). So if you query a change ID that
|
|
points to multiple changes you get back all of them.
|
|
|
|
This script just uses the first. Ideally one could filter by branch or by
|
|
some other distinguishing factor.
|
|
"""
|
|
|
|
query_cmd = gr_query_cmd % (user, change)
|
|
|
|
prog = subprocess.Popen(query_cmd, shell=True,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
|
|
stdout_data, stderr_data = prog.communicate()
|
|
if prog.returncode != 0:
|
|
print('`%s\' failed!' % query_cmd)
|
|
return False
|
|
|
|
commit = json.loads(stdout_data.decode('utf-8').splitlines()[0])
|
|
if 'id' not in commit:
|
|
print('%s is not a gerrit commit!?' % change)
|
|
print('Most likely you need to push the change.')
|
|
return None
|
|
|
|
return commit
|
|
|
|
def commit_info_from_gerrit_change_id(change_id):
|
|
"""
|
|
Return a dict with all the gerrit info from a gerrit change ID.
|
|
"""
|
|
|
|
return gerrit_query(change_id)
|
|
|
|
def commit_info_from_git_sha1(rev):
|
|
"""
|
|
Return a dict with all the gerrit info from a git sha1 rev.
|
|
"""
|
|
|
|
return gerrit_query(rev)
|
|
|
|
def commit_info_from_gerrit_cl(cmt):
|
|
"""
|
|
Return a dict with all the gerrit info from a Gerrit URL.
|
|
"""
|
|
|
|
cl = get_gerrit_url_id(cmt)
|
|
if not cl:
|
|
return None
|
|
|
|
return gerrit_query(cl)
|
|
|
|
def git_sha1_from_commit(commit_ish):
|
|
"""
|
|
Return sha1 revision from a commit-ish string, or None if this doesn't
|
|
appear to be a commit.
|
|
"""
|
|
|
|
prog = subprocess.Popen('git rev-parse %s' % commit_ish, shell=True,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
|
|
stdout_data, stderr_data = prog.communicate()
|
|
if prog.returncode != 0:
|
|
print('`git rev-parse %s\' failed?!' % commit_ish)
|
|
return None
|
|
|
|
return stdout_data.decode('utf-8')
|
|
|
|
def get_user_message(start_msg):
|
|
"""
|
|
Get a user message from the user. This is done with the default text
|
|
editor.
|
|
"""
|
|
|
|
editor = os.environ.get('EDITOR', 'vim')
|
|
|
|
with tempfile.NamedTemporaryFile(suffix=".tmp") as filp:
|
|
filp.write(start_msg)
|
|
filp.flush()
|
|
|
|
edit_cmd = editor + ' ' + filp.name
|
|
|
|
try:
|
|
subprocess.call(edit_cmd.split())
|
|
except:
|
|
print('Failed to run editor: `%s\'' % edit_cmd)
|
|
return None
|
|
|
|
filp.seek(0)
|
|
lines = filp.readlines()
|
|
|
|
return ''.join(filter(lambda l: not l.startswith('#'), lines))
|
|
|
|
def indent_lines(text, ind):
|
|
"""
|
|
Prepend each new line in the passed text with ind.
|
|
"""
|
|
return ''.join(ind + l + '\n' for l in text.splitlines())
|
|
|
|
def format_commits(commits_info, extra_message):
|
|
"""
|
|
Takes a list of the commit info objects and returns a string with the
|
|
text to print. This can either be directly printed or emailed.
|
|
"""
|
|
|
|
whole_template = """Hi All,
|
|
|
|
I would like you to review the following changes.
|
|
{extra_message}
|
|
{cmt_descriptions}
|
|
Thanks!
|
|
{cmt_verbose}"""
|
|
|
|
cmt_template = """
|
|
+----------------------------------------
|
|
| {url}
|
|
| Author: {author}
|
|
|
|
{cmtmsg}
|
|
{files}
|
|
"""
|
|
|
|
cmt_descriptions = ''
|
|
for c in commits_info:
|
|
cmt_descriptions += " %s - %s\n" % (c['url'], c['subject'])
|
|
|
|
# Add new lines around the extra_message, if applicable. Otherwise we don't
|
|
# want anything to show up for extra_message.
|
|
if extra_message:
|
|
extra_message = '\n%s\n' % extra_message
|
|
else:
|
|
extra_message = ''
|
|
|
|
cmt_verbose = ''
|
|
for c in commits_info:
|
|
# note: x['deletions'] is negative (or zero)
|
|
files = c['currentPatchSet']['files']
|
|
file_widths = [
|
|
len(x['file']) for x in files if x['file'] != u'/COMMIT_MSG']
|
|
|
|
# For merge commits there are no files in the commit itself. So this
|
|
# causes the below max() operation to choke. Given that there are no
|
|
# files it makes little sense for us to try and fill in any file
|
|
# details!
|
|
if file_widths:
|
|
width = max(file_widths)
|
|
file_template = ("{name: <{width}} | " +
|
|
"{sum:<3} (+{adds: <3} -{dels: <3})")
|
|
files_changed_desc = [file_template.format(
|
|
name=x['file'],
|
|
width=width,
|
|
sum=x['insertions'] - x['deletions'],
|
|
adds=x['insertions'],
|
|
dels=-x['deletions'])
|
|
for x in files if x['file'] != u'/COMMIT_MSG'
|
|
]
|
|
else:
|
|
files_changed_desc = ''
|
|
|
|
cmt_verbose += cmt_template.format(url=c['url'],
|
|
author=c['owner']['name'],
|
|
cmtmsg=indent_lines(
|
|
c['commitMessage'], ' '),
|
|
files=' ' + "\n ".join(
|
|
files_changed_desc))
|
|
|
|
return whole_template.format(cmt_descriptions=cmt_descriptions,
|
|
extra_message=extra_message,
|
|
cmt_verbose=cmt_verbose)
|
|
|
|
def format_msg_comment(subject, to_addrs, cc_addrs):
|
|
"""
|
|
Take the list of to_addrs and format them into a comment so that the sender
|
|
knows who they are sending an email to.
|
|
"""
|
|
|
|
msg_comment = """# RFR commit editor
|
|
#
|
|
# Lines that begin with a '#' are comments and will be ignored in the final
|
|
# message. '#' characters that appear else where in the line will be ignored.
|
|
# White space is not stripped from lines so, for example, ' # blah' will not
|
|
# be ignored!
|
|
#
|
|
"""
|
|
|
|
msg_comment += '# Subject: %s\n' % subject
|
|
|
|
for addr in to_addrs:
|
|
msg_comment += '# To: %s\n' % addr
|
|
for addr in cc_addrs:
|
|
msg_comment += '# Cc: %s\n' % addr
|
|
|
|
return msg_comment
|
|
|
|
def display_commits(commits_info, extra_message):
|
|
"""
|
|
Print the list of commits to the directly to the terminal.
|
|
"""
|
|
|
|
print(format_commits(commits_info, extra_message))
|
|
|
|
def email_commits(commits_info, sender, subject, args):
|
|
"""
|
|
Directly email commits to the nvgpu-core mailing list for review!
|
|
"""
|
|
|
|
# Lets you drop nvgpu-core from the to-addr if you desire. You can then
|
|
# add it back to CC if you wish.
|
|
if not args.no_default_addr:
|
|
args.to.append(to_addr)
|
|
|
|
to_addrs = rfr_ab_lookup(args.to, args.ignore_addrbook)
|
|
if to_addrs == None:
|
|
print('Junk address: aborting!')
|
|
print('Use `-I\' to ignore.')
|
|
return
|
|
cc_addrs = rfr_ab_lookup(args.cc, args.ignore_addrbook)
|
|
if cc_addrs == None:
|
|
print('Junk address: aborting!')
|
|
print('Use `-I\' to ignore.')
|
|
return
|
|
|
|
if args.no_msg:
|
|
args.msg = None
|
|
|
|
body = format_msg_comment(subject, to_addrs, cc_addrs) + \
|
|
format_commits(commits_info, args.msg)
|
|
|
|
# Most people like to write a little description of their patch series
|
|
# in the email body. This aims to take care of that.
|
|
#
|
|
# If arg_parser.msg is set then we can just use that. If there's no
|
|
# args.msg field then we will try to fire up a text editor and let the
|
|
# user write something we will copy into the email the same way as
|
|
# args.msg. This makes it easier to a write paragraph - passing that much
|
|
# text on the CLI is annoying!
|
|
#
|
|
# However, if the user decides no message is a must then they can use
|
|
# the '--no-msg' argument to supress this message. This arg will also
|
|
# supress a message supplied with '--msg'.
|
|
|
|
if not args.msg and not args.no_msg:
|
|
text = get_user_message(body)
|
|
|
|
if not text or text.strip() == '':
|
|
print 'Empty user message: aborting!'
|
|
return
|
|
|
|
msg = MIMEText(text)
|
|
else:
|
|
msg = MIMEText(body)
|
|
|
|
msg['To'] = ", ".join(to_addrs)
|
|
msg['Cc'] = ", ".join(cc_addrs)
|
|
msg['From'] = sender
|
|
msg['Subject'] = subject
|
|
|
|
s = smtplib.SMTP('mail.nvidia.com')
|
|
s.sendmail(sender,
|
|
to_addrs,
|
|
msg.as_string())
|
|
s.quit()
|
|
|
|
def main():
|
|
"""
|
|
The magic happens here.
|
|
"""
|
|
|
|
arg_parser = parse_args()
|
|
commits_info = [ ]
|
|
|
|
if arg_parser.version:
|
|
print('Version: %s' % VERSION)
|
|
exit(0)
|
|
|
|
if arg_parser.addrbook:
|
|
success = rfr_ab_load(arg_parser.addrbook)
|
|
if not success:
|
|
exit(1)
|
|
else:
|
|
rfr_ab_load(None)
|
|
|
|
if arg_parser.query_addrbook:
|
|
rfr_ab_query(arg_parser.query_addrbook)
|
|
exit(0)
|
|
|
|
if arg_parser.file:
|
|
filp = open(arg_parser.file, 'r')
|
|
|
|
if arg_parser.commits:
|
|
arg_parser.commits.extend(read_commit_file(filp))
|
|
else:
|
|
arg_parser.commits = read_commit_file(filp)
|
|
|
|
# Oops: no commits?
|
|
if not arg_parser.commits or len(arg_parser.commits) == 0:
|
|
print 'No commits!'
|
|
exit(-1)
|
|
|
|
# Builds a dictionary of Gerrit Change-Ids. From the Change-Ids we can then
|
|
# get the commit message and URL.
|
|
#
|
|
# This also builds an array of those same commit IDs to track the ordering
|
|
# of the commits so that the user can choose the order of the patches based
|
|
# on the order in which they pass the commits.
|
|
for cmt in arg_parser.commits:
|
|
|
|
if cmt[0] == 'I':
|
|
info = commit_info_from_gerrit_change_id(cmt)
|
|
elif get_gerrit_url_id(cmt):
|
|
info = commit_info_from_gerrit_cl(cmt)
|
|
else:
|
|
info = commit_info_from_git_sha1(git_sha1_from_commit(cmt))
|
|
|
|
if info:
|
|
commits_info.append(info)
|
|
else:
|
|
print('Warning: \'%s\' doesn\'t appear to be a commit!' % cmt)
|
|
|
|
if arg_parser.email:
|
|
# If no subject was specified then use the first commit subject as
|
|
# the subject.
|
|
subject = arg_parser.subject
|
|
if not subject:
|
|
subject = '[RFR] %s' % commits_info[0]['subject']
|
|
|
|
# If no specified sender then use the name from the environment as
|
|
# the sender (plus @nvidia.com). This arg is primarily useful for
|
|
# people who do not have a username for the Linux machine that matches
|
|
# their SSO ID.
|
|
sender = arg_parser.sender
|
|
if not sender:
|
|
sender = '%s@nvidia.com' % user
|
|
|
|
email_commits(commits_info, sender, subject, arg_parser)
|
|
else:
|
|
display_commits(commits_info, arg_parser.msg)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|