| # SPDX-License-Identifier: GPL-2.0+ |
| # Copyright (c) 2011 The Chromium OS Authors. |
| # |
| |
| from __future__ import print_function |
| |
| import collections |
| import concurrent.futures |
| import itertools |
| import os |
| import sys |
| import time |
| |
| from patman import get_maintainer |
| from patman import gitutil |
| from patman import settings |
| from u_boot_pylib import terminal |
| from u_boot_pylib import tools |
| |
| # Series-xxx tags that we understand |
| valid_series = ['to', 'cc', 'version', 'changes', 'prefix', 'notes', 'name', |
| 'cover_cc', 'process_log', 'links', 'patchwork_url', 'postfix'] |
| |
| 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 |
| """ |
| def __init__(self): |
| self.cc = [] |
| self.to = [] |
| self.cover_cc = [] |
| self.commits = [] |
| self.cover = None |
| self.notes = [] |
| self.changes = {} |
| self.allow_overwrite = False |
| |
| # Written in MakeCcFile() |
| # key: name of patch file |
| # value: list of email addresses |
| self._generated_cc = {} |
| |
| # These make us more like a dictionary |
| def __setattr__(self, name, value): |
| self[name] = value |
| |
| def __getattr__(self, name): |
| return self[name] |
| |
| def AddTag(self, commit, line, name, value): |
| """Add a new Series-xxx tag along with its value. |
| |
| Args: |
| line: Source line containing tag (useful for debug/error messages) |
| name: Tag name (part after 'Series-') |
| value: Tag value (part after 'Series-xxx: ') |
| |
| Returns: |
| String warning if something went wrong, else None |
| """ |
| # If we already have it, then add to our list |
| name = name.replace('-', '_') |
| if name in self and not self.allow_overwrite: |
| values = value.split(',') |
| values = [str.strip() for str in values] |
| if type(self[name]) != type([]): |
| raise ValueError("In %s: line '%s': Cannot add another value " |
| "'%s' to series '%s'" % |
| (commit.hash, line, values, self[name])) |
| self[name] += values |
| |
| # Otherwise just set the value |
| elif name in valid_series: |
| if name=="notes": |
| self[name] = [value] |
| else: |
| self[name] = value |
| else: |
| return ("In %s: line '%s': Unknown 'Series-%s': valid " |
| "options are %s" % (commit.hash, line, name, |
| ', '.join(valid_series))) |
| return None |
| |
| def AddCommit(self, commit): |
| """Add a commit into our list of commits |
| |
| We create a list of tags in the commit subject also. |
| |
| Args: |
| commit: Commit object to add |
| """ |
| commit.check_tags() |
| self.commits.append(commit) |
| |
| def ShowActions(self, args, cmd, process_tags): |
| """Show what actions we will/would perform |
| |
| Args: |
| args: List of patch files we created |
| cmd: The git command we would have run |
| process_tags: Process tags as if they were aliases |
| """ |
| to_set = set(gitutil.build_email_list(self.to)); |
| cc_set = set(gitutil.build_email_list(self.cc)); |
| |
| col = terminal.Color() |
| print('Dry run, so not doing much. But I would do this:') |
| print() |
| print('Send a total of %d patch%s with %scover letter.' % ( |
| len(args), '' if len(args) == 1 else 'es', |
| self.get('cover') and 'a ' or 'no ')) |
| |
| # TODO: Colour the patches according to whether they passed checks |
| for upto in range(len(args)): |
| commit = self.commits[upto] |
| print(col.build(col.GREEN, ' %s' % args[upto])) |
| cc_list = list(self._generated_cc[commit.patch]) |
| for email in sorted(set(cc_list) - to_set - cc_set): |
| if email == None: |
| email = col.build(col.YELLOW, '<alias not found>') |
| if email: |
| print(' Cc: ', email) |
| print |
| for item in sorted(to_set): |
| print('To:\t ', item) |
| for item in sorted(cc_set - to_set): |
| print('Cc:\t ', item) |
| print('Version: ', self.get('version')) |
| print('Prefix:\t ', self.get('prefix')) |
| print('Postfix:\t ', self.get('postfix')) |
| if self.cover: |
| print('Cover: %d lines' % len(self.cover)) |
| cover_cc = gitutil.build_email_list(self.get('cover_cc', '')) |
| all_ccs = itertools.chain(cover_cc, *self._generated_cc.values()) |
| for email in sorted(set(all_ccs) - to_set - cc_set): |
| print(' Cc: ', email) |
| if cmd: |
| print('Git command: %s' % cmd) |
| |
| def MakeChangeLog(self, commit): |
| """Create a list of changes for each version. |
| |
| Return: |
| The change log as a list of strings, one per line |
| |
| Changes in v4: |
| - Jog the dial back closer to the widget |
| |
| Changes in v2: |
| - Fix the widget |
| - Jog the dial |
| |
| If there are no new changes in a patch, a note will be added |
| |
| (no changes since v2) |
| |
| Changes in v2: |
| - Fix the widget |
| - Jog the dial |
| """ |
| # Collect changes from the series and this commit |
| changes = collections.defaultdict(list) |
| for version, changelist in self.changes.items(): |
| changes[version] += changelist |
| if commit: |
| for version, changelist in commit.changes.items(): |
| changes[version] += [[commit, text] for text in changelist] |
| |
| versions = sorted(changes, reverse=True) |
| newest_version = 1 |
| if 'version' in self: |
| newest_version = max(newest_version, int(self.version)) |
| if versions: |
| newest_version = max(newest_version, versions[0]) |
| |
| final = [] |
| process_it = self.get('process_log', '').split(',') |
| process_it = [item.strip() for item in process_it] |
| need_blank = False |
| for version in versions: |
| out = [] |
| for this_commit, text in changes[version]: |
| if commit and this_commit != commit: |
| continue |
| if 'uniq' not in process_it or text not in out: |
| out.append(text) |
| if 'sort' in process_it: |
| out = sorted(out) |
| have_changes = len(out) > 0 |
| line = 'Changes in v%d:' % version |
| if have_changes: |
| out.insert(0, line) |
| if version < newest_version and len(final) == 0: |
| out.insert(0, '') |
| out.insert(0, '(no changes since v%d)' % version) |
| newest_version = 0 |
| # Only add a new line if we output something |
| if need_blank: |
| out.insert(0, '') |
| need_blank = False |
| final += out |
| need_blank = need_blank or have_changes |
| |
| if len(final) > 0: |
| final.append('') |
| elif newest_version != 1: |
| final = ['(no changes since v1)', ''] |
| return final |
| |
| def DoChecks(self): |
| """Check that each version has a change log |
| |
| Print an error if something is wrong. |
| """ |
| col = terminal.Color() |
| if self.get('version'): |
| changes_copy = dict(self.changes) |
| for version in range(1, int(self.version) + 1): |
| if self.changes.get(version): |
| del changes_copy[version] |
| else: |
| if version > 1: |
| str = 'Change log missing for v%d' % version |
| print(col.build(col.RED, str)) |
| for version in changes_copy: |
| str = 'Change log for unknown version v%d' % version |
| print(col.build(col.RED, str)) |
| elif self.changes: |
| str = 'Change log exists, but no version is set' |
| print(col.build(col.RED, str)) |
| |
| def GetCcForCommit(self, commit, process_tags, warn_on_error, |
| add_maintainers, limit, get_maintainer_script, |
| all_skips): |
| """Get the email CCs to use with a particular commit |
| |
| Uses subject tags and get_maintainers.pl script to find people to cc |
| on a patch |
| |
| Args: |
| commit (Commit): Commit to process |
| process_tags (bool): Process tags as if they were aliases |
| warn_on_error (bool): True to print a warning when an alias fails to |
| match, False to ignore it. |
| add_maintainers (bool or list of str): Either: |
| True/False to call the get_maintainers to CC maintainers |
| List of maintainers to include (for testing) |
| limit (int): Limit the length of the Cc list (None if no limit) |
| get_maintainer_script (str): The file name of the get_maintainer.pl |
| script (or compatible). |
| all_skips (set of str): Updated to include the set of bouncing email |
| addresses that were dropped from the output. This is essentially |
| a return value from this function. |
| |
| Returns: |
| list of str: List of email addresses to cc |
| """ |
| cc = [] |
| if process_tags: |
| cc += gitutil.build_email_list(commit.tags, |
| warn_on_error=warn_on_error) |
| cc += gitutil.build_email_list(commit.cc_list, |
| warn_on_error=warn_on_error) |
| if type(add_maintainers) == type(cc): |
| cc += add_maintainers |
| elif add_maintainers: |
| cc += get_maintainer.get_maintainer(get_maintainer_script, |
| commit.patch) |
| all_skips |= set(cc) & set(settings.bounces) |
| cc = list(set(cc) - set(settings.bounces)) |
| if limit is not None: |
| cc = cc[:limit] |
| return cc |
| |
| def MakeCcFile(self, process_tags, cover_fname, warn_on_error, |
| add_maintainers, limit, get_maintainer_script): |
| """Make a cc file for us to use for per-commit Cc automation |
| |
| Also stores in self._generated_cc to make ShowActions() faster. |
| |
| Args: |
| process_tags (bool): Process tags as if they were aliases |
| cover_fname (str): If non-None the name of the cover letter. |
| warn_on_error (bool): True to print a warning when an alias fails to |
| match, False to ignore it. |
| add_maintainers (bool or list of str): Either: |
| True/False to call the get_maintainers to CC maintainers |
| List of maintainers to include (for testing) |
| limit (int): Limit the length of the Cc list (None if no limit) |
| get_maintainer_script (str): The file name of the get_maintainer.pl |
| script (or compatible). |
| Return: |
| Filename of temp file created |
| """ |
| col = terminal.Color() |
| # Look for commit tags (of the form 'xxx:' at the start of the subject) |
| fname = '/tmp/patman.%d' % os.getpid() |
| fd = open(fname, 'w', encoding='utf-8') |
| all_ccs = [] |
| all_skips = set() |
| with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor: |
| for i, commit in enumerate(self.commits): |
| commit.seq = i |
| commit.future = executor.submit( |
| self.GetCcForCommit, commit, process_tags, warn_on_error, |
| add_maintainers, limit, get_maintainer_script, all_skips) |
| |
| # Show progress any commits that are taking forever |
| lastlen = 0 |
| while True: |
| left = [commit for commit in self.commits |
| if not commit.future.done()] |
| if not left: |
| break |
| names = ', '.join(f'{c.seq + 1}:{c.subject}' |
| for c in left[:2]) |
| out = f'\r{len(left)} remaining: {names}'[:79] |
| spaces = ' ' * (lastlen - len(out)) |
| if lastlen: # Don't print anything the first time |
| print(out, spaces, end='') |
| sys.stdout.flush() |
| lastlen = len(out) |
| time.sleep(.25) |
| print(f'\rdone{" " * lastlen}\r', end='') |
| print('Cc processing complete') |
| |
| for commit in self.commits: |
| cc = commit.future.result() |
| all_ccs += cc |
| print(commit.patch, '\0'.join(sorted(set(cc))), file=fd) |
| self._generated_cc[commit.patch] = cc |
| |
| for x in sorted(all_skips): |
| print(col.build(col.YELLOW, f'Skipping "{x}"')) |
| |
| if cover_fname: |
| cover_cc = gitutil.build_email_list(self.get('cover_cc', '')) |
| cover_cc = list(set(cover_cc + all_ccs)) |
| if limit is not None: |
| cover_cc = cover_cc[:limit] |
| cc_list = '\0'.join([x for x in sorted(cover_cc)]) |
| print(cover_fname, cc_list, file=fd) |
| |
| fd.close() |
| return fname |
| |
| def AddChange(self, version, commit, info): |
| """Add a new change line to a version. |
| |
| This will later appear in the change log. |
| |
| Args: |
| version: version number to add change list to |
| info: change line for this version |
| """ |
| if not self.changes.get(version): |
| self.changes[version] = [] |
| self.changes[version].append([commit, info]) |
| |
| def GetPatchPrefix(self): |
| """Get the patch version string |
| |
| Return: |
| Patch string, like 'RFC PATCH v5' or just 'PATCH' |
| """ |
| git_prefix = gitutil.get_default_subject_prefix() |
| if git_prefix: |
| git_prefix = '%s][' % git_prefix |
| else: |
| git_prefix = '' |
| |
| version = '' |
| if self.get('version'): |
| version = ' v%s' % self['version'] |
| |
| # Get patch name prefix |
| prefix = '' |
| if self.get('prefix'): |
| prefix = '%s ' % self['prefix'] |
| |
| postfix = '' |
| if self.get('postfix'): |
| postfix = ' %s' % self['postfix'] |
| return '%s%sPATCH%s%s' % (git_prefix, prefix, postfix, version) |