From e0ddd895f66a401dd69f561e7960609d7730e5ec Mon Sep 17 00:00:00 2001 From: Sean Anderson Date: Sun, 27 Nov 2022 16:55:02 -0500 Subject: patman: Switch to setuptools distutils is about to meet its demise [1]. Switch to setuptools. [1] https://peps.python.org/pep-0632/ Signed-off-by: Sean Anderson Reviewed-by: Simon Glass --- tools/patman/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/patman/setup.py b/tools/patman/setup.py index 43fdc00..5643bf1 100644 --- a/tools/patman/setup.py +++ b/tools/patman/setup.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: GPL-2.0+ -from distutils.core import setup +from setuptools import setup setup(name='patman', version='1.0', license='GPL-2.0+', -- cgit v1.1 From 7943ae241c099edc6d0cd7c4dd3c5ee18ecb24f7 Mon Sep 17 00:00:00 2001 From: Yuepeng Xing Date: Fri, 2 Dec 2022 14:23:07 +0800 Subject: test:dm:fix typo Fix typos in the 'test/dm' directory. Signed-off-by: Yuepeng Xing Reviewed-by: Simon Glass --- test/dm/blk.c | 2 +- test/dm/button.c | 4 ++-- test/dm/gpio.c | 4 ++-- test/dm/host.c | 2 +- test/dm/remoteproc.c | 2 +- test/dm/scmi.c | 2 +- test/dm/spmi.c | 2 +- test/dm/test-fdt.c | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/dm/blk.c b/test/dm/blk.c index 612f3ff..0aa04c6 100644 --- a/test/dm/blk.c +++ b/test/dm/blk.c @@ -127,7 +127,7 @@ static int dm_test_blk_devnum(struct unit_test_state *uts) /* * Probe the devices, with the first one being probed last. This is the - * one with no alias / sequence numnber. + * one with no alias / sequence number. */ ut_assertok(uclass_get_device(UCLASS_MMC, 1, &dev)); ut_assertok(uclass_get_device(UCLASS_MMC, 2, &dev)); diff --git a/test/dm/button.c b/test/dm/button.c index f8a7fab..e76c1ad 100644 --- a/test/dm/button.c +++ b/test/dm/button.c @@ -46,7 +46,7 @@ static int dm_test_button_gpio(struct unit_test_state *uts) struct udevice *dev, *gpio; /* - * Check that we can manipulate an BUTTON. BUTTON 1 is connected to GPIO + * Check that we can manipulate a BUTTON. BUTTON 1 is connected to GPIO * bank gpio_a, offset 3. */ ut_assertok(uclass_get_device(UCLASS_BUTTON, 1, &dev)); @@ -64,7 +64,7 @@ static int dm_test_button_gpio(struct unit_test_state *uts) } DM_TEST(dm_test_button_gpio, UT_TESTF_SCAN_PDATA | UT_TESTF_SCAN_FDT); -/* Test obtaining an BUTTON by label */ +/* Test obtaining a BUTTON by label */ static int dm_test_button_label(struct unit_test_state *uts) { struct udevice *dev, *cmp; diff --git a/test/dm/gpio.c b/test/dm/gpio.c index a8c35d4..0d88ec2 100644 --- a/test/dm/gpio.c +++ b/test/dm/gpio.c @@ -348,7 +348,7 @@ static int dm_test_gpio_phandles(struct unit_test_state *uts) ut_asserteq(-ENOENT, gpio_request_by_name(dev, "test-gpios", 5, &desc, 0)); - /* Last GPIO is ignord as it comes after <0> */ + /* Last GPIO is ignored as it comes after <0> */ ut_asserteq(3, gpio_request_list_by_name(dev, "test-gpios", desc_list, ARRAY_SIZE(desc_list), 0)); ut_asserteq(-EBUSY, gpio_request_list_by_name(dev, "test-gpios", @@ -377,7 +377,7 @@ static int dm_test_gpio_phandles(struct unit_test_state *uts) ut_asserteq(6, gpio_request_list_by_name(dev, "test2-gpios", desc_list, ARRAY_SIZE(desc_list), 0)); - /* This was set to output previously but flags resetted to 0 = INPUT */ + /* This was set to output previously but flags reset to 0 = INPUT */ ut_asserteq(0, sandbox_gpio_get_flags(gpio_a, 1)); ut_asserteq(GPIOF_INPUT, gpio_get_function(gpio_a, 1, NULL)); diff --git a/test/dm/host.c b/test/dm/host.c index 4dafc24..355ba77 100644 --- a/test/dm/host.c +++ b/test/dm/host.c @@ -132,7 +132,7 @@ static int dm_test_cmd_host(struct unit_test_state *uts) ut_assertok(run_commandf("host bind fat %s", filename2)); - /* Check it is not removeable (no '-r') */ + /* Check it is not removable (no '-r') */ ut_assertok(uclass_next_device_err(&dev)); ut_assertok(blk_get_from_parent(dev, &blk)); desc = dev_get_uclass_plat(blk); diff --git a/test/dm/remoteproc.c b/test/dm/remoteproc.c index b5e9f9d..7a8ff47 100644 --- a/test/dm/remoteproc.c +++ b/test/dm/remoteproc.c @@ -139,7 +139,7 @@ static int dm_test_remoteproc_elf(struct unit_test_state *uts) 0x20, 0x00, 0x00, 0x00, /* memsz = filesz */ 0x20, 0x00, 0x00, 0x00, - /* flags : readable and exectuable */ + /* flags : readable and executable */ 0x05, 0x00, 0x00, 0x00, /* padding */ 0x00, 0x00, 0x00, 0x00, diff --git a/test/dm/scmi.c b/test/dm/scmi.c index 795f207..93c7d08 100644 --- a/test/dm/scmi.c +++ b/test/dm/scmi.c @@ -6,7 +6,7 @@ * uclass devices probe when a SCMI server exposes resources. * * Note in test.dts the protocol@10 node in scmi node. Protocol 0x10 is not - * implemented in U-Boot SCMI components but the implementation is exepected + * implemented in U-Boot SCMI components but the implementation is expected * to not complain on unknown protocol IDs, as long as it is not used. Note * in test.dts tests that SCMI drivers probing does not fail for such an * unknown SCMI protocol ID. diff --git a/test/dm/spmi.c b/test/dm/spmi.c index 114fd2d..9cc284b 100644 --- a/test/dm/spmi.c +++ b/test/dm/spmi.c @@ -17,7 +17,7 @@ #include #include -/* Test if bus childs got probed propperly*/ +/* Test if bus children got probed properly*/ static int dm_test_spmi_probe(struct unit_test_state *uts) { const char *name = "spmi@0"; diff --git a/test/dm/test-fdt.c b/test/dm/test-fdt.c index 8bb868b..7cd2d04 100644 --- a/test/dm/test-fdt.c +++ b/test/dm/test-fdt.c @@ -592,7 +592,7 @@ static int dm_test_fdt_translation(struct unit_test_state *uts) ut_asserteq_str("dev@2,200", dev->name); ut_asserteq(0xA000, dev_read_addr(dev)); - /* No translation for busses with #size-cells == 0 */ + /* No translation for buses with #size-cells == 0 */ ut_assertok(uclass_find_device_by_seq(UCLASS_TEST_DUMMY, 3, &dev)); ut_asserteq_str("dev@42", dev->name); ut_asserteq(0x42, dev_read_addr(dev)); -- cgit v1.1 From 291ab6c74c6d9f063d10fa97e526dd9312c1b746 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Fri, 16 Dec 2022 20:45:27 -0500 Subject: patman: remove extraneous imports * tools/patman/main.py: Remove extraneous imports and fix indentation. Signed-off-by: Maxim Cournoyer Reviewed-by: Simon Glass --- tools/patman/main.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tools/patman/main.py b/tools/patman/main.py index 5a7756a..8067a28 100755 --- a/tools/patman/main.py +++ b/tools/patman/main.py @@ -9,7 +9,6 @@ from argparse import ArgumentParser import os import re -import shutil import sys import traceback @@ -19,7 +18,6 @@ if __name__ == "__main__": sys.path.append(os.path.join(our_path, '..')) # Our modules -from patman import command from patman import control from patman import gitutil from patman import project @@ -136,7 +134,6 @@ if not args.debug: # Run our meagre tests if args.cmd == 'test': - import doctest from patman import func_test result = test_util.run_test_suites( @@ -183,7 +180,7 @@ elif args.cmd == 'status': args.show_comments, args.patchwork_url) except Exception as e: terminal.tprint('patman: %s: %s' % (type(e).__name__, e), - colour=terminal.Color.RED) + colour=terminal.Color.RED) if args.debug: print() traceback.print_exc() -- cgit v1.1 From 425bbed247e3361f45f4c83fbfdb432a0bcc2892 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Fri, 16 Dec 2022 20:45:28 -0500 Subject: patman: fix installation of README.rst data file This fixes a regression introduced in commit 74df491051d6 ("buildman: Convert documentation to rST"). Signed-off-by: Maxim Cournoyer Reviewed-by: Simon Glass --- tools/patman/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/patman/setup.py b/tools/patman/setup.py index 5643bf1..2ff791d 100644 --- a/tools/patman/setup.py +++ b/tools/patman/setup.py @@ -7,6 +7,6 @@ setup(name='patman', scripts=['patman'], packages=['patman'], package_dir={'patman': ''}, - package_data={'patman': ['README']}, + package_data={'patman': ['README.rst']}, classifiers=['Environment :: Console', 'Topic :: Software Development']) -- cgit v1.1 From a5197fc9c3329e3559e79dabb5acde9d0dd9e102 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Fri, 16 Dec 2022 20:45:29 -0500 Subject: patman: locate README.rst via importlib Rationale: this is more robust than assumptions about the file hierarchy layout of the installation of patman, for example on non file-hierarchy standard (FHS) systems such as Guix System or Nix OS. Signed-off-by: Maxim Cournoyer Reviewed-by: Simon Glass --- tools/patman/main.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tools/patman/main.py b/tools/patman/main.py index 8067a28..2a2a7ea 100755 --- a/tools/patman/main.py +++ b/tools/patman/main.py @@ -7,6 +7,7 @@ """See README for more information""" from argparse import ArgumentParser +import importlib.resources import os import re import sys @@ -160,11 +161,8 @@ elif args.cmd == 'send': fd.close() elif args.full_help: - tools.print_full_help( - os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), - 'README.rst') - ) - + with importlib.resources.path('patman', 'README.rst') as readme: + tools.print_full_help(str(readme)) else: # If we are not processing tags, no need to warning about bad ones if not args.process_tags: -- cgit v1.1 From f9e20e3bff50722d76720a5cd2ce47252bfe553a Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Mon, 19 Dec 2022 17:32:38 -0500 Subject: patman: cosmetic: Fix PEP 8 warnings for the gitutil module. This patch fixes all the PEP 8 warnings reported by Pyflake for the gitutil module. Reviewed-by: Simon Glass Signed-off-by: Maxim Cournoyer --- tools/patman/gitutil.py | 106 +++++++++++++++++++++++++++++------------------- 1 file changed, 65 insertions(+), 41 deletions(-) diff --git a/tools/patman/gitutil.py b/tools/patman/gitutil.py index ceaf2ce..74c6e94 100644 --- a/tools/patman/gitutil.py +++ b/tools/patman/gitutil.py @@ -2,21 +2,19 @@ # Copyright (c) 2011 The Chromium OS Authors. # -import re import os -import subprocess import sys from patman import command from patman import settings from patman import terminal -from patman import tools # True to use --no-decorate - we check this in setup() use_no_decorate = True + def log_cmd(commit_range, git_dir=None, oneline=False, reverse=False, - count=None): + count=None): """Create a command to perform a 'git log' Args: @@ -49,6 +47,7 @@ def log_cmd(commit_range, git_dir=None, oneline=False, reverse=False, cmd.append('--') return cmd + def count_commits_to_branch(branch): """Returns number of commits between HEAD and the tracking branch. @@ -68,13 +67,14 @@ def count_commits_to_branch(branch): rev_range = '@{upstream}..' pipe = [log_cmd(rev_range, oneline=True)] result = command.run_pipe(pipe, capture=True, capture_stderr=True, - oneline=True, raise_on_error=False) + oneline=True, raise_on_error=False) if result.return_code: raise ValueError('Failed to determine upstream: %s' % result.stderr.strip()) patch_count = len(result.stdout.splitlines()) return patch_count + def name_revision(commit_hash): """Gets the revision name for a commit @@ -91,6 +91,7 @@ def name_revision(commit_hash): name = stdout.split(' ')[1].strip() return name + def guess_upstream(git_dir, branch): """Tries to guess the upstream for a branch @@ -109,7 +110,7 @@ def guess_upstream(git_dir, branch): """ pipe = [log_cmd(branch, git_dir=git_dir, oneline=True, count=100)] result = command.run_pipe(pipe, capture=True, capture_stderr=True, - raise_on_error=False) + raise_on_error=False) if result.return_code: return None, "Branch '%s' not found" % branch for line in result.stdout.splitlines()[1:]: @@ -121,6 +122,7 @@ def guess_upstream(git_dir, branch): return name, "Guessing upstream as '%s'" % name return None, "Cannot find a suitable upstream for branch '%s'" % branch + def get_upstream(git_dir, branch): """Returns the name of the upstream for a branch @@ -135,10 +137,10 @@ def get_upstream(git_dir, branch): """ try: remote = command.output_one_line('git', '--git-dir', git_dir, 'config', - 'branch.%s.remote' % branch) + 'branch.%s.remote' % branch) merge = command.output_one_line('git', '--git-dir', git_dir, 'config', - 'branch.%s.merge' % branch) - except: + 'branch.%s.merge' % branch) + except Exception: upstream, msg = guess_upstream(git_dir, branch) return upstream, msg @@ -149,7 +151,8 @@ def get_upstream(git_dir, branch): return '%s/%s' % (remote, leaf), None else: raise ValueError("Cannot determine upstream branch for branch " - "'%s' remote='%s', merge='%s'" % (branch, remote, merge)) + "'%s' remote='%s', merge='%s'" + % (branch, remote, merge)) def get_range_in_branch(git_dir, branch, include_upstream=False): @@ -168,6 +171,7 @@ def get_range_in_branch(git_dir, branch, include_upstream=False): rstr = '%s%s..%s' % (upstream, '~' if include_upstream else '', branch) return rstr, msg + def count_commits_in_range(git_dir, range_expr): """Returns the number of commits in the given range. @@ -180,12 +184,13 @@ def count_commits_in_range(git_dir, range_expr): """ pipe = [log_cmd(range_expr, git_dir=git_dir, oneline=True)] result = command.run_pipe(pipe, capture=True, capture_stderr=True, - raise_on_error=False) + raise_on_error=False) if result.return_code: return None, "Range '%s' not found or is invalid" % range_expr patch_count = len(result.stdout.splitlines()) return patch_count, None + def count_commits_in_branch(git_dir, branch, include_upstream=False): """Returns the number of commits in the given branch. @@ -201,6 +206,7 @@ def count_commits_in_branch(git_dir, branch, include_upstream=False): return None, msg return count_commits_in_range(git_dir, range_expr) + def count_commits(commit_range): """Returns the number of commits in the given range. @@ -215,6 +221,7 @@ def count_commits(commit_range): patch_count = int(stdout) return patch_count + def checkout(commit_hash, git_dir=None, work_tree=None, force=False): """Checkout the selected commit for this build @@ -231,10 +238,11 @@ def checkout(commit_hash, git_dir=None, work_tree=None, force=False): pipe.append('-f') pipe.append(commit_hash) result = command.run_pipe([pipe], capture=True, raise_on_error=False, - capture_stderr=True) + capture_stderr=True) if result.return_code != 0: raise OSError('git checkout (%s): %s' % (pipe, result.stderr)) + def clone(git_dir, output_dir): """Checkout the selected commit for this build @@ -243,10 +251,11 @@ def clone(git_dir, output_dir): """ pipe = ['git', 'clone', git_dir, '.'] result = command.run_pipe([pipe], capture=True, cwd=output_dir, - capture_stderr=True) + capture_stderr=True) if result.return_code != 0: raise OSError('git clone: %s' % result.stderr) + def fetch(git_dir=None, work_tree=None): """Fetch from the origin repo @@ -263,6 +272,7 @@ def fetch(git_dir=None, work_tree=None): if result.return_code != 0: raise OSError('git fetch: %s' % result.stderr) + def check_worktree_is_available(git_dir): """Check if git-worktree functionality is available @@ -274,9 +284,10 @@ def check_worktree_is_available(git_dir): """ pipe = ['git', '--git-dir', git_dir, 'worktree', 'list'] result = command.run_pipe([pipe], capture=True, capture_stderr=True, - raise_on_error=False) + raise_on_error=False) return result.return_code == 0 + def add_worktree(git_dir, output_dir, commit_hash=None): """Create and checkout a new git worktree for this build @@ -290,10 +301,11 @@ def add_worktree(git_dir, output_dir, commit_hash=None): if commit_hash: pipe.append(commit_hash) result = command.run_pipe([pipe], capture=True, cwd=output_dir, - capture_stderr=True) + capture_stderr=True) if result.return_code != 0: raise OSError('git worktree add: %s' % result.stderr) + def prune_worktrees(git_dir): """Remove administrative files for deleted worktrees @@ -305,7 +317,8 @@ def prune_worktrees(git_dir): if result.return_code != 0: raise OSError('git worktree prune: %s' % result.stderr) -def create_patches(branch, start, count, ignore_binary, series, signoff = True): + +def create_patches(branch, start, count, ignore_binary, series, signoff=True): """Create a series of patches from the top of the current branch. The patch files are written to the current directory using @@ -321,9 +334,7 @@ def create_patches(branch, start, count, ignore_binary, series, signoff = True): Filename of cover letter (None if none) List of filenames of patch files """ - if series.get('version'): - version = '%s ' % series['version'] - cmd = ['git', 'format-patch', '-M' ] + cmd = ['git', 'format-patch', '-M'] if signoff: cmd.append('--signoff') if ignore_binary: @@ -341,9 +352,10 @@ def create_patches(branch, start, count, ignore_binary, series, signoff = True): # We have an extra file if there is a cover letter if series.get('cover'): - return files[0], files[1:] + return files[0], files[1:] else: - return None, files + return None, files + def build_email_list(in_list, tag=None, alias=None, warn_on_error=True): """Build a list of email addresses based on an input list. @@ -385,40 +397,43 @@ def build_email_list(in_list, tag=None, alias=None, warn_on_error=True): raw += lookup_email(item, alias, warn_on_error=warn_on_error) result = [] for item in raw: - if not item in result: + if item not in result: result.append(item) if tag: return ['%s %s%s%s' % (tag, quote, email, quote) for email in result] return result + def check_suppress_cc_config(): """Check if sendemail.suppresscc is configured correctly. Returns: True if the option is configured correctly, False otherwise. """ - suppresscc = command.output_one_line('git', 'config', 'sendemail.suppresscc', - raise_on_error=False) + suppresscc = command.output_one_line( + 'git', 'config', 'sendemail.suppresscc', raise_on_error=False) # Other settings should be fine. if suppresscc == 'all' or suppresscc == 'cccmd': col = terminal.Color() print((col.build(col.RED, "error") + - ": git config sendemail.suppresscc set to %s\n" % (suppresscc)) + - " patman needs --cc-cmd to be run to set the cc list.\n" + - " Please run:\n" + - " git config --unset sendemail.suppresscc\n" + - " Or read the man page:\n" + - " git send-email --help\n" + - " and set an option that runs --cc-cmd\n") + ": git config sendemail.suppresscc set to %s\n" + % (suppresscc)) + + " patman needs --cc-cmd to be run to set the cc list.\n" + + " Please run:\n" + + " git config --unset sendemail.suppresscc\n" + + " Or read the man page:\n" + + " git send-email --help\n" + + " and set an option that runs --cc-cmd\n") return False return True + 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, - smtp_server=None): + self_only=False, alias=None, in_reply_to=None, thread=False, + smtp_server=None): """Email a patch series. Args: @@ -487,9 +502,10 @@ send --cc-cmd cc-fname" cover p1 p2' "git config sendemail.to u-boot@lists.denx.de") return cc = build_email_list(list(set(series.get('cc')) - set(series.get('to'))), - '--cc', alias, warn_on_error) + '--cc', alias, warn_on_error) if self_only: - to = build_email_list([os.getenv('USER')], '--to', alias, warn_on_error) + to = build_email_list([os.getenv('USER')], '--to', + alias, warn_on_error) cc = [] cmd = ['git', 'send-email', '--annotate'] if smtp_server: @@ -565,7 +581,7 @@ def lookup_email(lookup_name, alias=None, warn_on_error=True, level=0): if not alias: alias = settings.alias lookup_name = lookup_name.strip() - if '@' in lookup_name: # Perhaps a real email address + if '@' in lookup_name: # Perhaps a real email address return [lookup_name] lookup_name = lookup_name.lower() @@ -581,7 +597,7 @@ def lookup_email(lookup_name, alias=None, warn_on_error=True, level=0): return out_list if lookup_name: - if not lookup_name in alias: + if lookup_name not in alias: msg = "Alias '%s' not found" % lookup_name if warn_on_error: print(col.build(col.RED, msg)) @@ -589,11 +605,12 @@ def lookup_email(lookup_name, alias=None, warn_on_error=True, level=0): for item in alias[lookup_name]: todo = lookup_email(item, alias, warn_on_error, level + 1) for new_item in todo: - if not new_item in out_list: + if new_item not in out_list: out_list.append(new_item) return out_list + def get_top_level(): """Return name of top-level directory for this git repo. @@ -608,6 +625,7 @@ def get_top_level(): """ return command.output_one_line('git', 'rev-parse', '--show-toplevel') + def get_alias_file(): """Gets the name of the git alias file. @@ -615,7 +633,7 @@ def get_alias_file(): Filename of git alias file, or None if none """ fname = command.output_one_line('git', 'config', 'sendemail.aliasesfile', - raise_on_error=False) + raise_on_error=False) if not fname: return None @@ -625,6 +643,7 @@ def get_alias_file(): return os.path.join(get_top_level(), fname) + def get_default_user_name(): """Gets the user.name from .gitconfig file. @@ -634,6 +653,7 @@ def get_default_user_name(): uname = command.output_one_line('git', 'config', '--global', 'user.name') return uname + def get_default_user_email(): """Gets the user.email from the global .gitconfig file. @@ -643,17 +663,19 @@ def get_default_user_email(): uemail = command.output_one_line('git', 'config', '--global', 'user.email') return uemail + def get_default_subject_prefix(): """Gets the format.subjectprefix from local .git/config file. Returns: Subject prefix found in local .git/config file, or None if none """ - sub_prefix = command.output_one_line('git', 'config', 'format.subjectprefix', - raise_on_error=False) + sub_prefix = command.output_one_line( + 'git', 'config', 'format.subjectprefix', raise_on_error=False) return sub_prefix + def setup(): """Set up git utils, by reading the alias files.""" # Check for a git alias file also @@ -666,6 +688,7 @@ def setup(): use_no_decorate = (command.run_pipe([cmd], raise_on_error=False) .return_code == 0) + def get_head(): """Get the hash of the current HEAD @@ -674,6 +697,7 @@ def get_head(): """ return command.output_one_line('git', 'show', '-s', '--pretty=format:%H') + if __name__ == "__main__": import doctest -- cgit v1.1 From 1c77598402890ef148c5e114bb5a3a475305ced8 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Mon, 19 Dec 2022 17:32:39 -0500 Subject: patman: locate test data files via __file__ and pathlib Previously it would rely on the executing script location, which could break for example when running the tests via 'pytest'. Reviewed-by: Simon Glass Signed-off-by: Maxim Cournoyer --- tools/patman/func_test.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/patman/func_test.py b/tools/patman/func_test.py index 7b92bc6..7fa4a00 100644 --- a/tools/patman/func_test.py +++ b/tools/patman/func_test.py @@ -7,6 +7,7 @@ """Functional tests for checking that patman behaves correctly""" import os +import pathlib import re import shutil import sys @@ -28,6 +29,10 @@ from patman.test_util import capture_sys_output import pygit2 from patman import status + +TEST_DATA_DIR = pathlib.Path(__file__).parent / 'test/' + + class TestFunctional(unittest.TestCase): """Functional tests for checking that patman behaves correctly""" leb = (b'Lord Edmund Blackadd\xc3\xabr '. @@ -57,8 +62,7 @@ class TestFunctional(unittest.TestCase): Returns: str: Full path to file in the test directory """ - return os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), - 'test', fname) + return TEST_DATA_DIR / fname @classmethod def _get_text(cls, fname): -- cgit v1.1 From 648d8186dd7f9c444fb07f355090d275dcdd4de4 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Mon, 19 Dec 2022 17:32:40 -0500 Subject: patman: invoke the checkpatch.pl script with '--u-boot' and '--strict' This resolves 10 out of 11 test failures seen when running './patman test' from the 'tools/patman' subdirectory. This was caused by the .checkpatch.conf configuration file at the root of the project not being picked up. Make the test suite of patman independent from it by always invoking the checkpatch.pl script with the minimally required arguments for the test suite to pass. Reviewed-by: Simon Glass Signed-off-by: Maxim Cournoyer --- tools/patman/checkpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/patman/checkpatch.py b/tools/patman/checkpatch.py index d1b902d..012c0d8 100644 --- a/tools/patman/checkpatch.py +++ b/tools/patman/checkpatch.py @@ -211,7 +211,7 @@ def check_patch(fname, verbose=False, show_types=False, use_tree=False): stdout: Full output of checkpatch """ chk = find_check_patch() - args = [chk] + args = [chk, '--u-boot', '--strict'] if not use_tree: args.append('--no-tree') if show_types: -- cgit v1.1 From 579916beb133ad74377964643c65c57fb3174cc6 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Mon, 19 Dec 2022 17:32:41 -0500 Subject: patman: rename main script to __main__.py This allows running the package as a Python module, like e.g.: $ python -m patman It also prevents Pytest from attempting to parse main.py, which would cause errors. Reviewed-by: Simon Glass Signed-off-by: Maxim Cournoyer Fix up main.py in __init__.py: Signed-off-by: Simon Glass --- tools/patman/__init__.py | 2 +- tools/patman/__main__.py | 186 +++++++++++++++++++++++++++++++++++++++++++++++ tools/patman/main.py | 186 ----------------------------------------------- tools/patman/patman | 2 +- 4 files changed, 188 insertions(+), 188 deletions(-) create mode 100755 tools/patman/__main__.py delete mode 100755 tools/patman/main.py diff --git a/tools/patman/__init__.py b/tools/patman/__init__.py index c9d3e35..1b98ec7 100644 --- a/tools/patman/__init__.py +++ b/tools/patman/__init__.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: GPL-2.0+ __all__ = ['checkpatch', 'command', 'commit', 'control', 'cros_subprocess', - 'func_test', 'get_maintainer', 'gitutil', 'main', 'patchstream', + 'func_test', 'get_maintainer', 'gitutil', '__main__', 'patchstream', 'project', 'series', 'setup', 'settings', 'terminal', 'test_checkpatch', 'test_util', 'tools', 'tout'] diff --git a/tools/patman/__main__.py b/tools/patman/__main__.py new file mode 100755 index 0000000..2a2a7ea --- /dev/null +++ b/tools/patman/__main__.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0+ +# +# Copyright (c) 2011 The Chromium OS Authors. +# + +"""See README for more information""" + +from argparse import ArgumentParser +import importlib.resources +import os +import re +import sys +import traceback + +if __name__ == "__main__": + # Allow 'from patman import xxx to work' + our_path = os.path.dirname(os.path.realpath(__file__)) + sys.path.append(os.path.join(our_path, '..')) + +# Our modules +from patman import control +from patman import gitutil +from patman import project +from patman import settings +from patman import terminal +from patman import test_util +from patman import test_checkpatch +from patman import tools + +epilog = '''Create patches from commits in a branch, check them and email them +as specified by tags you place in the commits. Use -n to do a dry run first.''' + +parser = ArgumentParser(epilog=epilog) +parser.add_argument('-b', '--branch', type=str, + help="Branch to process (by default, the current branch)") +parser.add_argument('-c', '--count', dest='count', type=int, + default=-1, help='Automatically create patches from top n commits') +parser.add_argument('-e', '--end', type=int, default=0, + help='Commits to skip at end of patch list') +parser.add_argument('-D', '--debug', action='store_true', + help='Enabling debugging (provides a full traceback on error)') +parser.add_argument('-p', '--project', default=project.detect_project(), + help="Project name; affects default option values and " + "aliases [default: %(default)s]") +parser.add_argument('-P', '--patchwork-url', + default='https://patchwork.ozlabs.org', + help='URL of patchwork server [default: %(default)s]') +parser.add_argument('-s', '--start', dest='start', type=int, + default=0, help='Commit to start creating patches from (0 = HEAD)') +parser.add_argument('-v', '--verbose', action='store_true', dest='verbose', + default=False, help='Verbose output of errors and warnings') +parser.add_argument('-H', '--full-help', action='store_true', dest='full_help', + default=False, help='Display the README file') + +subparsers = parser.add_subparsers(dest='cmd') +send = subparsers.add_parser('send') +send.add_argument('-i', '--ignore-errors', action='store_true', + dest='ignore_errors', default=False, + help='Send patches email even if patch errors are found') +send.add_argument('-l', '--limit-cc', dest='limit', type=int, default=None, + help='Limit the cc list to LIMIT entries [default: %(default)s]') +send.add_argument('-m', '--no-maintainers', action='store_false', + dest='add_maintainers', default=True, + help="Don't cc the file maintainers automatically") +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)") +send.add_argument('-r', '--in-reply-to', type=str, action='store', + help="Message ID that this series is in reply to") +send.add_argument('-t', '--ignore-bad-tags', action='store_true', + default=False, + help='Ignore bad tags / aliases (default=warn)') +send.add_argument('-T', '--thread', action='store_true', dest='thread', + default=False, help='Create patches as a single thread') +send.add_argument('--cc-cmd', dest='cc_cmd', type=str, action='store', + default=None, help='Output cc list for patch file (used by git)') +send.add_argument('--no-binary', action='store_true', dest='ignore_binary', + default=False, + help="Do not output contents of changes in binary files") +send.add_argument('--no-check', action='store_false', dest='check_patch', + default=True, + help="Don't check for patch compliance") +send.add_argument('--tree', dest='check_patch_use_tree', default=False, + action='store_true', + help=("Set `tree` to True. If `tree` is False then we'll " + "pass '--no-tree' to checkpatch (default: tree=%(default)s)")) +send.add_argument('--no-tree', dest='check_patch_use_tree', + action='store_false', help="Set `tree` to False") +send.add_argument('--no-tags', action='store_false', dest='process_tags', + default=True, help="Don't process subject tags as aliases") +send.add_argument('--no-signoff', action='store_false', dest='add_signoff', + default=True, help="Don't add Signed-off-by to patches") +send.add_argument('--smtp-server', type=str, + help="Specify the SMTP server to 'git send-email'") + +send.add_argument('patchfiles', nargs='*') + +test_parser = subparsers.add_parser('test', help='Run tests') +test_parser.add_argument('testname', type=str, default=None, nargs='?', + help="Specify the test to run") + +status = subparsers.add_parser('status', + help='Check status of patches in patchwork') +status.add_argument('-C', '--show-comments', action='store_true', + help='Show comments from each patch') +status.add_argument('-d', '--dest-branch', type=str, + help='Name of branch to create with collected responses') +status.add_argument('-f', '--force', action='store_true', + help='Force overwriting an existing branch') + +# Parse options twice: first to get the project and second to handle +# defaults properly (which depends on project) +# Use parse_known_args() in case 'cmd' is omitted +argv = sys.argv[1:] +args, rest = parser.parse_known_args(argv) +if hasattr(args, 'project'): + settings.Setup(gitutil, parser, args.project, '') + args, rest = parser.parse_known_args(argv) + +# If we have a command, it is safe to parse all arguments +if args.cmd: + args = parser.parse_args(argv) +else: + # No command, so insert it after the known arguments and before the ones + # that presumably relate to the 'send' subcommand + nargs = len(rest) + argv = argv[:-nargs] + ['send'] + rest + args = parser.parse_args(argv) + +if __name__ != "__main__": + pass + +if not args.debug: + sys.tracebacklimit = 0 + +# Run our meagre tests +if args.cmd == 'test': + from patman import func_test + + result = test_util.run_test_suites( + 'patman', False, False, False, None, None, None, + [test_checkpatch.TestPatch, func_test.TestFunctional, + 'gitutil', 'settings', 'terminal']) + + sys.exit(0 if result.wasSuccessful() else 1) + +# Process commits, produce patches files, check them, email them +elif args.cmd == 'send': + # Called from git with a patch filename as argument + # Printout a list of additional CC recipients for this patch + if args.cc_cmd: + fd = open(args.cc_cmd, 'r') + re_line = re.compile('(\S*) (.*)') + for line in fd.readlines(): + match = re_line.match(line) + if match and match.group(1) == args.patchfiles[0]: + for cc in match.group(2).split('\0'): + cc = cc.strip() + if cc: + print(cc) + fd.close() + + elif args.full_help: + with importlib.resources.path('patman', 'README.rst') as readme: + tools.print_full_help(str(readme)) + else: + # If we are not processing tags, no need to warning about bad ones + if not args.process_tags: + args.ignore_bad_tags = True + control.send(args) + +# Check status of patches in patchwork +elif args.cmd == 'status': + ret_code = 0 + try: + control.patchwork_status(args.branch, args.count, args.start, args.end, + args.dest_branch, args.force, + args.show_comments, args.patchwork_url) + except Exception as e: + terminal.tprint('patman: %s: %s' % (type(e).__name__, e), + colour=terminal.Color.RED) + if args.debug: + print() + traceback.print_exc() + ret_code = 1 + sys.exit(ret_code) diff --git a/tools/patman/main.py b/tools/patman/main.py deleted file mode 100755 index 2a2a7ea..0000000 --- a/tools/patman/main.py +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: GPL-2.0+ -# -# Copyright (c) 2011 The Chromium OS Authors. -# - -"""See README for more information""" - -from argparse import ArgumentParser -import importlib.resources -import os -import re -import sys -import traceback - -if __name__ == "__main__": - # Allow 'from patman import xxx to work' - our_path = os.path.dirname(os.path.realpath(__file__)) - sys.path.append(os.path.join(our_path, '..')) - -# Our modules -from patman import control -from patman import gitutil -from patman import project -from patman import settings -from patman import terminal -from patman import test_util -from patman import test_checkpatch -from patman import tools - -epilog = '''Create patches from commits in a branch, check them and email them -as specified by tags you place in the commits. Use -n to do a dry run first.''' - -parser = ArgumentParser(epilog=epilog) -parser.add_argument('-b', '--branch', type=str, - help="Branch to process (by default, the current branch)") -parser.add_argument('-c', '--count', dest='count', type=int, - default=-1, help='Automatically create patches from top n commits') -parser.add_argument('-e', '--end', type=int, default=0, - help='Commits to skip at end of patch list') -parser.add_argument('-D', '--debug', action='store_true', - help='Enabling debugging (provides a full traceback on error)') -parser.add_argument('-p', '--project', default=project.detect_project(), - help="Project name; affects default option values and " - "aliases [default: %(default)s]") -parser.add_argument('-P', '--patchwork-url', - default='https://patchwork.ozlabs.org', - help='URL of patchwork server [default: %(default)s]') -parser.add_argument('-s', '--start', dest='start', type=int, - default=0, help='Commit to start creating patches from (0 = HEAD)') -parser.add_argument('-v', '--verbose', action='store_true', dest='verbose', - default=False, help='Verbose output of errors and warnings') -parser.add_argument('-H', '--full-help', action='store_true', dest='full_help', - default=False, help='Display the README file') - -subparsers = parser.add_subparsers(dest='cmd') -send = subparsers.add_parser('send') -send.add_argument('-i', '--ignore-errors', action='store_true', - dest='ignore_errors', default=False, - help='Send patches email even if patch errors are found') -send.add_argument('-l', '--limit-cc', dest='limit', type=int, default=None, - help='Limit the cc list to LIMIT entries [default: %(default)s]') -send.add_argument('-m', '--no-maintainers', action='store_false', - dest='add_maintainers', default=True, - help="Don't cc the file maintainers automatically") -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)") -send.add_argument('-r', '--in-reply-to', type=str, action='store', - help="Message ID that this series is in reply to") -send.add_argument('-t', '--ignore-bad-tags', action='store_true', - default=False, - help='Ignore bad tags / aliases (default=warn)') -send.add_argument('-T', '--thread', action='store_true', dest='thread', - default=False, help='Create patches as a single thread') -send.add_argument('--cc-cmd', dest='cc_cmd', type=str, action='store', - default=None, help='Output cc list for patch file (used by git)') -send.add_argument('--no-binary', action='store_true', dest='ignore_binary', - default=False, - help="Do not output contents of changes in binary files") -send.add_argument('--no-check', action='store_false', dest='check_patch', - default=True, - help="Don't check for patch compliance") -send.add_argument('--tree', dest='check_patch_use_tree', default=False, - action='store_true', - help=("Set `tree` to True. If `tree` is False then we'll " - "pass '--no-tree' to checkpatch (default: tree=%(default)s)")) -send.add_argument('--no-tree', dest='check_patch_use_tree', - action='store_false', help="Set `tree` to False") -send.add_argument('--no-tags', action='store_false', dest='process_tags', - default=True, help="Don't process subject tags as aliases") -send.add_argument('--no-signoff', action='store_false', dest='add_signoff', - default=True, help="Don't add Signed-off-by to patches") -send.add_argument('--smtp-server', type=str, - help="Specify the SMTP server to 'git send-email'") - -send.add_argument('patchfiles', nargs='*') - -test_parser = subparsers.add_parser('test', help='Run tests') -test_parser.add_argument('testname', type=str, default=None, nargs='?', - help="Specify the test to run") - -status = subparsers.add_parser('status', - help='Check status of patches in patchwork') -status.add_argument('-C', '--show-comments', action='store_true', - help='Show comments from each patch') -status.add_argument('-d', '--dest-branch', type=str, - help='Name of branch to create with collected responses') -status.add_argument('-f', '--force', action='store_true', - help='Force overwriting an existing branch') - -# Parse options twice: first to get the project and second to handle -# defaults properly (which depends on project) -# Use parse_known_args() in case 'cmd' is omitted -argv = sys.argv[1:] -args, rest = parser.parse_known_args(argv) -if hasattr(args, 'project'): - settings.Setup(gitutil, parser, args.project, '') - args, rest = parser.parse_known_args(argv) - -# If we have a command, it is safe to parse all arguments -if args.cmd: - args = parser.parse_args(argv) -else: - # No command, so insert it after the known arguments and before the ones - # that presumably relate to the 'send' subcommand - nargs = len(rest) - argv = argv[:-nargs] + ['send'] + rest - args = parser.parse_args(argv) - -if __name__ != "__main__": - pass - -if not args.debug: - sys.tracebacklimit = 0 - -# Run our meagre tests -if args.cmd == 'test': - from patman import func_test - - result = test_util.run_test_suites( - 'patman', False, False, False, None, None, None, - [test_checkpatch.TestPatch, func_test.TestFunctional, - 'gitutil', 'settings', 'terminal']) - - sys.exit(0 if result.wasSuccessful() else 1) - -# Process commits, produce patches files, check them, email them -elif args.cmd == 'send': - # Called from git with a patch filename as argument - # Printout a list of additional CC recipients for this patch - if args.cc_cmd: - fd = open(args.cc_cmd, 'r') - re_line = re.compile('(\S*) (.*)') - for line in fd.readlines(): - match = re_line.match(line) - if match and match.group(1) == args.patchfiles[0]: - for cc in match.group(2).split('\0'): - cc = cc.strip() - if cc: - print(cc) - fd.close() - - elif args.full_help: - with importlib.resources.path('patman', 'README.rst') as readme: - tools.print_full_help(str(readme)) - else: - # If we are not processing tags, no need to warning about bad ones - if not args.process_tags: - args.ignore_bad_tags = True - control.send(args) - -# Check status of patches in patchwork -elif args.cmd == 'status': - ret_code = 0 - try: - control.patchwork_status(args.branch, args.count, args.start, args.end, - args.dest_branch, args.force, - args.show_comments, args.patchwork_url) - except Exception as e: - terminal.tprint('patman: %s: %s' % (type(e).__name__, e), - colour=terminal.Color.RED) - if args.debug: - print() - traceback.print_exc() - ret_code = 1 - sys.exit(ret_code) diff --git a/tools/patman/patman b/tools/patman/patman index 11a5d8e..5a427d1 120000 --- a/tools/patman/patman +++ b/tools/patman/patman @@ -1 +1 @@ -main.py \ No newline at end of file +__main__.py \ No newline at end of file -- cgit v1.1 From a3997a0a00a03a85fd0ad4ca147763f17ba88b5b Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Mon, 19 Dec 2022 17:32:42 -0500 Subject: patman: add pytest configuration file With this change, a user can run the patman test suite using Pytest the same as when using 'patman test': $ cd tools/patman && pytest [...] 44 passed, 8 warnings in 8.87s $ ./patman test Ran 44 tests in 8.460s Reviewed-by: Simon Glass Signed-off-by: Maxim Cournoyer --- tools/patman/pytest.ini | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 tools/patman/pytest.ini diff --git a/tools/patman/pytest.ini b/tools/patman/pytest.ini new file mode 100644 index 0000000..df3eb51 --- /dev/null +++ b/tools/patman/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --doctest-modules -- cgit v1.1 From 52c1c333eaade606be03a6087a348f8f37843f62 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Mon, 19 Dec 2022 17:32:43 -0500 Subject: patman: hide the 'test' command unless test data is available Some tests would fail when the test data is not available, so it doesn't make much sense to expose the action when patman is running outside of the u-boot git checkout. Reviewed-by: Simon Glass Signed-off-by: Maxim Cournoyer --- tools/patman/__main__.py | 9 ++++++--- tools/patman/patman.rst | 4 ++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/tools/patman/__main__.py b/tools/patman/__main__.py index 2a2a7ea..70968f6 100755 --- a/tools/patman/__main__.py +++ b/tools/patman/__main__.py @@ -20,6 +20,7 @@ if __name__ == "__main__": # Our modules from patman import control +from patman import func_test from patman import gitutil from patman import project from patman import settings @@ -95,9 +96,11 @@ send.add_argument('--smtp-server', type=str, send.add_argument('patchfiles', nargs='*') -test_parser = subparsers.add_parser('test', help='Run tests') -test_parser.add_argument('testname', type=str, default=None, nargs='?', - help="Specify the test to run") +# Only add the 'test' action if the test data files are available. +if os.path.exists(func_test.TEST_DATA_DIR): + test_parser = subparsers.add_parser('test', help='Run tests') + test_parser.add_argument('testname', type=str, default=None, nargs='?', + help="Specify the test to run") status = subparsers.add_parser('status', help='Check status of patches in patchwork') diff --git a/tools/patman/patman.rst b/tools/patman/patman.rst index 8c5c9cc..ff4c8b3 100644 --- a/tools/patman/patman.rst +++ b/tools/patman/patman.rst @@ -680,6 +680,10 @@ them: $ tools/patman/patman test +Note that since the test suite depends on data files only available in +the git checkout, the `test` command is hidden unless `patman` is +invoked from the U-Boot git repository. + Error handling doesn't always produce friendly error messages - e.g. putting an incorrect tag in a commit may provide a confusing message. -- cgit v1.1 From f393f59e5d222711b9f35a8d2d32f4ba374d4134 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Mon, 19 Dec 2022 17:32:44 -0500 Subject: patman: document how to run test suite via pytest Pytest offers useful features such as selecting tests by means of a regular expression, or running the pdb debugger upon encountering a test failure. Reviewed-by: Simon Glass Signed-off-by: Maxim Cournoyer --- tools/patman/patman.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/patman/patman.rst b/tools/patman/patman.rst index ff4c8b3..395fc0c 100644 --- a/tools/patman/patman.rst +++ b/tools/patman/patman.rst @@ -684,6 +684,12 @@ Note that since the test suite depends on data files only available in the git checkout, the `test` command is hidden unless `patman` is invoked from the U-Boot git repository. +Alternatively, you can run the test suite via Pytest: + +.. code-block:: bash + + $ cd tools/patman && pytest + Error handling doesn't always produce friendly error messages - e.g. putting an incorrect tag in a commit may provide a confusing message. -- cgit v1.1 From 30529307141339d4780bddb3557c115fbf1f51c1 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Mon, 19 Dec 2022 17:32:45 -0500 Subject: patman: document default 'send' command Document that this command is the default and what it's intended for. Reviewed-by: Simon Glass Signed-off-by: Maxim Cournoyer --- tools/patman/__main__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/patman/__main__.py b/tools/patman/__main__.py index 70968f6..ece59c5 100755 --- a/tools/patman/__main__.py +++ b/tools/patman/__main__.py @@ -55,7 +55,8 @@ parser.add_argument('-H', '--full-help', action='store_true', dest='full_help', default=False, help='Display the README file') subparsers = parser.add_subparsers(dest='cmd') -send = subparsers.add_parser('send') +send = subparsers.add_parser( + 'send', help='Format, check and email patches (default command)') send.add_argument('-i', '--ignore-errors', action='store_true', dest='ignore_errors', default=False, help='Send patches email even if patch errors are found') -- cgit v1.1 From 872f3a4ce245e9573a9572971074a8e57c1f6022 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Tue, 20 Dec 2022 00:38:36 -0500 Subject: patman: fix pep8 warnings in settings module Remove extraneous imports, variables and comply to PEP 8 maximum line width, among other PEP 8 changes suggested by Pyflake. Signed-off-by: Maxim Cournoyer Reviewed-by: Simon Glass --- tools/patman/settings.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/tools/patman/settings.py b/tools/patman/settings.py index 903d6fc..b6884a0 100644 --- a/tools/patman/settings.py +++ b/tools/patman/settings.py @@ -4,16 +4,13 @@ try: import configparser as ConfigParser -except: +except Exception: import ConfigParser import argparse import os import re -from patman import command -from patman import tools - """Default settings per-project. These are used by _ProjectConfigParser. Settings names should match @@ -32,6 +29,7 @@ _default_settings = { }, } + class _ProjectConfigParser(ConfigParser.SafeConfigParser): """ConfigParser that handles projects. @@ -83,8 +81,8 @@ class _ProjectConfigParser(ConfigParser.SafeConfigParser): def __init__(self, project_name): """Construct _ProjectConfigParser. - In addition to standard SafeConfigParser initialization, this also loads - project defaults. + In addition to standard SafeConfigParser initialization, this + also loads project defaults. Args: project_name: The name of the project. @@ -155,6 +153,7 @@ class _ProjectConfigParser(ConfigParser.SafeConfigParser): item_dict.update(project_items) return {(item, val) for item, val in item_dict.items()} + def ReadGitAliases(fname): """Read a git alias file. This is in the form used by git: @@ -170,7 +169,7 @@ def ReadGitAliases(fname): print("Warning: Cannot find alias file '%s'" % fname) return - re_line = re.compile('alias\s+(\S+)\s+(.*)') + re_line = re.compile(r'alias\s+(\S+)\s+(.*)') for line in fd.readlines(): line = line.strip() if not line or line[0] == '#': @@ -190,6 +189,7 @@ def ReadGitAliases(fname): fd.close() + def CreatePatmanConfigFile(gitutil, config_fname): """Creates a config file under $(HOME)/.patman if it can't find one. @@ -200,12 +200,12 @@ def CreatePatmanConfigFile(gitutil, config_fname): None """ name = gitutil.get_default_user_name() - if name == None: + if name is None: name = input("Enter name: ") email = gitutil.get_default_user_email() - if email == None: + if email is None: email = input("Enter email: ") try: @@ -220,7 +220,8 @@ me: %s <%s> [bounces] nxp = Zhikang Zhang ''' % (name, email), file=f) - f.close(); + f.close() + def _UpdateDefaults(main_parser, config): """Update the given OptionParser defaults based on config. @@ -242,8 +243,8 @@ def _UpdateDefaults(main_parser, config): # Find all the parsers and subparsers parsers = [main_parser] parsers += [subparser for action in main_parser._actions - if isinstance(action, argparse._SubParsersAction) - for _, subparser in action.choices.items()] + if isinstance(action, argparse._SubParsersAction) + for _, subparser in action.choices.items()] # Collect the defaults from each parser defaults = {} @@ -270,8 +271,9 @@ def _UpdateDefaults(main_parser, config): # Set all the defaults and manually propagate them to subparsers main_parser.set_defaults(**defaults) for parser, pdefs in zip(parsers, parser_defaults): - parser.set_defaults(**{ k: v for k, v in defaults.items() - if k in pdefs }) + parser.set_defaults(**{k: v for k, v in defaults.items() + if k in pdefs}) + def _ReadAliasFile(fname): """Read in the U-Boot git alias file if it exists. @@ -298,6 +300,7 @@ def _ReadAliasFile(fname): if bad_line: print(bad_line) + def _ReadBouncesFile(fname): """Read in the bounces file if it exists @@ -311,6 +314,7 @@ def _ReadBouncesFile(fname): continue bounces.add(line.strip()) + def GetItems(config, section): """Get the items from a section of the config. @@ -323,10 +327,9 @@ def GetItems(config, section): """ try: return config.items(section) - except ConfigParser.NoSectionError as e: + except ConfigParser.NoSectionError: return [] - except: - raise + def Setup(gitutil, parser, project_name, config_fname=''): """Set up the settings module by reading config files. @@ -358,6 +361,7 @@ def Setup(gitutil, parser, project_name, config_fname=''): _UpdateDefaults(parser, config) + # These are the aliases we understand, indexed by alias. Each member is a list. alias = {} bounces = set() -- cgit v1.1 From 8b73f9bf9e979b47aa4f7dcbd6727945a0da02f8 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Tue, 20 Dec 2022 00:38:37 -0500 Subject: patman: replace deprecated SafeConfigParser with ConfigParser The SafeConfigParser class has been renamed in Python 3.2 to ConfigParser, and the old alias has been deprecated since. Signed-off-by: Maxim Cournoyer Reviewed-by: Simon Glass --- tools/patman/settings.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tools/patman/settings.py b/tools/patman/settings.py index b6884a0..7fb9d6d 100644 --- a/tools/patman/settings.py +++ b/tools/patman/settings.py @@ -30,7 +30,7 @@ _default_settings = { } -class _ProjectConfigParser(ConfigParser.SafeConfigParser): +class _ProjectConfigParser(ConfigParser.ConfigParser): """ConfigParser that handles projects. There are two main goals of this class: @@ -81,14 +81,14 @@ class _ProjectConfigParser(ConfigParser.SafeConfigParser): def __init__(self, project_name): """Construct _ProjectConfigParser. - In addition to standard SafeConfigParser initialization, this - also loads project defaults. + In addition to standard ConfigParser initialization, this also + loads project defaults. Args: project_name: The name of the project. """ self._project_name = project_name - ConfigParser.SafeConfigParser.__init__(self) + ConfigParser.ConfigParser.__init__(self) # Update the project settings in the config based on # the _default_settings global. @@ -100,31 +100,31 @@ class _ProjectConfigParser(ConfigParser.SafeConfigParser): self.set(project_settings, setting_name, setting_value) def get(self, section, option, *args, **kwargs): - """Extend SafeConfigParser to try project_section before section. + """Extend ConfigParser to try project_section before section. Args: - See SafeConfigParser. + See ConfigParser. Returns: - See SafeConfigParser. + See ConfigParser. """ try: - val = ConfigParser.SafeConfigParser.get( + val = ConfigParser.ConfigParser.get( self, "%s_%s" % (self._project_name, section), option, *args, **kwargs ) except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): - val = ConfigParser.SafeConfigParser.get( + val = ConfigParser.ConfigParser.get( self, section, option, *args, **kwargs ) return val def items(self, section, *args, **kwargs): - """Extend SafeConfigParser to add project_section to section. + """Extend ConfigParser to add project_section to section. Args: - See SafeConfigParser. + See ConfigParser. Returns: - See SafeConfigParser. + See ConfigParser. """ project_items = [] has_project_section = False @@ -132,7 +132,7 @@ class _ProjectConfigParser(ConfigParser.SafeConfigParser): # Get items from the project section try: - project_items = ConfigParser.SafeConfigParser.items( + project_items = ConfigParser.ConfigParser.items( self, "%s_%s" % (self._project_name, section), *args, **kwargs ) has_project_section = True @@ -141,7 +141,7 @@ class _ProjectConfigParser(ConfigParser.SafeConfigParser): # Get top-level items try: - top_items = ConfigParser.SafeConfigParser.items( + top_items = ConfigParser.ConfigParser.items( self, section, *args, **kwargs ) except ConfigParser.NoSectionError: -- cgit v1.1 From 57e3b03fe1ea225eb45ccfa3b0f2d564d95a4717 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Tue, 20 Dec 2022 00:38:38 -0500 Subject: patman: import gitutil module where it is needed Instead of propagating it from the module entry point (main script). Signed-off-by: Maxim Cournoyer Reviewed-by: Simon Glass --- tools/patman/__main__.py | 3 +-- tools/patman/settings.py | 8 +++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tools/patman/__main__.py b/tools/patman/__main__.py index ece59c5..8e32655 100755 --- a/tools/patman/__main__.py +++ b/tools/patman/__main__.py @@ -21,7 +21,6 @@ if __name__ == "__main__": # Our modules from patman import control from patman import func_test -from patman import gitutil from patman import project from patman import settings from patman import terminal @@ -118,7 +117,7 @@ status.add_argument('-f', '--force', action='store_true', argv = sys.argv[1:] args, rest = parser.parse_known_args(argv) if hasattr(args, 'project'): - settings.Setup(gitutil, parser, args.project, '') + settings.Setup(parser, args.project, '') args, rest = parser.parse_known_args(argv) # If we have a command, it is safe to parse all arguments diff --git a/tools/patman/settings.py b/tools/patman/settings.py index 7fb9d6d..5efad5e 100644 --- a/tools/patman/settings.py +++ b/tools/patman/settings.py @@ -11,6 +11,8 @@ import argparse import os import re +from patman import gitutil + """Default settings per-project. These are used by _ProjectConfigParser. Settings names should match @@ -190,7 +192,7 @@ def ReadGitAliases(fname): fd.close() -def CreatePatmanConfigFile(gitutil, config_fname): +def CreatePatmanConfigFile(config_fname): """Creates a config file under $(HOME)/.patman if it can't find one. Args: @@ -331,7 +333,7 @@ def GetItems(config, section): return [] -def Setup(gitutil, parser, project_name, config_fname=''): +def Setup(parser, project_name, config_fname=''): """Set up the settings module by reading config files. Args: @@ -348,7 +350,7 @@ def Setup(gitutil, parser, project_name, config_fname=''): if not os.path.exists(config_fname): print("No config file found ~/.patman\nCreating one...\n") - CreatePatmanConfigFile(gitutil, config_fname) + CreatePatmanConfigFile(config_fname) config.read(config_fname) -- cgit v1.1 From 2c58a5e2753e4f0afc4c937b07b9380398bbe8fd Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Tue, 20 Dec 2022 00:38:39 -0500 Subject: patman: set the default config_fname argument value to None This better matches Python conventions, allowing to easily test whether the optional argument is provided. Signed-off-by: Maxim Cournoyer Reviewed-by: Simon Glass --- tools/patman/__main__.py | 2 +- tools/patman/settings.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tools/patman/__main__.py b/tools/patman/__main__.py index 8e32655..3748930 100755 --- a/tools/patman/__main__.py +++ b/tools/patman/__main__.py @@ -117,7 +117,7 @@ status.add_argument('-f', '--force', action='store_true', argv = sys.argv[1:] args, rest = parser.parse_known_args(argv) if hasattr(args, 'project'): - settings.Setup(parser, args.project, '') + settings.Setup(parser, args.project) args, rest = parser.parse_known_args(argv) # If we have a command, it is safe to parse all arguments diff --git a/tools/patman/settings.py b/tools/patman/settings.py index 5efad5e..8b84679 100644 --- a/tools/patman/settings.py +++ b/tools/patman/settings.py @@ -333,19 +333,20 @@ def GetItems(config, section): return [] -def Setup(parser, project_name, config_fname=''): +def Setup(parser, project_name, config_fname=None): """Set up the settings module by reading config files. Args: - parser: The parser to update + parser: The parser to update. project_name: Name of project that we're working on; we'll look for sections named "project_section" as well. - config_fname: Config filename to read ('' for default) + config_fname: Config filename to read. """ # First read the git alias file if available _ReadAliasFile('doc/git-mailrc') config = _ProjectConfigParser(project_name) - if config_fname == '': + + if not config_fname: config_fname = '%s/.patman' % os.getenv('HOME') if not os.path.exists(config_fname): -- cgit v1.1 From db16edd8ced27c06c075999ef95c6a1a57db98e0 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Tue, 20 Dec 2022 00:38:40 -0500 Subject: patman: fail early in Setup when provided config file does not exist Rationale: if the user explicitly provide this argument, they probably intend for it to be used. Signed-off-by: Maxim Cournoyer Reviewed-by: Simon Glass --- tools/patman/settings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/patman/settings.py b/tools/patman/settings.py index 8b84679..c05efd2 100644 --- a/tools/patman/settings.py +++ b/tools/patman/settings.py @@ -340,12 +340,16 @@ def Setup(parser, project_name, config_fname=None): parser: The parser to update. project_name: Name of project that we're working on; we'll look for sections named "project_section" as well. - config_fname: Config filename to read. + config_fname: Config filename to read. An error is raised if it + does not exist. """ # First read the git alias file if available _ReadAliasFile('doc/git-mailrc') config = _ProjectConfigParser(project_name) + if config_fname and not os.path.exists(config_fname): + raise Exception(f'provided {config_fname} does not exist') + if not config_fname: config_fname = '%s/.patman' % os.getenv('HOME') -- cgit v1.1 From 8f8d3f72f28732bff0dc6362a18e3ec55fba2ac1 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Tue, 20 Dec 2022 00:38:41 -0500 Subject: patman: additionally honor a local .patman config file This enables versioning a project specific patman configuration file. It also makes it possible to declare the project name, which is not a useful thing to do in $HOME/.patman. A new test is added, along updated documentation. Signed-off-by: Maxim Cournoyer Reviewed-by: Simon Glass --- tools/patman/patman.rst | 8 +++++- tools/patman/settings.py | 24 +++++++++++++--- tools/patman/test_settings.py | 67 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 tools/patman/test_settings.py diff --git a/tools/patman/patman.rst b/tools/patman/patman.rst index 395fc0c..d7994c8 100644 --- a/tools/patman/patman.rst +++ b/tools/patman/patman.rst @@ -74,7 +74,7 @@ out where to send patches pretty well. 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. -To add your own, create a file ~/.patman like this:: +To add your own, create a file `~/.patman` like this:: # patman alias file @@ -85,6 +85,12 @@ To add your own, create a file ~/.patman like this:: wolfgang: Wolfgang Denk others: Mike Frysinger , Fred Bloggs +Patman will also look for a `.patman` configuration file at the root +of the current project git repository, which makes it possible to +override the `project` settings variable or anything else in a +project-specific way. The values of this "local" configuration file +take precedence over those of the "global" one. + Aliases are recursive. The checkpatch.pl in the U-Boot tools/ subdirectory will be located and diff --git a/tools/patman/settings.py b/tools/patman/settings.py index c05efd2..636983e 100644 --- a/tools/patman/settings.py +++ b/tools/patman/settings.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: GPL-2.0+ # Copyright (c) 2011 The Chromium OS Authors. +# Copyright (c) 2022 Maxim Cournoyer # try: @@ -336,6 +337,12 @@ def GetItems(config, section): def Setup(parser, project_name, config_fname=None): """Set up the settings module by reading config files. + Unless `config_fname` is specified, a `.patman` config file local + to the git repository is consulted, followed by the global + `$HOME/.patman`. If none exists, the later is created. Values + defined in the local config file take precedence over those + defined in the global one. + Args: parser: The parser to update. project_name: Name of project that we're working on; we'll look @@ -352,12 +359,21 @@ def Setup(parser, project_name, config_fname=None): if not config_fname: config_fname = '%s/.patman' % os.getenv('HOME') + has_config = os.path.exists(config_fname) - if not os.path.exists(config_fname): - print("No config file found ~/.patman\nCreating one...\n") - CreatePatmanConfigFile(config_fname) + git_local_config_fname = os.path.join(gitutil.get_top_level(), '.patman') + has_git_local_config = os.path.exists(git_local_config_fname) - config.read(config_fname) + # Read the git local config last, so that its values override + # those of the global config, if any. + if has_config: + config.read(config_fname) + if has_git_local_config: + config.read(git_local_config_fname) + + if not (has_config or has_git_local_config): + print("No config file found.\nCreating ~/.patman...\n") + CreatePatmanConfigFile(config_fname) for name, value in GetItems(config, 'alias'): alias[name] = value.split(',') diff --git a/tools/patman/test_settings.py b/tools/patman/test_settings.py new file mode 100644 index 0000000..c768a2f --- /dev/null +++ b/tools/patman/test_settings.py @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: GPL-2.0+ +# +# Copyright (c) 2022 Maxim Cournoyer +# + +import argparse +import contextlib +import os +import sys +import tempfile + +from patman import settings +from patman import tools + + +@contextlib.contextmanager +def empty_git_repository(): + with tempfile.TemporaryDirectory() as tmpdir: + os.chdir(tmpdir) + tools.run('git', 'init', raise_on_error=True) + yield tmpdir + + +@contextlib.contextmanager +def cleared_command_line_args(): + old_value = sys.argv[:] + sys.argv = [sys.argv[0]] + try: + yield + finally: + sys.argv = old_value + + +def test_git_local_config(): + # Clearing the command line arguments is required, otherwise + # arguments passed to the test running such as in 'pytest -k + # filter' would be processed by _UpdateDefaults and fail. + with cleared_command_line_args(): + with empty_git_repository(): + with tempfile.NamedTemporaryFile() as global_config: + global_config.write(b'[settings]\n' + b'project=u-boot\n') + global_config.flush() + parser = argparse.ArgumentParser() + parser.add_argument('-p', '--project', default='unknown') + subparsers = parser.add_subparsers(dest='cmd') + send = subparsers.add_parser('send') + send.add_argument('--no-check', action='store_false', + dest='check_patch', default=True) + + # Test "global" config is used. + settings.Setup(parser, 'unknown', global_config.name) + args, _ = parser.parse_known_args([]) + assert args.project == 'u-boot' + send_args, _ = send.parse_known_args([]) + assert send_args.check_patch + + # Test local config can shadow it. + with open('.patman', 'w', buffering=1) as f: + f.write('[settings]\n' + 'project: guix-patches\n' + 'check_patch: False\n') + settings.Setup(parser, 'unknown', global_config.name) + args, _ = parser.parse_known_args([]) + assert args.project == 'guix-patches' + send_args, _ = send.parse_known_args([]) + assert not send_args.check_patch -- cgit v1.1 From 8c042fb7f9f475367804b26a892fd522ad8fcfcc Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Tue, 20 Dec 2022 00:28:46 -0500 Subject: 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 Signed-off-by: Maxim Cournoyer --- tools/patman/__main__.py | 7 ++++++ tools/patman/control.py | 12 +++++---- tools/patman/func_test.py | 49 ++++++++++++++++++++++++++++++++++-- tools/patman/get_maintainer.py | 57 ++++++++++++++++++++++++++---------------- tools/patman/gitutil.py | 3 ++- tools/patman/patman.rst | 30 ++++++++++++++++------ tools/patman/series.py | 9 ++++--- 7 files changed, 127 insertions(+), 40 deletions(-) diff --git a/tools/patman/__main__.py b/tools/patman/__main__.py index 3748930..749e634 100755 --- a/tools/patman/__main__.py +++ b/tools/patman/__main__.py @@ -21,6 +21,7 @@ if __name__ == "__main__": # Our modules from patman import control from patman import func_test +from patman import gitutil from patman import project from patman import settings 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', dest='add_maintainers', default=True, 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', default=False, help="Do a dry run (create but don't email patches)") send.add_argument('-r', '--in-reply-to', type=str, action='store', diff --git a/tools/patman/control.py b/tools/patman/control.py index bf426cf..38e98da 100644 --- a/tools/patman/control.py +++ b/tools/patman/control.py @@ -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, - ignore_bad_tags, add_maintainers, limit, dry_run, in_reply_to, - thread, smtp_server): + ignore_bad_tags, add_maintainers, get_maintainer_script, limit, + dry_run, in_reply_to, thread, smtp_server): """Email patches to the recipients 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, False to halt with an error 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 patch or the cover letter (None if no limit) 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) """ 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) cmd = '' @@ -174,8 +176,8 @@ def send(args): email_patches( col, series, cover_fname, patch_files, args.process_tags, its_a_go, args.ignore_bad_tags, args.add_maintainers, - args.limit, args.dry_run, args.in_reply_to, args.thread, - args.smtp_server) + args.get_maintainer_script, args.limit, args.dry_run, + args.in_reply_to, args.thread, args.smtp_server) def patchwork_status(branch, count, start, end, dest_branch, force, show_comments, url): diff --git a/tools/patman/func_test.py b/tools/patman/func_test.py index 7fa4a00..c25a47b 100644 --- a/tools/patman/func_test.py +++ b/tools/patman/func_test.py @@ -6,6 +6,7 @@ """Functional tests for checking that patman behaves correctly""" +import contextlib import os import pathlib import re @@ -29,8 +30,19 @@ from patman.test_util import capture_sys_output import pygit2 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): @@ -204,6 +216,8 @@ class TestFunctional(unittest.TestCase): text = self._get_text('test01.txt') series = patchstream.get_metadata_for_test(text) 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: patchstream.fix_patches(series, args) if cover_fname and series.get('cover'): @@ -211,7 +225,7 @@ class TestFunctional(unittest.TestCase): series.DoChecks() cc_file = series.MakeCcFile(process_tags, cover_fname, not ignore_bad_tags, add_maintainers, - None) + None, get_maintainer_script) cmd = gitutil.email_patches( series, cover_fname, args, dry_run, not ignore_bad_tags, cc_file, in_reply_to=in_reply_to, thread=None) @@ -506,6 +520,37 @@ complicated as possible''') finally: 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): """Test collection of tags in a patchstream""" text = '''This is a patch diff --git a/tools/patman/get_maintainer.py b/tools/patman/get_maintainer.py index e1d15ff..f7011be 100644 --- a/tools/patman/get_maintainer.py +++ b/tools/patman/get_maintainer.py @@ -1,48 +1,61 @@ # SPDX-License-Identifier: GPL-2.0+ # Copyright (c) 2012 The Chromium OS Authors. +# Copyright (c) 2022 Maxim Cournoyer # import os +import shlex +import shutil from patman import command +from patman import gitutil -def find_get_maintainer(try_list): - """Look for the get_maintainer.pl script. - Args: - try_list: List of directories to try for the get_maintainer.pl script +def find_get_maintainer(script_file_name): + """Try to find where `script_file_name` is. - Returns: - If the script is found we'll return a path to it; else None. + It searches in PATH and falls back to a path relative to the top + of the current git repository. """ - # Look in the list - for path in try_list: - fname = os.path.join(path, 'get_maintainer.pl') - if os.path.isfile(fname): - return fname + get_maintainer = shutil.which(script_file_name) + if get_maintainer: + return get_maintainer + + git_relative_script = os.path.join(gitutil.get_top_level(), + script_file_name) + if os.path.exists(git_relative_script): + return git_relative_script - return None -def get_maintainer(dir_list, fname, verbose=False): - """Run get_maintainer.pl on a file if we find it. +def get_maintainer(script_file_name, fname, verbose=False): + """Run `script_file_name` on a file. - We look for get_maintainer.pl in the 'scripts' directory at the top of - git. If we find it we'll run it. If we don't find get_maintainer.pl - 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: - dir_list: List of directories to try for the get_maintainer.pl script - fname: Path to the patch file to run get_maintainer.pl on. + script_file_name: The file name of the get_maintainer.pl script + (or compatible). + fname: File name of the patch to process with get_maintainer.pl. Returns: 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 verbose: print("WARNING: Couldn't find get_maintainer.pl") return [] - stdout = command.output(get_maintainer, '--norolestats', fname) + stdout = command.output(get_maintainer, *arguments, fname) lines = stdout.splitlines() - return [ x.replace('"', '') for x in lines ] + return [x.replace('"', '') for x in lines] diff --git a/tools/patman/gitutil.py b/tools/patman/gitutil.py index 74c6e94..5e74210 100644 --- a/tools/patman/gitutil.py +++ b/tools/patman/gitutil.py @@ -433,7 +433,7 @@ def check_suppress_cc_config(): 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, - smtp_server=None): + smtp_server=None, get_maintainer_script=None): """Email a patch series. 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 all patches reply to cover-letter or first patch in series) smtp_server: SMTP server to use to send patches + get_maintainer_script: File name of script to get maintainers emails Returns: Git command that was/would be run diff --git a/tools/patman/patman.rst b/tools/patman/patman.rst index d7994c8..6113962 100644 --- a/tools/patman/patman.rst +++ b/tools/patman/patman.rst @@ -1,6 +1,7 @@ .. SPDX-License-Identifier: GPL-2.0+ .. Copyright (c) 2011 The Chromium OS Authors .. Simon Glass +.. Maxim Cournoyer .. v1, v2, 19-Oct-11 .. revised v3 24-Nov-11 .. revised v4 Independence Day 2020, with Patchwork integration @@ -68,8 +69,23 @@ this once:: git config sendemail.aliasesfile doc/git-mailrc -For both Linux and U-Boot the 'scripts/get_maintainer.pl' handles figuring -out where to send patches pretty well. +For both Linux and U-Boot the 'scripts/get_maintainer.pl' handles +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 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 others: Mike Frysinger , Fred Bloggs -Patman will also look for a `.patman` configuration file at the root -of the current project git repository, which makes it possible to -override the `project` settings variable or anything else in a -project-specific way. The values of this "local" configuration file -take precedence over those of the "global" one. +As hinted above, Patman will also look for a `.patman` configuration +file at the root of the current project git repository, which makes it +possible to override the `project` settings variable or anything else +in a project-specific way. The values of this "local" configuration +file take precedence over those of the "global" one. Aliases are recursive. diff --git a/tools/patman/series.py b/tools/patman/series.py index 3075378..2eeeef7 100644 --- a/tools/patman/series.py +++ b/tools/patman/series.py @@ -235,7 +235,7 @@ class Series(dict): print(col.build(col.RED, str)) 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 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 List of maintainers to include (for testing) 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: Filename of temp file created """ @@ -267,8 +269,9 @@ class Series(dict): if type(add_maintainers) == type(cc): cc += 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): print(col.build(col.YELLOW, 'Skipping "%s"' % x)) cc = list(set(cc) - set(settings.bounces)) -- cgit v1.1