patman: add '--get-maintainer-script' argument

This makes it possible to configure a project to use some other
location or script than the default scripts/get_maintainer.pl one used
in the U-Boot and Linux projects. It can be configured via a .patman
configuration file and accepts arguments, as documented.

Reviewed-by: Simon Glass <sjg@chromium.org>
Signed-off-by: Maxim Cournoyer <maxim.cournoyer@savoirfairelinux.com>
This commit is contained in:
Maxim Cournoyer 2022-12-20 00:28:46 -05:00 committed by Simon Glass
parent 8f8d3f72f2
commit 8c042fb7f9
7 changed files with 127 additions and 40 deletions

View File

@ -21,6 +21,7 @@ if __name__ == "__main__":
# Our modules # Our modules
from patman import control from patman import control
from patman import func_test from patman import func_test
from patman import gitutil
from patman import project from patman import project
from patman import settings from patman import settings
from patman import terminal from patman import terminal
@ -64,6 +65,12 @@ send.add_argument('-l', '--limit-cc', dest='limit', type=int, default=None,
send.add_argument('-m', '--no-maintainers', action='store_false', send.add_argument('-m', '--no-maintainers', action='store_false',
dest='add_maintainers', default=True, dest='add_maintainers', default=True,
help="Don't cc the file maintainers automatically") help="Don't cc the file maintainers automatically")
send.add_argument(
'--get-maintainer-script', dest='get_maintainer_script', type=str,
action='store',
default=os.path.join(gitutil.get_top_level(), 'scripts',
'get_maintainer.pl') + ' --norolestats',
help='File name of the get_maintainer.pl (or compatible) script.')
send.add_argument('-n', '--dry-run', action='store_true', dest='dry_run', send.add_argument('-n', '--dry-run', action='store_true', dest='dry_run',
default=False, help="Do a dry run (create but don't email patches)") default=False, help="Do a dry run (create but don't email patches)")
send.add_argument('-r', '--in-reply-to', type=str, action='store', send.add_argument('-r', '--in-reply-to', type=str, action='store',

View File

@ -94,8 +94,8 @@ def check_patches(series, patch_files, run_checkpatch, verbose, use_tree):
def email_patches(col, series, cover_fname, patch_files, process_tags, its_a_go, def email_patches(col, series, cover_fname, patch_files, process_tags, its_a_go,
ignore_bad_tags, add_maintainers, limit, dry_run, in_reply_to, ignore_bad_tags, add_maintainers, get_maintainer_script, limit,
thread, smtp_server): dry_run, in_reply_to, thread, smtp_server):
"""Email patches to the recipients """Email patches to the recipients
This emails out the patches and cover letter using 'git send-email'. Each This emails out the patches and cover letter using 'git send-email'. Each
@ -123,6 +123,8 @@ def email_patches(col, series, cover_fname, patch_files, process_tags, its_a_go,
ignore_bad_tags (bool): True to just print a warning for unknown tags, ignore_bad_tags (bool): True to just print a warning for unknown tags,
False to halt with an error False to halt with an error
add_maintainers (bool): Run the get_maintainer.pl script for each patch add_maintainers (bool): Run the get_maintainer.pl script for each patch
get_maintainer_script (str): The script used to retrieve which
maintainers to cc
limit (int): Limit on the number of people that can be cc'd on a single limit (int): Limit on the number of people that can be cc'd on a single
patch or the cover letter (None if no limit) patch or the cover letter (None if no limit)
dry_run (bool): Don't actually email the patches, just print out what dry_run (bool): Don't actually email the patches, just print out what
@ -134,7 +136,7 @@ def email_patches(col, series, cover_fname, patch_files, process_tags, its_a_go,
smtp_server (str): SMTP server to use to send patches (None for default) smtp_server (str): SMTP server to use to send patches (None for default)
""" """
cc_file = series.MakeCcFile(process_tags, cover_fname, not ignore_bad_tags, cc_file = series.MakeCcFile(process_tags, cover_fname, not ignore_bad_tags,
add_maintainers, limit) add_maintainers, limit, get_maintainer_script)
# Email the patches out (giving the user time to check / cancel) # Email the patches out (giving the user time to check / cancel)
cmd = '' cmd = ''
@ -174,8 +176,8 @@ def send(args):
email_patches( email_patches(
col, series, cover_fname, patch_files, args.process_tags, col, series, cover_fname, patch_files, args.process_tags,
its_a_go, args.ignore_bad_tags, args.add_maintainers, its_a_go, args.ignore_bad_tags, args.add_maintainers,
args.limit, args.dry_run, args.in_reply_to, args.thread, args.get_maintainer_script, args.limit, args.dry_run,
args.smtp_server) args.in_reply_to, args.thread, args.smtp_server)
def patchwork_status(branch, count, start, end, dest_branch, force, def patchwork_status(branch, count, start, end, dest_branch, force,
show_comments, url): show_comments, url):

View File

@ -6,6 +6,7 @@
"""Functional tests for checking that patman behaves correctly""" """Functional tests for checking that patman behaves correctly"""
import contextlib
import os import os
import pathlib import pathlib
import re import re
@ -29,8 +30,19 @@ from patman.test_util import capture_sys_output
import pygit2 import pygit2
from patman import status from patman import status
PATMAN_DIR = pathlib.Path(__file__).parent
TEST_DATA_DIR = PATMAN_DIR / 'test/'
TEST_DATA_DIR = pathlib.Path(__file__).parent / 'test/'
@contextlib.contextmanager
def directory_excursion(directory):
"""Change directory to `directory` for a limited to the context block."""
current = os.getcwd()
try:
os.chdir(directory)
yield
finally:
os.chdir(current)
class TestFunctional(unittest.TestCase): class TestFunctional(unittest.TestCase):
@ -204,6 +216,8 @@ class TestFunctional(unittest.TestCase):
text = self._get_text('test01.txt') text = self._get_text('test01.txt')
series = patchstream.get_metadata_for_test(text) series = patchstream.get_metadata_for_test(text)
cover_fname, args = self._create_patches_for_test(series) cover_fname, args = self._create_patches_for_test(series)
get_maintainer_script = str(pathlib.Path(__file__).parent.parent.parent
/ 'get_maintainer.pl') + ' --norolestats'
with capture_sys_output() as out: with capture_sys_output() as out:
patchstream.fix_patches(series, args) patchstream.fix_patches(series, args)
if cover_fname and series.get('cover'): if cover_fname and series.get('cover'):
@ -211,7 +225,7 @@ class TestFunctional(unittest.TestCase):
series.DoChecks() series.DoChecks()
cc_file = series.MakeCcFile(process_tags, cover_fname, cc_file = series.MakeCcFile(process_tags, cover_fname,
not ignore_bad_tags, add_maintainers, not ignore_bad_tags, add_maintainers,
None) None, get_maintainer_script)
cmd = gitutil.email_patches( cmd = gitutil.email_patches(
series, cover_fname, args, dry_run, not ignore_bad_tags, series, cover_fname, args, dry_run, not ignore_bad_tags,
cc_file, in_reply_to=in_reply_to, thread=None) cc_file, in_reply_to=in_reply_to, thread=None)
@ -506,6 +520,37 @@ complicated as possible''')
finally: finally:
os.chdir(orig_dir) os.chdir(orig_dir)
def test_custom_get_maintainer_script(self):
"""Validate that a custom get_maintainer script gets used."""
self.make_git_tree()
with directory_excursion(self.gitdir):
# Setup git.
os.environ['GIT_CONFIG_GLOBAL'] = '/dev/null'
os.environ['GIT_CONFIG_SYSTEM'] = '/dev/null'
tools.run('git', 'config', 'user.name', 'Dummy')
tools.run('git', 'config', 'user.email', 'dumdum@dummy.com')
tools.run('git', 'branch', 'upstream')
tools.run('git', 'branch', '--set-upstream-to=upstream')
tools.run('git', 'add', '.')
tools.run('git', 'commit', '-m', 'new commit')
# Setup patman configuration.
with open('.patman', 'w', buffering=1) as f:
f.write('[settings]\n'
'get_maintainer_script: dummy-script.sh\n'
'check_patch: False\n')
with open('dummy-script.sh', 'w', buffering=1) as f:
f.write('#!/usr/bin/env python\n'
'print("hello@there.com")\n')
os.chmod('dummy-script.sh', 0x555)
# Finally, do the test
with capture_sys_output():
output = tools.run(PATMAN_DIR / 'patman', '--dry-run')
# Assert the email address is part of the dry-run
# output.
self.assertIn('hello@there.com', output)
def test_tags(self): def test_tags(self):
"""Test collection of tags in a patchstream""" """Test collection of tags in a patchstream"""
text = '''This is a patch text = '''This is a patch

View File

@ -1,48 +1,61 @@
# SPDX-License-Identifier: GPL-2.0+ # SPDX-License-Identifier: GPL-2.0+
# Copyright (c) 2012 The Chromium OS Authors. # Copyright (c) 2012 The Chromium OS Authors.
# Copyright (c) 2022 Maxim Cournoyer <maxim.cournoyer@savoirfairelinux.com>
# #
import os import os
import shlex
import shutil
from patman import command from patman import command
from patman import gitutil
def find_get_maintainer(try_list):
"""Look for the get_maintainer.pl script.
Args: def find_get_maintainer(script_file_name):
try_list: List of directories to try for the get_maintainer.pl script """Try to find where `script_file_name` is.
Returns: It searches in PATH and falls back to a path relative to the top
If the script is found we'll return a path to it; else None. of the current git repository.
""" """
# Look in the list get_maintainer = shutil.which(script_file_name)
for path in try_list: if get_maintainer:
fname = os.path.join(path, 'get_maintainer.pl') return get_maintainer
if os.path.isfile(fname):
return fname
return None git_relative_script = os.path.join(gitutil.get_top_level(),
script_file_name)
if os.path.exists(git_relative_script):
return git_relative_script
def get_maintainer(dir_list, fname, verbose=False):
"""Run get_maintainer.pl on a file if we find it.
We look for get_maintainer.pl in the 'scripts' directory at the top of def get_maintainer(script_file_name, fname, verbose=False):
git. If we find it we'll run it. If we don't find get_maintainer.pl """Run `script_file_name` on a file.
then we fail silently.
`script_file_name` should be a get_maintainer.pl-like script that
takes a patch file name as an input and return the email addresses
of the associated maintainers to standard output, one per line.
If `script_file_name` does not exist we fail silently.
Args: Args:
dir_list: List of directories to try for the get_maintainer.pl script script_file_name: The file name of the get_maintainer.pl script
fname: Path to the patch file to run get_maintainer.pl on. (or compatible).
fname: File name of the patch to process with get_maintainer.pl.
Returns: Returns:
A list of email addresses to CC to. A list of email addresses to CC to.
""" """
get_maintainer = find_get_maintainer(dir_list) # Expand `script_file_name` into a file name and its arguments, if
# any.
cmd_args = shlex.split(script_file_name)
file_name = cmd_args[0]
arguments = cmd_args[1:]
get_maintainer = find_get_maintainer(file_name)
if not get_maintainer: if not get_maintainer:
if verbose: if verbose:
print("WARNING: Couldn't find get_maintainer.pl") print("WARNING: Couldn't find get_maintainer.pl")
return [] return []
stdout = command.output(get_maintainer, '--norolestats', fname) stdout = command.output(get_maintainer, *arguments, fname)
lines = stdout.splitlines() lines = stdout.splitlines()
return [ x.replace('"', '') for x in lines ] return [x.replace('"', '') for x in lines]

View File

@ -433,7 +433,7 @@ def check_suppress_cc_config():
def email_patches(series, cover_fname, args, dry_run, warn_on_error, cc_fname, def email_patches(series, cover_fname, args, dry_run, warn_on_error, cc_fname,
self_only=False, alias=None, in_reply_to=None, thread=False, self_only=False, alias=None, in_reply_to=None, thread=False,
smtp_server=None): smtp_server=None, get_maintainer_script=None):
"""Email a patch series. """Email a patch series.
Args: Args:
@ -450,6 +450,7 @@ def email_patches(series, cover_fname, args, dry_run, warn_on_error, cc_fname,
thread: True to add --thread to git send-email (make thread: True to add --thread to git send-email (make
all patches reply to cover-letter or first patch in series) all patches reply to cover-letter or first patch in series)
smtp_server: SMTP server to use to send patches smtp_server: SMTP server to use to send patches
get_maintainer_script: File name of script to get maintainers emails
Returns: Returns:
Git command that was/would be run Git command that was/would be run

View File

@ -1,6 +1,7 @@
.. SPDX-License-Identifier: GPL-2.0+ .. SPDX-License-Identifier: GPL-2.0+
.. Copyright (c) 2011 The Chromium OS Authors .. Copyright (c) 2011 The Chromium OS Authors
.. Simon Glass <sjg@chromium.org> .. Simon Glass <sjg@chromium.org>
.. Maxim Cournoyer <maxim.cournoyer@savoirfairelinux.com>
.. v1, v2, 19-Oct-11 .. v1, v2, 19-Oct-11
.. revised v3 24-Nov-11 .. revised v3 24-Nov-11
.. revised v4 Independence Day 2020, with Patchwork integration .. revised v4 Independence Day 2020, with Patchwork integration
@ -68,8 +69,23 @@ this once::
git config sendemail.aliasesfile doc/git-mailrc git config sendemail.aliasesfile doc/git-mailrc
For both Linux and U-Boot the 'scripts/get_maintainer.pl' handles figuring For both Linux and U-Boot the 'scripts/get_maintainer.pl' handles
out where to send patches pretty well. figuring out where to send patches pretty well. For other projects,
you may want to specify a different script to be run, for example via
a project-specific `.patman` file::
# .patman configuration file at the root of some project
[settings]
get_maintainer_script: etc/teams.scm get-maintainer
The `get_maintainer_script` option corresponds to the
`--get-maintainer-script` argument of the `send` command. It is
looked relatively to the root of the current git repository, as well
as on PATH. It can also be provided arguments, as shown above. The
contract is that the script should accept a patch file name and return
a list of email addresses, one per line, like `get_maintainer.pl`
does.
During the first run patman creates a config file for you by taking the default During the first run patman creates a config file for you by taking the default
user name and email address from the global .gitconfig file. user name and email address from the global .gitconfig file.
@ -85,11 +101,11 @@ To add your own, create a file `~/.patman` like this::
wolfgang: Wolfgang Denk <wd@denx.de> wolfgang: Wolfgang Denk <wd@denx.de>
others: Mike Frysinger <vapier@gentoo.org>, Fred Bloggs <f.bloggs@napier.net> others: Mike Frysinger <vapier@gentoo.org>, Fred Bloggs <f.bloggs@napier.net>
Patman will also look for a `.patman` configuration file at the root As hinted above, Patman will also look for a `.patman` configuration
of the current project git repository, which makes it possible to file at the root of the current project git repository, which makes it
override the `project` settings variable or anything else in a possible to override the `project` settings variable or anything else
project-specific way. The values of this "local" configuration file in a project-specific way. The values of this "local" configuration
take precedence over those of the "global" one. file take precedence over those of the "global" one.
Aliases are recursive. Aliases are recursive.

View File

@ -235,7 +235,7 @@ class Series(dict):
print(col.build(col.RED, str)) print(col.build(col.RED, str))
def MakeCcFile(self, process_tags, cover_fname, warn_on_error, def MakeCcFile(self, process_tags, cover_fname, warn_on_error,
add_maintainers, limit): add_maintainers, limit, get_maintainer_script):
"""Make a cc file for us to use for per-commit Cc automation """Make a cc file for us to use for per-commit Cc automation
Also stores in self._generated_cc to make ShowActions() faster. Also stores in self._generated_cc to make ShowActions() faster.
@ -249,6 +249,8 @@ class Series(dict):
True/False to call the get_maintainers to CC maintainers True/False to call the get_maintainers to CC maintainers
List of maintainers to include (for testing) List of maintainers to include (for testing)
limit: Limit the length of the Cc list (None if no limit) limit: Limit the length of the Cc list (None if no limit)
get_maintainer_script: The file name of the get_maintainer.pl
script (or compatible).
Return: Return:
Filename of temp file created Filename of temp file created
""" """
@ -267,8 +269,9 @@ class Series(dict):
if type(add_maintainers) == type(cc): if type(add_maintainers) == type(cc):
cc += add_maintainers cc += add_maintainers
elif add_maintainers: elif add_maintainers:
dir_list = [os.path.join(gitutil.get_top_level(), 'scripts')]
cc += get_maintainer.get_maintainer(dir_list, commit.patch) cc += get_maintainer.get_maintainer(get_maintainer_script,
commit.patch)
for x in set(cc) & set(settings.bounces): for x in set(cc) & set(settings.bounces):
print(col.build(col.YELLOW, 'Skipping "%s"' % x)) print(col.build(col.YELLOW, 'Skipping "%s"' % x))
cc = list(set(cc) - set(settings.bounces)) cc = list(set(cc) - set(settings.bounces))