Compare commits

...

20 Commits
loadp ... patc

Author SHA1 Message Date
Simon Glass
a9e75823fb patman: Support collecting all lines in the commit message
In some cases we want to collect all lines in the commit message so that
the commit can be recreated with the same message as before, or perhaps
with light filtering.

Add support for this.

Series-to: u-boot
Cover-letter:
patman: Minor improvements to prepare for series handling
This series includes a number of internal improvements to patman:
- Tidy-up of parsing
- Adjust how tests create the git tree
- Support for creating patches in a different git directory
- Faster determination of the upstream branch
- Ability to collect the body of a commit message as a series of lines
END

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:23 +02:00
Simon Glass
e656a9573b patman: Tidy up function comments in the series module
This module is missing some comments, so add them.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:23 +02:00
Simon Glass
57c9d0eba0 patman: Move -H out of the send command
This is the help for the whole of patman, so move it to the start of the
control function, rather than being inside 'patman send'.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:23 +02:00
Simon Glass
40007b4532 patman: Move arguments for sent into the correct parser
Most of the arguments for the main parser are actually arguments for the
'send' parser. Move them there, in a separate function.

Fix a pylint warning for -D and the imports while here.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:23 +02:00
Simon Glass
2bf9787256 patman: Split subparsers into their own functions
Simplify the main parser by moving subparser code into separate
functions. Fix a few pylint warnings while here.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:23 +02:00
Simon Glass
a5b21961ea patman: Add tests for help
Add a few tests to make sure that help is provided when requested.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:23 +02:00
Simon Glass
a1b7913341 patman: Split parser creation from parsing
Tests may want to parse their own arguments. Refactor the parser code to
support this and allow settings to receive arguments as well.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:23 +02:00
Simon Glass
fbbc3b80a5 patman: Correct some pylint and asyncio issues
Correct some pylint warnings in this file. Make use of the existing
asyncio event loop, instead of creating a new one, since this also
destroys it afterwards, making it unavailable for tests which want to
share an event loop. Use tools.write_file() to avoid a warning about
encoding.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:23 +02:00
Simon Glass
bfa4939072 patman: Use git to set up the test config
At present the branch information is dropped when writing the
configuration. It is easier to get git to set up the config anyway, so
update the test to do this.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:23 +02:00
Simon Glass
af9f4fb7de patman: Clean up creation of the git tree
The test starts with the HEAD pointing to the wrong place, so that the
created files appear to be deleted. Fix this by resetting the tree
before tests start. Add a check that the tree is clean.

Update pygit2 so that the enums are available.
2025-05-08 07:11:23 +02:00
Simon Glass
50bb69c7be patman: Use variables for series ID and title
Rather than hard-coding these values in the sample patches, use
variables so that we can refer to these in tests.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:23 +02:00
Simon Glass
f2d0ef86e2 patman: Use .git as the git directory
In tests, the 'git' directory is a subdirectory of the temporary
directory. Rename it to '.git' so that git will automatically find it
when git operations are done in the temporary directory. Set up the
config before the first git operation, so that this works correctly.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:23 +02:00
Simon Glass
396f9a6eee patman: Use itr instead of lines for iterator
The variables 'lines' is used to hold a list of output lines within a
test, but also to hold an iterator through those lines. Use 'itr' for
the latter, to avoid confusion.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:23 +02:00
Simon Glass
7c84f896db patman: Correct use of str in code
Since str() is a reserved function we should not use it as a variable.
Fix this in the send module.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:23 +02:00
Simon Glass
f68c357f7f patman: Allow setting a git directory when sending
Support specifying the git-directory when creating and sending patches.
This will allow better testing of this functionality, since we can use a
test directory.

For count_commits_to_branch() support an end commit while we are here.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:22 +02:00
Simon Glass
c83fbc36e8 patman: Allow setting the current directory when sending
Plumb a current-working-directory (cwd) through from send all the way to
the command gitutil libraries. This will allow better testing of this
functionality, since we can use a test directory.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:22 +02:00
Simon Glass
d7a1359623 patman: Add all files to __init__.py
Some files are missing from the __all__ list, so add then. Reformat the
list to use more of the width of each line.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:22 +02:00
Simon Glass
6127d9d262 u_boot_pylib: Speed up determining the upstream branch
Use --decorate to quickly detect the upstream branch, since this is much
faster than using 'git name-rev' on every possible commit.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:22 +02:00
Simon Glass
493007c6ef u_boot_pylib: Provide directories to gitutil functions
For testing it is useful to be able to set the current directory used
for git operations, as well as the git-repo directory. Update some of
the functions to support this.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:22 +02:00
Simon Glass
b38314bcd4 u_boot_pylib: Tidy up quoting of cc and to
The current approach to calling 'git send-email' puts double quotes
around each email address to ensure that it will pass the shell
correctly. This is a bit cumbersome and requires using a shell to sort
it all out.

Drop the quotes and use command.run() instead, to simplify things. This
will also make it possible to (later) set the current directory.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-05-08 07:11:22 +02:00
13 changed files with 528 additions and 304 deletions

View File

@@ -1,5 +1,8 @@
# SPDX-License-Identifier: GPL-2.0+
__all__ = ['checkpatch', 'commit', 'control', 'func_test', 'get_maintainer',
'__main__', 'patchstream', 'project', 'series',
'settings', 'setup', 'status', 'test_checkpatch', 'test_settings']
__all__ = [
'checkpatch', 'cmdline', 'commit', 'control', 'func_test',
'get_maintainer', '__main__', 'patchstream', 'patchwork', 'project',
'send', 'series', 'settings', 'setup', 'status', 'test_checkpatch',
'test_settings'
]

View File

