diff options
Diffstat (limited to 'tools/patman/cser_helper.py')
| -rw-r--r-- | tools/patman/cser_helper.py | 1524 |
1 files changed, 0 insertions, 1524 deletions
diff --git a/tools/patman/cser_helper.py b/tools/patman/cser_helper.py deleted file mode 100644 index 81ad212daee..00000000000 --- a/tools/patman/cser_helper.py +++ /dev/null @@ -1,1524 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0+ -# -# Copyright 2025 Simon Glass <sjg@chromium.org> -# -"""Helper functions for handling the 'series' subcommand -""" - -import asyncio -from collections import OrderedDict, defaultdict, namedtuple -from datetime import datetime -import hashlib -import os -import re -import sys -import time -from types import SimpleNamespace - -import aiohttp -import pygit2 -from pygit2.enums import CheckoutStrategy - -from u_boot_pylib import gitutil -from u_boot_pylib import terminal -from u_boot_pylib import tout - -from patman import patchstream -from patman.database import Database, Pcommit, SerVer -from patman import patchwork -from patman.series import Series -from patman import status - - -# Tag to use for Change IDs -CHANGE_ID_TAG = 'Change-Id' - -# Length of hash to display -HASH_LEN = 10 - -# Shorter version of some states, to save horizontal space -SHORTEN_STATE = { - 'handled-elsewhere': 'elsewhere', - 'awaiting-upstream': 'awaiting', - 'not-applicable': 'n/a', - 'changes-requested': 'changes', -} - -# Summary info returned from Cseries.link_auto_all() -AUTOLINK = namedtuple('autolink', 'name,version,link,desc,result') - - -def oid(oid_val): - """Convert a hash string into a shortened hash - - The number of hex digits git uses for showing hashes depends on the size of - the repo. For the purposes of showing hashes to the user in lists, we use a - fixed value for now - - Args: - str or Pygit2.oid: Hash value to shorten - - Return: - str: Shortened hash - """ - return str(oid_val)[:HASH_LEN] - - -def split_name_version(in_name): - """Split a branch name into its series name and its version - - For example: - 'series' returns ('series', 1) - 'series3' returns ('series', 3) - Args: - in_name (str): Name to parse - - Return: - tuple: - str: series name - int: series version, or None if there is none in in_name - """ - m_ver = re.match(r'([^0-9]*)(\d*)', in_name) - version = None - if m_ver: - name = m_ver.group(1) - if m_ver.group(2): - version = int(m_ver.group(2)) - else: - name = in_name - return name, version - - -class CseriesHelper: - """Helper functions for Cseries - - This class handles database read/write as well as operations in a git - directory to update series information. - """ - def __init__(self, topdir=None, colour=terminal.COLOR_IF_TERMINAL): - """Set up a new CseriesHelper - - Args: - topdir (str): Top-level directory of the repo - colour (terminal.enum): Whether to enable ANSI colour or not - - Properties: - gitdir (str): Git directory (typically topdir + '/.git') - db (Database): Database handler - col (terminal.Colour): Colour object - _fake_time (float): Holds the current fake time for tests, in - seconds - _fake_sleep (func): Function provided by a test; called to fake a - 'time.sleep()' call and take whatever action it wants to take. - The only argument is the (Float) time to sleep for; it returns - nothing - loop (asyncio event loop): Loop used for Patchwork operations - """ - self.topdir = topdir - self.gitdir = None - self.db = None - self.col = terminal.Color(colour) - self._fake_time = None - self._fake_sleep = None - self.fake_now = None - self.loop = asyncio.get_event_loop() - - def open_database(self): - """Open the database ready for use""" - if not self.topdir: - self.topdir = gitutil.get_top_level() - if not self.topdir: - raise ValueError('No git repo detected in current directory') - self.gitdir = os.path.join(self.topdir, '.git') - fname = f'{self.topdir}/.patman.db' - - # For the first instance, start it up with the expected schema - self.db, is_new = Database.get_instance(fname) - if is_new: - self.db.start() - else: - # If a previous test has already checked the schema, just open it - self.db.open_it() - - def close_database(self): - """Close the database""" - if self.db: - self.db.close() - - def commit(self): - """Commit changes to the database""" - self.db.commit() - - def rollback(self): - """Roll back changes to the database""" - self.db.rollback() - - def set_fake_time(self, fake_sleep): - """Setup the fake timer - - Args: - fake_sleep (func(float)): Function to call to fake a sleep - """ - self._fake_time = 0 - self._fake_sleep = fake_sleep - - def inc_fake_time(self, inc_s): - """Increment the fake time - - Args: - inc_s (float): Amount to increment the fake time by - """ - self._fake_time += inc_s - - def get_time(self): - """Get the current time, fake or real - - This function should always be used to read the time so that faking the - time works correctly in tests. - - Return: - float: Fake time, if time is being faked, else real time - """ - if self._fake_time is not None: - return self._fake_time - return time.monotonic() - - def sleep(self, time_s): - """Sleep for a while - - This function should always be used to sleep so that faking the time - works correctly in tests. - - Args: - time_s (float): Amount of seconds to sleep for - """ - print(f'Sleeping for {time_s} seconds') - if self._fake_time is not None: - self._fake_sleep(time_s) - else: - time.sleep(time_s) - - def get_now(self): - """Get the time now - - This function should always be used to read the datetime, so that - faking the time works correctly in tests - - Return: - DateTime object - """ - if self.fake_now: - return self.fake_now - return datetime.now() - - def get_ser_ver_list(self): - """Get a list of patchwork entries from the database - - Return: - list of SER_VER - """ - return self.db.ser_ver_get_list() - - def get_ser_ver_dict(self): - """Get a dict of patchwork entries from the database - - Return: dict contain all records: - key (int): ser_ver id - value (SER_VER): Information about one ser_ver record - """ - svlist = self.get_ser_ver_list() - svdict = {} - for sver in svlist: - svdict[sver.idnum] = sver - return svdict - - def get_upstream_dict(self): - """Get a list of upstream entries from the database - - Return: - OrderedDict: - key (str): upstream name - value (str): url - """ - return self.db.upstream_get_dict() - - def get_pcommit_dict(self, find_svid=None): - """Get a dict of pcommits entries from the database - - Args: - find_svid (int): If not None, finds the records associated with a - particular series and version - - Return: - OrderedDict: - key (int): record ID if find_svid is None, else seq - value (PCOMMIT): record data - """ - pcdict = OrderedDict() - for rec in self.db.pcommit_get_list(find_svid): - if find_svid is not None: - pcdict[rec.seq] = rec - else: - pcdict[rec.idnum] = rec - return pcdict - - def _get_series_info(self, idnum): - """Get information for a series from the database - - Args: - idnum (int): Series ID to look up - - Return: tuple: - str: Series name - str: Series description - - Raises: - ValueError: Series is not found - """ - return self.db.series_get_info(idnum) - - def prep_series(self, name, end=None): - """Prepare to work with a series - - Args: - name (str): Branch name with version appended, e.g. 'fix2' - end (str or None): Commit to end at, e.g. 'my_branch~16'. Only - commits up to that are processed. None to process commits up to - the upstream branch - - Return: tuple: - str: Series name, e.g. 'fix' - Series: Collected series information, including name - int: Version number, e.g. 2 - str: Message to show - """ - ser, version = self._parse_series_and_version(name, None) - if not name: - name = self._get_branch_name(ser.name, version) - - # First check we have a branch with this name - if not gitutil.check_branch(name, git_dir=self.gitdir): - raise ValueError(f"No branch named '{name}'") - - count = gitutil.count_commits_to_branch(name, self.gitdir, end) - if not count: - raise ValueError('Cannot detect branch automatically: ' - 'Perhaps use -U <upstream-commit> ?') - - series = patchstream.get_metadata(name, 0, count, git_dir=self.gitdir) - self._copy_db_fields_to(series, ser) - msg = None - if end: - repo = pygit2.Repository(self.gitdir) - target = repo.revparse_single(end) - first_line = target.message.splitlines()[0] - msg = f'Ending before {oid(target.id)} {first_line}' - - return name, series, version, msg - - def _copy_db_fields_to(self, series, in_series): - """Copy over fields used by Cseries from one series to another - - This copes desc, idnum and name - - Args: - series (Series): Series to copy to - in_series (Series): Series to copy from - """ - series.desc = in_series.desc - series.idnum = in_series.idnum - series.name = in_series.name - - def _handle_mark(self, branch_name, in_series, version, mark, - allow_unmarked, force_version, dry_run): - """Handle marking a series, checking for unmarked commits, etc. - - Args: - branch_name (str): Name of branch to sync, or None for current one - in_series (Series): Series object - version (int): branch version, e.g. 2 for 'mychange2' - mark (bool): True to mark each commit with a change ID - allow_unmarked (str): True to not require each commit to be marked - force_version (bool): True if ignore a Series-version tag that - doesn't match its branch name - dry_run (bool): True to do a dry run - - Returns: - Series: New series object, if the series was marked; - copy_db_fields_to() is used to copy fields over - - Raises: - ValueError: Series being unmarked when it should be marked, etc. - """ - series = in_series - if 'version' in series and int(series.version) != version: - msg = (f"Series name '{branch_name}' suggests version {version} " - f"but Series-version tag indicates {series.version}") - if not force_version: - raise ValueError(msg + ' (see --force-version)') - - tout.warning(msg) - tout.warning(f'Updating Series-version tag to version {version}') - self.update_series(branch_name, series, int(series.version), - new_name=None, dry_run=dry_run, - add_vers=version) - - # Collect the commits again, as the hashes have changed - series = patchstream.get_metadata(branch_name, 0, - len(series.commits), - git_dir=self.gitdir) - self._copy_db_fields_to(series, in_series) - - if mark: - add_oid = self._mark_series(branch_name, series, dry_run=dry_run) - - # Collect the commits again, as the hashes have changed - series = patchstream.get_metadata(add_oid, 0, len(series.commits), - git_dir=self.gitdir) - self._copy_db_fields_to(series, in_series) - - bad_count = 0 - for commit in series.commits: - if not commit.change_id: - bad_count += 1 - if bad_count and not allow_unmarked: - raise ValueError( - f'{bad_count} commit(s) are unmarked; please use -m or -M') - - return series - - def _add_series_commits(self, series, svid): - """Add a commits from a series into the database - - Args: - series (Series): Series containing commits to add - svid (int): ser_ver-table ID to use for each commit - """ - to_add = [Pcommit(None, seq, commit.subject, None, commit.change_id, - None, None, None) - for seq, commit in enumerate(series.commits)] - - self.db.pcommit_add_list(svid, to_add) - - def get_series_by_name(self, name, include_archived=False): - """Get a Series object from the database by name - - Args: - name (str): Name of series to get - include_archived (bool): True to search in archives series - - Return: - Series: Object containing series info, or None if none - """ - idnum = self.db.series_find_by_name(name, include_archived) - if not idnum: - return None - name, desc = self.db.series_get_info(idnum) - - return Series.from_fields(idnum, name, desc) - - def _get_branch_name(self, name, version): - """Get the branch name for a particular version - - Args: - name (str): Base name of branch - version (int): Version number to use - """ - return name + (f'{version}' if version > 1 else '') - - def _ensure_version(self, ser, version): - """Ensure that a version exists in a series - - Args: - ser (Series): Series information, with idnum and name used here - version (int): Version to check - - Returns: - list of int: List of versions - """ - versions = self._get_version_list(ser.idnum) - if version not in versions: - raise ValueError( - f"Series '{ser.name}' does not have a version {version}") - return versions - - def _set_link(self, ser_id, name, version, link, update_commit, - dry_run=False): - """Add / update a series-links link for a series - - Args: - ser_id (int): Series ID number - name (str): Series name (used to find the branch) - version (int): Version number (used to update the database) - link (str): Patchwork link-string for the series - update_commit (bool): True to update the current commit with the - link - dry_run (bool): True to do a dry run - - Return: - bool: True if the database was update, False if the ser_id or - version was not found - """ - if update_commit: - branch_name = self._get_branch_name(name, version) - _, ser, max_vers, _ = self.prep_series(branch_name) - self.update_series(branch_name, ser, max_vers, add_vers=version, - dry_run=dry_run, add_link=link) - if link is None: - link = '' - updated = 1 if self.db.ser_ver_set_link(ser_id, version, link) else 0 - if dry_run: - self.rollback() - else: - self.commit() - - return updated - - def _get_autolink_dict(self, sdict, link_all_versions): - """Get a dict of ser_vers to fetch, along with their patchwork links - - Note that this returns items that already have links, as well as those - without links - - Args: - sdict: - key: series ID - value: Series with idnum, name and desc filled out - link_all_versions (bool): True to sync all versions of a series, - False to sync only the latest version - - Return: tuple: - dict: - key (int): svid - value (tuple): - int: series ID - str: series name - int: series version - str: patchwork link for the series, or None if none - desc: cover-letter name / series description - """ - svdict = self.get_ser_ver_dict() - to_fetch = {} - - if link_all_versions: - for svinfo in self.get_ser_ver_list(): - ser = sdict[svinfo.series_id] - - pwc = self.get_pcommit_dict(svinfo.idnum) - count = len(pwc) - branch = self._join_name_version(ser.name, svinfo.version) - series = patchstream.get_metadata(branch, 0, count, - git_dir=self.gitdir) - self._copy_db_fields_to(series, ser) - - to_fetch[svinfo.idnum] = (svinfo.series_id, series.name, - svinfo.version, svinfo.link, series) - else: - # Find the maximum version for each series - max_vers = self._series_all_max_versions() - - # Get a list of links to fetch - for svid, ser_id, version in max_vers: - svinfo = svdict[svid] - ser = sdict[ser_id] - - pwc = self.get_pcommit_dict(svid) - count = len(pwc) - branch = self._join_name_version(ser.name, version) - series = patchstream.get_metadata(branch, 0, count, - git_dir=self.gitdir) - self._copy_db_fields_to(series, ser) - - to_fetch[svid] = (ser_id, series.name, version, svinfo.link, - series) - return to_fetch - - def _get_version_list(self, idnum): - """Get a list of the versions available for a series - - Args: - idnum (int): ID of series to look up - - Return: - str: List of versions - """ - if idnum is None: - raise ValueError('Unknown series idnum') - return self.db.series_get_version_list(idnum) - - def _join_name_version(self, in_name, version): - """Convert a series name plus a version into a branch name - - For example: - ('series', 1) returns 'series' - ('series', 3) returns 'series3' - - Args: - in_name (str): Series name - version (int): Version number - - Return: - str: associated branch name - """ - if version == 1: - return in_name - return f'{in_name}{version}' - - def _parse_series(self, name, include_archived=False): - """Parse the name of a series, or detect it from the current branch - - Args: - name (str or None): name of series - include_archived (bool): True to search in archives series - - Return: - Series: New object with the name set; idnum is also set if the - series exists in the database - """ - if not name: - name = gitutil.get_branch(self.gitdir) - name, _ = split_name_version(name) - ser = self.get_series_by_name(name, include_archived) - if not ser: - ser = Series() - ser.name = name - return ser - - def _parse_series_and_version(self, in_name, in_version): - """Parse name and version of a series, or detect from current branch - - Figures out the name from in_name, or if that is None, from the current - branch. - - Uses the version in_version, or if that is None, uses the int at the - end of the name (e.g. 'series' is version 1, 'series4' is version 4) - - Args: - in_name (str or None): name of series - in_version (str or None): version of series - - Return: - tuple: - Series: New object with the name set; idnum is also set if the - series exists in the database - int: Series version-number detected from the name - (e.g. 'fred' is version 1, 'fred2' is version 2) - """ - name = in_name - if not name: - name = gitutil.get_branch(self.gitdir) - if not name: - raise ValueError('No branch detected: please use -s <series>') - name, version = split_name_version(name) - if not name: - raise ValueError(f"Series name '{in_name}' cannot be a number, " - f"use '<name><version>'") - if in_version: - if version and version != in_version: - tout.warning( - f"Version mismatch: -V has {in_version} but branch name " - f'indicates {version}') - version = in_version - if not version: - version = 1 - if version > 99: - raise ValueError(f"Version {version} exceeds 99") - ser = self.get_series_by_name(name) - if not ser: - ser = Series() - ser.name = name - return ser, version - - def _series_get_version_stats(self, idnum, vers): - """Get the stats for a series - - Args: - idnum (int): ID number of series to process - vers (int): Version number to process - - Return: - tuple: - str: Status string, '<accepted>/<count>' - OrderedDict: - key (int): record ID if find_svid is None, else seq - value (PCOMMIT): record data - """ - svid, link = self._get_series_svid_link(idnum, vers) - pwc = self.get_pcommit_dict(svid) - count = len(pwc.values()) - if link: - accepted = 0 - for pcm in pwc.values(): - accepted += pcm.state == 'accepted' - else: - accepted = '-' - return f'{accepted}/{count}', pwc - - def get_series_svid(self, series_id, version): - """Get the patchwork ID of a series version - - Args: - series_id (int): id of the series to look up - version (int): version number to look up - - Return: - str: link found - - Raises: - ValueError: No matching series found - """ - return self._get_series_svid_link(series_id, version)[0] - - def _get_series_svid_link(self, series_id, version): - """Get the patchwork ID of a series version - - Args: - series_id (int): series ID to look up - version (int): version number to look up - - Return: - tuple: - int: record id - str: link - """ - recs = self.get_ser_ver(series_id, version) - return recs.idnum, recs.link - - def get_ser_ver(self, series_id, version): - """Get the patchwork details for a series version - - Args: - series_id (int): series ID to look up - version (int): version number to look up - - Return: - SER_VER: Requested information - - Raises: - ValueError: There is no matching idnum/version - """ - return self.db.ser_ver_get_for_series(series_id, version) - - def _prepare_process(self, name, count, new_name=None, quiet=False): - """Get ready to process all commits in a branch - - Args: - name (str): Name of the branch to process - count (int): Number of commits - new_name (str or None): New name, if a new branch is to be created - quiet (bool): True to avoid output (used for testing) - - Return: tuple: - pygit2.repo: Repo to use - pygit2.oid: Upstream commit, onto which commits should be added - Pygit2.branch: Original branch, for later use - str: (Possibly new) name of branch to process - list of Commit: commits to process, in order - pygit2.Reference: Original head before processing started - """ - upstream_guess = gitutil.get_upstream(self.gitdir, name)[0] - - tout.debug(f"_process_series name '{name}' new_name '{new_name}' " - f"upstream_guess '{upstream_guess}'") - dirty = gitutil.check_dirty(self.gitdir, self.topdir) - if dirty: - raise ValueError( - f"Modified files exist: use 'git status' to check: " - f'{dirty[:5]}') - repo = pygit2.Repository(self.gitdir) - - commit = None - upstream_name = None - if upstream_guess: - try: - upstream = repo.lookup_reference(upstream_guess) - upstream_name = upstream.name - commit = upstream.peel(pygit2.enums.ObjectType.COMMIT) - except KeyError: - pass - except pygit2.repository.InvalidSpecError as exc: - print(f"Error '{exc}'") - if not upstream_name: - upstream_name = f'{name}~{count}' - commit = repo.revparse_single(upstream_name) - - branch = repo.lookup_branch(name) - if not quiet: - tout.info( - f'Checking out upstream commit {upstream_name}: ' - f'{oid(commit.oid)}') - - old_head = repo.head - if old_head.shorthand == name: - old_head = None - else: - old_head = repo.head - - if new_name: - name = new_name - repo.set_head(commit.oid) - - commits = [] - cmt = repo.get(branch.target) - for _ in range(count): - commits.append(cmt) - cmt = cmt.parents[0] - - return (repo, repo.head, branch, name, commit, list(reversed(commits)), - old_head) - - def _pick_commit(self, repo, cmt): - """Apply a commit to the source tree, without committing it - - _prepare_process() must be called before starting to pick commits - - This function must be called before _finish_commit() - - Note that this uses a cherry-pick method, creating a new tree_id each - time, so can make source-code changes - - Args: - repo (pygit2.repo): Repo to use - cmt (Commit): Commit to apply - - Return: tuple: - tree_id (pygit2.oid): Oid of index with source-changes applied - commit (pygit2.oid): Old commit being cherry-picked - """ - tout.detail(f"- adding {oid(cmt.hash)} {cmt}") - repo.cherrypick(cmt.hash) - if repo.index.conflicts: - raise ValueError('Conflicts detected') - - tree_id = repo.index.write_tree() - cherry = repo.get(cmt.hash) - tout.detail(f"cherry {oid(cherry.oid)}") - return tree_id, cherry - - def _finish_commit(self, repo, tree_id, commit, cur, msg=None): - """Complete a commit - - This must be called after _pick_commit(). - - Args: - repo (pygit2.repo): Repo to use - tree_id (pygit2.oid): Oid of index with source-changes applied; if - None then the existing commit.tree_id is used - commit (pygit2.oid): Old commit being cherry-picked - cur (pygit2.reference): Reference to parent to use for the commit - msg (str): Commit subject and message; None to use commit.message - """ - if msg is None: - msg = commit.message - if not tree_id: - tree_id = commit.tree_id - repo.create_commit('HEAD', commit.author, commit.committer, - msg, tree_id, [cur.target]) - return repo.head - - def _finish_process(self, repo, branch, name, cur, old_head, new_name=None, - switch=False, dry_run=False, quiet=False): - """Finish processing commits - - Args: - repo (pygit2.repo): Repo to use - branch (pygit2.branch): Branch returned by _prepare_process() - name (str): Name of the branch to process - new_name (str or None): New name, if a new branch is being created - switch (bool): True to switch to the new branch after processing; - otherwise HEAD remains at the original branch, as amended - dry_run (bool): True to do a dry run, restoring the original tree - afterwards - quiet (bool): True to avoid output (used for testing) - - Return: - pygit2.reference: Final commit after everything is completed - """ - repo.state_cleanup() - - # Update the branch - target = repo.revparse_single('HEAD') - if not quiet: - tout.info(f'Updating branch {name} from {oid(branch.target)} to ' - f'{str(target.oid)[:HASH_LEN]}') - if dry_run: - if new_name: - repo.head.set_target(branch.target) - else: - branch_oid = branch.peel(pygit2.enums.ObjectType.COMMIT).oid - repo.head.set_target(branch_oid) - repo.head.set_target(branch.target) - repo.set_head(branch.name) - else: - if new_name: - new_branch = repo.branches.create(new_name, target) - if branch.upstream: - new_branch.upstream = branch.upstream - branch = new_branch - else: - branch.set_target(cur.target) - repo.set_head(branch.name) - if old_head: - if not switch: - repo.set_head(old_head.name) - return target - - def make_change_id(self, commit): - """Make a Change ID for a commit - - This is similar to the gerrit script: - git var GIT_COMMITTER_IDENT ; echo "$refhash" ; cat "README"; } - | git hash-object --stdin) - - Args: - commit (pygit2.commit): Commit to process - - Return: - Change ID in hex format - """ - sig = commit.committer - val = hashlib.sha1() - to_hash = f'{sig.name} <{sig.email}> {sig.time} {sig.offset}' - val.update(to_hash.encode('utf-8')) - val.update(str(commit.tree_id).encode('utf-8')) - val.update(commit.message.encode('utf-8')) - return val.hexdigest() - - def _filter_commits(self, name, series, seq_to_drop): - """Filter commits to drop one - - This function rebases the current branch, dropping a single commit, - thus changing the resulting code in the tree. - - Args: - name (str): Name of the branch to process - series (Series): Series object - seq_to_drop (int): Commit sequence to drop; commits are numbered - from 0, which is the one after the upstream branch, to - count - 1 - """ - count = len(series.commits) - (repo, cur, branch, name, commit, _, _) = self._prepare_process( - name, count, quiet=True) - repo.checkout_tree(commit, strategy=CheckoutStrategy.FORCE | - CheckoutStrategy.RECREATE_MISSING) - repo.set_head(commit.oid) - for seq, cmt in enumerate(series.commits): - if seq != seq_to_drop: - tree_id, cherry = self._pick_commit(repo, cmt) - cur = self._finish_commit(repo, tree_id, cherry, cur) - self._finish_process(repo, branch, name, cur, None, quiet=True) - - def process_series(self, name, series, new_name=None, switch=False, - dry_run=False): - """Rewrite a series commit messages, leaving code alone - - This uses a 'vals' namespace to pass things to the controlling - function. - - Each time _process_series() yields, it sets up: - commit (Commit): The pygit2 commit that is being processed - msg (str): Commit message, which can be modified - info (str): Initially empty; the controlling function can add a - short message here which will be shown to the user - final (bool): True if this is the last commit to apply - seq (int): Current sequence number in the commits to apply (0,,n-1) - - It also sets git HEAD at the commit before this commit being - processed - - The function can change msg and info, e.g. to add or remove tags from - the commit. - - Args: - name (str): Name of the branch to process - series (Series): Series object - new_name (str or None): New name, if a new branch is to be created - switch (bool): True to switch to the new branch after processing; - otherwise HEAD remains at the original branch, as amended - dry_run (bool): True to do a dry run, restoring the original tree - afterwards - - Return: - pygit.oid: oid of the new branch - """ - count = len(series.commits) - repo, cur, branch, name, _, commits, old_head = self._prepare_process( - name, count, new_name) - vals = SimpleNamespace() - vals.final = False - tout.info(f"Processing {count} commits from branch '{name}'") - - # Record the message lines - lines = [] - for seq, cmt in enumerate(series.commits): - commit = commits[seq] - vals.commit = commit - vals.msg = commit.message - vals.info = '' - vals.final = seq == len(series.commits) - 1 - vals.seq = seq - yield vals - - cur = self._finish_commit(repo, None, commit, cur, vals.msg) - lines.append([vals.info.strip(), - f'{oid(cmt.hash)} as {oid(cur.target)} {cmt}']) - - max_len = max(len(info) for info, rest in lines) + 1 - for info, rest in lines: - if info: - info += ':' - tout.info(f'- {info.ljust(max_len)} {rest}') - target = self._finish_process(repo, branch, name, cur, old_head, - new_name, switch, dry_run) - vals.oid = target.oid - - def _mark_series(self, name, series, dry_run=False): - """Mark a series with Change-Id tags - - Args: - name (str): Name of the series to mark - series (Series): Series object - dry_run (bool): True to do a dry run, restoring the original tree - afterwards - - Return: - pygit.oid: oid of the new branch - """ - vals = None - for vals in self.process_series(name, series, dry_run=dry_run): - if CHANGE_ID_TAG not in vals.msg: - change_id = self.make_change_id(vals.commit) - vals.msg = vals.msg + f'\n{CHANGE_ID_TAG}: {change_id}' - tout.detail(" - adding mark") - vals.info = 'marked' - else: - vals.info = 'has mark' - - return vals.oid - - def update_series(self, branch_name, series, max_vers, new_name=None, - dry_run=False, add_vers=None, add_link=None, - add_rtags=None, switch=False): - """Rewrite a series to update the Series-version/Series-links lines - - This updates the series in git; it does not update the database - - Args: - branch_name (str): Name of the branch to process - series (Series): Series object - max_vers (int): Version number of the series being updated - new_name (str or None): New name, if a new branch is to be created - dry_run (bool): True to do a dry run, restoring the original tree - afterwards - add_vers (int or None): Version number to add to the series, if any - add_link (str or None): Link to add to the series, if any - add_rtags (list of dict): List of review tags to add, one item for - each commit, each a dict: - key: Response tag (e.g. 'Reviewed-by') - value: Set of people who gave that response, each a name/email - string - switch (bool): True to switch to the new branch after processing; - otherwise HEAD remains at the original branch, as amended - - Return: - pygit.oid: oid of the new branch - """ - def _do_version(): - if add_vers: - if add_vers == 1: - vals.info += f'rm v{add_vers} ' - else: - vals.info += f'add v{add_vers} ' - out.append(f'Series-version: {add_vers}') - - def _do_links(new_links): - if add_link: - if 'add' not in vals.info: - vals.info += 'add ' - vals.info += f"links '{new_links}' " - else: - vals.info += f"upd links '{new_links}' " - out.append(f'Series-links: {new_links}') - - added_version = False - added_link = False - for vals in self.process_series(branch_name, series, new_name, switch, - dry_run): - out = [] - for line in vals.msg.splitlines(): - m_ver = re.match('Series-version:(.*)', line) - m_links = re.match('Series-links:(.*)', line) - if m_ver and add_vers: - if ('version' in series and - int(series.version) != max_vers): - tout.warning( - f'Branch {branch_name}: Series-version tag ' - f'{series.version} does not match expected ' - f'version {max_vers}') - _do_version() - added_version = True - elif m_links: - links = series.get_links(m_links.group(1), max_vers) - if add_link: - links[max_vers] = add_link - _do_links(series.build_links(links)) - added_link = True - else: - out.append(line) - if vals.final: - if not added_version and add_vers and add_vers > 1: - _do_version() - if not added_link and add_link: - _do_links(f'{max_vers}:{add_link}') - - vals.msg = '\n'.join(out) + '\n' - if add_rtags and add_rtags[vals.seq]: - lines = [] - for tag, people in add_rtags[vals.seq].items(): - for who in people: - lines.append(f'{tag}: {who}') - vals.msg = patchstream.insert_tags(vals.msg.rstrip(), - sorted(lines)) - vals.info += (f'added {len(lines)} ' - f"tag{'' if len(lines) == 1 else 's'}") - - def _build_col(self, state, prefix='', base_str=None): - """Build a patch-state string with colour - - Args: - state (str): State to colourise (also indicates the colour to use) - prefix (str): Prefix string to also colourise - base_str (str or None): String to show instead of state, or None to - show state - - Return: - str: String with ANSI colour characters - """ - bright = True - if state == 'accepted': - col = self.col.GREEN - elif state == 'awaiting-upstream': - bright = False - col = self.col.GREEN - elif state in ['changes-requested']: - col = self.col.CYAN - elif state in ['rejected', 'deferred', 'not-applicable', 'superseded', - 'handled-elsewhere']: - col = self.col.RED - elif not state: - state = 'unknown' - col = self.col.MAGENTA - else: - # under-review, rfc, needs-review-ack - col = self.col.WHITE - out = base_str or SHORTEN_STATE.get(state, state) - pad = ' ' * (10 - len(out)) - col_state = self.col.build(col, prefix + out, bright) - return col_state, pad - - def _get_patches(self, series, version): - """Get a Series object containing the patches in a series - - Args: - series (str): Name of series to use, or None to use current branch - version (int): Version number, or None to detect from name - - Return: tuple: - str: Name of branch, e.g. 'mary2' - Series: Series object containing the commits and idnum, desc, name - int: Version number of series, e.g. 2 - OrderedDict: - key (int): record ID if find_svid is None, else seq - value (PCOMMIT): record data - str: series name (for this version) - str: patchwork link - str: cover_id - int: cover_num_comments - """ - ser, version = self._parse_series_and_version(series, version) - if not ser.idnum: - raise ValueError(f"Unknown series '{series}'") - self._ensure_version(ser, version) - svinfo = self.get_ser_ver(ser.idnum, version) - pwc = self.get_pcommit_dict(svinfo.idnum) - - count = len(pwc) - branch = self._join_name_version(ser.name, version) - series = patchstream.get_metadata(branch, 0, count, - git_dir=self.gitdir) - self._copy_db_fields_to(series, ser) - - return (branch, series, version, pwc, svinfo.name, svinfo.link, - svinfo.cover_id, svinfo.cover_num_comments) - - def _list_patches(self, branch, pwc, series, desc, cover_id, num_comments, - show_commit, show_patch, list_patches, state_totals): - """List patches along with optional status info - - Args: - branch (str): Branch name if self.show_progress - pwc (dict): pcommit records: - key (int): seq - value (PCOMMIT): Record from database - series (Series): Series to show, or None to just use the database - desc (str): Series title - cover_id (int): Cover-letter ID - num_comments (int): The number of comments on the cover letter - show_commit (bool): True to show the commit and diffstate - show_patch (bool): True to show the patch - list_patches (bool): True to list all patches for each series, - False to just show the series summary on a single line - state_totals (dict): Holds totals for each state across all patches - key (str): state name - value (int): Number of patches in that state - - Return: - bool: True if OK, False if any commit subjects don't match their - patchwork subjects - """ - lines = [] - states = defaultdict(int) - count = len(pwc) - ok = True - for seq, item in enumerate(pwc.values()): - if series: - cmt = series.commits[seq] - if cmt.subject != item.subject: - ok = False - - col_state, pad = self._build_col(item.state) - patch_id = item.patch_id if item.patch_id else '' - if item.num_comments: - comments = str(item.num_comments) - elif item.num_comments is None: - comments = '-' - else: - comments = '' - - if show_commit or show_patch: - subject = self.col.build(self.col.BLACK, item.subject, - bright=False, back=self.col.YELLOW) - else: - subject = item.subject - - line = (f'{seq:3} {col_state}{pad} {comments.rjust(3)} ' - f'{patch_id:7} {oid(cmt.hash)} {subject}') - lines.append(line) - states[item.state] += 1 - out = '' - for state, freq in states.items(): - out += ' ' + self._build_col(state, f'{freq}:')[0] - state_totals[state] += freq - name = '' - if not list_patches: - name = desc or series.desc - name = self.col.build(self.col.YELLOW, name[:41].ljust(41)) - if not ok: - out = '*' + out[1:] - print(f"{branch:16} {name} {len(pwc):5} {out}") - return ok - print(f"Branch '{branch}' (total {len(pwc)}):{out}{name}") - - print(self.col.build( - self.col.MAGENTA, - f"Seq State Com PatchId {'Commit'.ljust(HASH_LEN)} Subject")) - - comments = '' if num_comments is None else str(num_comments) - if desc or comments or cover_id: - cov = 'Cov' if cover_id else '' - print(self.col.build( - self.col.WHITE, - f"{cov:14} {comments.rjust(3)} {cover_id or '':7} " - f'{desc or series.desc}', - bright=False)) - for seq in range(count): - line = lines[seq] - print(line) - if show_commit or show_patch: - print() - cmt = series.commits[seq] if series else '' - msg = gitutil.show_commit( - cmt.hash, show_commit, True, show_patch, - colour=self.col.enabled(), git_dir=self.gitdir) - sys.stdout.write(msg) - if seq != count - 1: - print() - print() - - return ok - - def _find_matched_commit(self, commits, pcm): - """Find a commit in a list of possible matches - - Args: - commits (dict of Commit): Possible matches - key (int): sequence number of patch (from 0) - value (Commit): Commit object - pcm (PCOMMIT): Patch to check - - Return: - int: Sequence number of matching commit, or None if not found - """ - for seq, cmt in commits.items(): - tout.debug(f"- match subject: '{cmt.subject}'") - if pcm.subject == cmt.subject: - return seq - return None - - def _find_matched_patch(self, patches, cmt): - """Find a patch in a list of possible matches - - Args: - patches: dict of ossible matches - key (int): sequence number of patch - value (PCOMMIT): patch - cmt (Commit): Commit to check - - Return: - int: Sequence number of matching patch, or None if not found - """ - for seq, pcm in patches.items(): - tout.debug(f"- match subject: '{pcm.subject}'") - if cmt.subject == pcm.subject: - return seq - return None - - def _sync_one(self, svid, series_name, version, show_comments, - show_cover_comments, gather_tags, cover, patches, dry_run): - """Sync one series to the database - - Args: - svid (int): Ser/ver ID - cover (dict or None): Cover letter from patchwork, with keys: - id (int): Cover-letter ID in patchwork - num_comments (int): Number of comments - name (str): Cover-letter name - patches (list of Patch): Patches in the series - """ - pwc = self.get_pcommit_dict(svid) - if gather_tags: - count = len(pwc) - branch = self._join_name_version(series_name, version) - series = patchstream.get_metadata(branch, 0, count, - git_dir=self.gitdir) - - _, new_rtag_list = status.do_show_status( - series, cover, patches, show_comments, show_cover_comments, - self.col, warnings_on_stderr=False) - self.update_series(branch, series, version, None, dry_run, - add_rtags=new_rtag_list) - - updated = 0 - for seq, item in enumerate(pwc.values()): - if seq >= len(patches): - continue - patch = patches[seq] - if patch.id: - if self.db.pcommit_update( - Pcommit(item.idnum, seq, None, None, None, patch.state, - patch.id, len(patch.comments))): - updated += 1 - if cover: - info = SerVer(svid, None, None, None, cover.id, - cover.num_comments, cover.name, None) - else: - info = SerVer(svid, None, None, None, None, None, patches[0].name, - None) - self.db.ser_ver_set_info(info) - - return updated, 1 if cover else 0 - - async def _gather(self, pwork, link, show_cover_comments): - """Sync the series status from patchwork - - Creates a new client sesion and calls _sync() - - Args: - pwork (Patchwork): Patchwork object to use - link (str): Patchwork link for the series - show_cover_comments (bool): True to show the comments on the cover - letter - - Return: tuple: - COVER object, or None if none or not read_cover_comments - list of PATCH objects - """ - async with aiohttp.ClientSession() as client: - return await pwork.series_get_state(client, link, True, - show_cover_comments) - - def _get_fetch_dict(self, sync_all_versions): - """Get a dict of ser_vers to fetch, along with their patchwork links - - Args: - sync_all_versions (bool): True to sync all versions of a series, - False to sync only the latest version - - Return: tuple: - dict: things to fetch - key (int): svid - value (str): patchwork link for the series - int: number of series which are missing a link - """ - missing = 0 - svdict = self.get_ser_ver_dict() - sdict = self.db.series_get_dict_by_id() - to_fetch = {} - - if sync_all_versions: - for svinfo in self.get_ser_ver_list(): - ser_ver = svdict[svinfo.idnum] - if svinfo.link: - to_fetch[svinfo.idnum] = patchwork.STATE_REQ( - svinfo.link, svinfo.series_id, - sdict[svinfo.series_id].name, svinfo.version, False, - False) - else: - missing += 1 - else: - # Find the maximum version for each series - max_vers = self._series_all_max_versions() - - # Get a list of links to fetch - for svid, series_id, version in max_vers: - ser_ver = svdict[svid] - if series_id not in sdict: - # skip archived item - continue - if ser_ver.link: - to_fetch[svid] = patchwork.STATE_REQ( - ser_ver.link, series_id, sdict[series_id].name, - version, False, False) - else: - missing += 1 - - # order by series name, version - ordered = OrderedDict() - for svid in sorted( - to_fetch, - key=lambda k: (to_fetch[k].series_name, to_fetch[k].version)): - sync = to_fetch[svid] - ordered[svid] = sync - - return ordered, missing - - async def _sync_all(self, client, pwork, to_fetch): - """Sync all series status from patchwork - - Args: - pwork (Patchwork): Patchwork object to use - sync_all_versions (bool): True to sync all versions of a series, - False to sync only the latest version - gather_tags (bool): True to gather review/test tags - - Return: list of tuple: - COVER object, or None if none or not read_cover_comments - list of PATCH objects - """ - with pwork.collect_stats() as stats: - tasks = [pwork.series_get_state(client, sync.link, True, True) - for sync in to_fetch.values() if sync.link] - result = await asyncio.gather(*tasks) - return result, stats.request_count - - async def _do_series_sync_all(self, pwork, to_fetch): - async with aiohttp.ClientSession() as client: - return await self._sync_all(client, pwork, to_fetch) - - def _progress_one(self, ser, show_all_versions, list_patches, - state_totals): - """Show progress information for all versions in a series - - Args: - ser (Series): Series to use - show_all_versions (bool): True to show all versions of a series, - False to show only the final version - list_patches (bool): True to list all patches for each series, - False to just show the series summary on a single line - state_totals (dict): Holds totals for each state across all patches - key (str): state name - value (int): Number of patches in that state - - Return: tuple - int: Number of series shown - int: Number of patches shown - int: Number of version which need a 'scan' - """ - max_vers = self._series_max_version(ser.idnum) - name, desc = self._get_series_info(ser.idnum) - coloured = self.col.build(self.col.BLACK, desc, bright=False, - back=self.col.YELLOW) - versions = self._get_version_list(ser.idnum) - vstr = list(map(str, versions)) - - if list_patches: - print(f"{name}: {coloured} (versions: {' '.join(vstr)})") - add_blank_line = False - total_series = 0 - total_patches = 0 - need_scan = 0 - for ver in versions: - if not show_all_versions and ver != max_vers: - continue - if add_blank_line: - print() - _, pwc = self._series_get_version_stats(ser.idnum, ver) - count = len(pwc) - branch = self._join_name_version(ser.name, ver) - series = patchstream.get_metadata(branch, 0, count, - git_dir=self.gitdir) - svinfo = self.get_ser_ver(ser.idnum, ver) - self._copy_db_fields_to(series, ser) - - ok = self._list_patches( - branch, pwc, series, svinfo.name, svinfo.cover_id, - svinfo.cover_num_comments, False, False, list_patches, - state_totals) - if not ok: - need_scan += 1 - add_blank_line = list_patches - total_series += 1 - total_patches += count - return total_series, total_patches, need_scan - - def _summary_one(self, ser): - """Show summary information for the latest version in a series - - Args: - series (str): Name of series to use, or None to show progress for - all series - """ - max_vers = self._series_max_version(ser.idnum) - name, desc = self._get_series_info(ser.idnum) - stats, pwc = self._series_get_version_stats(ser.idnum, max_vers) - states = {x.state for x in pwc.values()} - state = 'accepted' - for val in ['awaiting-upstream', 'changes-requested', 'rejected', - 'deferred', 'not-applicable', 'superseded', - 'handled-elsewhere']: - if val in states: - state = val - state_str, pad = self._build_col(state, base_str=name) - print(f"{state_str}{pad} {stats.rjust(6)} {desc}") - - def _series_max_version(self, idnum): - """Find the latest version of a series - - Args: - idnum (int): Series ID to look up - - Return: - int: maximum version - """ - return self.db.series_get_max_version(idnum) - - def _series_all_max_versions(self): - """Find the latest version of all series - - Return: list of: - int: ser_ver ID - int: series ID - int: Maximum version - """ - return self.db.series_get_all_max_versions() |
