summaryrefslogtreecommitdiff
path: root/tools/patman
diff options
context:
space:
mode:
Diffstat (limited to 'tools/patman')
-rw-r--r--tools/patman/__init__.py4
-rwxr-xr-xtools/patman/__main__.py2
-rw-r--r--tools/patman/checkpatch.py2
-rw-r--r--tools/patman/cmdline.py2
-rw-r--r--tools/patman/commit.py2
-rw-r--r--tools/patman/control.py5
-rw-r--r--tools/patman/func_test.py61
-rw-r--r--tools/patman/get_maintainer.py2
-rw-r--r--tools/patman/gitutil.py706
-rw-r--r--tools/patman/patchstream.py56
-rw-r--r--tools/patman/patman.rst15
-rw-r--r--tools/patman/project.py2
-rw-r--r--tools/patman/requirements.txt5
-rw-r--r--tools/patman/series.py4
-rw-r--r--tools/patman/settings.py10
-rw-r--r--tools/patman/setup.py1
-rw-r--r--tools/patman/test/0002-fdt-Correct-cast-for-sandbox-in-fdtdec_setup_mem_siz.patch2
-rw-r--r--tools/patman/test/test01.txt2
-rw-r--r--tools/patman/test_checkpatch.py3
19 files changed, 146 insertions, 740 deletions
diff --git a/tools/patman/__init__.py b/tools/patman/__init__.py
index 08eeffdf6d2..6de0e9fba10 100644
--- a/tools/patman/__init__.py
+++ b/tools/patman/__init__.py
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: GPL-2.0+
__all__ = ['checkpatch', 'commit', 'control', 'func_test', 'get_maintainer',
- 'gitutil', '__main__', 'patchstream', 'project', 'series',
- 'settings','setup', 'status', 'test_checkpatch', 'test_settings']
+ '__main__', 'patchstream', 'project', 'series',
+ 'settings', 'setup', 'status', 'test_checkpatch', 'test_settings']
diff --git a/tools/patman/__main__.py b/tools/patman/__main__.py
index f645b38b647..36f1c08507c 100755
--- a/tools/patman/__main__.py
+++ b/tools/patman/__main__.py
@@ -49,7 +49,7 @@ def run_patman():
result = test_util.run_test_suites(
'patman', False, False, False, None, None, None,
[test_checkpatch.TestPatch, func_test.TestFunctional,
- 'gitutil', 'settings'])
+ 'settings'])
sys.exit(0 if result.wasSuccessful() else 1)
diff --git a/tools/patman/checkpatch.py b/tools/patman/checkpatch.py
index e03cac115e4..2975881705c 100644
--- a/tools/patman/checkpatch.py
+++ b/tools/patman/checkpatch.py
@@ -8,8 +8,8 @@ import os
import re
import sys
-from patman import gitutil
from u_boot_pylib import command
+from u_boot_pylib import gitutil
from u_boot_pylib import terminal
EMACS_PREFIX = r'(?:[0-9]{4}.*\.patch:[0-9]+: )?'
diff --git a/tools/patman/cmdline.py b/tools/patman/cmdline.py
index d6496c0cb78..562bc823f60 100644
--- a/tools/patman/cmdline.py
+++ b/tools/patman/cmdline.py
@@ -13,8 +13,8 @@ import os
import pathlib
import sys
-from patman import gitutil
from patman import project
+from u_boot_pylib import gitutil
from patman import settings
PATMAN_DIR = pathlib.Path(__file__).parent
diff --git a/tools/patman/commit.py b/tools/patman/commit.py
index 684225c0e60..ce37a3d95eb 100644
--- a/tools/patman/commit.py
+++ b/tools/patman/commit.py
@@ -6,7 +6,7 @@ import collections
import re
# Separates a tag: at the beginning of the subject from the rest of it
-re_subject_tag = re.compile('([^:\s]*):\s*(.*)')
+re_subject_tag = re.compile(r'([^:\s]*):\s*(.*)')
class Commit:
"""Holds information about a single commit/patch in the series.
diff --git a/tools/patman/control.py b/tools/patman/control.py
index b292da9dc27..b8a45912058 100644
--- a/tools/patman/control.py
+++ b/tools/patman/control.py
@@ -12,8 +12,8 @@ import os
import sys
from patman import checkpatch
-from patman import gitutil
from patman import patchstream
+from u_boot_pylib import gitutil
from u_boot_pylib import terminal
@@ -63,7 +63,8 @@ def prepare_patches(col, branch, count, start, end, ignore_binary, signoff,
branch, start, to_do, ignore_binary, series, signoff)
# Fix up the patch files to our liking, and insert the cover letter
- patchstream.fix_patches(series, patch_files, keep_change_id)
+ patchstream.fix_patches(series, patch_files, keep_change_id,
+ insert_base_commit=not cover_fname)
if cover_fname and series.get('cover'):
patchstream.insert_cover_letter(cover_fname, series, to_do)
return series, cover_fname, patch_files
diff --git a/tools/patman/func_test.py b/tools/patman/func_test.py
index e3918497cf4..720746e21f5 100644
--- a/tools/patman/func_test.py
+++ b/tools/patman/func_test.py
@@ -18,11 +18,11 @@ import unittest
from patman.commit import Commit
from patman import control
-from patman import gitutil
from patman import patchstream
from patman.patchstream import PatchStream
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
from u_boot_pylib.test_util import capture_sys_output
@@ -211,10 +211,13 @@ class TestFunctional(unittest.TestCase):
'u-boot': ['u-boot@lists.denx.de'],
'simon': [self.leb],
'fred': [self.fred],
+ 'joe': [self.joe],
}
text = self._get_text('test01.txt')
series = patchstream.get_metadata_for_test(text)
+ series.base_commit = Commit('1a44532')
+ series.branch = 'mybranch'
cover_fname, args = self._create_patches_for_test(series)
get_maintainer_script = str(pathlib.Path(__file__).parent.parent.parent
/ 'get_maintainer.pl') + ' --norolestats'
@@ -259,6 +262,7 @@ class TestFunctional(unittest.TestCase):
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' % self.leb,
next(lines))
self.assertEqual(' Cc: %s' % mel, next(lines))
@@ -272,7 +276,8 @@ class TestFunctional(unittest.TestCase):
self.assertEqual(('%s %s\0%s' % (args[0], rick, stefan)), cc_lines[0])
self.assertEqual(
- '%s %s\0%s\0%s\0%s' % (args[1], self.fred, self.leb, rick, stefan),
+ '%s %s\0%s\0%s\0%s\0%s' % (args[1], self.fred, self.joe, self.leb,
+ rick, stefan),
cc_lines[1])
expected = '''
@@ -290,6 +295,7 @@ Changes in v4:
change
- Some changes
- Some notes for the cover letter
+- fdt: Correct cast for sandbox in fdtdec_setup_mem_size_base()
Simon Glass (2):
pci: Correct cast for sandbox
@@ -304,6 +310,8 @@ Simon Glass (2):
--\x20
2.7.4
+base-commit: 1a44532
+branch: mybranch
'''
lines = open(cover_fname, encoding='utf-8').read().splitlines()
self.assertEqual(
@@ -339,6 +347,7 @@ Changes in v4:
- Multi
line
change
+- New
- Some changes
Changes in v2:
@@ -348,6 +357,31 @@ Changes in v2:
expected = expected.splitlines()
self.assertEqual(expected, lines[start:(start+len(expected))])
+ 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')
+ text = orig_text[:pos]
+ series = patchstream.get_metadata_for_test(text)
+ series.base_commit = Commit('1a44532')
+ series.branch = 'mybranch'
+ cover_fname, args = self._create_patches_for_test(series)
+ self.assertFalse(cover_fname)
+ with capture_sys_output() as out:
+ patchstream.fix_patches(series, args, insert_base_commit=True)
+ self.assertEqual('Cleaned 1 patch\n', out[0].getvalue())
+ lines = tools.read_file(args[0], binary=False).splitlines()
+ pos = lines.index('-- ')
+
+ # We expect these lines at the end:
+ # -- (with trailing space)
+ # 2.7.4
+ # (empty)
+ # base-commit: xxx
+ # branch: xxx
+ self.assertEqual('base-commit: 1a44532', lines[pos + 3])
+ self.assertEqual('branch: mybranch', lines[pos + 4])
+
def make_commit_with_file(self, subject, body, fname, text):
"""Create a file and add it to the git repo with a new commit
@@ -506,12 +540,23 @@ complicated as possible''')
# Check that it can detect a different branch
self.assertEqual(3, gitutil.count_commits_to_branch('second'))
with capture_sys_output() as _:
- _, cover_fname, patch_files = control.prepare_patches(
+ series, cover_fname, patch_files = control.prepare_patches(
col, branch='second', count=-1, start=0, end=0,
ignore_binary=False, signoff=True)
self.assertIsNotNone(cover_fname)
self.assertEqual(3, len(patch_files))
+ cover = tools.read_file(cover_fname, binary=False)
+ lines = cover.splitlines()[-2:]
+ base = repo.lookup_reference('refs/heads/base').target
+ self.assertEqual(f'base-commit: {base}', lines[0])
+ self.assertEqual('branch: second', lines[1])
+
+ # Make sure that the base-commit is not present when it is in the
+ # cover letter
+ for fname in patch_files:
+ self.assertNotIn(b'base-commit:', tools.read_file(fname))
+
# Check that it can skip patches at the end
with capture_sys_output() as _:
_, cover_fname, patch_files = control.prepare_patches(
@@ -519,6 +564,13 @@ complicated as possible''')
ignore_binary=False, signoff=True)
self.assertIsNotNone(cover_fname)
self.assertEqual(2, len(patch_files))
+
+ cover = tools.read_file(cover_fname, binary=False)
+ lines = cover.splitlines()[-2:]
+ base2 = repo.lookup_reference('refs/heads/second')
+ ref = base2.peel(pygit2.GIT_OBJ_COMMIT).parents[0].parents[0].id
+ self.assertEqual(f'base-commit: {ref}', lines[0])
+ self.assertEqual('branch: second', lines[1])
finally:
os.chdir(orig_dir)
@@ -540,7 +592,8 @@ complicated as possible''')
with open('.patman', 'w', buffering=1) as f:
f.write('[settings]\n'
'get_maintainer_script: dummy-script.sh\n'
- 'check_patch: False\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')
diff --git a/tools/patman/get_maintainer.py b/tools/patman/get_maintainer.py
index 8df3d124bac..200ee96551d 100644
--- a/tools/patman/get_maintainer.py
+++ b/tools/patman/get_maintainer.py
@@ -7,8 +7,8 @@ import os
import shlex
import shutil
-from patman import gitutil
from u_boot_pylib import command
+from u_boot_pylib import gitutil
def find_get_maintainer(script_file_name):
diff --git a/tools/patman/gitutil.py b/tools/patman/gitutil.py
deleted file mode 100644
index 10ea5ff39f5..00000000000
--- a/tools/patman/gitutil.py
+++ /dev/null
@@ -1,706 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0+
-# Copyright (c) 2011 The Chromium OS Authors.
-#
-
-import os
-import sys
-
-from patman import settings
-from u_boot_pylib import command
-from u_boot_pylib import terminal
-
-# 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):
- """Create a command to perform a 'git log'
-
- Args:
- commit_range: Range expression to use for log, None for none
- git_dir: Path to git repository (None to use default)
- oneline: True to use --oneline, else False
- reverse: True to reverse the log (--reverse)
- count: Number of commits to list, or None for no limit
- Return:
- List containing command and arguments to run
- """
- cmd = ['git']
- if git_dir:
- cmd += ['--git-dir', git_dir]
- cmd += ['--no-pager', 'log', '--no-color']
- if oneline:
- cmd.append('--oneline')
- if use_no_decorate:
- cmd.append('--no-decorate')
- if reverse:
- cmd.append('--reverse')
- if count is not None:
- cmd.append('-n%d' % count)
- if commit_range:
- cmd.append(commit_range)
-
- # Add this in case we have a branch with the same name as a directory.
- # This avoids messages like this, for example:
- # fatal: ambiguous argument 'test': both revision and filename
- cmd.append('--')
- return cmd
-
-
-def count_commits_to_branch(branch):
- """Returns number of commits between HEAD and the tracking branch.
-
- This looks back to the tracking branch and works out the number of commits
- since then.
-
- Args:
- branch: Branch to count from (None for current branch)
-
- Return:
- Number of patches that exist on top of the branch
- """
- if branch:
- us, msg = get_upstream('.git', branch)
- rev_range = '%s..%s' % (us, branch)
- else:
- 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)
- 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
-
- Args:
- commit_hash: Commit hash to look up
-
- Return:
- Name of revision, if any, else None
- """
- pipe = ['git', 'name-rev', commit_hash]
- stdout = command.run_pipe([pipe], capture=True, oneline=True).stdout
-
- # We expect a commit, a space, then a revision name
- name = stdout.split(' ')[1].strip()
- return name
-
-
-def guess_upstream(git_dir, branch):
- """Tries to guess the upstream for a branch
-
- This lists out top commits on a branch and tries to find a suitable
- upstream. It does this by looking for the first commit where
- 'git name-rev' returns a plain branch name, with no ! or ^ modifiers.
-
- Args:
- git_dir: Git directory containing repo
- branch: Name of branch
-
- Returns:
- Tuple:
- Name of upstream branch (e.g. 'upstream/master') or None if none
- Warning/error message, or None if none
- """
- 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)
- if result.return_code:
- return None, "Branch '%s' not found" % branch
- 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, "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
-
- Args:
- git_dir: Git directory containing repo
- branch: Name of branch
-
- Returns:
- Tuple:
- Name of upstream branch (e.g. 'upstream/master') or None if none
- Warning/error message, or None if none
- """
- try:
- remote = command.output_one_line('git', '--git-dir', git_dir, 'config',
- 'branch.%s.remote' % branch)
- merge = command.output_one_line('git', '--git-dir', git_dir, 'config',
- 'branch.%s.merge' % branch)
- except Exception:
- upstream, msg = guess_upstream(git_dir, branch)
- return upstream, msg
-
- if remote == '.':
- return merge, None
- elif remote and merge:
- # Drop the initial refs/heads from merge
- leaf = merge.split('/', maxsplit=2)[2:]
- return '%s/%s' % (remote, '/'.join(leaf)), None
- else:
- raise ValueError("Cannot determine upstream branch for branch "
- "'%s' remote='%s', merge='%s'"
- % (branch, remote, merge))
-
-
-def get_range_in_branch(git_dir, branch, include_upstream=False):
- """Returns an expression for the commits in the given branch.
-
- Args:
- git_dir: Directory containing git repo
- branch: Name of branch
- Return:
- Expression in the form 'upstream..branch' which can be used to
- access the commits. If the branch does not exist, returns None.
- """
- upstream, msg = get_upstream(git_dir, branch)
- if not upstream:
- return None, msg
- 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.
-
- Args:
- git_dir: Directory containing git repo
- range_expr: Range to check
- Return:
- Number of patches that exist in the supplied range or None if none
- were found
- """
- 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)
- 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.
-
- Args:
- git_dir: Directory containing git repo
- branch: Name of branch
- Return:
- Number of patches that exist on top of the branch, or None if the
- branch does not exist.
- """
- range_expr, msg = get_range_in_branch(git_dir, branch, include_upstream)
- if not range_expr:
- 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.
-
- Args:
- commit_range: Range of commits to count (e.g. 'HEAD..base')
- Return:
- Number of patches that exist on top of the branch
- """
- pipe = [log_cmd(commit_range, oneline=True),
- ['wc', '-l']]
- stdout = command.run_pipe(pipe, capture=True, oneline=True).stdout
- 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
-
- Args:
- commit_hash: Commit hash to check out
- """
- pipe = ['git']
- if git_dir:
- pipe.extend(['--git-dir', git_dir])
- if work_tree:
- pipe.extend(['--work-tree', work_tree])
- pipe.append('checkout')
- if force:
- pipe.append('-f')
- pipe.append(commit_hash)
- result = command.run_pipe([pipe], capture=True, raise_on_error=False,
- 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
-
- Args:
- commit_hash: Commit hash to check out
- """
- pipe = ['git', 'clone', git_dir, '.']
- result = command.run_pipe([pipe], capture=True, cwd=output_dir,
- 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
-
- Args:
- commit_hash: Commit hash to check out
- """
- pipe = ['git']
- if git_dir:
- pipe.extend(['--git-dir', git_dir])
- if work_tree:
- pipe.extend(['--work-tree', work_tree])
- pipe.append('fetch')
- result = command.run_pipe([pipe], capture=True, capture_stderr=True)
- 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
-
- Args:
- git_dir: The repository to test in
-
- Returns:
- True if git-worktree commands will work, False otherwise.
- """
- pipe = ['git', '--git-dir', git_dir, 'worktree', 'list']
- result = command.run_pipe([pipe], capture=True, capture_stderr=True,
- 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
-
- Args:
- git_dir: The repository to checkout the worktree from
- output_dir: Path for the new worktree
- commit_hash: Commit hash to checkout
- """
- # We need to pass --detach to avoid creating a new branch
- pipe = ['git', '--git-dir', git_dir, 'worktree', 'add', '.', '--detach']
- if commit_hash:
- pipe.append(commit_hash)
- result = command.run_pipe([pipe], capture=True, cwd=output_dir,
- 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
-
- Args:
- git_dir: The repository whose deleted worktrees should be pruned
- """
- pipe = ['git', '--git-dir', git_dir, 'worktree', 'prune']
- result = command.run_pipe([pipe], capture=True, capture_stderr=True)
- if result.return_code != 0:
- raise OSError('git worktree prune: %s' % result.stderr)
-
-
-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
- git format-patch.
-
- Args:
- branch: Branch to create patches from (None for current branch)
- start: Commit to start from: 0=HEAD, 1=next one, etc.
- count: number of commits to include
- ignore_binary: Don't generate patches for binary files
- series: Series object for this series (set of patches)
- Return:
- Filename of cover letter (None if none)
- List of filenames of patch files
- """
- cmd = ['git', 'format-patch', '-M']
- if signoff:
- cmd.append('--signoff')
- if ignore_binary:
- cmd.append('--no-binary')
- if series.get('cover'):
- cmd.append('--cover-letter')
- prefix = series.GetPatchPrefix()
- if prefix:
- cmd += ['--subject-prefix=%s' % prefix]
- brname = branch or 'HEAD'
- cmd += ['%s~%d..%s~%d' % (brname, start + count, brname, start)]
-
- stdout = command.run_list(cmd)
- files = stdout.splitlines()
-
- # We have an extra file if there is a cover letter
- if series.get('cover'):
- return files[0], files[1:]
- else:
- 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.
-
- Takes a list of email addresses and aliases, and turns this into a list
- of only email address, by resolving any aliases that are present.
-
- If the tag is given, then each email address is prepended with this
- tag and a space. If the tag starts with a minus sign (indicating a
- command line parameter) then the email address is quoted.
-
- Args:
- in_list: List of aliases/email addresses
- tag: Text to put before each address
- alias: Alias dictionary
- warn_on_error: True to raise an error when an alias fails to match,
- False to just print a message.
-
- Returns:
- List of email addresses
-
- >>> alias = {}
- >>> alias['fred'] = ['f.bloggs@napier.co.nz']
- >>> alias['john'] = ['j.bloggs@napier.co.nz']
- >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
- >>> alias['boys'] = ['fred', ' john']
- >>> alias['all'] = ['fred ', 'john', ' mary ']
- >>> build_email_list(['john', 'mary'], None, alias)
- ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
- >>> build_email_list(['john', 'mary'], '--to', alias)
- ['--to "j.bloggs@napier.co.nz"', \
-'--to "Mary Poppins <m.poppins@cloud.net>"']
- >>> build_email_list(['john', 'mary'], 'Cc', alias)
- ['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)
- result = []
- for item in raw:
- 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)
-
- # 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")
- 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, get_maintainer_script=None):
- """Email a patch series.
-
- Args:
- series: Series object containing destination info
- cover_fname: filename of cover letter
- args: list of filenames of patch files
- dry_run: Just return the command that would be run
- warn_on_error: True to print a warning when an alias fails to match,
- False to ignore it.
- cc_fname: Filename of Cc file for per-commit Cc
- self_only: True to just email to yourself as a test
- in_reply_to: If set we'll pass this to git as --in-reply-to.
- Should be a message ID that this is in reply to.
- 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
-
- # For the duration of this doctest pretend that we ran patman with ./patman
- >>> _old_argv0 = sys.argv[0]
- >>> sys.argv[0] = './patman'
-
- >>> alias = {}
- >>> alias['fred'] = ['f.bloggs@napier.co.nz']
- >>> alias['john'] = ['j.bloggs@napier.co.nz']
- >>> alias['mary'] = ['m.poppins@cloud.net']
- >>> alias['boys'] = ['fred', ' john']
- >>> alias['all'] = ['fred ', 'john', ' mary ']
- >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
- >>> series = {}
- >>> series['to'] = ['fred']
- >>> series['cc'] = ['mary']
- >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
- False, alias)
- 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
-"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
- >>> email_patches(series, None, ['p1'], True, True, 'cc-fname', False, \
- alias)
- 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
-"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" p1'
- >>> series['cc'] = ['all']
- >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
- True, alias)
- 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
-send --cc-cmd cc-fname" cover p1 p2'
- >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
- False, alias)
- 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
-"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
-"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
-
- # Restore argv[0] since we clobbered it.
- >>> sys.argv[0] = _old_argv0
- """
- to = build_email_list(series.get('to'), '--to', alias, warn_on_error)
- if not to:
- git_config_to = command.output('git', 'config', 'sendemail.to',
- raise_on_error=False)
- if not git_config_to:
- print("No recipient.\n"
- "Please add something like this to a commit\n"
- "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n"
- "Or do something like this\n"
- "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)
- if self_only:
- to = build_email_list([os.getenv('USER')], '--to',
- alias, warn_on_error)
- cc = []
- cmd = ['git', 'send-email', '--annotate']
- if smtp_server:
- cmd.append('--smtp-server=%s' % smtp_server)
- if in_reply_to:
- cmd.append('--in-reply-to="%s"' % in_reply_to)
- if thread:
- cmd.append('--thread')
-
- cmd += to
- cmd += cc
- cmd += ['--cc-cmd', '"%s send --cc-cmd %s"' % (sys.argv[0], cc_fname)]
- if cover_fname:
- cmd.append(cover_fname)
- cmd += args
- cmdstr = ' '.join(cmd)
- if not dry_run:
- os.system(cmdstr)
- return cmdstr
-
-
-def lookup_email(lookup_name, alias=None, warn_on_error=True, level=0):
- """If an email address is an alias, look it up and return the full name
-
- TODO: Why not just use git's own alias feature?
-
- Args:
- lookup_name: Alias or email address to look up
- alias: Dictionary containing aliases (None to use settings default)
- warn_on_error: True to print a warning when an alias fails to match,
- False to ignore it.
-
- Returns:
- tuple:
- list containing a list of email addresses
-
- Raises:
- OSError if a recursive alias reference was found
- ValueError if an alias was not found
-
- >>> alias = {}
- >>> alias['fred'] = ['f.bloggs@napier.co.nz']
- >>> alias['john'] = ['j.bloggs@napier.co.nz']
- >>> alias['mary'] = ['m.poppins@cloud.net']
- >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
- >>> alias['all'] = ['fred ', 'john', ' mary ']
- >>> alias['loop'] = ['other', 'john', ' mary ']
- >>> alias['other'] = ['loop', 'john', ' mary ']
- >>> lookup_email('mary', alias)
- ['m.poppins@cloud.net']
- >>> lookup_email('arthur.wellesley@howe.ro.uk', alias)
- ['arthur.wellesley@howe.ro.uk']
- >>> lookup_email('boys', alias)
- ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
- >>> lookup_email('all', alias)
- ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
- >>> lookup_email('odd', alias)
- Alias 'odd' not found
- []
- >>> lookup_email('loop', alias)
- Traceback (most recent call last):
- ...
- OSError: Recursive email alias at 'other'
- >>> lookup_email('odd', alias, warn_on_error=False)
- []
- >>> # In this case the loop part will effectively be ignored.
- >>> lookup_email('loop', alias, warn_on_error=False)
- Recursive email alias at 'other'
- Recursive email alias at 'john'
- Recursive email alias at 'mary'
- ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
- """
- if not alias:
- alias = settings.alias
- lookup_name = lookup_name.strip()
- if '@' in lookup_name: # Perhaps a real email address
- return [lookup_name]
-
- lookup_name = lookup_name.lower()
- col = terminal.Color()
-
- out_list = []
- if level > 10:
- msg = "Recursive email alias at '%s'" % lookup_name
- if warn_on_error:
- raise OSError(msg)
- else:
- print(col.build(col.RED, msg))
- return out_list
-
- if lookup_name:
- if lookup_name not in alias:
- msg = "Alias '%s' not found" % lookup_name
- if warn_on_error:
- print(col.build(col.RED, msg))
- return out_list
- for item in alias[lookup_name]:
- todo = lookup_email(item, alias, warn_on_error, level + 1)
- for new_item in todo:
- 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.
-
- Returns:
- Full path to git top-level directory
-
- This test makes sure that we are running tests in the right subdir
-
- >>> os.path.realpath(os.path.dirname(__file__)) == \
- os.path.join(get_top_level(), 'tools', 'patman')
- True
- """
- return command.output_one_line('git', 'rev-parse', '--show-toplevel')
-
-
-def get_alias_file():
- """Gets the name of the git alias file.
-
- Returns:
- Filename of git alias file, or None if none
- """
- fname = command.output_one_line('git', 'config', 'sendemail.aliasesfile',
- raise_on_error=False)
- if not fname:
- return None
-
- fname = os.path.expanduser(fname.strip())
- if os.path.isabs(fname):
- return fname
-
- return os.path.join(get_top_level(), fname)
-
-
-def get_default_user_name():
- """Gets the user.name from .gitconfig file.
-
- Returns:
- User name found in .gitconfig file, or None if none
- """
- uname = command.output_one_line('git', 'config', '--global', '--includes', 'user.name')
- return uname
-
-
-def get_default_user_email():
- """Gets the user.email from the global .gitconfig file.
-
- Returns:
- User's email found in .gitconfig file, or None if none
- """
- uemail = command.output_one_line('git', 'config', '--global', '--includes', '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)
-
- return sub_prefix
-
-
-def setup():
- """Set up git utils, by reading the alias files."""
- # Check for a git alias file also
- global use_no_decorate
-
- alias_fname = get_alias_file()
- if alias_fname:
- settings.ReadGitAliases(alias_fname)
- cmd = log_cmd(None, count=0)
- use_no_decorate = (command.run_pipe([cmd], raise_on_error=False)
- .return_code == 0)
-
-
-def get_head():
- """Get the hash of the current HEAD
-
- Returns:
- Hash of HEAD
- """
- return command.output_one_line('git', 'show', '-s', '--pretty=format:%H')
-
-
-if __name__ == "__main__":
- import doctest
-
- doctest.testmod()
diff --git a/tools/patman/patchstream.py b/tools/patman/patchstream.py
index e2e2a83e677..7a695c37c27 100644
--- a/tools/patman/patchstream.py
+++ b/tools/patman/patchstream.py
@@ -15,9 +15,9 @@ import shutil
import tempfile
from patman import commit
-from patman import gitutil
from patman.series import Series
from u_boot_pylib import command
+from u_boot_pylib import gitutil
# Tags that we detect and remove
RE_REMOVE = re.compile(r'^BUG=|^TEST=|^BRANCH=|^Review URL:'
@@ -48,7 +48,7 @@ RE_TAG = re.compile('^(Tested-by|Acked-by|Reviewed-by|Patch-cc|Fixes): (.*)')
RE_COMMIT = re.compile('^commit ([0-9a-f]*)$')
# We detect these since checkpatch doesn't always do it
-RE_SPACE_BEFORE_TAB = re.compile('^[+].* \t')
+RE_SPACE_BEFORE_TAB = re.compile(r'^[+].* \t')
# Match indented lines for changes
RE_LEADING_WHITESPACE = re.compile(r'^\s')
@@ -76,8 +76,13 @@ class PatchStream:
are interested in. We can also process a patch file in order to remove
unwanted tags or inject additional ones. These correspond to the two
phases of processing.
+
+ Args:
+ keep_change_id (bool): Keep the Change-Id tag
+ insert_base_commit (bool): True to add the base commit to the end
"""
- def __init__(self, series, is_log=False, keep_change_id=False):
+ def __init__(self, series, is_log=False, keep_change_id=False,
+ insert_base_commit=False):
self.skip_blank = False # True to skip a single blank line
self.found_test = False # Found a TEST= line
self.lines_after_test = 0 # Number of lines found after TEST=
@@ -103,6 +108,7 @@ class PatchStream:
self.recent_quoted = collections.deque([], 5)
self.recent_unquoted = queue.Queue()
self.was_quoted = None
+ self.insert_base_commit = insert_base_commit
@staticmethod
def process_text(text, is_comment=False):
@@ -475,6 +481,13 @@ class PatchStream:
elif name == 'changes':
self.in_change = 'Commit'
self.change_version = self._parse_version(value, line)
+ elif name == 'cc':
+ self.commit.add_cc(value.split(','))
+ elif name == 'added-in':
+ version = self._parse_version(value, line)
+ self.commit.add_change(version, '- New')
+ self.series.AddChange(version, None, '- %s' %
+ self.commit.subject)
else:
self._add_warn('Line %d: Ignoring Commit-%s' %
(self.linenum, name))
@@ -651,6 +664,13 @@ class PatchStream:
outfd.write(line + '\n')
self.blank_count = 0
self.finalise()
+ if self.insert_base_commit:
+ if self.series.base_commit:
+ print(f'base-commit: {self.series.base_commit.hash}',
+ file=outfd)
+ if self.series.branch:
+ print(f'branch: {self.series.branch}', file=outfd)
+
def insert_tags(msg, tags_to_emit):
"""Add extra tags to a commit message
@@ -704,7 +724,7 @@ def get_list(commit_range, git_dir=None, count=None):
"""
params = gitutil.log_cmd(commit_range, reverse=True, count=count,
git_dir=git_dir)
- return command.run_pipe([params], capture=True).stdout
+ return command.run_one(*params, capture=True).stdout
def get_metadata_for_list(commit_range, git_dir=None, count=None,
series=None, allow_overwrite=False):
@@ -748,8 +768,12 @@ def get_metadata(branch, start, count):
Returns:
Series: Object containing information about the commits.
"""
- return get_metadata_for_list(
- '%s~%d' % (branch if branch else 'HEAD', start), None, count)
+ 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.branch = branch or gitutil.get_branch()
+ series.top = top
+ return series
def get_metadata_for_test(text):
"""Process metadata from a file containing a git log. Used for tests
@@ -767,7 +791,8 @@ def get_metadata_for_test(text):
pst.finalise()
return series
-def fix_patch(backup_dir, fname, series, cmt, keep_change_id=False):
+def fix_patch(backup_dir, fname, series, cmt, keep_change_id=False,
+ insert_base_commit=False):
"""Fix up a patch file, by adding/removing as required.
We remove our tags from the patch file, insert changes lists, etc.
@@ -781,6 +806,7 @@ def fix_patch(backup_dir, fname, series, cmt, keep_change_id=False):
series (Series): Series information about this patch set
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
Return:
list: A list of errors, each str, or [] if all ok.
@@ -788,7 +814,8 @@ def fix_patch(backup_dir, fname, series, cmt, keep_change_id=False):
handle, tmpname = tempfile.mkstemp()
outfd = os.fdopen(handle, 'w', encoding='utf-8')
infd = open(fname, 'r', encoding='utf-8')
- pst = PatchStream(series, keep_change_id=keep_change_id)
+ pst = PatchStream(series, keep_change_id=keep_change_id,
+ insert_base_commit=insert_base_commit)
pst.commit = cmt
pst.process_stream(infd, outfd)
infd.close()
@@ -800,7 +827,7 @@ 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):
+def fix_patches(series, fnames, keep_change_id=False, insert_base_commit=False):
"""Fix up a list of patches identified by filenames
The patch files are processed in place, and overwritten.
@@ -809,6 +836,7 @@ def fix_patches(series, fnames, keep_change_id=False):
series (Series): The Series object
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
"""
# Current workflow creates patches, so we shouldn't need a backup
backup_dir = None #tempfile.mkdtemp('clean-patch')
@@ -818,7 +846,8 @@ def fix_patches(series, fnames, keep_change_id=False):
cmt.patch = fname
cmt.count = count
result = fix_patch(backup_dir, fname, series, cmt,
- keep_change_id=keep_change_id)
+ keep_change_id=keep_change_id,
+ insert_base_commit=insert_base_commit)
if result:
print('%d warning%s for %s:' %
(len(result), 's' if len(result) > 1 else '', fname))
@@ -861,4 +890,11 @@ def insert_cover_letter(fname, series, count):
out = series.MakeChangeLog(None)
line += '\n' + '\n'.join(out)
fil.write(line)
+
+ # Insert the base commit and branch
+ if series.base_commit:
+ print(f'base-commit: {series.base_commit.hash}', file=fil)
+ if series.branch:
+ print(f'branch: {series.branch}', file=fil)
+
fil.close()
diff --git a/tools/patman/patman.rst b/tools/patman/patman.rst
index f4588c00fc1..63b95a6b161 100644
--- a/tools/patman/patman.rst
+++ b/tools/patman/patman.rst
@@ -350,7 +350,20 @@ Cover-changes: n
- This line will only appear in the cover letter
<blank line>
-Patch-cc: Their Name <email>
+Commit-added-in: n
+ Add a change noting the version this commit was added in. This is
+ equivalent to::
+
+ Commit-changes: n
+ - New
+
+ Cover-changes: n
+ - <commit subject>
+
+ It is a convenient shorthand for suppressing the '(no changes in vN)'
+ message.
+
+Patch-cc / Commit-cc: Their Name <email>
This copies a single patch to another email address. Note that the
Cc: used by git send-email is ignored by patman, but will be
interpreted by git send-email if you use it.
diff --git a/tools/patman/project.py b/tools/patman/project.py
index 4459042b5d4..d6143a67066 100644
--- a/tools/patman/project.py
+++ b/tools/patman/project.py
@@ -4,7 +4,7 @@
import os.path
-from patman import gitutil
+from u_boot_pylib import gitutil
def detect_project():
"""Autodetect the name of the current project.
diff --git a/tools/patman/requirements.txt b/tools/patman/requirements.txt
new file mode 100644
index 00000000000..e8cbc6cf0c3
--- /dev/null
+++ b/tools/patman/requirements.txt
@@ -0,0 +1,5 @@
+ConfigParser==7.1.0
+importlib_resources==6.5.2
+pygit2==1.13.3
+Requests==2.32.3
+setuptools==75.8.0
diff --git a/tools/patman/series.py b/tools/patman/series.py
index 6866e1dbd08..b73e9c58de4 100644
--- a/tools/patman/series.py
+++ b/tools/patman/series.py
@@ -12,8 +12,8 @@ import sys
import time
from patman import get_maintainer
-from patman import gitutil
from patman import settings
+from u_boot_pylib import gitutil
from u_boot_pylib import terminal
from u_boot_pylib import tools
@@ -42,6 +42,8 @@ class Series(dict):
self.notes = []
self.changes = {}
self.allow_overwrite = False
+ self.base_commit = None
+ self.branch = None
# Written in MakeCcFile()
# key: name of patch file
diff --git a/tools/patman/settings.py b/tools/patman/settings.py
index 636983e32da..d66b22be1df 100644
--- a/tools/patman/settings.py
+++ b/tools/patman/settings.py
@@ -12,7 +12,7 @@ import argparse
import os
import re
-from patman import gitutil
+from u_boot_pylib import gitutil
"""Default settings per-project.
@@ -59,25 +59,25 @@ class _ProjectConfigParser(ConfigParser.ConfigParser):
# Check to make sure that bogus project gets general alias.
>>> config = _ProjectConfigParser("zzz")
- >>> config.readfp(StringIO(sample_config))
+ >>> config.read_file(StringIO(sample_config))
>>> str(config.get("alias", "enemies"))
'Evil <evil@example.com>'
# Check to make sure that alias gets overridden by project.
>>> config = _ProjectConfigParser("sm")
- >>> config.readfp(StringIO(sample_config))
+ >>> config.read_file(StringIO(sample_config))
>>> str(config.get("alias", "enemies"))
'Green G. <ugly@example.com>'
# Check to make sure that settings get merged with project.
>>> config = _ProjectConfigParser("linux")
- >>> config.readfp(StringIO(sample_config))
+ >>> config.read_file(StringIO(sample_config))
>>> sorted((str(a), str(b)) for (a, b) in config.items("settings"))
[('am_hero', 'True'), ('check_patch_use_tree', 'True'), ('process_tags', 'False')]
# Check to make sure that settings works with unknown project.
>>> config = _ProjectConfigParser("unknown")
- >>> config.readfp(StringIO(sample_config))
+ >>> config.read_file(StringIO(sample_config))
>>> sorted((str(a), str(b)) for (a, b) in config.items("settings"))
[('am_hero', 'True')]
"""
diff --git a/tools/patman/setup.py b/tools/patman/setup.py
index 2ff791da0f7..bcaad69a1c2 100644
--- a/tools/patman/setup.py
+++ b/tools/patman/setup.py
@@ -3,7 +3,6 @@
from setuptools import setup
setup(name='patman',
version='1.0',
- license='GPL-2.0+',
scripts=['patman'],
packages=['patman'],
package_dir={'patman': ''},
diff --git a/tools/patman/test/0002-fdt-Correct-cast-for-sandbox-in-fdtdec_setup_mem_siz.patch b/tools/patman/test/0002-fdt-Correct-cast-for-sandbox-in-fdtdec_setup_mem_siz.patch
index 56278a6ce9b..48ea1793b47 100644
--- a/tools/patman/test/0002-fdt-Correct-cast-for-sandbox-in-fdtdec_setup_mem_siz.patch
+++ b/tools/patman/test/0002-fdt-Correct-cast-for-sandbox-in-fdtdec_setup_mem_siz.patch
@@ -21,7 +21,9 @@ Series-cc: Stefan Brüns <stefan.bruens@rwth-aachen.de>
Cover-letter-cc: Lord Mëlchett <clergy@palace.gov>
Series-version: 3
Patch-cc: fred
+Commit-cc: joe
Series-process-log: sort, uniq
+Commit-added-in: 4
Series-changes: 4
- Some changes
- Multi
diff --git a/tools/patman/test/test01.txt b/tools/patman/test/test01.txt
index fc3066e50b4..b2d73c5972c 100644
--- a/tools/patman/test/test01.txt
+++ b/tools/patman/test/test01.txt
@@ -49,7 +49,9 @@ Date: Sat Apr 15 15:39:08 2017 -0600
Cover-letter-cc: Lord Mëlchett <clergy@palace.gov>
Series-version: 3
Patch-cc: fred
+ Commit-cc: joe
Series-process-log: sort, uniq
+ Commit-added-in: 4
Series-changes: 4
- Some changes
- Multi
diff --git a/tools/patman/test_checkpatch.py b/tools/patman/test_checkpatch.py
index db7860f551d..3bf16febbf6 100644
--- a/tools/patman/test_checkpatch.py
+++ b/tools/patman/test_checkpatch.py
@@ -11,10 +11,10 @@ import tempfile
import unittest
from patman import checkpatch
-from patman import gitutil
from patman import patchstream
from patman import series
from patman import commit
+from u_boot_pylib import gitutil
class Line:
@@ -530,4 +530,3 @@ index 0000000..2234c87
if __name__ == "__main__":
unittest.main()
- gitutil.RunTests()