mirror of
https://source.denx.de/u-boot/u-boot.git
synced 2025-12-19 16:31:27 +01:00
Add a module which includes helper functions for dealing with Cseries objects. Signed-off-by: Simon Glass <sjg@chromium.org>
1525 lines
55 KiB
Python
1525 lines
55 KiB
Python
# 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.init_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.init_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()
|