@@ -1925,6 +1925,9 @@ dist_patch_DATA = \
%D%/packages/patches/twinkle-bcg729.patch \
%D%/packages/patches/u-boot-allow-disabling-openssl.patch \
%D%/packages/patches/u-boot-infodocs-target.patch \
+ %D%/packages/patches/u-boot-patman-fix-help.patch \
+ %D%/packages/patches/u-boot-patman-get-maintainer.patch \
+ %D%/packages/patches/u-boot-patman-local-conf.patch \
%D%/packages/patches/u-boot-nintendo-nes-serial.patch \
%D%/packages/patches/u-boot-rockchip-inno-usb.patch \
%D%/packages/patches/u-boot-sifive-prevent-reloc-initrd-fdt.patch \
@@ -69,7 +69,10 @@ (define-module (gnu packages bootloaders)
#:use-module (gnu packages valgrind)
#:use-module (gnu packages virtualization)
#:use-module (gnu packages xorg)
+ #:use-module (gnu packages python-web)
+ #:use-module (gnu packages python-xyz)
#:use-module (guix build-system gnu)
+ #:use-module (guix build-system pyproject)
#:use-module (guix build-system trivial)
#:use-module (guix download)
#:use-module (guix gexp)
@@ -640,7 +643,10 @@ (define u-boot
%u-boot-allow-disabling-openssl-patch
%u-boot-sifive-prevent-relocating-initrd-fdt
%u-boot-rk3399-enable-emmc-phy-patch
- (search-patch "u-boot-infodocs-target.patch")))
+ (search-patch "u-boot-infodocs-target.patch")
+ (search-patch "u-boot-patman-fix-help.patch")
+ (search-patch "u-boot-patman-local-conf.patch")
+ (search-patch "u-boot-patman-get-maintainer.patch")))
(method url-fetch)
(uri (string-append
"https://ftp.denx.de/pub/u-boot/"
@@ -816,6 +822,34 @@ (define-public u-boot-tools
" This package provides board-independent tools "
"of U-Boot."))))
+;;; This is packaged separately, as it can be used in other contexts than for
+;;; U-Boot development.
+(define-public patman
+ (package
+ (inherit u-boot)
+ (name "patman")
+ (build-system pyproject-build-system)
+ (arguments
+ ;; The test suite strongly relies on the git metadata being available (23
+ ;; failed, 14 passed).
+ (list
+ #:tests? #f
+ #:phases
+ #~(modify-phases %standard-phases
+ (add-after 'unpack 'chdir
+ (lambda _
+ (chdir "tools/patman"))))))
+ (inputs (list python-pygit2 python-requests))
+ (synopsis "Patch automation tool")
+ (description "Patman is a patch automation script which:
+@itemize
+@item Creates patches directly from your branch
+@item Cleans them up by removing unwanted tags
+@item Inserts a cover letter with change lists
+@item Runs the patches through automated checks
+@item Optionally emails them out to selected people.
+@end itemize")))
+
(define*-public (make-u-boot-package board triplet
#:key
defconfig
new file mode 100644
@@ -0,0 +1,40 @@
+Upstream status: https://patchwork.ozlabs.org/project/uboot/list/?series=333156
+
+diff --git a/tools/patman/main.py b/tools/patman/main.py
+index 5a7756a221..bf300c6e64 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 shutil
+@@ -163,11 +164,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:
+diff --git a/tools/patman/setup.py b/tools/patman/setup.py
+index 43fdc00ce6..ce9bb4aa63 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'])
new file mode 100644
@@ -0,0 +1,285 @@
+Upstream status: https://patchwork.ozlabs.org/project/uboot/list/?series=333427
+
+diff --git a/tools/patman/control.py b/tools/patman/control.py
+index bf426cf7bc..38e98dab84 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 7b92bc67be..6fa6e6a75e 100644
+--- a/tools/patman/func_test.py
++++ b/tools/patman/func_test.py
+@@ -200,6 +200,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 = (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'):
+@@ -207,7 +209,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)
+diff --git a/tools/patman/get_maintainer.py b/tools/patman/get_maintainer.py
+index e1d15ff6ab..eb06423c19 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 <maxim.cournoyer@savoirfairelinux.com>
+ #
+
+ 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.exist(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 ceaf2ce150..a72ba4f42d 100644
+--- a/tools/patman/gitutil.py
++++ b/tools/patman/gitutil.py
+@@ -417,8 +417,8 @@ def check_suppress_cc_config():
+ 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, get_maintainer_script=None):
+ """Email a patch series.
+
+ Args:
+@@ -435,6 +435,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/main.py b/tools/patman/main.py
+index fb5ad60b20..c39c0424f9 100755
+--- a/tools/patman/main.py
++++ b/tools/patman/main.py
+@@ -64,6 +64,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/patman.rst b/tools/patman/patman.rst
+index 7828899879..5a3554f961 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 <sjg@chromium.org>
++.. Maxim Cournoyer <maxim.cournoyer@savoirfairelinux.com>
+ .. 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 <wd@denx.de>
+ others: Mike Frysinger <vapier@gentoo.org>, Fred Bloggs <f.bloggs@napier.net>
+
+-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 3075378ac1..2eeeef71dc 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))
new file mode 100644
@@ -0,0 +1,176 @@
+Upstream status: https://patchwork.ozlabs.org/project/uboot/list/?series=333354
+
+diff --git a/tools/patman/main.py b/tools/patman/main.py
+index bf300c6e64..3616b28f27 100755
+--- a/tools/patman/main.py
++++ b/tools/patman/main.py
+@@ -116,7 +116,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/patman.rst b/tools/patman/patman.rst
+index 8c5c9cc2cc..7828899879 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 <wd@denx.de>
+ others: Mike Frysinger <vapier@gentoo.org>, Fred Bloggs <f.bloggs@napier.net>
+
++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 903d6fcb0b..e8e2908f1f 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 <maxim.cournoyer@savoirfairelinux.com>
+ #
+
+ try:
+@@ -11,8 +12,7 @@ import argparse
+ import os
+ import re
+
+-from patman import command
+-from patman import tools
++from patman import gitutil
+
+ """Default settings per-project.
+
+@@ -190,7 +190,8 @@ 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:
+@@ -328,26 +329,46 @@ def GetItems(config, section):
+ except:
+ raise
+
+-def Setup(gitutil, parser, project_name, config_fname=''):
++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
++ 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. 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 == '':
++
++ 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')
++ has_config = os.path.exists(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)
+
+- if not os.path.exists(config_fname):
+- print("No config file found ~/.patman\nCreating one...\n")
+- CreatePatmanConfigFile(gitutil, 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)
+
+- config.read(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 0000000000..9c14b4aaa3
+--- /dev/null
++++ b/tools/patman/test_settings.py
+@@ -0,0 +1,43 @@
++# SPDX-License-Identifier: GPL-2.0+
++#
++# Copyright (c) 2022 Maxim Cournoyer <maxim.cournoyer@savoirfairelinux.com>
++#
++
++import argparse
++import contextlib
++import os
++import subprocess
++import tempfile
++
++from patman import settings
++
++
++@contextlib.contextmanager
++def empty_git_repository():
++ with tempfile.TemporaryDirectory() as tmpdir:
++ os.chdir(tmpdir)
++ subprocess.check_call(['git', 'init'])
++ yield tmpdir
++
++
++def test_git_local_config():
++ 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')
++
++ # Test "global" config is used.
++ settings.Setup(parser, 'unknown', global_config.name)
++ args, _ = parser.parse_known_args()
++ assert args.project == 'u-boot'
++
++ # Test local config can shadow it.
++ with open('.patman', 'w', buffering=1) as f:
++ f.write('[settings]\n'
++ 'project=guix-patches\n')
++ settings.Setup(parser, 'unknown', global_config.name)
++ args, _ = parser.parse_known_args([])
++ assert args.project == 'guix-patches'