summaryrefslogtreecommitdiff
path: root/tools/patman/cseries.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/patman/cseries.py')
-rw-r--r--tools/patman/cseries.py1165
1 files changed, 0 insertions, 1165 deletions
diff --git a/tools/patman/cseries.py b/tools/patman/cseries.py
deleted file mode 100644
index 0844b5f0257..00000000000
--- a/tools/patman/cseries.py
+++ /dev/null
@@ -1,1165 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0+
-#
-# Copyright 2025 Google LLC
-#
-"""Handles the 'series' subcommand
-"""
-
-import asyncio
-from collections import OrderedDict, defaultdict
-
-import pygit2
-
-from u_boot_pylib import cros_subprocess
-from u_boot_pylib import gitutil
-from u_boot_pylib import terminal
-from u_boot_pylib import tout
-
-from patman import patchstream
-from patman import cser_helper
-from patman.cser_helper import AUTOLINK, oid
-from patman import send
-from patman import status
-
-
-class Cseries(cser_helper.CseriesHelper):
- """Database with information about series
-
- 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 Cseries
-
- Args:
- topdir (str): Top-level directory of the repo
- colour (terminal.enum): Whether to enable ANSI colour or not
- """
- super().__init__(topdir, colour)
-
- def add(self, branch_name, desc=None, mark=False, allow_unmarked=False,
- end=None, force_version=False, dry_run=False):
- """Add a series (or new version of a series) to the database
-
- Args:
- branch_name (str): Name of branch to sync, or None for current one
- desc (str): Description to use, or None to use the series subject
- mark (str): True to mark each commit with a change ID
- allow_unmarked (str): True to not require each commit to be marked
- end (str): Add only commits up to but exclu
- 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
- """
- name, ser, version, msg = self.prep_series(branch_name, end)
- tout.info(f"Adding series '{ser.name}' v{version}: mark {mark} "
- f'allow_unmarked {allow_unmarked}')
- if msg:
- tout.info(msg)
- if desc is None:
- if not ser.cover:
- raise ValueError(f"Branch '{name}' has no cover letter - "
- 'please provide description')
- desc = ser['cover'][0]
-
- ser = self._handle_mark(name, ser, version, mark, allow_unmarked,
- force_version, dry_run)
- link = ser.get_link_for_version(version)
-
- msg = 'Added'
- added = False
- series_id = self.db.series_find_by_name(ser.name)
- if not series_id:
- series_id = self.db.series_add(ser.name, desc)
- added = True
- msg += f" series '{ser.name}'"
-
- if version not in self._get_version_list(series_id):
- svid = self.db.ser_ver_add(series_id, version, link)
- msg += f" v{version}"
- if not added:
- msg += f" to existing series '{ser.name}'"
- added = True
-
- self._add_series_commits(ser, svid)
- count = len(ser.commits)
- msg += f" ({count} commit{'s' if count > 1 else ''})"
- if not added:
- tout.info(f"Series '{ser.name}' v{version} already exists")
- msg = None
- elif not dry_run:
- self.commit()
- else:
- self.rollback()
- series_id = None
- ser.desc = desc
- ser.idnum = series_id
-
- if msg:
- tout.info(msg)
- if dry_run:
- tout.info('Dry run completed')
-
- def decrement(self, series, dry_run=False):
- """Decrement a series to the previous version and delete the branch
-
- Args:
- series (str): Name of series to use, or None to use current branch
- dry_run (bool): True to do a dry run
- """
- ser = self._parse_series(series)
- if not ser.idnum:
- raise ValueError(f"Series '{ser.name}' not found in database")
-
- max_vers = self._series_max_version(ser.idnum)
- if max_vers < 2:
- raise ValueError(f"Series '{ser.name}' only has one version")
-
- tout.info(f"Removing series '{ser.name}' v{max_vers}")
-
- new_max = max_vers - 1
-
- repo = pygit2.Repository(self.gitdir)
- if not dry_run:
- name = self._get_branch_name(ser.name, new_max)
- branch = repo.lookup_branch(name)
- try:
- repo.checkout(branch)
- except pygit2.errors.GitError:
- tout.warning(f"Failed to checkout branch {name}")
- raise
-
- del_name = f'{ser.name}{max_vers}'
- del_branch = repo.lookup_branch(del_name)
- branch_oid = del_branch.peel(pygit2.enums.ObjectType.COMMIT).oid
- del_branch.delete()
- print(f"Deleted branch '{del_name}' {oid(branch_oid)}")
-
- self.db.ser_ver_remove(ser.idnum, max_vers)
- if not dry_run:
- self.commit()
- else:
- self.rollback()
-
- def increment(self, series_name, dry_run=False):
- """Increment a series to the next version and create a new branch
-
- Args:
- series_name (str): Name of series to use, or None to use current
- branch
- dry_run (bool): True to do a dry run
- """
- ser = self._parse_series(series_name)
- if not ser.idnum:
- raise ValueError(f"Series '{ser.name}' not found in database")
-
- max_vers = self._series_max_version(ser.idnum)
-
- branch_name = self._get_branch_name(ser.name, max_vers)
- on_branch = gitutil.get_branch(self.gitdir) == branch_name
- svid = self.get_series_svid(ser.idnum, max_vers)
- pwc = self.get_pcommit_dict(svid)
- count = len(pwc.values())
- series = patchstream.get_metadata(branch_name, 0, count,
- git_dir=self.gitdir)
- tout.info(f"Increment '{ser.name}' v{max_vers}: {count} patches")
-
- # Create a new branch
- vers = max_vers + 1
- new_name = self._join_name_version(ser.name, vers)
-
- self.update_series(branch_name, series, max_vers, new_name, dry_run,
- add_vers=vers, switch=on_branch)
-
- old_svid = self.get_series_svid(ser.idnum, max_vers)
- pcd = self.get_pcommit_dict(old_svid)
-
- svid = self.db.ser_ver_add(ser.idnum, vers)
- self.db.pcommit_add_list(svid, pcd.values())
- if not dry_run:
- self.commit()
- else:
- self.rollback()
-
- # repo.head.set_target(amended)
- tout.info(f'Added new branch {new_name}')
- if dry_run:
- tout.info('Dry run completed')
-
- def link_set(self, series_name, version, link, update_commit):
- """Add / update a series-links link for a series
-
- Args:
- series_name (str): Name of series to use, or None to use current
- branch
- version (int): Version number, or None to detect from name
- link (str): Patchwork link-string for the series
- update_commit (bool): True to update the current commit with the
- link
- """
- ser, version = self._parse_series_and_version(series_name, version)
- self._ensure_version(ser, version)
-
- self._set_link(ser.idnum, ser.name, version, link, update_commit)
- self.commit()
- tout.info(f"Setting link for series '{ser.name}' v{version} to {link}")
-
- def link_get(self, series, version):
- """Get the patchwork link for a version of a series
-
- Args:
- series (str): Name of series to use, or None to use current branch
- version (int): Version number or None for current
-
- Return:
- str: Patchwork link as a string, e.g. '12325'
- """
- ser, version = self._parse_series_and_version(series, version)
- self._ensure_version(ser, version)
- return self.db.ser_ver_get_link(ser.idnum, version)
-
- def link_search(self, pwork, series, version):
- """Search patch for the link for a series
-
- Returns either the single match, or None, in which case the second part
- of the tuple is filled in
-
- Args:
- pwork (Patchwork): Patchwork object to use
- series (str): Series name to search for, or None for current series
- that is checked out
- version (int): Version to search for, or None for current version
- detected from branch name
-
- Returns:
- tuple:
- int: ID of the series found, or None
- list of possible matches, or None, each a dict:
- 'id': series ID
- 'name': series name
- str: series name
- int: series version
- str: series description
- """
- _, ser, version, _, _, _, _, _ = self._get_patches(series, version)
-
- if not ser.desc:
- raise ValueError(f"Series '{ser.name}' has an empty description")
-
- pws, options = self.loop.run_until_complete(pwork.find_series(
- ser, version))
- return pws, options, ser.name, version, ser.desc
-
- def link_auto(self, pwork, series, version, update_commit, wait_s=0):
- """Automatically find a series link by looking in patchwork
-
- Args:
- pwork (Patchwork): Patchwork object to use
- series (str): Series name to search for, or None for current series
- that is checked out
- version (int): Version to search for, or None for current version
- detected from branch name
- update_commit (bool): True to update the current commit with the
- link
- wait_s (int): Number of seconds to wait for the autolink to succeed
- """
- start = self.get_time()
- stop = start + wait_s
- sleep_time = 5
- while True:
- pws, options, name, version, desc = self.link_search(
- pwork, series, version)
- if pws:
- if wait_s:
- tout.info('Link completed after '
- f'{self.get_time() - start} seconds')
- break
-
- print(f"Possible matches for '{name}' v{version} desc '{desc}':")
- print(' Link Version Description')
- for opt in options:
- print(f"{opt['id']:6} {opt['version']:7} {opt['name']}")
- if not wait_s or self.get_time() > stop:
- delay = f' after {wait_s} seconds' if wait_s else ''
- raise ValueError(f"Cannot find series '{desc}{delay}'")
-
- self.sleep(sleep_time)
-
- self.link_set(name, version, pws, update_commit)
-
- def link_auto_all(self, pwork, update_commit, link_all_versions,
- replace_existing, dry_run, show_summary=True):
- """Automatically find a series link by looking in patchwork
-
- Args:
- pwork (Patchwork): Patchwork object to use
- update_commit (bool): True to update the current commit with the
- link
- link_all_versions (bool): True to sync all versions of a series,
- False to sync only the latest version
- replace_existing (bool): True to sync a series even if it already
- has a link
- dry_run (bool): True to do a dry run
- show_summary (bool): True to show a summary of how things went
-
- Return:
- OrderedDict of summary info:
- key (int): ser_ver ID
- value (AUTOLINK): result of autolinking on this ser_ver
- """
- sdict = self.db.series_get_dict_by_id()
- all_ser_vers = self._get_autolink_dict(sdict, link_all_versions)
-
- # Get rid of things without a description
- valid = {}
- state = {}
- no_desc = 0
- not_found = 0
- updated = 0
- failed = 0
- already = 0
- for svid, (ser_id, name, version, link, desc) in all_ser_vers.items():
- if link and not replace_existing:
- state[svid] = f'already:{link}'
- already += 1
- elif desc:
- valid[svid] = ser_id, version, link, desc
- else:
- no_desc += 1
- state[svid] = 'missing description'
-
- results, requests = self.loop.run_until_complete(
- pwork.find_series_list(valid))
-
- for svid, ser_id, link, _ in results:
- if link:
- version = all_ser_vers[svid][2]
- if self._set_link(ser_id, sdict[ser_id].name, version,
- link, update_commit, dry_run=dry_run):
- updated += 1
- state[svid] = f'linked:{link}'
- else:
- failed += 1
- state[svid] = 'failed'
- else:
- not_found += 1
- state[svid] = 'not found'
-
- # Create a summary sorted by name and version
- summary = OrderedDict()
- for svid in sorted(all_ser_vers, key=lambda k: all_ser_vers[k][1:2]):
- _, name, version, link, ser = all_ser_vers[svid]
- summary[svid] = AUTOLINK(name, version, link, ser.desc,
- state[svid])
-
- if show_summary:
- msg = f'{updated} series linked'
- if already:
- msg += f', {already} already linked'
- if not_found:
- msg += f', {not_found} not found'
- if no_desc:
- msg += f', {no_desc} missing description'
- if failed:
- msg += f', {failed} updated failed'
- tout.info(msg + f' ({requests} requests)')
-
- tout.info('')
- tout.info(f"{'Name':15} Version {'Description':40} Result")
- border = f"{'-' * 15} ------- {'-' * 40} {'-' * 15}"
- tout.info(border)
- for name, version, link, desc, state in summary.values():
- bright = True
- if state.startswith('already'):
- col = self.col.GREEN
- bright = False
- elif state.startswith('linked'):
- col = self.col.MAGENTA
- else:
- col = self.col.RED
- col_state = self.col.build(col, state, bright)
- tout.info(f"{name:16.16} {version:7} {desc or '':40.40} "
- f'{col_state}')
- tout.info(border)
- if dry_run:
- tout.info('Dry run completed')
-
- return summary
-
- def series_list(self):
- """List all series
-
- Lines all series along with their description, number of patches
- accepted and the available versions
- """
- sdict = self.db.series_get_dict()
- print(f"{'Name':15} {'Description':40} Accepted Versions")
- border = f"{'-' * 15} {'-' * 40} -------- {'-' * 15}"
- print(border)
- for name in sorted(sdict):
- ser = sdict[name]
- versions = self._get_version_list(ser.idnum)
- stat = self._series_get_version_stats(
- ser.idnum, self._series_max_version(ser.idnum))[0]
-
- vlist = ' '.join([str(ver) for ver in sorted(versions)])
-
- print(f'{name:16.16} {ser.desc:41.41} {stat.rjust(8)} {vlist}')
- print(border)
-
- def list_patches(self, series, version, show_commit=False,
- show_patch=False):
- """List 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
- show_commit (bool): True to show the commit and diffstate
- show_patch (bool): True to show the patch
- """
- branch, series, version, pwc, name, _, cover_id, num_comments = (
- self._get_patches(series, version))
- with terminal.pager():
- state_totals = defaultdict(int)
- self._list_patches(branch, pwc, series, name, cover_id,
- num_comments, show_commit, show_patch, True,
- state_totals)
-
- def mark(self, in_name, allow_marked=False, dry_run=False):
- """Add Change-Id tags to a series
-
- Args:
- in_name (str): Name of the series to unmark
- allow_marked (bool): Allow commits to be (already) marked
- dry_run (bool): True to do a dry run, restoring the original tree
- afterwards
-
- Return:
- pygit.oid: oid of the new branch
- """
- name, ser, _, _ = self.prep_series(in_name)
- tout.info(f"Marking series '{name}': allow_marked {allow_marked}")
-
- if not allow_marked:
- bad = []
- for cmt in ser.commits:
- if cmt.change_id:
- bad.append(cmt)
- if bad:
- print(f'{len(bad)} commit(s) already have marks')
- for cmt in bad:
- print(f' - {oid(cmt.hash)} {cmt.subject}')
- raise ValueError(
- f'Marked commits {len(bad)}/{len(ser.commits)}')
- new_oid = self._mark_series(in_name, ser, dry_run=dry_run)
-
- if dry_run:
- tout.info('Dry run completed')
- return new_oid
-
- def unmark(self, name, allow_unmarked=False, dry_run=False):
- """Remove Change-Id tags from a series
-
- Args:
- name (str): Name of the series to unmark
- allow_unmarked (bool): Allow commits to be (already) unmarked
- dry_run (bool): True to do a dry run, restoring the original tree
- afterwards
-
- Return:
- pygit.oid: oid of the new branch
- """
- name, ser, _, _ = self.prep_series(name)
- tout.info(
- f"Unmarking series '{name}': allow_unmarked {allow_unmarked}")
-
- if not allow_unmarked:
- bad = []
- for cmt in ser.commits:
- if not cmt.change_id:
- bad.append(cmt)
- if bad:
- print(f'{len(bad)} commit(s) are missing marks')
- for cmt in bad:
- print(f' - {oid(cmt.hash)} {cmt.subject}')
- raise ValueError(
- f'Unmarked commits {len(bad)}/{len(ser.commits)}')
- vals = None
- for vals in self.process_series(name, ser, dry_run=dry_run):
- if cser_helper.CHANGE_ID_TAG in vals.msg:
- lines = vals.msg.splitlines()
- updated = [line for line in lines
- if not line.startswith(cser_helper.CHANGE_ID_TAG)]
- vals.msg = '\n'.join(updated)
-
- tout.detail(" - removing mark")
- vals.info = 'unmarked'
- else:
- vals.info = 'no mark'
-
- if dry_run:
- tout.info('Dry run completed')
- return vals.oid
-
- def open(self, pwork, name, version):
- """Open the patchwork page for a series
-
- Args:
- pwork (Patchwork): Patchwork object to use
- name (str): Name of series to open
- version (str): Version number to open
- """
- ser, version = self._parse_series_and_version(name, version)
- link = self.link_get(ser.name, version)
- pwork.url = 'https://patchwork.ozlabs.org'
- url = self.loop.run_until_complete(pwork.get_series_url(link))
- print(f'Opening {url}')
-
- # With Firefox, GTK produces lots of warnings, so suppress them
- # Gtk-Message: 06:48:20.692: Failed to load module "xapp-gtk3-module"
- # Gtk-Message: 06:48:20.692: Not loading module "atk-bridge": The
- # functionality is provided by GTK natively. Please try to not load it.
- # Gtk-Message: 06:48:20.692: Failed to load module "appmenu-gtk-module"
- # Gtk-Message: 06:48:20.692: Failed to load module "appmenu-gtk-module"
- # [262145, Main Thread] WARNING: GTK+ module /snap/firefox/5987/
- # gnome-platform/usr/lib/gtk-2.0/modules/libcanberra-gtk-module.so
- # cannot be loaded.
- # GTK+ 2.x symbols detected. Using GTK+ 2.x and GTK+ 3 in the same
- # process # is not supported.: 'glib warning', file /build/firefox/
- # parts/firefox/build/toolkit/xre/nsSigHandlers.cpp:201
- #
- # (firefox_firefox:262145): Gtk-WARNING **: 06:48:20.728: GTK+ module
- # /snap/firefox/5987/gnome-platform/usr/lib/gtk-2.0/modules/
- # libcanberra-gtk-module.so cannot be loaded.
- # GTK+ 2.x symbols detected. Using GTK+ 2.x and GTK+ 3 in the same
- # process is not supported.
- # Gtk-Message: 06:48:20.728: Failed to load module
- # "canberra-gtk-module"
- # [262145, Main Thread] WARNING: GTK+ module /snap/firefox/5987/
- # gnome-platform/usr/lib/gtk-2.0/modules/libcanberra-gtk-module.so
- # cannot be loaded.
- # GTK+ 2.x symbols detected. Using GTK+ 2.x and GTK+ 3 in the same
- # process is not supported.: 'glib warning', file /build/firefox/
- # parts/firefox/build/toolkit/xre/nsSigHandlers.cpp:201
- #
- # (firefox_firefox:262145): Gtk-WARNING **: 06:48:20.729: GTK+ module
- # /snap/firefox/5987/gnome-platform/usr/lib/gtk-2.0/modules/
- # libcanberra-gtk-module.so cannot be loaded.
- # GTK+ 2.x symbols detected. Using GTK+ 2.x and GTK+ 3 in the same
- # process is not supported.
- # Gtk-Message: 06:48:20.729: Failed to load module
- # "canberra-gtk-module"
- # ATTENTION: default value of option mesa_glthread overridden by
- # environment.
- cros_subprocess.Popen(['xdg-open', url])
-
- def progress(self, series, show_all_versions, list_patches):
- """Show progress information for all versions in a series
-
- Args:
- series (str): Name of series to use, or None to show progress for
- all series
- 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
- """
- with terminal.pager():
- state_totals = defaultdict(int)
- if series is not None:
- _, _, need_scan = self._progress_one(
- self._parse_series(series), show_all_versions,
- list_patches, state_totals)
- if need_scan:
- tout.warning(
- 'Inconsistent commit-subject: Please use '
- "'patman series -s <branch> scan' to resolve this")
- return
-
- total_patches = 0
- total_series = 0
- sdict = self.db.series_get_dict()
- border = None
- total_need_scan = 0
- if not list_patches:
- print(self.col.build(
- self.col.MAGENTA,
- f"{'Name':16} {'Description':41} Count {'Status'}"))
- border = f"{'-' * 15} {'-' * 40} ----- {'-' * 15}"
- print(border)
- for name in sorted(sdict):
- ser = sdict[name]
- num_series, num_patches, need_scan = self._progress_one(
- ser, show_all_versions, list_patches, state_totals)
- total_need_scan += need_scan
- if list_patches:
- print()
- total_series += num_series
- total_patches += num_patches
- if not list_patches:
- print(border)
- total = f'{total_series} series'
- out = ''
- for state, freq in state_totals.items():
- out += ' ' + self._build_col(state, f'{freq}:')[0]
- if total_need_scan:
- out = '*' + out[1:]
-
- print(f"{total:15} {'':40} {total_patches:5} {out}")
- if total_need_scan:
- tout.info(
- f'Series marked * ({total_need_scan}) have commit '
- 'subjects which mismatch their patches and need to be '
- 'scanned')
-
- def project_set(self, pwork, name, quiet=False):
- """Set the name of the project
-
- Args:
- pwork (Patchwork): Patchwork object to use
- name (str): Name of the project to use in patchwork
- quiet (bool): True to skip writing the message
- """
- res = self.loop.run_until_complete(pwork.get_projects())
- proj_id = None
- link_name = None
- for proj in res:
- if proj['name'] == name:
- proj_id = proj['id']
- link_name = proj['link_name']
- if not proj_id:
- raise ValueError(f"Unknown project name '{name}'")
- self.db.settings_update(name, proj_id, link_name)
- self.commit()
- if not quiet:
- tout.info(f"Project '{name}' patchwork-ID {proj_id} "
- f'link-name {link_name}')
-
- def project_get(self):
- """Get the details of the project
-
- Returns:
- tuple or None if there are no settings:
- name (str): Project name, e.g. 'U-Boot'
- proj_id (int): Patchworks project ID for this project
- link_name (str): Patchwork's link-name for the project
- """
- return self.db.settings_get()
-
- def remove(self, name, dry_run=False):
- """Remove a series from the database
-
- Args:
- name (str): Name of series to remove, or None to use current one
- dry_run (bool): True to do a dry run
- """
- ser = self._parse_series(name)
- name = ser.name
- if not ser.idnum:
- raise ValueError(f"No such series '{name}'")
-
- self.db.ser_ver_remove(ser.idnum, None)
- if not dry_run:
- self.commit()
- else:
- self.rollback()
-
- self.commit()
- tout.info(f"Removed series '{name}'")
- if dry_run:
- tout.info('Dry run completed')
-
- def rename(self, series, name, dry_run=False):
- """Rename a series
-
- Renames a series and changes the name of any branches which match
- versions present in the database
-
- Args:
- series (str): Name of series to use, or None to use current branch
- name (str): new name to use (must not include version number)
- dry_run (bool): True to do a dry run
- """
- old_ser, _ = self._parse_series_and_version(series, None)
- if not old_ser.idnum:
- raise ValueError(f"Series '{old_ser.name}' not found in database")
- if old_ser.name != series:
- raise ValueError(f"Invalid series name '{series}': "
- 'did you use the branch name?')
- chk, _ = cser_helper.split_name_version(name)
- if chk != name:
- raise ValueError(
- f"Invalid series name '{name}': did you use the branch name?")
- if chk == old_ser.name:
- raise ValueError(
- f"Cannot rename series '{old_ser.name}' to itself")
- if self.get_series_by_name(name):
- raise ValueError(f"Cannot rename: series '{name}' already exists")
-
- versions = self._get_version_list(old_ser.idnum)
- missing = []
- exists = []
- todo = {}
- for ver in versions:
- ok = True
- old_branch = self._get_branch_name(old_ser.name, ver)
- if not gitutil.check_branch(old_branch, self.gitdir):
- missing.append(old_branch)
- ok = False
-
- branch = self._get_branch_name(name, ver)
- if gitutil.check_branch(branch, self.gitdir):
- exists.append(branch)
- ok = False
-
- if ok:
- todo[ver] = [old_branch, branch]
-
- if missing or exists:
- msg = 'Cannot rename'
- if missing:
- msg += f": branches missing: {', '.join(missing)}"
- if exists:
- msg += f": branches exist: {', '.join(exists)}"
- raise ValueError(msg)
-
- for old_branch, branch in todo.values():
- tout.info(f"Renaming branch '{old_branch}' to '{branch}'")
- if not dry_run:
- gitutil.rename_branch(old_branch, branch, self.gitdir)
-
- # Change the series name; nothing needs to change in ser_ver
- self.db.series_set_name(old_ser.idnum, name)
-
- if not dry_run:
- self.commit()
- else:
- self.rollback()
-
- tout.info(f"Renamed series '{series}' to '{name}'")
- if dry_run:
- tout.info('Dry run completed')
-
- def scan(self, branch_name, mark=False, allow_unmarked=False, end=None,
- dry_run=False):
- """Scan a branch and make updates to the database if it has changed
-
- Args:
- branch_name (str): Name of branch to sync, or None for current one
- mark (str): True to mark each commit with a change ID
- allow_unmarked (str): True to not require each commit to be marked
- end (str): Add only commits up to but exclu
- dry_run (bool): True to do a dry run
- """
- def _show_item(oper, seq, subject):
- col = None
- if oper == '+':
- col = self.col.GREEN
- elif oper == '-':
- col = self.col.RED
- out = self.col.build(col, subject) if col else subject
- tout.info(f'{oper} {seq:3} {out}')
-
- name, ser, version, msg = self.prep_series(branch_name, end)
- svid = self.get_ser_ver(ser.idnum, version).idnum
- pcdict = self.get_pcommit_dict(svid)
-
- tout.info(
- f"Syncing series '{name}' v{version}: mark {mark} "
- f'allow_unmarked {allow_unmarked}')
- if msg:
- tout.info(msg)
-
- ser = self._handle_mark(name, ser, version, mark, allow_unmarked,
- False, dry_run)
-
- # First check for new patches that are not in the database
- to_add = dict(enumerate(ser.commits))
- for pcm in pcdict.values():
- tout.debug(f'pcm {pcm.subject}')
- i = self._find_matched_commit(to_add, pcm)
- if i is not None:
- del to_add[i]
-
- # Now check for patches in the database that are not in the branch
- to_remove = dict(enumerate(pcdict.values()))
- for cmt in ser.commits:
- tout.debug(f'cmt {cmt.subject}')
- i = self._find_matched_patch(to_remove, cmt)
- if i is not None:
- del to_remove[i]
-
- for seq, cmt in enumerate(ser.commits):
- if seq in to_remove:
- _show_item('-', seq, to_remove[seq].subject)
- del to_remove[seq]
- if seq in to_add:
- _show_item('+', seq, to_add[seq].subject)
- del to_add[seq]
- else:
- _show_item(' ', seq, cmt.subject)
- seq = len(ser.commits)
- for cmt in to_add.items():
- _show_item('+', seq, cmt.subject)
- seq += 1
- for seq, pcm in to_remove.items():
- _show_item('+', seq, pcm.subject)
-
- self.db.pcommit_delete(svid)
- self._add_series_commits(ser, svid)
- if not dry_run:
- self.commit()
- else:
- self.rollback()
- tout.info('Dry run completed')
-
- def send(self, pwork, name, autolink, autolink_wait, args):
- """Send out a series
-
- Args:
- pwork (Patchwork): Patchwork object to use
- name (str): Series name to search for, or None for current series
- that is checked out
- autolink (bool): True to auto-link the series after sending
- args (argparse.Namespace): 'send' arguments provided
- autolink_wait (int): Number of seconds to wait for the autolink to
- succeed
- """
- ser, version = self._parse_series_and_version(name, None)
- if not ser.idnum:
- raise ValueError(f"Series '{ser.name}' not found in database")
-
- args.branch = self._get_branch_name(ser.name, version)
- likely_sent = send.send(args, git_dir=self.gitdir, cwd=self.topdir)
-
- if likely_sent and autolink:
- print(f'Autolinking with Patchwork ({autolink_wait} seconds)')
- self.link_auto(pwork, name, version, True, wait_s=autolink_wait)
-
- def archive(self, series):
- """Archive a series
-
- Args:
- series (str): Name of series to use, or None to use current branch
- """
- ser = self._parse_series(series, include_archived=True)
- if not ser.idnum:
- raise ValueError(f"Series '{ser.name}' not found in database")
-
- svlist = self.db.ser_ver_get_for_series(ser.idnum)
-
- # Figure out the tags we will create
- tag_info = {}
- now = self.get_now()
- now_str = now.strftime('%d%b%y').lower()
- for svi in svlist:
- name = self._get_branch_name(ser.name, svi.version)
- if not gitutil.check_branch(name, git_dir=self.gitdir):
- raise ValueError(f"No branch named '{name}'")
- tag_info[svi.version] = [svi.idnum, name, f'{name}-{now_str}']
-
- # Create the tags
- repo = pygit2.Repository(self.gitdir)
- for _, (idnum, name, tag_name) in tag_info.items():
- commit = repo.revparse_single(name)
- repo.create_tag(tag_name, commit.hex,
- pygit2.enums.ObjectType.COMMIT,
- commit.author, commit.message)
-
- # Update the database
- for idnum, name, tag_name in tag_info.values():
- self.db.ser_ver_set_archive_tag(idnum, tag_name)
-
- # Delete the branches
- for idnum, name, tag_name in tag_info.values():
- # Detach HEAD from the branch if pointing to this branch
- commit = repo.revparse_single(name)
- if repo.head.target == commit.oid:
- repo.set_head(commit.oid)
-
- repo.branches.delete(name)
-
- self.db.series_set_archived(ser.idnum, True)
- self.commit()
-
- def unarchive(self, series):
- """Unarchive a series
-
- Args:
- series (str): Name of series to use, or None to use current branch
- """
- ser = self._parse_series(series, include_archived=True)
- if not ser.idnum:
- raise ValueError(f"Series '{ser.name}' not found in database")
- self.db.series_set_archived(ser.idnum, False)
-
- svlist = self.db.ser_ver_get_for_series(ser.idnum)
-
- # Collect the tags
- repo = pygit2.Repository(self.gitdir)
- tag_info = {}
- for svi in svlist:
- name = self._get_branch_name(ser.name, svi.version)
- target = repo.revparse_single(svi.archive_tag)
- tag_info[svi.idnum] = name, svi.archive_tag, target
-
- # Make sure the branches don't exist
- for name, tag_name, tag in tag_info.values():
- if name in repo.branches:
- raise ValueError(
- f"Cannot restore branch '{name}': already exists")
-
- # Recreate the branches
- for name, tag_name, tag in tag_info.values():
- target = repo.get(tag.target)
- repo.branches.create(name, target)
-
- # Delete the tags
- for name, tag_name, tag in tag_info.values():
- repo.references.delete(f'refs/tags/{tag_name}')
-
- # Update the database
- for idnum, (name, tag_name, tag) in tag_info.items():
- self.db.ser_ver_set_archive_tag(idnum, None)
-
- self.commit()
-
- def status(self, pwork, series, version, show_comments,
- show_cover_comments=False):
- """Show the series status from patchwork
-
- Args:
- pwork (Patchwork): Patchwork object to use
- series (str): Name of series to use, or None to use current branch
- version (int): Version number, or None to detect from name
- show_comments (bool): Show all comments on each patch
- show_cover_comments (bool): Show all comments on the cover letter
- """
- branch, series, version, _, _, link, _, _ = self._get_patches(
- series, version)
- if not link:
- raise ValueError(
- f"Series '{series.name}' v{version} has no patchwork link: "
- f"Try 'patman series -s {branch} autolink'")
- status.check_and_show_status(
- series, link, branch, None, False, show_comments,
- show_cover_comments, pwork, self.gitdir)
-
- def summary(self, series):
- """Show summary information for all series
-
- Args:
- series (str): Name of series to use
- """
- print(f"{'Name':17} Status Description")
- print(f"{'-' * 17} {'-' * 6} {'-' * 30}")
- if series is not None:
- self._summary_one(self._parse_series(series))
- return
-
- sdict = self.db.series_get_dict()
- for ser in sdict.values():
- self._summary_one(ser)
-
- def gather(self, pwork, series, version, show_comments,
- show_cover_comments, gather_tags, dry_run=False):
- """Gather any new tags from Patchwork, optionally showing comments
-
- Args:
- pwork (Patchwork): Patchwork object to use
- series (str): Name of series to use, or None to use current branch
- version (int): Version number, or None to detect from name
- show_comments (bool): True to show the comments on each patch
- show_cover_comments (bool): True to show the comments on the cover
- letter
- gather_tags (bool): True to gather review/test tags
- dry_run (bool): True to do a dry run (database is not updated)
- """
- ser, version = self._parse_series_and_version(series, version)
- self._ensure_version(ser, version)
- svid, link = self._get_series_svid_link(ser.idnum, version)
- if not link:
- raise ValueError(
- "No patchwork link is available: use 'patman series autolink'")
- tout.info(
- f"Updating series '{ser.name}' version {version} "
- f"from link '{link}'")
-
- loop = asyncio.get_event_loop()
- with pwork.collect_stats() as stats:
- cover, patches = loop.run_until_complete(self._gather(
- pwork, link, show_cover_comments))
-
- with terminal.pager():
- updated, updated_cover = self._sync_one(
- svid, ser.name, version, show_comments, show_cover_comments,
- gather_tags, cover, patches, dry_run)
- tout.info(f"{updated} patch{'es' if updated != 1 else ''}"
- f"{' and cover letter' if updated_cover else ''} "
- f'updated ({stats.request_count} requests)')
-
- if not dry_run:
- self.commit()
- else:
- self.rollback()
- tout.info('Dry run completed')
-
- def gather_all(self, pwork, show_comments, show_cover_comments,
- sync_all_versions, gather_tags, dry_run=False):
- to_fetch, missing = self._get_fetch_dict(sync_all_versions)
-
- loop = asyncio.get_event_loop()
- result, requests = loop.run_until_complete(self._do_series_sync_all(
- pwork, to_fetch))
-
- with terminal.pager():
- tot_updated = 0
- tot_cover = 0
- add_newline = False
- for (svid, sync), (cover, patches) in zip(to_fetch.items(),
- result):
- if add_newline:
- tout.info('')
- tout.info(f"Syncing '{sync.series_name}' v{sync.version}")
- updated, updated_cover = self._sync_one(
- svid, sync.series_name, sync.version, show_comments,
- show_cover_comments, gather_tags, cover, patches, dry_run)
- tot_updated += updated
- tot_cover += updated_cover
- add_newline = gather_tags
-
- tout.info('')
- tout.info(
- f"{tot_updated} patch{'es' if tot_updated != 1 else ''} and "
- f"{tot_cover} cover letter{'s' if tot_cover != 1 else ''} "
- f'updated, {missing} missing '
- f"link{'s' if missing != 1 else ''} ({requests} requests)")
- if not dry_run:
- self.commit()
- else:
- self.rollback()
- tout.info('Dry run completed')
-
- def upstream_add(self, name, url):
- """Add a new upstream tree
-
- Args:
- name (str): Name of the tree
- url (str): URL for the tree
- """
- self.db.upstream_add(name, url)
- self.commit()
-
- def upstream_list(self):
- """List the upstream repos
-
- Shows a list of the repos, obtained from the database
- """
- udict = self.get_upstream_dict()
-
- for name, items in udict.items():
- url, is_default = items
- default = 'default' if is_default else ''
- print(f'{name:15.15} {default:8} {url}')
-
- def upstream_set_default(self, name):
- """Set the default upstream target
-
- Args:
- name (str): Name of the upstream remote to set as default, or None
- for none
- """
- self.db.upstream_set_default(name)
- self.commit()
-
- def upstream_get_default(self):
- """Get the default upstream target
-
- Return:
- str: Name of the upstream remote to set as default, or None if none
- """
- return self.db.upstream_get_default()
-
- def upstream_delete(self, name):
- """Delete an upstream target
-
- Args:
- name (str): Name of the upstream remote to delete
- """
- self.db.upstream_delete(name)
- self.commit()
-
- def version_remove(self, name, version, dry_run=False):
- """Remove a version of a series from the database
-
- Args:
- name (str): Name of series to remove, or None to use current one
- version (int): Version number to remove
- dry_run (bool): True to do a dry run
- """
- ser, version = self._parse_series_and_version(name, version)
- name = ser.name
-
- versions = self._ensure_version(ser, version)
-
- if versions == [version]:
- raise ValueError(
- f"Series '{ser.name}' only has one version: remove the series")
-
- self.db.ser_ver_remove(ser.idnum, version)
- if not dry_run:
- self.commit()
- else:
- self.rollback()
-
- tout.info(f"Removed version {version} from series '{name}'")
- if dry_run:
- tout.info('Dry run completed')
-
- def version_change(self, name, version, new_version, dry_run=False):
- """Change a version of a series to be a different version
-
- Args:
- name (str): Name of series to remove, or None to use current one
- version (int): Version number to change
- new_version (int): New version
- dry_run (bool): True to do a dry run
- """
- ser, version = self._parse_series_and_version(name, version)
- name = ser.name
-
- versions = self._ensure_version(ser, version)
- vstr = list(map(str, versions))
- if version not in versions:
- raise ValueError(
- f"Series '{ser.name}' does not have v{version}: "
- f"{' '.join(vstr)}")
-
- if not new_version:
- raise ValueError('Please provide a new version number')
-
- if new_version in versions:
- raise ValueError(
- f"Series '{ser.name}' already has a v{new_version}: "
- f"{' '.join(vstr)}")
-
- new_name = self._join_name_version(ser.name, new_version)
-
- svid = self.get_series_svid(ser.idnum, version)
- pwc = self.get_pcommit_dict(svid)
- count = len(pwc.values())
- series = patchstream.get_metadata(name, 0, count, git_dir=self.gitdir)
-
- self.update_series(name, series, version, new_name, dry_run,
- add_vers=new_version, switch=True)
- self.db.ser_ver_set_version(svid, new_version)
-
- if not dry_run:
- self.commit()
- else:
- self.rollback()
-
- tout.info(f"Changed version {version} in series '{ser.name}' "
- f"to {new_version} named '{new_name}'")
- if dry_run:
- tout.info('Dry run completed')