@@ -187,7 +187,8 @@ def check_patch_parse(checkpatch_output, verbose=False):
return result
def check_patch(fname, verbose=False, show_types=False, use_tree=False):
def check_patch(fname, verbose=False, show_types=False, use_tree=False,
cwd=None):
"""Run checkpatch.pl on a file and parse the results.
Args:
@@ -196,6 +197,7 @@ def check_patch(fname, verbose=False, show_types=False, use_tree=False):
parsed
show_types: Tell checkpatch to show the type (number) of each message
use_tree (bool): If False we'll pass '--no-tree' to checkpatch.
cwd (str): Path to use for patch files (None to use current dir)
Returns:
namedtuple containing:
@@ -217,7 +219,8 @@ def check_patch(fname, verbose=False, show_types=False, use_tree=False):
args.append('--no-tree')
if show_types:
args.append('--show-types')
output = command.output(*args, fname, raise_on_error=False)
output = command.output(*args, os.path.join(cwd or '', fname),
raise_on_error=False)
return check_patch_parse(output, verbose)
@@ -240,7 +243,7 @@ def get_warning_msg(col, msg_type, fname, line, msg):
line_str = '' if line is None else '%d' % line
return '%s:%s: %s: %s\n' % (fname, line_str, msg_type, msg)
def check_patches(verbose, args, use_tree):
def check_patches(verbose, args, use_tree, cwd):
'''Run the checkpatch.pl script on each patch'''
error_count, warning_count, check_count = 0, 0, 0
col = terminal.Color()
@@ -248,7 +251,8 @@ def check_patches(verbose, args, use_tree):
with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor:
futures = []
for fname in args:
f = executor.submit(check_patch, fname, verbose, use_tree=use_tree)
f = executor.submit(check_patch, fname, verbose, use_tree=use_tree,
cwd=cwd)
futures.append(f)
for fname, f in zip(args, futures):

View File

@@ -13,114 +13,115 @@ import os
import pathlib
import sys
from patman import project
from u_boot_pylib import gitutil
from patman import project
from patman import settings
PATMAN_DIR = pathlib.Path(__file__).parent
HAS_TESTS = os.path.exists(PATMAN_DIR / "func_test.py")
def parse_args():
"""Parse command line arguments from sys.argv[]
Returns:
tuple containing:
options: command line options
args: command lin arguments
def add_send_args(par):
"""Add arguments for the 'send' command
Arguments:
par (ArgumentParser): Parser to add to
"""
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 = argparse.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,
par.add_argument(
'-c', '--count', dest='count', type=int, default=-1,
help='Automatically create patches from top n commits')
par.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(
'-N', '--no-capture', action='store_true',
help='Disable capturing of console output in tests')
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(
'-X', '--test-preserve-dirs', action='store_true',
help='Preserve and display test-created directories')
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', 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')
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(
par.add_argument(
'-i', '--ignore-errors', action='store_true',
dest='ignore_errors', default=False,
help='Send patches email even if patch errors are found')
par.add_argument(
'-l', '--limit-cc', dest='limit', type=int, default=None,
help='Limit the cc list to LIMIT entries [default: %(default)s]')
par.add_argument(
'-m', '--no-maintainers', action='store_false',
dest='add_maintainers', default=True,
help="Don't cc the file maintainers automatically")
par.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',
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(
par.add_argument(
'-r', '--in-reply-to', type=str, action='store',
help="Message ID that this series is in reply to")
par.add_argument(
'-s', '--start', dest='start', type=int, default=0,
help='Commit to start creating patches from (0 = HEAD)')
par.add_argument(
'-t', '--ignore-bad-tags', action='store_true', default=False,
help='Ignore bad tags / aliases (default=warn)')
par.add_argument(
'--no-binary', action='store_true', dest='ignore_binary',
default=False,
help="Do not output contents of changes in binary files")
par.add_argument(
'--no-check', action='store_false', dest='check_patch', default=True,
help="Don't check for patch compliance")
par.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(
par.add_argument(
'--no-tree', dest='check_patch_use_tree', action='store_false',
help="Set `tree` to False")
par.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('--keep-change-id', action='store_true',
help='Preserve Change-Id tags in patches to send.')
par.add_argument(
'--no-signoff', action='store_false', dest='add_signoff',
default=True, help="Don't add Signed-off-by to patches")
par.add_argument(
'--smtp-server', type=str,
help="Specify the SMTP server to 'git send-email'")
par.add_argument(
'--keep-change-id', action='store_true',
help='Preserve Change-Id tags in patches to send.')
def add_send_subparser(subparsers):
"""Add the 'send' subparser
Args:
subparsers (argparse action): Subparser parent
Return:
ArgumentParser: send subparser
"""
send = subparsers.add_parser(
'send', help='Format, check and email patches (default command)')
send.add_argument(
'-b', '--branch', type=str,
help="Branch to process (by default, the current branch)")
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(
'--cc-cmd', dest='cc_cmd', type=str, action='store',
default=None, help='Output cc list for patch file (used by git)')
add_send_args(send)
send.add_argument('patchfiles', nargs='*')
return send
# Only add the 'test' action if the test data files are available.
if HAS_TESTS:
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")
def add_status_subparser(subparsers):
"""Add the 'status' subparser
Args:
subparsers (argparse action): Subparser parent
Return:
ArgumentParser: status subparser
"""
status = subparsers.add_parser('status',
help='Check status of patches in patchwork')
status.add_argument('-C', '--show-comments', action='store_true',
@@ -132,20 +133,89 @@ def parse_args():
help='Force overwriting an existing branch')
status.add_argument('-T', '--single-thread', action='store_true',
help='Disable multithreading when reading patchwork')
return status
def setup_parser():
"""Set up command-line parser
Returns:
argparse.Parser object
"""
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 = argparse.ArgumentParser(epilog=epilog)
parser.add_argument(
'-D', '--debug', action='store_true',
help='Enabling debugging (provides a full traceback on error)')
parser.add_argument(
'-N', '--no-capture', action='store_true',
help='Disable capturing of console output in tests')
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(
'-T', '--thread', action='store_true', dest='thread',
default=False, help='Create patches as a single thread')
parser.add_argument(
'-v', '--verbose', action='store_true', dest='verbose', default=False,
help='Verbose output of errors and warnings')
parser.add_argument(
'-X', '--test-preserve-dirs', action='store_true',
help='Preserve and display test-created directories')
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')
add_send_subparser(subparsers)
add_status_subparser(subparsers)
# Only add the 'test' action if the test data files are available.
if HAS_TESTS:
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")
return parser
def parse_args(argv=None, config_fname=None, parser=None):
"""Parse command line arguments from sys.argv[]
Args:
argv (str or None): Arguments to process, or None to use sys.argv[1:]
config_fname (str): Config file to read, or None for default, or False
for an empty config
Returns:
tuple containing:
options: command line options
args: command lin arguments
"""
if not parser:
parser = setup_parser()
# 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:]
if not argv:
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, argv, config_fname)
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:
elif not args.full_help:
# No command, so insert it after the known arguments and before the ones
# that presumably relate to the 'send' subcommand
nargs = len(rest)

View File

@@ -110,6 +110,15 @@ def patchwork_status(branch, count, start, end, dest_branch, force,
def do_patman(args):
"""Process a patman command
Args:
args (Namespace): Arguments to process
"""
if args.full_help:
with resources.path('patman', 'README.rst') as readme:
tools.print_full_help(str(readme))
return 0
if args.cmd == 'send':
# Called from git with a patch filename as argument
# Printout a list of additional CC recipients for this patch
@@ -123,15 +132,12 @@ def do_patman(args):
cca = cca.strip()
if cca:
print(cca)
elif args.full_help:
with 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
do_send(args)
return 0
ret_code = 0
try:

View File

@@ -16,6 +16,12 @@ import sys
import tempfile
import unittest
import pygit2
from u_boot_pylib import command
from u_boot_pylib import gitutil
from u_boot_pylib import terminal
from u_boot_pylib import tools
from patman.commit import Commit
from patman import control
@@ -24,12 +30,6 @@ from patman.patchstream import PatchStream
from patman import patchwork
from patman import send
from patman.series import Series
from patman import settings
from u_boot_pylib import gitutil
from u_boot_pylib import terminal
from u_boot_pylib import tools
import pygit2
from patman import status
PATMAN_DIR = pathlib.Path(__file__).parent
@@ -59,6 +59,10 @@ class TestFunctional(unittest.TestCase):
verbosity = False
preserve_outdirs = False
# Fake patchwork info for testing
SERIES_ID_SECOND_V1 = 456
TITLE_SECOND = 'Series for my board'
@classmethod
def setup_test_args(cls, preserve_indir=False, preserve_outdirs=False,
toolpath=None, verbosity=None, no_capture=False):
@@ -78,8 +82,10 @@ class TestFunctional(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.mkdtemp(prefix='patman.')
self.gitdir = os.path.join(self.tmpdir, 'git')
self.gitdir = os.path.join(self.tmpdir, '.git')
self.repo = None
self._patman_pathname = sys.argv[0]
self._patman_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
def tearDown(self):
if self.preserve_outdirs:
@@ -223,7 +229,8 @@ class TestFunctional(unittest.TestCase):
"""
process_tags = True
ignore_bad_tags = False
stefan = b'Stefan Br\xc3\xbcns <stefan.bruens@rwth-aachen.de>'.decode('utf-8')
stefan = (b'Stefan Br\xc3\xbcns <stefan.bruens@rwth-aachen.de>'
.decode('utf-8'))
rick = 'Richard III <richard@palace.gov>'
mel = b'Lord M\xc3\xablchett <clergy@palace.gov>'.decode('utf-8')
add_maintainers = [stefan, rick]
@@ -260,43 +267,43 @@ class TestFunctional(unittest.TestCase):
cc_lines = open(cc_file, encoding='utf-8').read().splitlines()
os.remove(cc_file)
lines = iter(out[0].getvalue().splitlines())
itr = iter(out[0].getvalue().splitlines())
self.assertEqual('Cleaned %s patches' % len(series.commits),
next(lines))
self.assertEqual('Change log missing for v2', next(lines))
self.assertEqual('Change log missing for v3', next(lines))
self.assertEqual('Change log for unknown version v4', next(lines))
self.assertEqual("Alias 'pci' not found", next(lines))
while next(lines) != 'Cc processing complete':
next(itr))
self.assertEqual('Change log missing for v2', next(itr))
self.assertEqual('Change log missing for v3', next(itr))
self.assertEqual('Change log for unknown version v4', next(itr))
self.assertEqual("Alias 'pci' not found", next(itr))
while next(itr) != 'Cc processing complete':
pass
self.assertIn('Dry run', next(lines))
self.assertEqual('', next(lines))
self.assertIn('Send a total of %d patches' % count, next(lines))
prev = next(lines)
for i, commit in enumerate(series.commits):
self.assertIn('Dry run', next(itr))
self.assertEqual('', next(itr))
self.assertIn('Send a total of %d patches' % count, next(itr))
prev = next(itr)
for i in range(len(series.commits)):
self.assertEqual(' %s' % args[i], prev)
while True:
prev = next(lines)
prev = next(itr)
if 'Cc:' not in prev:
break
self.assertEqual('To: u-boot@lists.denx.de', prev)
self.assertEqual('Cc: %s' % stefan, next(lines))
self.assertEqual('Version: 3', next(lines))
self.assertEqual('Prefix:\t RFC', next(lines))
self.assertEqual('Postfix:\t some-branch', next(lines))
self.assertEqual('Cover: 4 lines', next(lines))
self.assertEqual(' Cc: %s' % self.fred, next(lines))
self.assertEqual(' Cc: %s' % self.joe, next(lines))
self.assertEqual('Cc: %s' % stefan, next(itr))
self.assertEqual('Version: 3', next(itr))
self.assertEqual('Prefix:\t RFC', next(itr))
self.assertEqual('Postfix:\t some-branch', next(itr))
self.assertEqual('Cover: 4 lines', next(itr))
self.assertEqual(' Cc: %s' % self.fred, next(itr))
self.assertEqual(' Cc: %s' % self.joe, next(itr))
self.assertEqual(' Cc: %s' % self.leb,
next(lines))
self.assertEqual(' Cc: %s' % mel, next(lines))
self.assertEqual(' Cc: %s' % rick, next(lines))
next(itr))
self.assertEqual(' Cc: %s' % mel, next(itr))
self.assertEqual(' Cc: %s' % rick, next(itr))
expected = ('Git command: git send-email --annotate '
'--in-reply-to="%s" --to "u-boot@lists.denx.de" '
'--in-reply-to="%s" --to u-boot@lists.denx.de '
'--cc "%s" --cc-cmd "%s send --cc-cmd %s" %s %s'
% (in_reply_to, stefan, sys.argv[0], cc_file, cover_fname,
' '.join(args)))
self.assertEqual(expected, next(lines))
self.assertEqual(expected, next(itr))
self.assertEqual(('%s %s\0%s' % (args[0], rick, stefan)), cc_lines[0])
self.assertEqual(
@@ -384,7 +391,8 @@ Changes in v2:
def test_base_commit(self):
"""Test adding a base commit with no cover letter"""
orig_text = self._get_text('test01.txt')
pos = orig_text.index('commit 5ab48490f03051875ab13d288a4bf32b507d76fd')
pos = orig_text.index(
'commit 5ab48490f03051875ab13d288a4bf32b507d76fd')
text = orig_text[:pos]
series = patchstream.get_metadata_for_test(text)
series.base_commit = Commit('1a44532')
@@ -415,7 +423,7 @@ Changes in v2:
fname (str): Filename of file to create
text (str): Text to put into the file
"""
path = os.path.join(self.gitdir, fname)
path = os.path.join(self.tmpdir, fname)
tools.write_file(path, text, binary=False)
index = self.repo.index
index.add(fname)
@@ -443,6 +451,11 @@ Changes in v2:
self.repo = repo
new_tree = repo.TreeBuilder().write()
common = ['git', f'--git-dir={self.gitdir}', 'config']
tools.run(*(common + ['user.name', 'Dummy']), cwd=self.gitdir)
tools.run(*(common + ['user.email', 'dumdum@dummy.com']),
cwd=self.gitdir)
# pylint doesn't seem to find this
# pylint: disable=E1101
author = pygit2.Signature('Test user', 'test@email.com')
@@ -498,7 +511,7 @@ better than before''')
target = repo.revparse_single('HEAD~2')
# pylint doesn't seem to find this
# pylint: disable=E1101
repo.reset(target.oid, pygit2.GIT_CHECKOUT_FORCE)
repo.reset(target.oid, pygit2.enums.ResetMode.HARD)
self.make_commit_with_file('video: Some video improvements', '''
Fix up the video so that
it looks more purple. Purple is
@@ -507,16 +520,17 @@ a very nice colour.
Purple and purple
Even more purple
Could not be any more purple''')
self.make_commit_with_file('serial: Add a serial driver', '''
self.make_commit_with_file('serial: Add a serial driver', f'''
Here is the serial driver
for my chip.
Cover-letter:
Series for my board
{self.TITLE_SECOND}
This series implements support
for my glorious board.
END
Series-links: 183237
Series-to: u-boot
Series-links: {self.SERIES_ID_SECOND_V1}
''', 'serial.c', '''The code for the
serial driver is here''')
self.make_commit_with_file('bootm: Make it boot', '''
@@ -537,6 +551,13 @@ complicated as possible''')
repo.config.set_multivar('branch.second.merge', '', 'refs/heads/base')
repo.branches.local.create('base', base_target)
target = repo.lookup_reference('refs/heads/first')
repo.checkout(target, strategy=pygit2.GIT_CHECKOUT_FORCE)
target = repo.revparse_single('HEAD')
repo.reset(target.oid, pygit2.enums.ResetMode.HARD)
self.assertFalse(gitutil.check_dirty(self.gitdir, self.tmpdir))
return repo
def test_branch(self):
@@ -549,7 +570,7 @@ complicated as possible''')
control.setup()
orig_dir = os.getcwd()
try:
os.chdir(self.gitdir)
os.chdir(self.tmpdir)
# Check that it can detect the current branch
self.assertEqual(2, gitutil.count_commits_to_branch(None))
@@ -564,7 +585,7 @@ complicated as possible''')
# Check that it can detect a different branch
self.assertEqual(3, gitutil.count_commits_to_branch('second'))
with terminal.capture() as _:
series, cover_fname, patch_files = send.prepare_patches(
_, cover_fname, patch_files = send.prepare_patches(
col, branch='second', count=-1, start=0, end=0,
ignore_binary=False, signoff=True)
self.assertIsNotNone(cover_fname)
@@ -601,7 +622,7 @@ complicated as possible''')
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):
with directory_excursion(self.tmpdir):
# Setup git.
os.environ['GIT_CONFIG_GLOBAL'] = '/dev/null'
os.environ['GIT_CONFIG_SYSTEM'] = '/dev/null'
@@ -609,19 +630,18 @@ complicated as possible''')
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'
'add_maintainers: True\n')
with open('dummy-script.sh', 'w', buffering=1) as f:
f.write('#!/usr/bin/env python\n'
'print("hello@there.com")\n')
tools.write_file('.patman', '[settings]\n'
'get_maintainer_script: dummy-script.sh\n'
'check_patch: False\n'
'add_maintainers: True\n', binary=False)
tools.write_file('dummy-script.sh',
'#!/usr/bin/env python\n'
'print("hello@there.com")\n', binary=False)
os.chmod('dummy-script.sh', 0x555)
tools.run('git', 'add', '.')
tools.run('git', 'commit', '-m', 'new commit')
# Finally, do the test
with terminal.capture():
@@ -651,7 +671,7 @@ Tested-by: %s
Serie-version: 2
'''
with self.assertRaises(ValueError) as exc:
pstrm = PatchStream.process_text(text)
PatchStream.process_text(text)
self.assertEqual("Line 3: Invalid tag = 'Serie-version: 2'",
str(exc.exception))
@@ -729,9 +749,9 @@ index c072e54..942244f 100644
--- a/lib/fdtdec.c
+++ b/lib/fdtdec.c
@@ -1200,7 +1200,8 @@ int fdtdec_setup_mem_size_base(void)
}
\t}
gd->ram_size = (phys_size_t)(res.end - res.start + 1);
\tgd->ram_size = (phys_size_t)(res.end - res.start + 1);
- debug("%s: Initial DRAM size %llx\n", __func__, (u64)gd->ram_size);
+ debug("%s: Initial DRAM size %llx\n", __func__,
+ (unsigned long long)gd->ram_size);
@@ -767,6 +787,28 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c
finally:
os.chdir(orig_dir)
def _RunPatman(self, *args):
all_args = [self._patman_pathname] + list(args)
return command.run_one(*all_args, capture=True, capture_stderr=True)
def testFullHelp(self):
command.TEST_RESULT = None
result = self._RunPatman('-H')
help_file = os.path.join(self._patman_dir, 'README.rst')
# Remove possible extraneous strings
extra = '::::::::::::::\n' + help_file + '\n::::::::::::::\n'
gothelp = result.stdout.replace(extra, '')
self.assertEqual(len(gothelp), os.path.getsize(help_file))
self.assertEqual(0, len(result.stderr))
self.assertEqual(0, result.return_code)
def testHelp(self):
command.TEST_RESULT = None
result = self._RunPatman('-h')
self.assertTrue(len(result.stdout) > 1000)
self.assertEqual(0, len(result.stderr))
self.assertEqual(0, result.return_code)
@staticmethod
def _fake_patchwork(subpath):
"""Fake Patchwork server for the function below
@@ -789,7 +831,8 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c
"""Test Patchwork patches not matching the series"""
pwork = patchwork.Patchwork.for_testing(self._fake_patchwork)
with terminal.capture() as (_, err):
patches = asyncio.run(status.check_status(1234, pwork))
loop = asyncio.get_event_loop()
patches = loop.run_until_complete(status.check_status(1234, pwork))
status.check_patch_count(0, len(patches))
self.assertIn('Warning: Patchwork reports 1 patches, series has 0',
err.getvalue())
@@ -797,7 +840,8 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c
def test_status_read_patch(self):
"""Test handling a single patch in Patchwork"""
pwork = patchwork.Patchwork.for_testing(self._fake_patchwork)
patches = asyncio.run(status.check_status(1234, pwork))
loop = asyncio.get_event_loop()
patches = loop.run_until_complete(status.check_status(1234, pwork))
self.assertEqual(1, len(patches))
patch = patches[0]
self.assertEqual('1', patch.id)
@@ -997,7 +1041,6 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c
# things behaves as expected
self.commits = [commit1, commit2]
self.patches = [patch1, patch2]
count = 2
# Check that the tags are picked up on the first patch
new_rtags, _ = status.process_reviews(patch1.content, patch1.comments,
@@ -1041,39 +1084,39 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c
pwork = patchwork.Patchwork.for_testing(self._fake_patchwork2)
status.check_and_show_status(series, '1234', None, None, False, False,
pwork)
lines = iter(terminal.get_print_test_lines())
itr = iter(terminal.get_print_test_lines())
col = terminal.Color()
self.assertEqual(terminal.PrintLine(' 1 Subject 1', col.YELLOW),
next(lines))
next(itr))
self.assertEqual(
terminal.PrintLine(' Reviewed-by: ', col.GREEN, newline=False,
bright=False),
next(lines))
next(itr))
self.assertEqual(terminal.PrintLine(self.joe, col.WHITE, bright=False),
next(lines))
next(itr))
self.assertEqual(terminal.PrintLine(' 2 Subject 2', col.YELLOW),
next(lines))
next(itr))
self.assertEqual(
terminal.PrintLine(' Reviewed-by: ', col.GREEN, newline=False,
bright=False),
next(lines))
self.assertEqual(terminal.PrintLine(self.fred, col.WHITE, bright=False),
next(lines))
next(itr))
self.assertEqual(terminal.PrintLine(self.fred, col.WHITE,
bright=False), next(itr))
self.assertEqual(
terminal.PrintLine(' Tested-by: ', col.GREEN, newline=False,
bright=False),
next(lines))
next(itr))
self.assertEqual(terminal.PrintLine(self.leb, col.WHITE, bright=False),
next(lines))
next(itr))
self.assertEqual(
terminal.PrintLine(' + Reviewed-by: ', col.GREEN, newline=False),
next(lines))
next(itr))
self.assertEqual(terminal.PrintLine(self.mary, col.WHITE),
next(lines))
next(itr))
self.assertEqual(terminal.PrintLine(
'1 new response available in patchwork (use -d to write them to a new branch)',
None), next(lines))
None), next(itr))
def _fake_patchwork3(self, subpath):
"""Fake Patchwork server for the function below
@@ -1107,7 +1150,7 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c
branch = 'first'
dest_branch = 'first2'
count = 2
gitdir = os.path.join(self.gitdir, '.git')
gitdir = self.gitdir
# Set up the test git tree. We use branch 'first' which has two commits
# in it
@@ -1175,18 +1218,18 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c
# Now check the actual test of the first commit message. We expect to
# see the new tags immediately below the old ones.
stdout = patchstream.get_list(dest_branch, count=count, git_dir=gitdir)
lines = iter([line.strip() for line in stdout.splitlines()
if '-by:' in line])
itr = iter([line.strip() for line in stdout.splitlines()
if '-by:' in line])
# First patch should have the review tag
self.assertEqual('Reviewed-by: %s' % self.joe, next(lines))
self.assertEqual('Reviewed-by: %s' % self.joe, next(itr))
# Second patch should have the sign-off then the tested-by and two
# reviewed-by tags
self.assertEqual('Signed-off-by: %s' % self.leb, next(lines))
self.assertEqual('Reviewed-by: %s' % self.fred, next(lines))
self.assertEqual('Reviewed-by: %s' % self.mary, next(lines))
self.assertEqual('Tested-by: %s' % self.leb, next(lines))
self.assertEqual('Signed-off-by: %s' % self.leb, next(itr))
self.assertEqual('Reviewed-by: %s' % self.fred, next(itr))
self.assertEqual('Reviewed-by: %s' % self.mary, next(itr))
self.assertEqual('Tested-by: %s' % self.leb, next(itr))
def test_parse_snippets(self):
"""Test parsing of review snippets"""
@@ -1262,8 +1305,9 @@ line8
'And another comment'],
['> File: file.c',
'> Line: 153 / 143: def check_patch(fname, show_types=False):',
'> and more code', '> +Addition here', '> +Another addition here',
'> codey', '> more codey', 'and another thing in same file'],
'> and more code', '> +Addition here',
'> +Another addition here', '> codey', '> more codey',
'and another thing in same file'],
['> File: file.c', '> Line: 253 / 243',
'> with no function context', 'one more thing'],
['> File: tools/patman/main.py', '> +line of code',
@@ -1357,75 +1401,77 @@ Reviewed-by: %s
pwork = patchwork.Patchwork.for_testing(self._fake_patchwork2)
status.check_and_show_status(series, '1234', None, None, False, True,
pwork)
lines = iter(terminal.get_print_test_lines())
itr = iter(terminal.get_print_test_lines())
col = terminal.Color()
self.assertEqual(terminal.PrintLine(' 1 Subject 1', col.YELLOW),
next(lines))
next(itr))
self.assertEqual(
terminal.PrintLine(' + Reviewed-by: ', col.GREEN, newline=False),
next(lines))
self.assertEqual(terminal.PrintLine(self.joe, col.WHITE), next(lines))
next(itr))
self.assertEqual(terminal.PrintLine(self.joe, col.WHITE), next(itr))
self.assertEqual(terminal.PrintLine('Review: %s' % self.joe, col.RED),
next(lines))
self.assertEqual(terminal.PrintLine(' Hi Fred,', None), next(lines))
self.assertEqual(terminal.PrintLine('', None), next(lines))
next(itr))
self.assertEqual(terminal.PrintLine(' Hi Fred,', None), next(itr))
self.assertEqual(terminal.PrintLine('', None), next(itr))
self.assertEqual(terminal.PrintLine(' > File: file.c', col.MAGENTA),
next(lines))
next(itr))
self.assertEqual(terminal.PrintLine(' > Some code', col.MAGENTA),
next(lines))
self.assertEqual(terminal.PrintLine(' > and more code', col.MAGENTA),
next(lines))
next(itr))
self.assertEqual(terminal.PrintLine(' > and more code',
col.MAGENTA),
next(itr))
self.assertEqual(terminal.PrintLine(
' Here is my comment above the above...', None), next(lines))
self.assertEqual(terminal.PrintLine('', None), next(lines))
' Here is my comment above the above...', None), next(itr))
self.assertEqual(terminal.PrintLine('', None), next(itr))
self.assertEqual(terminal.PrintLine(' 2 Subject 2', col.YELLOW),
next(lines))
next(itr))
self.assertEqual(
terminal.PrintLine(' + Reviewed-by: ', col.GREEN, newline=False),
next(lines))
next(itr))
self.assertEqual(terminal.PrintLine(self.fred, col.WHITE),
next(lines))
next(itr))
self.assertEqual(
terminal.PrintLine(' + Reviewed-by: ', col.GREEN, newline=False),
next(lines))
next(itr))
self.assertEqual(terminal.PrintLine(self.mary, col.WHITE),
next(lines))
next(itr))
self.assertEqual(
terminal.PrintLine(' + Tested-by: ', col.GREEN, newline=False),
next(lines))
next(itr))
self.assertEqual(terminal.PrintLine(self.leb, col.WHITE),
next(lines))
next(itr))
self.assertEqual(terminal.PrintLine('Review: %s' % self.fred, col.RED),
next(lines))
self.assertEqual(terminal.PrintLine(' Hi Fred,', None), next(lines))
self.assertEqual(terminal.PrintLine('', None), next(lines))
next(itr))
self.assertEqual(terminal.PrintLine(' Hi Fred,', None), next(itr))
self.assertEqual(terminal.PrintLine('', None), next(itr))
self.assertEqual(terminal.PrintLine(
' > File: tools/patman/commit.py', col.MAGENTA), next(lines))
' > File: tools/patman/commit.py', col.MAGENTA), next(itr))
self.assertEqual(terminal.PrintLine(
' > Line: 41 / 41: class Commit:', col.MAGENTA), next(lines))
' > Line: 41 / 41: class Commit:', col.MAGENTA), next(itr))
self.assertEqual(terminal.PrintLine(
' > + return self.subject', col.MAGENTA), next(lines))
' > + return self.subject', col.MAGENTA), next(itr))
self.assertEqual(terminal.PrintLine(
' > +', col.MAGENTA), next(lines))
' > +', col.MAGENTA), next(itr))
self.assertEqual(
terminal.PrintLine(' > def add_change(self, version, info):',
col.MAGENTA),
next(lines))
terminal.PrintLine(
' > def add_change(self, version, info):',
col.MAGENTA),
next(itr))
self.assertEqual(terminal.PrintLine(
' > """Add a new change line to the change list for a version.',
col.MAGENTA), next(lines))
col.MAGENTA), next(itr))
self.assertEqual(terminal.PrintLine(
' >', col.MAGENTA), next(lines))
' >', col.MAGENTA), next(itr))
self.assertEqual(terminal.PrintLine(
' A comment', None), next(lines))
self.assertEqual(terminal.PrintLine('', None), next(lines))
' A comment', None), next(itr))
self.assertEqual(terminal.PrintLine('', None), next(itr))
self.assertEqual(terminal.PrintLine(
'4 new responses available in patchwork (use -d to write them to a new branch)',
None), next(lines))
None), next(itr))
def test_insert_tags(self):
"""Test inserting of review tags"""

View File

@@ -109,6 +109,8 @@ class PatchStream:
self.recent_unquoted = queue.Queue()
self.was_quoted = None
self.insert_base_commit = insert_base_commit
self.lines = [] # All lines in a commit message
self.msg = None # Full commit message including subject
@staticmethod
def process_text(text, is_comment=False):
@@ -190,11 +192,22 @@ class PatchStream:
"""
self.commit.add_rtag(rtag_type, who)
def _close_commit(self):
"""Save the current commit into our commit list, and reset our state"""
def _close_commit(self, skip_last_line):
"""Save the current commit into our commit list, and reset our state
Args:
skip_last_line (bool): True to omit the final line of self.lines
when building the commit message. This is normally the blank
line between two commits, except at the end of the log, where
there is no blank line
"""
if self.commit and self.is_log:
# Skip the blank line before the subject
lines = self.lines[:-1] if skip_last_line else self.lines
self.commit.msg = '\n'.join(lines[1:]) + '\n'
self.series.AddCommit(self.commit)
self.commit = None
self.lines = []
# If 'END' is missing in a 'Cover-letter' section, and that section
# happens to show up at the very end of the commit message, this is
# the chance for us to fix it up.
@@ -345,6 +358,8 @@ class PatchStream:
self.state += 1
elif commit_match:
self.state = STATE_MSG_HEADER
if self.state != STATE_MSG_HEADER:
self.lines.append(line)
# If a tag is detected, or a new commit starts
if series_tag_match or commit_tag_match or change_id_match or \
@@ -499,7 +514,7 @@ class PatchStream:
# Detect the start of a new commit
elif commit_match:
self._close_commit()
self._close_commit(True)
self.commit = commit.Commit(commit_match.group(1))
# Detect tags in the commit message
@@ -579,7 +594,7 @@ class PatchStream:
"""Close out processing of this patch stream"""
self._finalise_snippet()
self._finalise_change()
self._close_commit()
self._close_commit(False)
if self.lines_after_test:
self._add_warn('Found %d lines after TEST=' % self.lines_after_test)
@@ -754,7 +769,7 @@ def get_metadata_for_list(commit_range, git_dir=None, count=None,
pst.finalise()
return series
def get_metadata(branch, start, count):
def get_metadata(branch, start, count, git_dir=None):
"""Reads out patch series metadata from the commits
This does a 'git log' on the relevant commits and pulls out the tags we
@@ -769,8 +784,9 @@ def get_metadata(branch, start, count):
Series: Object containing information about the commits.
"""
top = f"{branch if branch else 'HEAD'}~{start}"
series = get_metadata_for_list(top, None, count)
series.base_commit = commit.Commit(gitutil.get_hash(f'{top}~{count}'))
series = get_metadata_for_list(top, git_dir, count)
series.base_commit = commit.Commit(
gitutil.get_hash(f'{top}~{count}', git_dir))
series.branch = branch or gitutil.get_branch()
series.top = top
return series
@@ -792,7 +808,7 @@ def get_metadata_for_test(text):
return series
def fix_patch(backup_dir, fname, series, cmt, keep_change_id=False,
insert_base_commit=False):
insert_base_commit=False, cwd=None):
"""Fix up a patch file, by adding/removing as required.
We remove our tags from the patch file, insert changes lists, etc.
@@ -807,10 +823,12 @@ def fix_patch(backup_dir, fname, series, cmt, keep_change_id=False,
cmt (Commit): Commit object for this patch file
keep_change_id (bool): Keep the Change-Id tag.
insert_base_commit (bool): True to add the base commit to the end
cwd (str): Directory containing filename, or None for current
Return:
list: A list of errors, each str, or [] if all ok.
"""
fname = os.path.join(cwd or '', fname)
handle, tmpname = tempfile.mkstemp()
outfd = os.fdopen(handle, 'w', encoding='utf-8')
infd = open(fname, 'r', encoding='utf-8')
@@ -827,7 +845,8 @@ def fix_patch(backup_dir, fname, series, cmt, keep_change_id=False,
shutil.move(tmpname, fname)
return cmt.warn
def fix_patches(series, fnames, keep_change_id=False, insert_base_commit=False):
def fix_patches(series, fnames, keep_change_id=False, insert_base_commit=False,
cwd=None):
"""Fix up a list of patches identified by filenames
The patch files are processed in place, and overwritten.
@@ -837,6 +856,7 @@ def fix_patches(series, fnames, keep_change_id=False, insert_base_commit=False):
fnames (:type: list of str): List of patch files to process
keep_change_id (bool): Keep the Change-Id tag.
insert_base_commit (bool): True to add the base commit to the end
cwd (str): Directory containing the patch files, or None for current
"""
# Current workflow creates patches, so we shouldn't need a backup
backup_dir = None #tempfile.mkdtemp('clean-patch')
@@ -847,7 +867,7 @@ def fix_patches(series, fnames, keep_change_id=False, insert_base_commit=False):
cmt.count = count
result = fix_patch(backup_dir, fname, series, cmt,
keep_change_id=keep_change_id,
insert_base_commit=insert_base_commit)
insert_base_commit=insert_base_commit, cwd=cwd)
if result:
print('%d warning%s for %s:' %
(len(result), 's' if len(result) > 1 else '', fname))
@@ -857,14 +877,16 @@ def fix_patches(series, fnames, keep_change_id=False, insert_base_commit=False):
count += 1
print('Cleaned %d patch%s' % (count, 'es' if count > 1 else ''))
def insert_cover_letter(fname, series, count):
def insert_cover_letter(fname, series, count, cwd=None):
"""Inserts a cover letter with the required info into patch 0
Args:
fname (str): Input / output filename of the cover letter file
series (Series): Series object
count (int): Number of patches in the series
cwd (str): Directory containing filename, or None for current
"""
fname = os.path.join(cwd or '', fname)
fil = open(fname, 'r')
lines = fil.readlines()
fil.close()

View File

@@ -1,6 +1,6 @@
aiohttp==3.9.1
ConfigParser==7.1.0
importlib_resources==6.5.2
pygit2==1.13.3
pygit2==1.14.1
Requests==2.32.3
setuptools==75.8.0

View File

@@ -15,7 +15,7 @@ from u_boot_pylib import gitutil
from u_boot_pylib import terminal
def check_patches(series, patch_files, run_checkpatch, verbose, use_tree):
def check_patches(series, patch_files, run_checkpatch, verbose, use_tree, cwd):
"""Run some checks on a set of patches
This santiy-checks the patman tags like Series-version and runs the patches
@@ -29,6 +29,7 @@ def check_patches(series, patch_files, run_checkpatch, verbose, use_tree):
verbose (bool): True to print out every line of the checkpatch output as
it is parsed
use_tree (bool): If False we'll pass '--no-tree' to checkpatch.
cwd (str): Path to use for patch files (None to use current dir)
Returns:
bool: True if the patches had no errors, False if they did
@@ -38,7 +39,7 @@ def check_patches(series, patch_files, run_checkpatch, verbose, use_tree):
# Check the patches
if run_checkpatch:
ok = checkpatch.check_patches(verbose, patch_files, use_tree)
ok = checkpatch.check_patches(verbose, patch_files, use_tree, cwd)
else:
ok = True
return ok
@@ -46,7 +47,7 @@ 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, get_maintainer_script, limit,
dry_run, in_reply_to, thread, smtp_server):
dry_run, in_reply_to, thread, smtp_server, cwd=None):
"""Email patches to the recipients
This emails out the patches and cover letter using 'git send-email'. Each
@@ -85,18 +86,19 @@ def email_patches(col, series, cover_fname, patch_files, process_tags, its_a_go,
thread (bool): True to add --thread to git send-email (make all patches
reply to cover-letter or first patch in series)
smtp_server (str): SMTP server to use to send patches (None for default)
cwd (str): Path to use for patch files (None to use current dir)
"""
cc_file = series.MakeCcFile(process_tags, cover_fname, not ignore_bad_tags,
add_maintainers, limit, get_maintainer_script,
settings.alias)
settings.alias, cwd)
# Email the patches out (giving the user time to check / cancel)
cmd = ''
if its_a_go:
cmd = gitutil.email_patches(
series, cover_fname, patch_files, dry_run, not ignore_bad_tags,
cc_file, settings.alias, in_reply_to=in_reply_to, thread=thread,
smtp_server=smtp_server)
cc_file, alias=settings.alias, in_reply_to=in_reply_to,
thread=thread, smtp_server=smtp_server, cwd=cwd)
else:
print(col.build(col.RED, "Not sending emails due to errors/warnings"))
@@ -110,7 +112,7 @@ def email_patches(col, series, cover_fname, patch_files, process_tags, its_a_go,
def prepare_patches(col, branch, count, start, end, ignore_binary, signoff,
keep_change_id=False):
keep_change_id=False, git_dir=None, cwd=None):
"""Figure out what patches to generate, then generate them
The patch files are written to the current directory, e.g. 0001_xxx.patch
@@ -121,11 +123,13 @@ def prepare_patches(col, branch, count, start, end, ignore_binary, signoff,
branch (str): Branch to create patches from (None = current)
count (int): Number of patches to produce, or -1 to produce patches for
the current branch back to the upstream commit
start (int): Start partch to use (0=first / top of branch)
start (int): Start patch to use (0=first / top of branch)
end (int): End patch to use (0=last one in series, 1=one before that,
etc.)
ignore_binary (bool): Don't generate patches for binary files
keep_change_id (bool): Preserve the Change-Id tag.
git_dir (str): Path to git repository (None to use default)
cwd (str): Path to use for git operations (None to use current dir)
Returns:
Tuple:
@@ -136,40 +140,43 @@ def prepare_patches(col, branch, count, start, end, ignore_binary, signoff,
"""
if count == -1:
# Work out how many patches to send if we can
count = (gitutil.count_commits_to_branch(branch) - start)
count = (gitutil.count_commits_to_branch(branch, git_dir=git_dir) -
start)
if not count:
str = 'No commits found to process - please use -c flag, or run:\n' \
msg = 'No commits found to process - please use -c flag, or run:\n' \
' git branch --set-upstream-to remote/branch'
sys.exit(col.build(col.RED, str))
sys.exit(col.build(col.RED, msg))
# Read the metadata from the commits
to_do = count - end
series = patchstream.get_metadata(branch, start, to_do)
series = patchstream.get_metadata(branch, start, to_do, git_dir)
cover_fname, patch_files = gitutil.create_patches(
branch, start, to_do, ignore_binary, series, signoff)
branch, start, to_do, ignore_binary, series, signoff, git_dir=git_dir,
cwd=cwd)
# Fix up the patch files to our liking, and insert the cover letter
patchstream.fix_patches(series, patch_files, keep_change_id,
insert_base_commit=not cover_fname)
insert_base_commit=not cover_fname, cwd=cwd)
if cover_fname and series.get('cover'):
patchstream.insert_cover_letter(cover_fname, series, to_do)
patchstream.insert_cover_letter(cover_fname, series, to_do, cwd=cwd)
return series, cover_fname, patch_files
def send(args):
def send(args, git_dir=None, cwd=None):
"""Create, check and send patches by email
Args:
args (argparse.Namespace): Arguments to patman
cwd (str): Path to use for git operations
"""
col = terminal.Color()
series, cover_fname, patch_files = prepare_patches(
col, args.branch, args.count, args.start, args.end,
args.ignore_binary, args.add_signoff,
keep_change_id=args.keep_change_id)
keep_change_id=args.keep_change_id, git_dir=git_dir, cwd=cwd)
ok = check_patches(series, patch_files, args.check_patch,
args.verbose, args.check_patch_use_tree)
args.verbose, args.check_patch_use_tree, cwd)
ok = ok and gitutil.check_suppress_cc_config()
@@ -178,4 +185,4 @@ def send(args):
col, series, cover_fname, patch_files, args.process_tags,
its_a_go, args.ignore_bad_tags, args.add_maintainers,
args.get_maintainer_script, args.limit, args.dry_run,
args.in_reply_to, args.thread, args.smtp_server)
args.in_reply_to, args.thread, args.smtp_server, cwd=cwd)

View File

@@ -25,13 +25,23 @@ class Series(dict):
"""Holds information about a patch series, including all tags.
Vars:
cc: List of aliases/emails to Cc all patches to
commits: List of Commit objects, one for each patch
cover: List of lines in the cover letter
notes: List of lines in the notes
changes: (dict) List of changes for each version, The key is
the integer version number
allow_overwrite: Allow tags to overwrite an existing tag
cc (list of str): Aliases/emails to Cc all patches to
to (list of str): Aliases/emails to send patches to
commits (list of Commit): Commit objects, one for each patch
cover (list of str): Lines in the cover letter
notes (list of str): Lines in the notes
changes: (dict) List of changes for each version:
key (int): version number
value: tuple:
commit (Commit): Commit this relates to, or None if related to a
cover letter
info (str): change lines for this version (separated by \n)
allow_overwrite (bool): Allow tags to overwrite an existing tag
base_commit (Commit): Commit object at the base of this series
branch (str): Branch name of this series
_generated_cc (dict) written in MakeCcFile()
key: name of patch file
value: list of email addresses
"""
def __init__(self):
self.cc = []
@@ -44,10 +54,6 @@ class Series(dict):
self.allow_overwrite = False
self.base_commit = None
self.branch = None
# Written in MakeCcFile()
# key: name of patch file
# value: list of email addresses
self._generated_cc = {}
# These make us more like a dictionary
@@ -245,7 +251,7 @@ class Series(dict):
def GetCcForCommit(self, commit, process_tags, warn_on_error,
add_maintainers, limit, get_maintainer_script,
all_skips, alias):
all_skips, alias, cwd):
"""Get the email CCs to use with a particular commit
Uses subject tags and get_maintainers.pl script to find people to cc
@@ -268,6 +274,7 @@ class Series(dict):
alias (dict): Alias dictionary
key: alias
value: list of aliases or email addresses
cwd (str): Path to use for patch filenames (None to use current dir)
Returns:
list of str: List of email addresses to cc
@@ -281,8 +288,8 @@ class Series(dict):
if type(add_maintainers) == type(cc):
cc += add_maintainers
elif add_maintainers:
cc += get_maintainer.get_maintainer(get_maintainer_script,
commit.patch)
fname = os.path.join(cwd or '', commit.patch)
cc += get_maintainer.get_maintainer(get_maintainer_script, fname)
all_skips |= set(cc) & set(settings.bounces)
cc = list(set(cc) - set(settings.bounces))
if limit is not None:
@@ -290,7 +297,8 @@ class Series(dict):
return cc
def MakeCcFile(self, process_tags, cover_fname, warn_on_error,
add_maintainers, limit, get_maintainer_script, alias):
add_maintainers, limit, get_maintainer_script, alias,
cwd=None):
"""Make a cc file for us to use for per-commit Cc automation
Also stores in self._generated_cc to make ShowActions() faster.
@@ -309,6 +317,7 @@ class Series(dict):
alias (dict): Alias dictionary
key: alias
value: list of aliases or email addresses
cwd (str): Path to use for patch filenames (None to use current dir)
Return:
Filename of temp file created
"""
@@ -324,7 +333,7 @@ class Series(dict):
commit.future = executor.submit(
self.GetCcForCommit, commit, process_tags, warn_on_error,
add_maintainers, limit, get_maintainer_script, all_skips,
alias)
alias, cwd)
# Show progress any commits that are taking forever
lastlen = 0
@@ -372,8 +381,10 @@ class Series(dict):
This will later appear in the change log.
Args:
version: version number to add change list to
info: change line for this version
version (int): version number to add change list to
commit (Commit): Commit this relates to, or None if related to a
cover letter
info (str): change lines for this version (separated by \n)
"""
if not self.changes.get(version):
self.changes[version] = []

View File

@@ -226,7 +226,7 @@ nxp = Zhikang Zhang <zhikang.zhang@nxp.com>
f.close()
def _UpdateDefaults(main_parser, config):
def _UpdateDefaults(main_parser, config, argv):
"""Update the given OptionParser defaults based on config.
We'll walk through all of the settings from all parsers.
@@ -242,6 +242,7 @@ def _UpdateDefaults(main_parser, config):
updated.
config: An instance of _ProjectConfigParser that we will query
for settings.
argv (list of str or None): Arguments to parse
"""
# Find all the parsers and subparsers
parsers = [main_parser]
@@ -252,6 +253,7 @@ def _UpdateDefaults(main_parser, config):
# Collect the defaults from each parser
defaults = {}
parser_defaults = []
argv = list(argv)
for parser in parsers:
pdefs = parser.parse_known_args()[0]
parser_defaults.append(pdefs)
@@ -273,9 +275,11 @@ def _UpdateDefaults(main_parser, config):
# Set all the defaults and manually propagate them to subparsers
main_parser.set_defaults(**defaults)
assert len(parsers) == len(parser_defaults)
for parser, pdefs in zip(parsers, parser_defaults):
parser.set_defaults(**{k: v for k, v in defaults.items()
if k in pdefs})
return defaults
def _ReadAliasFile(fname):
@@ -334,7 +338,7 @@ def GetItems(config, section):
return []
def Setup(parser, project_name, config_fname=None):
def Setup(parser, project_name, argv, config_fname=None):
"""Set up the settings module by reading config files.
Unless `config_fname` is specified, a `.patman` config file local
@@ -347,8 +351,9 @@ 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. An error is raised if it
does not exist.
config_fname: Config filename to read, or None for default, or False
for an empty config. An error is raised if it does not exist.
argv (list of str or None): Arguments to parse, or None for default
"""
# First read the git alias file if available
_ReadAliasFile('doc/git-mailrc')
@@ -357,12 +362,15 @@ def Setup(parser, project_name, config_fname=None):
if config_fname and not os.path.exists(config_fname):
raise Exception(f'provided {config_fname} does not exist')
if not config_fname:
if config_fname is None:
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)
has_config = False
has_git_local_config = False
if config_fname is not False:
has_config = os.path.exists(config_fname)
has_git_local_config = os.path.exists(git_local_config_fname)
# Read the git local config last, so that its values override
# those of the global config, if any.
@@ -371,7 +379,7 @@ def Setup(parser, project_name, config_fname=None):
if has_git_local_config:
config.read(git_local_config_fname)
if not (has_config or has_git_local_config):
if config_fname is not False and not (has_config or has_git_local_config):
print("No config file found.\nCreating ~/.patman...\n")
CreatePatmanConfigFile(config_fname)
@@ -382,7 +390,7 @@ def Setup(parser, project_name, config_fname=None):
for name, value in GetItems(config, 'bounces'):
bounces.add(value)
_UpdateDefaults(parser, config)
return _UpdateDefaults(parser, config, argv)
# These are the aliases we understand, indexed by alias. Each member is a list.

View File

@@ -49,7 +49,7 @@ def test_git_local_config():
dest='check_patch', default=True)
# Test "global" config is used.
settings.Setup(parser, 'unknown', global_config.name)
settings.Setup(parser, 'unknown', None, global_config.name)
args, _ = parser.parse_known_args([])
assert args.project == 'u-boot'
send_args, _ = send.parse_known_args([])

View File

@@ -203,7 +203,7 @@ def run_one(*cmd, **kwargs):
return run_pipe([cmd], **kwargs)
def run_list(cmd):
def run_list(cmd, **kwargs):
"""Run a command and return its output
Args:
@@ -211,8 +211,9 @@ def run_list(cmd):
Returns:
str: output of command
**kwargs (dict of args): Extra arguments to pass in
"""
return run_pipe([cmd], capture=True).stdout
return run_pipe([cmd], capture=True, **kwargs).stdout
def stop_all():

View File

@@ -13,7 +13,7 @@ USE_NO_DECORATE = True
def log_cmd(commit_range, git_dir=None, oneline=False, reverse=False,
count=None):
count=None, decorate=False):
"""Create a command to perform a 'git log'
Args:
@@ -31,8 +31,10 @@ def log_cmd(commit_range, git_dir=None, oneline=False, reverse=False,
cmd += ['--no-pager', 'log', '--no-color']
if oneline:
cmd.append('--oneline')
if USE_NO_DECORATE:
if USE_NO_DECORATE and not decorate:
cmd.append('--no-decorate')
if decorate:
cmd.append('--decorate')
if reverse:
cmd.append('--reverse')
if count is not None:
@@ -47,7 +49,7 @@ def log_cmd(commit_range, git_dir=None, oneline=False, reverse=False,
return cmd
def count_commits_to_branch(branch):
def count_commits_to_branch(branch, git_dir=None, end=None):
"""Returns number of commits between HEAD and the tracking branch.
This looks back to the tracking branch and works out the number of commits
@@ -55,16 +57,22 @@ def count_commits_to_branch(branch):
Args:
branch (str or None): Branch to count from (None for current branch)
git_dir (str): Path to git repository (None to use default)
end (str): End commit to stop before
Return:
Number of patches that exist on top of the branch
"""
if branch:
us, _ = get_upstream('.git', branch)
if end:
rev_range = f'{end}..{branch}'
elif branch:
us, msg = get_upstream(git_dir or '.git', branch)
if not us:
raise ValueError(msg)
rev_range = f'{us}..{branch}'
else:
rev_range = '@{upstream}..'
cmd = log_cmd(rev_range, oneline=True)
cmd = log_cmd(rev_range, git_dir=git_dir, oneline=True)
result = command.run_one(*cmd, capture=True, capture_stderr=True,
oneline=True, raise_on_error=False)
if result.return_code:
@@ -84,9 +92,11 @@ def name_revision(commit_hash):
Name of revision, if any, else None
"""
stdout = command.output_one_line('git', 'name-rev', commit_hash)
if not stdout:
return None
# We expect a commit, a space, then a revision name
name = stdout.split(' ')[1].strip()
name = stdout.split()[1].strip()
return name
@@ -106,18 +116,21 @@ def guess_upstream(git_dir, branch):
Name of upstream branch (e.g. 'upstream/master') or None if none
Warning/error message, or None if none
"""
cmd = log_cmd(branch, git_dir=git_dir, oneline=True, count=100)
cmd = log_cmd(branch, git_dir=git_dir, oneline=True, count=100,
decorate=True)
result = command.run_one(*cmd, capture=True, capture_stderr=True,
raise_on_error=False)
if result.return_code:
return None, f"Branch '{branch}' not found"
for line in result.stdout.splitlines()[1:]:
commit_hash = line.split(' ')[0]
name = name_revision(commit_hash)
if '~' not in name and '^' not in name:
if name.startswith('remotes/'):
name = name[8:]
return name, f"Guessing upstream as '{name}'"
parts = line.split(maxsplit=1)
if len(parts) >= 2 and parts[1].startswith('('):
commit_hash = parts[0]
name = name_revision(commit_hash)
if '~' not in name and '^' not in name:
if name.startswith('remotes/'):
name = name[8:]
return name, f"Guessing upstream as '{name}'"
return None, f"Cannot find a suitable upstream for branch '{branch}'"
@@ -321,7 +334,8 @@ def prune_worktrees(git_dir):
raise OSError(f'git worktree prune: {result.stderr}')
def create_patches(branch, start, count, ignore_binary, series, signoff=True):
def create_patches(branch, start, count, ignore_binary, series, signoff=True,
git_dir=None, cwd=None):
"""Create a series of patches from the top of the current branch.
The patch files are written to the current directory using
@@ -334,11 +348,16 @@ def create_patches(branch, start, count, ignore_binary, series, signoff=True):
ignore_binary (bool): Don't generate patches for binary files
series (Series): Series object for this series (set of patches)
signoff (bool): True to add signoff lines automatically
git_dir (str): Path to git repository (None to use default)
cwd (str): Path to use for git operations
Return:
Filename of cover letter (None if none)
List of filenames of patch files
"""
cmd = ['git', 'format-patch', '-M']
cmd = ['git']
if git_dir:
cmd += ['--git-dir', git_dir]
cmd += ['format-patch', '-M']
if signoff:
cmd.append('--signoff')
if ignore_binary:
@@ -351,7 +370,7 @@ def create_patches(branch, start, count, ignore_binary, series, signoff=True):
brname = branch or 'HEAD'
cmd += [f'{brname}~{start + count}..{brname}~{start}']
stdout = command.run_list(cmd)
stdout = command.run_list(cmd, cwd=cwd)
files = stdout.splitlines()
# We have an extra file if there is a cover letter
@@ -396,7 +415,6 @@ def build_email_list(in_list, alias, tag=None, warn_on_error=True):
>>> build_email_list(['john', 'mary'], alias, 'Cc')
['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
"""
quote = '"' if tag and tag[0] == '-' else ''
raw = []
for item in in_list:
raw += lookup_email(item, alias, warn_on_error=warn_on_error)
@@ -405,7 +423,7 @@ def build_email_list(in_list, alias, tag=None, warn_on_error=True):
if item not in result:
result.append(item)
if tag:
return [f'{tag} {quote}{email}{quote}' for email in result]
return [x for email in result for x in (tag, email)]
return result
@@ -437,7 +455,7 @@ def check_suppress_cc_config():
def email_patches(series, cover_fname, args, dry_run, warn_on_error, cc_fname,
alias, self_only=False, in_reply_to=None, thread=False,
smtp_server=None):
smtp_server=None, cwd=None):
"""Email a patch series.
Args:
@@ -457,6 +475,7 @@ def email_patches(series, cover_fname, args, dry_run, warn_on_error, cc_fname,
thread (bool): True to add --thread to git send-email (make
all patches reply to cover-letter or first patch in series)
smtp_server (str or None): SMTP server to use to send patches
cwd (str): Path to use for patch files (None to use current dir)
Returns:
Git command that was/would be run
@@ -524,13 +543,14 @@ send --cc-cmd cc-fname" cover p1 p2'
cmd += to
cmd += cc
cmd += ['--cc-cmd', f'"{sys.argv[0]} send --cc-cmd {cc_fname}"']
cmd += ['--cc-cmd', f'{sys.argv[0]} send --cc-cmd {cc_fname}']
if cover_fname:
cmd.append(cover_fname)
cmd += args
cmdstr = ' '.join(cmd)
if not dry_run:
os.system(cmdstr)
command.run(*cmd, capture=False, capture_stderr=False, cwd=cwd)
cmdstr = ' '.join([f'"{x}"' if ' ' in x and not '"' in x else x
for x in cmd])
return cmdstr
@@ -695,7 +715,7 @@ def setup():
.return_code == 0)
def get_hash(spec):
def get_hash(spec, git_dir=None):
"""Get the hash of a commit
Args:
@@ -704,8 +724,11 @@ def get_hash(spec):
Returns:
str: Hash of commit
"""
return command.output_one_line('git', 'show', '-s', '--pretty=format:%H',
spec)
cmd = ['git']
if git_dir:
cmd += ['--git-dir', git_dir]
cmd += ['show', '-s', '--pretty=format:%H', spec]
return command.output_one_line(*cmd)
def get_head():
@@ -717,18 +740,41 @@ def get_head():
return get_hash('HEAD')
def get_branch():
def get_branch(git_dir=None):
"""Get the branch we are currently on
Return:
str: branch name, or None if none
git_dir (str): Path to git repository (None to use default)
"""
out = command.output_one_line('git', 'rev-parse', '--abbrev-ref', 'HEAD')
cmd = ['git']
if git_dir:
cmd += ['--git-dir', git_dir]
cmd += ['rev-parse', '--abbrev-ref', 'HEAD']
out = command.output_one_line(*cmd, raise_on_error=False)
if out == 'HEAD':
return None
return out
def check_dirty(git_dir=None, work_tree=None):
"""Check if the tree is dirty
Args:
git_dir (str): Path to git repository (None to use default)
Return:
str: List of dirty filenames and state
"""
cmd = ['git']
if git_dir:
cmd += ['--git-dir', git_dir]
if work_tree:
cmd += ['--work-tree', work_tree]
cmd += ['status', '--porcelain', '--untracked-files=no']
return command.output(*cmd).splitlines()
if __name__ == "__main__":
import doctest