Hello community,

here is the log from the commit of package python-git-pw for openSUSE:Factory 
checked in at 2020-06-24 15:49:31
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-git-pw (Old)
 and      /work/SRC/openSUSE:Factory/.python-git-pw.new.2956 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-git-pw"

Wed Jun 24 15:49:31 2020 rev:2 rq:816755 version:1.9.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-git-pw/python-git-pw.changes      
2020-04-01 19:12:19.115368612 +0200
+++ /work/SRC/openSUSE:Factory/.python-git-pw.new.2956/python-git-pw.changes    
2020-06-24 15:49:34.592578647 +0200
@@ -1,0 +2,11 @@
+Wed Jun 24 04:27:25 UTC 2020 - Steve Kowalik <[email protected]>
+
+- Update to 1.9.0:
+  * Adds support for Patchwork API v1.2 and introduces five new commands:
+    + bundle create
+    + bundle update
+    + bundle delete
+    + bundle add
+    + bundle remove 
+
+-------------------------------------------------------------------

Old:
----
  git-pw-1.8.1.tar.gz

New:
----
  git-pw-1.9.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-git-pw.spec ++++++
--- /var/tmp/diff_new_pack.taUyBw/_old  2020-06-24 15:49:35.228581312 +0200
+++ /var/tmp/diff_new_pack.taUyBw/_new  2020-06-24 15:49:35.232581330 +0200
@@ -19,7 +19,7 @@
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 %define modname git-pw
 Name:           python-git-pw
-Version:        1.8.1
+Version:        1.9.0
 Release:        0
 Summary:        A tool for integrating Git with Patchwork
 License:        MIT

++++++ git-pw-1.8.1.tar.gz -> git-pw-1.9.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/AUTHORS new/git-pw-1.9.0/AUTHORS
--- old/git-pw-1.8.1/AUTHORS    2020-03-31 18:06:45.000000000 +0200
+++ new/git-pw-1.9.0/AUTHORS    2020-04-18 01:10:02.000000000 +0200
@@ -1,4 +1,3 @@
 Ezequiel Garcia <[email protected]>
 Michael Ellerman <[email protected]>
 Stephen Finucane <[email protected]>
-Tom Tromey <[email protected]>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/ChangeLog new/git-pw-1.9.0/ChangeLog
--- old/git-pw-1.8.1/ChangeLog  2020-03-31 18:06:45.000000000 +0200
+++ new/git-pw-1.9.0/ChangeLog  2020-04-18 01:10:02.000000000 +0200
@@ -1,6 +1,19 @@
 CHANGES
 =======
 
+1.9.0
+-----
+
+* docs: Highlight the impending removal of Python 2.7
+* docs: Update requirements
+* man: Update man pages to reflect latest changes
+* Add 'bundle add', 'bundle remove' commands
+* Add 'bundle delete' command
+* Add 'bundle update' command
+* Add 'bundle create' command
+* Improve API of 'git\_pw.api' module
+* tox: Update click-man version
+
 1.8.1
 -----
 
@@ -69,16 +82,3 @@
 * Add support for multiple output formats
 * Handle bytestring decode in 'git\_config' helper
 * requirements: Make requirements less strict
-* docs: Remove unnecessary settings from 'conf.py'
-* api: Rename 'put' -> 'patch'
-* Revert "travis: Start testing Python 3.7, PyPy"
-* travis: Use travis to publish packages
-* travis: Start testing Python 3.7, PyPy
-
-1.4.0
------
-
-* Don't exit if multiple matches found for a filter
-* Add support for filtering by user/submitter ID
-* Re-add support for filtering by submitter name, user email
-* Fix a simple typo
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/PKG-INFO new/git-pw-1.9.0/PKG-INFO
--- old/git-pw-1.8.1/PKG-INFO   2020-03-31 18:06:45.756854800 +0200
+++ new/git-pw-1.9.0/PKG-INFO   2020-04-18 01:10:02.455363300 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: git-pw
-Version: 1.8.1
+Version: 1.9.0
 Summary: Git-Patchwork integration tool
 Home-page: https://github.com/getpatchwork/git-pw
 Author: Stephen Finucane
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/docs/requirements.txt 
new/git-pw-1.9.0/docs/requirements.txt
--- old/git-pw-1.8.1/docs/requirements.txt      2020-03-31 18:06:27.000000000 
+0200
+++ new/git-pw-1.9.0/docs/requirements.txt      2020-04-18 01:09:43.000000000 
+0200
@@ -1,5 +1,5 @@
 -r ../requirements.txt
 sphinx>=1.5,<2.0
-sphinx-click>=1.0,<2.0
-reno>=2.0,<3.0
-sphinx_rtd_theme==0.3.1
+sphinx-click>=2.0,<3.0
+reno>=3.0,<4.0
+sphinx-rtd-theme==0.4.3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/git_pw/api.py 
new/git-pw-1.9.0/git_pw/api.py
--- old/git-pw-1.8.1/git_pw/api.py      2020-03-31 18:06:27.000000000 +0200
+++ new/git-pw-1.9.0/git_pw/api.py      2020-04-18 01:09:43.000000000 +0200
@@ -6,6 +6,7 @@
 import logging
 import os.path
 import re
+import pty
 import sys
 import tempfile
 
@@ -16,10 +17,14 @@
 from git_pw import config
 
 if 0:  # noqa
+    from typing import Any  # noqa
+    from typing import Callable  # noqa
     from typing import Dict  # noqa
+    from typing import IO  # noqa
     from typing import List  # noqa
     from typing import Optional  # noqa
     from typing import Tuple  # noqa
+    from typing import Union  # noqa
 
     Filters = List[Tuple[str, str]]
 
@@ -121,20 +126,7 @@
         sys.exit(1)
 
 
-def version():
-    # type: () -> Optional[Tuple[int, int]]
-    """Get the version of the server from the URL, if present."""
-    server = _get_server()
-
-    version = re.match(r'.*/(\d)\.(\d)$', server)
-    if version:
-        return (int(version.group(1)), int(version.group(2)))
-
-    # return the oldest version we support if no version provided
-    return (1, 0)
-
-
-def get(url, params=None, stream=False):
+def _get(url, params=None, stream=False):
     # type: (str, Filters, bool) -> requests.Response
     """Make GET request and handle errors."""
     LOG.debug('GET %s', url)
@@ -155,7 +147,24 @@
     return rsp
 
 
-def patch(url, data):
+def _post(url, data):
+    # type: (str, dict) -> requests.Response
+    """Make POST request and handle errors."""
+    LOG.debug('POST %s, data=%r', url, data)
+
+    try:
+        rsp = requests.post(url, auth=_get_auth(), headers=_get_headers(),
+                            data=data)
+        rsp.raise_for_status()
+    except requests.exceptions.RequestException as exc:
+        _handle_error('create', exc)
+
+    LOG.debug('Got response')
+
+    return rsp
+
+
+def _patch(url, data):
     # type: (str, dict) -> requests.Response
     """Make PATCH request and handle errors."""
     LOG.debug('PATCH %s, data=%r', url, data)
@@ -172,39 +181,80 @@
     return rsp
 
 
-def download(url, params=None):
-    # type: (str, Filters) -> str
-    """Retrieve a specific API resource and save it to a file.
+def _delete(url):
+    # type: (str) -> requests.Response
+    """Make DELETE request and handle errors."""
+    LOG.debug('DELETE %s', url)
 
-    GET /{resource}/{resourceID}/
+    try:
+        rsp = requests.delete(url, auth=_get_auth(), headers=_get_headers())
+        rsp.raise_for_status()
+    except requests.exceptions.RequestException as exc:
+        _handle_error('delete', exc)
+
+    LOG.debug('Got response')
+
+    return rsp
+
+
+def version():
+    # type: () -> Optional[Tuple[int, int]]
+    """Get the version of the server from the URL, if present."""
+    server = _get_server()
+
+    version = re.match(r'.*/(\d)\.(\d)$', server)
+    if version:
+        return (int(version.group(1)), int(version.group(2)))
+
+    # return the oldest version we support if no version provided
+    return (1, 0)
+
+
+def download(url, params=None, output=None):
+    # type: (str, Filters, IO) -> Optional[str]
+    """Retrieve a specific API resource and save it to a file/stdout.
 
     The ``Content-Disposition`` header is assumed to be present and
-    will be used for the output filename.
+    will be used for the output filename, if not writing to stdout.
 
     Arguments:
         url: The resource URL.
         params: Additional parameters.
+        output: The output file. If provided, the caller is responsible for
+            closing. If None, a temporary file will be used.
 
     Returns:
-        A path to an output file containing the content.
+        A path to an output file containing the content, else None if stdout
+        used.
     """
-    rsp = get(url, params, stream=True)
+    rsp = _get(url, params, stream=True)
 
     # we don't catch anything here because we should break if these are missing
-    header = re.search('filename=(.+)',
-                       rsp.headers.get('content-disposition') or '')
+    header = re.search(
+        'filename=(.+)', rsp.headers.get('content-disposition') or '',
+    )
     if not header:
         LOG.error('Filename was expected but was not provided in response')
         sys.exit(1)
 
-    output_path = os.path.join(tempfile.mkdtemp(prefix='git-pw'),
-                               header.group(1))
+    if output:
+        output_path = None
+        if output.fileno() != pty.STDOUT_FILENO:
+            LOG.debug('Saving to %s', output.name)
+            output_path = output.name
 
-    with open(output_path, 'wb') as output_file:
-        LOG.debug('Saving to %s', output_path)
         # we use iter_content because patches can be binary
         for block in rsp.iter_content(1024):
-            output_file.write(block)
+            output.write(block)
+    else:
+        output_path = os.path.join(
+            tempfile.mkdtemp(prefix='git-pw'), header.group(1),
+        )
+        with open(output_path, 'wb') as output_file:
+            LOG.debug('Saving to %s', output_path)
+            # we use iter_content because patches can be binary
+            for block in rsp.iter_content(1024):
+                output_file.write(block)
 
     return output_path
 
@@ -233,7 +283,7 @@
     params = params or []
     params.append(('project', _get_project()))
 
-    return get(url, params).json()
+    return _get(url, params).json()
 
 
 def detail(resource_type, resource_id, params=None):
@@ -253,11 +303,49 @@
     # NOTE(stephenfin): All resources must have a trailing '/'
     url = '/'.join([_get_server(), resource_type, str(resource_id), ''])
 
-    return get(url, params, stream=False).json()
+    return _get(url, params, stream=False).json()
+
+
+def create(resource_type, data):
+    # type: (str, dict) -> dict
+    """Create a new API resource.
+
+    POST /{resource}/
+
+    Arguments:
+        resource_type: The resource endpoint name.
+        params: Fields to update.
+
+    Returns:
+        A dictionary representing the detailed view of a given resource.
+    """
+    # NOTE(stephenfin): All resources must have a trailing '/'
+    url = '/'.join([_get_server(), resource_type, ''])
+
+    return _post(url, data).json()
+
+
+def delete(resource_type, resource_id):
+    # type: (str, Union[str, int]) -> None
+    """Delete a specific API resource.
+
+    DELETE /{resource}/{resourceID}/
+
+    Arguments:
+        resource_type: The resource endpoint name.
+        resource_id: The ID for the specific resource.
+
+    Returns:
+        A dictionary representing the detailed view of a given resource.
+    """
+    # NOTE(stephenfin): All resources must have a trailing '/'
+    url = '/'.join([_get_server(), resource_type, str(resource_id), ''])
+
+    _delete(url)
 
 
 def update(resource_type, resource_id, data):
-    # type: (str, int, dict) -> dict
+    # type: (str, Union[int, str], dict) -> dict
     """Update a specific API resource.
 
     PATCH /{resource}/{resourceID}/
@@ -273,7 +361,24 @@
     # NOTE(stephenfin): All resources must have a trailing '/'
     url = '/'.join([_get_server(), resource_type, str(resource_id), ''])
 
-    return patch(url, data).json()
+    return _patch(url, data).json()
+
+
+def validate_minimum_version(min_version, msg):
+    # type: (Tuple[int, int], str) -> Callable[[Any], Any]
+
+    def inner(f):
+        @click.pass_context
+        def new_func(ctx, *args, **kwargs):
+            if version() < min_version:
+                LOG.error(msg)
+                sys.exit(1)
+
+            return ctx.invoke(f, *args, **kwargs)
+
+        return update_wrapper(new_func, f)
+
+    return inner
 
 
 def validate_multiple_filter_support(f):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/git_pw/bundle.py 
new/git-pw-1.9.0/git_pw/bundle.py
--- old/git-pw-1.8.1/git_pw/bundle.py   2020-03-31 18:06:27.000000000 +0200
+++ new/git-pw-1.9.0/git_pw/bundle.py   2020-04-18 01:09:43.000000000 +0200
@@ -3,7 +3,6 @@
 """
 
 import logging
-import pty
 import sys
 
 import click
@@ -72,31 +71,13 @@
     path = None
     bundle = _get_bundle(bundle_id)
 
-    if output:
-        content = api.get(bundle['mbox']).content
-
-        output.write(content)
-
-        if output.fileno() != pty.STDOUT_FILENO:
-            path = output.name
-    else:
-        path = api.download(bundle['mbox'])
+    path = api.download(bundle['mbox'], output=output)
 
     if path:
         LOG.info('Downloaded bundle to %s', path)
 
 
[email protected](name='show')
[email protected]_options
[email protected]('bundle_id')
-def show_cmd(fmt, bundle_id):
-    """Show information about bundle.
-
-    Retrieve Patchwork metadata for a bundle.
-    """
-    LOG.debug('Showing bundle: id=%s', bundle_id)
-
-    bundle = _get_bundle(bundle_id)
+def _show_bundle(bundle, fmt):
 
     def _format_patch(patch):
         return '%-4d %s' % (patch.get('id'), patch.get('name'))
@@ -117,6 +98,21 @@
     utils.echo(output, ['Property', 'Value'], fmt=fmt)
 
 
[email protected](name='show')
[email protected]_options
[email protected]('bundle_id')
+def show_cmd(fmt, bundle_id):
+    """Show information about bundle.
+
+    Retrieve Patchwork metadata for a bundle.
+    """
+    LOG.debug('Showing bundle: id=%s', bundle_id)
+
+    bundle = _get_bundle(bundle_id)
+
+    _show_bundle(bundle, fmt)
+
+
 @click.command(name='list')
 @click.option('--owner', metavar='OWNER', multiple=True,
               help='Show only bundles with these owners. Should be an email, '
@@ -171,3 +167,153 @@
             output[-1].append(item[idx])
 
     utils.echo_via_pager(output, headers, fmt=fmt)
+
+
[email protected](name='create')
[email protected]('--public/--private', default=False,
+              help='Allow other users to view this bundle. If private, only '
+              'you will be able to see this bundle.')
[email protected]('name')
[email protected]('patch_ids', type=click.INT, nargs=-1, required=True)
[email protected]_minimum_version(
+    (1, 2), 'Creating bundles is only supported from API version 1.2',
+)
[email protected]_options
+def create_cmd(name, patch_ids, public, fmt):
+    """Create a bundle.
+
+    Create a bundle with the given NAME and patches from PATCH_ID.
+
+    Requires API version 1.2 or greater.
+    """
+    LOG.debug('Create bundle: name=%s, patches=%s, public=%s',
+              name, patch_ids, public)
+
+    data = [
+        ('name', name),
+        ('patches', patch_ids),
+        ('public', public),
+    ]
+
+    bundle = api.create('bundles', data)
+
+    _show_bundle(bundle, fmt)
+
+
[email protected](name='update')
[email protected]('--name')
[email protected]('--patch', 'patch_ids', type=click.INT, multiple=True,
+              help='Add the specified patch(es) to the bundle.')
[email protected]('--public/--private', default=None,
+              help='Allow other users to view this bundle. If private, only '
+              'you will be able to see this bundle.')
[email protected]('bundle_id')
[email protected]_minimum_version(
+    (1, 2), 'Updating bundles is only supported from API version 1.2',
+)
[email protected]_options
+def update_cmd(bundle_id, name, patch_ids, public, fmt):
+    """Update a bundle.
+
+    Update bundle BUNDLE_ID. If PATCH_IDs are specified, this will overwrite
+    all patches in the bundle. Use 'bundle add' and 'bundle remove' to add or
+    remove patches.
+
+    Requires API version 1.2 or greater.
+    """
+    LOG.debug(
+        'Updating bundle: id=%s, name=%s, patches=%s, public=%s',
+        bundle_id, name, patch_ids, public,
+    )
+
+    data = []
+
+    for key, value in [('name', name), ('public', public)]:
+        if value is None:
+            continue
+
+        data.append((key, value))
+
+    if patch_ids:  # special case patches to ignore the empty set
+        data.append(('patches', patch_ids))
+
+    bundle = api.update('bundles', bundle_id, data)
+
+    _show_bundle(bundle, fmt)
+
+
[email protected](name='delete')
[email protected]('bundle_id')
[email protected]_minimum_version(
+    (1, 2), 'Deleting bundles is only supported from API version 1.2',
+)
[email protected]_options
+def delete_cmd(bundle_id, fmt):
+    """Delete a bundle.
+
+    Delete bundle BUNDLE_ID.
+
+    Requires API version 1.2 or greater.
+    """
+    LOG.debug('Delete bundle: id=%s', bundle_id)
+
+    api.delete('bundles', bundle_id)
+
+
[email protected](name='add')
[email protected]('bundle_id')
[email protected]('patch_ids', type=click.INT, nargs=-1, required=True)
[email protected]_minimum_version(
+    (1, 2), 'Modifying bundles is only supported from API version 1.2',
+)
[email protected]_options
+def add_cmd(bundle_id, patch_ids, fmt):
+    """Add one or more patches to a bundle.
+
+    Append the provided PATCH_IDS to bundle BUNDLE_ID.
+
+    Requires API version 1.2 or greater.
+    """
+    LOG.debug('Add to bundle: id=%s, patches=%s', bundle_id, patch_ids)
+
+    bundle = _get_bundle(bundle_id)
+
+    data = [
+        ('patches', patch_ids + tuple([p['id'] for p in bundle['patches']])),
+    ]
+
+    bundle = api.update('bundles', bundle_id, data)
+
+    _show_bundle(bundle, fmt)
+
+
[email protected](name='remove')
[email protected]('bundle_id')
[email protected]('patch_ids', type=click.INT, nargs=-1, required=True)
[email protected]_minimum_version(
+    (1, 2), 'Modifying bundles is only supported from API version 1.2',
+)
[email protected]_options
+def remove_cmd(bundle_id, patch_ids, fmt):
+    """Remove one or more patches from a bundle.
+
+    Remove the provided PATCH_IDS to bundle BUNDLE_ID.
+
+    Requires API version 1.2 or greater.
+    """
+    LOG.debug('Remove from bundle: id=%s, patches=%s', bundle_id, patch_ids)
+
+    bundle = _get_bundle(bundle_id)
+
+    patches = [p['id'] for p in bundle['patches'] if p['id'] not in patch_ids]
+    if not patches:
+        LOG.error(
+            'Bundles cannot be empty. Consider deleting the bundle instead'
+        )
+        sys.exit(1)
+
+    data = [('patches', tuple(patches))]
+
+    bundle = api.update('bundles', bundle_id, data)
+
+    _show_bundle(bundle, fmt)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/git_pw/patch.py 
new/git-pw-1.9.0/git_pw/patch.py
--- old/git-pw-1.8.1/git_pw/patch.py    2020-03-31 18:06:27.000000000 +0200
+++ new/git-pw-1.9.0/git_pw/patch.py    2020-04-18 01:09:43.000000000 +0200
@@ -75,24 +75,20 @@
     path = None
     patch = api.detail('patches', patch_id)
 
-    if output:
-        if fmt == 'diff':
-            content = patch['diff']
+    if fmt == 'diff':
+        if output:
+            output.write(patch['diff'])
+            if output.fileno() != pty.STDOUT_FILENO:
+                path = output.name
         else:
-            content = api.get(patch['mbox']).content
-
-        output.write(content)
-
-        if output.fileno() != pty.STDOUT_FILENO:
-            path = output.name
-    else:
-        if fmt == 'diff':
             # TODO(stephenfin): We discard the 'diff' field so we can get the
             # filename and save to the correct file. We should expose this
             # information via the API
-            path = api.download(patch['mbox'].replace('mbox', 'raw'))
-        else:
-            path = api.download(patch['mbox'])
+            path = api.download(
+                patch['mbox'].replace('mbox', 'raw'), output=output,
+            )
+    else:
+        path = api.download(patch['mbox'], output=output)
 
     if path:
         LOG.info('Downloaded patch to %s', path)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/git_pw/series.py 
new/git-pw-1.9.0/git_pw/series.py
--- old/git-pw-1.8.1/git_pw/series.py   2020-03-31 18:06:27.000000000 +0200
+++ new/git-pw-1.9.0/git_pw/series.py   2020-04-18 01:09:43.000000000 +0200
@@ -3,7 +3,6 @@
 """
 
 import logging
-import pty
 
 import arrow
 import click
@@ -51,15 +50,7 @@
     path = None
     series = api.detail('series', series_id)
 
-    if output:
-        content = api.get(series['mbox']).content
-
-        output.write(content)
-
-        if output.fileno() != pty.STDOUT_FILENO:
-            path = output.name
-    else:
-        path = api.download(series['mbox'])
+    path = api.download(series['mbox'], output=output)
 
     if path:
         LOG.info('Downloaded series to %s', path)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/git_pw/shell.py 
new/git-pw-1.9.0/git_pw/shell.py
--- old/git-pw-1.8.1/git_pw/shell.py    2020-03-31 18:06:27.000000000 +0200
+++ new/git-pw-1.9.0/git_pw/shell.py    2020-04-18 01:09:43.000000000 +0200
@@ -123,3 +123,8 @@
 bundle.add_command(bundle_cmds.show_cmd)
 bundle.add_command(bundle_cmds.download_cmd)
 bundle.add_command(bundle_cmds.list_cmd)
+bundle.add_command(bundle_cmds.create_cmd)
+bundle.add_command(bundle_cmds.update_cmd)
+bundle.add_command(bundle_cmds.delete_cmd)
+bundle.add_command(bundle_cmds.add_cmd)
+bundle.add_command(bundle_cmds.remove_cmd)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/git_pw.egg-info/PKG-INFO 
new/git-pw-1.9.0/git_pw.egg-info/PKG-INFO
--- old/git-pw-1.8.1/git_pw.egg-info/PKG-INFO   2020-03-31 18:06:45.000000000 
+0200
+++ new/git-pw-1.9.0/git_pw.egg-info/PKG-INFO   2020-04-18 01:10:02.000000000 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: git-pw
-Version: 1.8.1
+Version: 1.9.0
 Summary: Git-Patchwork integration tool
 Home-page: https://github.com/getpatchwork/git-pw
 Author: Stephen Finucane
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/git_pw.egg-info/SOURCES.txt 
new/git-pw-1.9.0/git_pw.egg-info/SOURCES.txt
--- old/git-pw-1.8.1/git_pw.egg-info/SOURCES.txt        2020-03-31 
18:06:45.000000000 +0200
+++ new/git-pw-1.9.0/git_pw.egg-info/SOURCES.txt        2020-04-18 
01:10:02.000000000 +0200
@@ -33,10 +33,15 @@
 git_pw.egg-info/pbr.json
 git_pw.egg-info/requires.txt
 git_pw.egg-info/top_level.txt
+man/git-pw-bundle-add.1
 man/git-pw-bundle-apply.1
+man/git-pw-bundle-create.1
+man/git-pw-bundle-delete.1
 man/git-pw-bundle-download.1
 man/git-pw-bundle-list.1
+man/git-pw-bundle-remove.1
 man/git-pw-bundle-show.1
+man/git-pw-bundle-update.1
 man/git-pw-bundle.1
 man/git-pw-patch-apply.1
 man/git-pw-patch-download.1
@@ -51,6 +56,7 @@
 man/git-pw-series.1
 man/git-pw.1
 releasenotes/notes/api-v1-1-5c804713ef435739.yaml
+releasenotes/notes/bundle-crud-47aadae6eb7a20ad.yaml
 releasenotes/notes/drop-pypy-support-f670deb05ef527fe.yaml
 releasenotes/notes/drop-python34-support-5e01360fff605972.yaml
 releasenotes/notes/enforce-filtering-by-project-59ed29c4b7edc0a5.yaml
@@ -68,6 +74,7 @@
 releasenotes/notes/issue-49-865c4f1657b97fce.yaml
 releasenotes/notes/passthrough-git-am-arguments-23cd0b292304d648.yaml
 releasenotes/notes/patch-states-b88240569f8474f1.yaml
+releasenotes/notes/python-2-deprecation-c87e311384eab29b.yaml
 releasenotes/notes/random-fixes-3da473a63c253f2d.yaml
 releasenotes/notes/remove-python-3-2-3-3-support-8987031bed2c0333.yaml
 releasenotes/notes/require-server-version-93ac0818c293b85e.yaml
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/git_pw.egg-info/pbr.json 
new/git-pw-1.9.0/git_pw.egg-info/pbr.json
--- old/git-pw-1.8.1/git_pw.egg-info/pbr.json   2020-03-31 18:06:45.000000000 
+0200
+++ new/git-pw-1.9.0/git_pw.egg-info/pbr.json   2020-04-18 01:10:02.000000000 
+0200
@@ -1 +1 @@
-{"git_version": "786c0f0", "is_release": true}
\ No newline at end of file
+{"git_version": "f1f4178", "is_release": true}
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-bundle-add.1 
new/git-pw-1.9.0/man/git-pw-bundle-add.1
--- old/git-pw-1.8.1/man/git-pw-bundle-add.1    1970-01-01 01:00:00.000000000 
+0100
+++ new/git-pw-1.9.0/man/git-pw-bundle-add.1    2020-04-18 01:09:43.000000000 
+0200
@@ -0,0 +1,16 @@
+.TH "GIT-PW BUNDLE ADD" "1" "2020-04-17" "1.9.0" "git-pw bundle add Manual"
+.SH NAME
+git-pw\-bundle\-add \- Add one or more patches to a bundle.
+.SH SYNOPSIS
+.B git-pw bundle add
+[OPTIONS] BUNDLE_ID PATCH_IDS...
+.SH DESCRIPTION
+Add one or more patches to a bundle.
+.PP
+Append the provided PATCH_IDS to bundle BUNDLE_ID.
+.PP
+Requires API version 1.2 or greater.
+.SH OPTIONS
+.TP
+\fB\-f,\fP \-\-format [simple|table|csv]
+Output format. Defaults to the value of 'git config pw.format' else 'table'.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-bundle-apply.1 
new/git-pw-1.9.0/man/git-pw-bundle-apply.1
--- old/git-pw-1.8.1/man/git-pw-bundle-apply.1  2020-03-31 18:06:27.000000000 
+0200
+++ new/git-pw-1.9.0/man/git-pw-bundle-apply.1  2020-04-18 01:09:43.000000000 
+0200
@@ -1,4 +1,4 @@
-.TH "GIT-PW BUNDLE APPLY" "1" "08-Dec-2019" "1.8.0" "git-pw bundle apply 
Manual"
+.TH "GIT-PW BUNDLE APPLY" "1" "2020-04-17" "1.9.0" "git-pw bundle apply Manual"
 .SH NAME
 git-pw\-bundle\-apply \- Apply bundle.
 .SH SYNOPSIS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-bundle-create.1 
new/git-pw-1.9.0/man/git-pw-bundle-create.1
--- old/git-pw-1.8.1/man/git-pw-bundle-create.1 1970-01-01 01:00:00.000000000 
+0100
+++ new/git-pw-1.9.0/man/git-pw-bundle-create.1 2020-04-18 01:09:43.000000000 
+0200
@@ -0,0 +1,19 @@
+.TH "GIT-PW BUNDLE CREATE" "1" "2020-04-17" "1.9.0" "git-pw bundle create 
Manual"
+.SH NAME
+git-pw\-bundle\-create \- Create a bundle.
+.SH SYNOPSIS
+.B git-pw bundle create
+[OPTIONS] NAME PATCH_IDS...
+.SH DESCRIPTION
+Create a bundle.
+.PP
+Create a bundle with the given NAME and patches from PATCH_ID.
+.PP
+Requires API version 1.2 or greater.
+.SH OPTIONS
+.TP
+\fB\-\-public\fP / \-\-private
+Allow other users to view this bundle. If private, only you will be able to 
see this bundle.
+.TP
+\fB\-f,\fP \-\-format [simple|table|csv]
+Output format. Defaults to the value of 'git config pw.format' else 'table'.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-bundle-delete.1 
new/git-pw-1.9.0/man/git-pw-bundle-delete.1
--- old/git-pw-1.8.1/man/git-pw-bundle-delete.1 1970-01-01 01:00:00.000000000 
+0100
+++ new/git-pw-1.9.0/man/git-pw-bundle-delete.1 2020-04-18 01:09:43.000000000 
+0200
@@ -0,0 +1,16 @@
+.TH "GIT-PW BUNDLE DELETE" "1" "2020-04-17" "1.9.0" "git-pw bundle delete 
Manual"
+.SH NAME
+git-pw\-bundle\-delete \- Delete a bundle.
+.SH SYNOPSIS
+.B git-pw bundle delete
+[OPTIONS] BUNDLE_ID
+.SH DESCRIPTION
+Delete a bundle.
+.PP
+Delete bundle BUNDLE_ID.
+.PP
+Requires API version 1.2 or greater.
+.SH OPTIONS
+.TP
+\fB\-f,\fP \-\-format [simple|table|csv]
+Output format. Defaults to the value of 'git config pw.format' else 'table'.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-bundle-download.1 
new/git-pw-1.9.0/man/git-pw-bundle-download.1
--- old/git-pw-1.8.1/man/git-pw-bundle-download.1       2020-03-31 
18:06:27.000000000 +0200
+++ new/git-pw-1.9.0/man/git-pw-bundle-download.1       2020-04-18 
01:09:43.000000000 +0200
@@ -1,4 +1,4 @@
-.TH "GIT-PW BUNDLE DOWNLOAD" "1" "08-Dec-2019" "1.8.0" "git-pw bundle download 
Manual"
+.TH "GIT-PW BUNDLE DOWNLOAD" "1" "2020-04-17" "1.9.0" "git-pw bundle download 
Manual"
 .SH NAME
 git-pw\-bundle\-download \- Download bundle in mbox format.
 .SH SYNOPSIS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-bundle-list.1 
new/git-pw-1.9.0/man/git-pw-bundle-list.1
--- old/git-pw-1.8.1/man/git-pw-bundle-list.1   2020-03-31 18:06:27.000000000 
+0200
+++ new/git-pw-1.9.0/man/git-pw-bundle-list.1   2020-04-18 01:09:43.000000000 
+0200
@@ -1,4 +1,4 @@
-.TH "GIT-PW BUNDLE LIST" "1" "08-Dec-2019" "1.8.0" "git-pw bundle list Manual"
+.TH "GIT-PW BUNDLE LIST" "1" "2020-04-17" "1.9.0" "git-pw bundle list Manual"
 .SH NAME
 git-pw\-bundle\-list \- List bundles.
 .SH SYNOPSIS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-bundle-remove.1 
new/git-pw-1.9.0/man/git-pw-bundle-remove.1
--- old/git-pw-1.8.1/man/git-pw-bundle-remove.1 1970-01-01 01:00:00.000000000 
+0100
+++ new/git-pw-1.9.0/man/git-pw-bundle-remove.1 2020-04-18 01:09:43.000000000 
+0200
@@ -0,0 +1,16 @@
+.TH "GIT-PW BUNDLE REMOVE" "1" "2020-04-17" "1.9.0" "git-pw bundle remove 
Manual"
+.SH NAME
+git-pw\-bundle\-remove \- Remove one or more patches from a bundle.
+.SH SYNOPSIS
+.B git-pw bundle remove
+[OPTIONS] BUNDLE_ID PATCH_IDS...
+.SH DESCRIPTION
+Remove one or more patches from a bundle.
+.PP
+Remove the provided PATCH_IDS to bundle BUNDLE_ID.
+.PP
+Requires API version 1.2 or greater.
+.SH OPTIONS
+.TP
+\fB\-f,\fP \-\-format [simple|table|csv]
+Output format. Defaults to the value of 'git config pw.format' else 'table'.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-bundle-show.1 
new/git-pw-1.9.0/man/git-pw-bundle-show.1
--- old/git-pw-1.8.1/man/git-pw-bundle-show.1   2020-03-31 18:06:27.000000000 
+0200
+++ new/git-pw-1.9.0/man/git-pw-bundle-show.1   2020-04-18 01:09:43.000000000 
+0200
@@ -1,4 +1,4 @@
-.TH "GIT-PW BUNDLE SHOW" "1" "08-Dec-2019" "1.8.0" "git-pw bundle show Manual"
+.TH "GIT-PW BUNDLE SHOW" "1" "2020-04-17" "1.9.0" "git-pw bundle show Manual"
 .SH NAME
 git-pw\-bundle\-show \- Show information about bundle.
 .SH SYNOPSIS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-bundle-update.1 
new/git-pw-1.9.0/man/git-pw-bundle-update.1
--- old/git-pw-1.8.1/man/git-pw-bundle-update.1 1970-01-01 01:00:00.000000000 
+0100
+++ new/git-pw-1.9.0/man/git-pw-bundle-update.1 2020-04-18 01:09:43.000000000 
+0200
@@ -0,0 +1,27 @@
+.TH "GIT-PW BUNDLE UPDATE" "1" "2020-04-17" "1.9.0" "git-pw bundle update 
Manual"
+.SH NAME
+git-pw\-bundle\-update \- Update a bundle.
+.SH SYNOPSIS
+.B git-pw bundle update
+[OPTIONS] BUNDLE_ID
+.SH DESCRIPTION
+Update a bundle.
+.PP
+Update bundle BUNDLE_ID. If PATCH_IDs are specified, this will overwrite
+all patches in the bundle. Use 'bundle add' and 'bundle remove' to add or
+remove patches.
+.PP
+Requires API version 1.2 or greater.
+.SH OPTIONS
+.TP
+\fB\-\-name\fP TEXT
+.PP
+.TP
+\fB\-\-patch\fP INTEGER
+Add the specified patch(es) to the bundle.
+.TP
+\fB\-\-public\fP / \-\-private
+Allow other users to view this bundle. If private, only you will be able to 
see this bundle.
+.TP
+\fB\-f,\fP \-\-format [simple|table|csv]
+Output format. Defaults to the value of 'git config pw.format' else 'table'.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-bundle.1 
new/git-pw-1.9.0/man/git-pw-bundle.1
--- old/git-pw-1.8.1/man/git-pw-bundle.1        2020-03-31 18:06:27.000000000 
+0200
+++ new/git-pw-1.9.0/man/git-pw-bundle.1        2020-04-18 01:09:43.000000000 
+0200
@@ -1,4 +1,4 @@
-.TH "GIT-PW BUNDLE" "1" "08-Dec-2019" "1.8.0" "git-pw bundle Manual"
+.TH "GIT-PW BUNDLE" "1" "2020-04-17" "1.9.0" "git-pw bundle Manual"
 .SH NAME
 git-pw\-bundle \- Interact with bundles.
 .SH SYNOPSIS
@@ -31,3 +31,23 @@
 \fBlist\fP
   List bundles.
   See \fBgit-pw bundle-list(1)\fP for full documentation on the \fBlist\fP 
command.
+.PP
+\fBcreate\fP
+  Create a bundle.
+  See \fBgit-pw bundle-create(1)\fP for full documentation on the \fBcreate\fP 
command.
+.PP
+\fBupdate\fP
+  Update a bundle.
+  See \fBgit-pw bundle-update(1)\fP for full documentation on the \fBupdate\fP 
command.
+.PP
+\fBdelete\fP
+  Delete a bundle.
+  See \fBgit-pw bundle-delete(1)\fP for full documentation on the \fBdelete\fP 
command.
+.PP
+\fBadd\fP
+  Add one or more patches to a bundle.
+  See \fBgit-pw bundle-add(1)\fP for full documentation on the \fBadd\fP 
command.
+.PP
+\fBremove\fP
+  Remove one or more patches from a bundle.
+  See \fBgit-pw bundle-remove(1)\fP for full documentation on the \fBremove\fP 
command.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-patch-apply.1 
new/git-pw-1.9.0/man/git-pw-patch-apply.1
--- old/git-pw-1.8.1/man/git-pw-patch-apply.1   2020-03-31 18:06:27.000000000 
+0200
+++ new/git-pw-1.9.0/man/git-pw-patch-apply.1   2020-04-18 01:09:43.000000000 
+0200
@@ -1,4 +1,4 @@
-.TH "GIT-PW PATCH APPLY" "1" "08-Dec-2019" "1.8.0" "git-pw patch apply Manual"
+.TH "GIT-PW PATCH APPLY" "1" "2020-04-17" "1.9.0" "git-pw patch apply Manual"
 .SH NAME
 git-pw\-patch\-apply \- Apply patch.
 .SH SYNOPSIS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-patch-download.1 
new/git-pw-1.9.0/man/git-pw-patch-download.1
--- old/git-pw-1.8.1/man/git-pw-patch-download.1        2020-03-31 
18:06:27.000000000 +0200
+++ new/git-pw-1.9.0/man/git-pw-patch-download.1        2020-04-18 
01:09:43.000000000 +0200
@@ -1,4 +1,4 @@
-.TH "GIT-PW PATCH DOWNLOAD" "1" "08-Dec-2019" "1.8.0" "git-pw patch download 
Manual"
+.TH "GIT-PW PATCH DOWNLOAD" "1" "2020-04-17" "1.9.0" "git-pw patch download 
Manual"
 .SH NAME
 git-pw\-patch\-download \- Download patch in diff or mbox format.
 .SH SYNOPSIS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-patch-list.1 
new/git-pw-1.9.0/man/git-pw-patch-list.1
--- old/git-pw-1.8.1/man/git-pw-patch-list.1    2020-03-31 18:06:27.000000000 
+0200
+++ new/git-pw-1.9.0/man/git-pw-patch-list.1    2020-04-18 01:09:43.000000000 
+0200
@@ -1,4 +1,4 @@
-.TH "GIT-PW PATCH LIST" "1" "08-Dec-2019" "1.8.0" "git-pw patch list Manual"
+.TH "GIT-PW PATCH LIST" "1" "2020-04-17" "1.9.0" "git-pw patch list Manual"
 .SH NAME
 git-pw\-patch\-list \- List patches.
 .SH SYNOPSIS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-patch-show.1 
new/git-pw-1.9.0/man/git-pw-patch-show.1
--- old/git-pw-1.8.1/man/git-pw-patch-show.1    2020-03-31 18:06:27.000000000 
+0200
+++ new/git-pw-1.9.0/man/git-pw-patch-show.1    2020-04-18 01:09:43.000000000 
+0200
@@ -1,4 +1,4 @@
-.TH "GIT-PW PATCH SHOW" "1" "08-Dec-2019" "1.8.0" "git-pw patch show Manual"
+.TH "GIT-PW PATCH SHOW" "1" "2020-04-17" "1.9.0" "git-pw patch show Manual"
 .SH NAME
 git-pw\-patch\-show \- Show information about patch.
 .SH SYNOPSIS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-patch-update.1 
new/git-pw-1.9.0/man/git-pw-patch-update.1
--- old/git-pw-1.8.1/man/git-pw-patch-update.1  2020-03-31 18:06:27.000000000 
+0200
+++ new/git-pw-1.9.0/man/git-pw-patch-update.1  2020-04-18 01:09:43.000000000 
+0200
@@ -1,4 +1,4 @@
-.TH "GIT-PW PATCH UPDATE" "1" "08-Dec-2019" "1.8.0" "git-pw patch update 
Manual"
+.TH "GIT-PW PATCH UPDATE" "1" "2020-04-17" "1.9.0" "git-pw patch update Manual"
 .SH NAME
 git-pw\-patch\-update \- Update one or more patches.
 .SH SYNOPSIS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-patch.1 
new/git-pw-1.9.0/man/git-pw-patch.1
--- old/git-pw-1.8.1/man/git-pw-patch.1 2020-03-31 18:06:27.000000000 +0200
+++ new/git-pw-1.9.0/man/git-pw-patch.1 2020-04-18 01:09:43.000000000 +0200
@@ -1,4 +1,4 @@
-.TH "GIT-PW PATCH" "1" "08-Dec-2019" "1.8.0" "git-pw patch Manual"
+.TH "GIT-PW PATCH" "1" "2020-04-17" "1.9.0" "git-pw patch Manual"
 .SH NAME
 git-pw\-patch \- Interact with patches.
 .SH SYNOPSIS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-series-apply.1 
new/git-pw-1.9.0/man/git-pw-series-apply.1
--- old/git-pw-1.8.1/man/git-pw-series-apply.1  2020-03-31 18:06:27.000000000 
+0200
+++ new/git-pw-1.9.0/man/git-pw-series-apply.1  2020-04-18 01:09:43.000000000 
+0200
@@ -1,4 +1,4 @@
-.TH "GIT-PW SERIES APPLY" "1" "08-Dec-2019" "1.8.0" "git-pw series apply 
Manual"
+.TH "GIT-PW SERIES APPLY" "1" "2020-04-17" "1.9.0" "git-pw series apply Manual"
 .SH NAME
 git-pw\-series\-apply \- Apply series.
 .SH SYNOPSIS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-series-download.1 
new/git-pw-1.9.0/man/git-pw-series-download.1
--- old/git-pw-1.8.1/man/git-pw-series-download.1       2020-03-31 
18:06:27.000000000 +0200
+++ new/git-pw-1.9.0/man/git-pw-series-download.1       2020-04-18 
01:09:43.000000000 +0200
@@ -1,4 +1,4 @@
-.TH "GIT-PW SERIES DOWNLOAD" "1" "08-Dec-2019" "1.8.0" "git-pw series download 
Manual"
+.TH "GIT-PW SERIES DOWNLOAD" "1" "2020-04-17" "1.9.0" "git-pw series download 
Manual"
 .SH NAME
 git-pw\-series\-download \- Download series in mbox format.
 .SH SYNOPSIS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-series-list.1 
new/git-pw-1.9.0/man/git-pw-series-list.1
--- old/git-pw-1.8.1/man/git-pw-series-list.1   2020-03-31 18:06:27.000000000 
+0200
+++ new/git-pw-1.9.0/man/git-pw-series-list.1   2020-04-18 01:09:43.000000000 
+0200
@@ -1,4 +1,4 @@
-.TH "GIT-PW SERIES LIST" "1" "08-Dec-2019" "1.8.0" "git-pw series list Manual"
+.TH "GIT-PW SERIES LIST" "1" "2020-04-17" "1.9.0" "git-pw series list Manual"
 .SH NAME
 git-pw\-series\-list \- List series.
 .SH SYNOPSIS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-series-show.1 
new/git-pw-1.9.0/man/git-pw-series-show.1
--- old/git-pw-1.8.1/man/git-pw-series-show.1   2020-03-31 18:06:27.000000000 
+0200
+++ new/git-pw-1.9.0/man/git-pw-series-show.1   2020-04-18 01:09:43.000000000 
+0200
@@ -1,4 +1,4 @@
-.TH "GIT-PW SERIES SHOW" "1" "08-Dec-2019" "1.8.0" "git-pw series show Manual"
+.TH "GIT-PW SERIES SHOW" "1" "2020-04-17" "1.9.0" "git-pw series show Manual"
 .SH NAME
 git-pw\-series\-show \- Show information about series.
 .SH SYNOPSIS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw-series.1 
new/git-pw-1.9.0/man/git-pw-series.1
--- old/git-pw-1.8.1/man/git-pw-series.1        2020-03-31 18:06:27.000000000 
+0200
+++ new/git-pw-1.9.0/man/git-pw-series.1        2020-04-18 01:09:43.000000000 
+0200
@@ -1,4 +1,4 @@
-.TH "GIT-PW SERIES" "1" "08-Dec-2019" "1.8.0" "git-pw series Manual"
+.TH "GIT-PW SERIES" "1" "2020-04-17" "1.9.0" "git-pw series Manual"
 .SH NAME
 git-pw\-series \- Interact with series.
 .SH SYNOPSIS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/man/git-pw.1 
new/git-pw-1.9.0/man/git-pw.1
--- old/git-pw-1.8.1/man/git-pw.1       2020-03-31 18:06:27.000000000 +0200
+++ new/git-pw-1.9.0/man/git-pw.1       2020-04-18 01:09:43.000000000 +0200
@@ -1,4 +1,4 @@
-.TH "GIT-PW" "1" "08-Dec-2019" "1.8.0" "git-pw Manual"
+.TH "GIT-PW" "1" "2020-04-17" "1.9.0" "git-pw Manual"
 .SH NAME
 git-pw \- git-pw is a tool for integrating Git with...
 .SH SYNOPSIS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/git-pw-1.8.1/releasenotes/notes/bundle-crud-47aadae6eb7a20ad.yaml 
new/git-pw-1.9.0/releasenotes/notes/bundle-crud-47aadae6eb7a20ad.yaml
--- old/git-pw-1.8.1/releasenotes/notes/bundle-crud-47aadae6eb7a20ad.yaml       
1970-01-01 01:00:00.000000000 +0100
+++ new/git-pw-1.9.0/releasenotes/notes/bundle-crud-47aadae6eb7a20ad.yaml       
2020-04-18 01:09:43.000000000 +0200
@@ -0,0 +1,16 @@
+---
+features:
+  - |
+    The following ``bundle`` commands have been added:
+
+    - ``bundle create``
+    - ``bundle update``
+    - ``bundle delete``
+    - ``bundle add``
+    - ``bundle remove``
+
+    Together, these allow for creation, modification and deletion of bundles.
+    Bundles are custom, user-defined groups of patches that can be used to keep
+    patch lists, preserving order, for future inclusion in a tree.
+
+    These commands require API v1.2.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/git-pw-1.8.1/releasenotes/notes/python-2-deprecation-c87e311384eab29b.yaml 
new/git-pw-1.9.0/releasenotes/notes/python-2-deprecation-c87e311384eab29b.yaml
--- 
old/git-pw-1.8.1/releasenotes/notes/python-2-deprecation-c87e311384eab29b.yaml  
    1970-01-01 01:00:00.000000000 +0100
+++ 
new/git-pw-1.9.0/releasenotes/notes/python-2-deprecation-c87e311384eab29b.yaml  
    2020-04-18 01:09:43.000000000 +0200
@@ -0,0 +1,5 @@
+---
+other:
+  - |
+    *git-pw* 1.9.0 will be the last version to support Python 2.7. *git-pw*
+    2.0.0 will require Python 3.5 or greater.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/tests/test_bundle.py 
new/git-pw-1.9.0/tests/test_bundle.py
--- old/git-pw-1.8.1/tests/test_bundle.py       2020-03-31 18:06:27.000000000 
+0200
+++ new/git-pw-1.9.0/tests/test_bundle.py       2020-04-18 01:09:43.000000000 
+0200
@@ -1,6 +1,7 @@
 import unittest
 
 from click.testing import CliRunner as CLIRunner
+from click import utils as click_utils
 import mock
 
 from git_pw import bundle
@@ -94,10 +95,9 @@
 
 @mock.patch('git_pw.bundle._get_bundle')
 @mock.patch('git_pw.api.download')
[email protected]('git_pw.api.get')
 class DownloadTestCase(unittest.TestCase):
 
-    def test_download(self, mock_get, mock_download, mock_get_bundle):
+    def test_download(self, mock_download, mock_get_bundle):
         """Validate standard behavior."""
 
         rsp = {'mbox': 'http://example.com/api/patches/123/mbox/'}
@@ -109,33 +109,24 @@
 
         assert result.exit_code == 0, result
         mock_get_bundle.assert_called_once_with('123')
-        mock_download.assert_called_once_with(rsp['mbox'])
-        mock_get.assert_not_called()
+        mock_download.assert_called_once_with(rsp['mbox'], output=None)
 
-    def test_download_to_file(self, mock_get, mock_download, mock_get_bundle):
+    def test_download_to_file(self, mock_download, mock_get_bundle):
         """Validate downloading to a file."""
 
-        class MockResponse(object):
-            @property
-            def content(self):
-                return b'alpha-beta'
-
         rsp = {'mbox': 'http://example.com/api/patches/123/mbox/'}
         mock_get_bundle.return_value = rsp
-        mock_get.return_value = MockResponse()
 
         runner = CLIRunner()
-        with runner.isolated_filesystem():
-            result = runner.invoke(bundle.download_cmd, ['123', 'test.patch'])
+        result = runner.invoke(bundle.download_cmd, ['123', 'test.patch'])
 
-            assert result.exit_code == 0, result
-
-            with open('test.patch') as output:
-                assert ['alpha-beta'] == output.readlines()
+        assert result.exit_code == 0, result
 
         mock_get_bundle.assert_called_once_with('123')
-        mock_get.assert_called_once_with(rsp['mbox'])
-        mock_download.assert_not_called()
+        mock_download.assert_called_once_with(rsp['mbox'], output=mock.ANY)
+        assert isinstance(
+            mock_download.call_args[1]['output'], click_utils.LazyFile,
+        )
 
 
 class ShowTestCase(unittest.TestCase):
@@ -154,7 +145,17 @@
             'project': {
                 'name': 'bar',
             },
-            'patches': [],
+            'patches': [
+                {
+                    'id': 42,
+                    'date': '2017-01-01 00:00:00',
+                    'web_url': 'https://example.com/project/foo/patch/123/',
+                    'msgid': '<[email protected]>',
+                    'list_archive_url': None,
+                    'name': 'Test',
+                    'mbox': 'https://example.com/project/foo/patch/123/mbox/',
+                },
+            ],
             'public': True,
         }
 
@@ -317,3 +318,246 @@
 
         # We shouldn't see a warning about multiple versions either
         assert not mock_log.warning.called
+
+
[email protected]('git_pw.api.version', return_value=(1, 2))
[email protected]('git_pw.api.create')
[email protected]('git_pw.utils.echo_via_pager')
+class CreateTestCase(unittest.TestCase):
+
+    @staticmethod
+    def _get_bundle(**kwargs):
+        return ShowTestCase._get_bundle(**kwargs)
+
+    def test_create(self, mock_echo, mock_create, mock_version):
+        """Validate standard behavior."""
+
+        mock_create.return_value = self._get_bundle()
+
+        runner = CLIRunner()
+        result = runner.invoke(bundle.create_cmd, ['hello', '1', '2'])
+
+        assert result.exit_code == 0, result
+        mock_create.assert_called_once_with(
+            'bundles',
+            [('name', 'hello'), ('patches', (1, 2)), ('public', False)]
+        )
+
+    def test_create_with_public(self, mock_echo, mock_create, mock_version):
+        """Validate behavior with --public option."""
+
+        mock_create.return_value = self._get_bundle()
+
+        runner = CLIRunner()
+        result = runner.invoke(bundle.create_cmd, [
+            'hello', '1', '2', '--public'])
+
+        assert result.exit_code == 0, result
+        mock_create.assert_called_once_with(
+            'bundles',
+            [('name', 'hello'), ('patches', (1, 2)), ('public', True)]
+        )
+
+    @mock.patch('git_pw.api.LOG')
+    def test_create_api_v1_1(
+        self, mock_log, mock_echo, mock_create, mock_version
+    ):
+
+        mock_version.return_value = (1, 1)
+
+        runner = CLIRunner()
+        result = runner.invoke(bundle.create_cmd, ['hello', '1', '2'])
+
+        assert result.exit_code == 1, result
+        assert mock_log.error.called
+
+
[email protected]('git_pw.api.version', return_value=(1, 2))
[email protected]('git_pw.api.update')
[email protected]('git_pw.api.detail')
[email protected]('git_pw.utils.echo_via_pager')
+class UpdateTestCase(unittest.TestCase):
+
+    @staticmethod
+    def _get_bundle(**kwargs):
+        return ShowTestCase._get_bundle(**kwargs)
+
+    def test_update(self, mock_echo, mock_detail, mock_update, mock_version):
+        """Validate standard behavior."""
+
+        mock_update.return_value = self._get_bundle()
+
+        runner = CLIRunner()
+        result = runner.invoke(
+            bundle.update_cmd,
+            ['1', '--name', 'hello', '--patch', '1', '--patch', '2'],
+        )
+
+        assert result.exit_code == 0, result
+        mock_detail.assert_not_called()
+        mock_update.assert_called_once_with(
+            'bundles', '1', [('name', 'hello'), ('patches', (1, 2))]
+        )
+
+    def test_update_with_public(
+        self, mock_echo, mock_detail, mock_update, mock_version,
+    ):
+        """Validate behavior with --public option."""
+
+        mock_update.return_value = self._get_bundle()
+
+        runner = CLIRunner()
+        result = runner.invoke(bundle.update_cmd, ['1', '--public'])
+
+        assert result.exit_code == 0, result
+        mock_detail.assert_not_called()
+        mock_update.assert_called_once_with('bundles', '1', [('public', True)])
+
+    @mock.patch('git_pw.api.LOG')
+    def test_update_api_v1_1(
+        self, mock_log, mock_echo, mock_detail, mock_update, mock_version,
+    ):
+
+        mock_version.return_value = (1, 1)
+
+        runner = CLIRunner()
+        result = runner.invoke(bundle.update_cmd, ['1', '--name', 'hello'])
+
+        assert result.exit_code == 1, result
+        assert mock_log.error.called
+
+
[email protected]('git_pw.api.version', return_value=(1, 2))
[email protected]('git_pw.api.delete')
[email protected]('git_pw.utils.echo_via_pager')
+class DeleteTestCase(unittest.TestCase):
+
+    def test_delete(self, mock_echo, mock_delete, mock_version):
+        """Validate standard behavior."""
+
+        mock_delete.return_value = None
+
+        runner = CLIRunner()
+        result = runner.invoke(bundle.delete_cmd, ['hello'])
+
+        assert result.exit_code == 0, result
+        mock_delete.assert_called_once_with('bundles', 'hello')
+
+    @mock.patch('git_pw.api.LOG')
+    def test_delete_api_v1_1(
+        self, mock_log, mock_echo, mock_delete, mock_version,
+    ):
+        """Validate standard behavior."""
+
+        mock_version.return_value = (1, 1)
+
+        runner = CLIRunner()
+        result = runner.invoke(bundle.delete_cmd, ['hello'])
+
+        assert result.exit_code == 1, result
+        assert mock_log.error.called
+
+
[email protected]('git_pw.api.version', return_value=(1, 2))
[email protected]('git_pw.api.update')
[email protected]('git_pw.api.detail')
[email protected]('git_pw.utils.echo_via_pager')
+class AddTestCase(unittest.TestCase):
+
+    @staticmethod
+    def _get_bundle(**kwargs):
+        return ShowTestCase._get_bundle(**kwargs)
+
+    def test_add(
+        self, mock_echo, mock_detail, mock_update, mock_version,
+    ):
+        """Validate standard behavior."""
+
+        mock_detail.return_value = self._get_bundle()
+        mock_update.return_value = self._get_bundle()
+
+        runner = CLIRunner()
+        result = runner.invoke(bundle.add_cmd, ['1', '1', '2'])
+
+        assert result.exit_code == 0, result
+        mock_detail.assert_called_once_with('bundles', '1')
+        mock_update.assert_called_once_with(
+            'bundles', '1', [('patches', (1, 2, 42))],
+        )
+
+    @mock.patch('git_pw.api.LOG')
+    def test_add_api_v1_1(
+        self, mock_log, mock_echo, mock_detail, mock_update, mock_version,
+    ):
+        """Validate behavior with API v1.1."""
+
+        mock_version.return_value = (1, 1)
+
+        runner = CLIRunner()
+        result = runner.invoke(bundle.add_cmd, ['1', '1', '2'])
+
+        assert result.exit_code == 1, result
+        assert mock_log.error.called
+
+
[email protected]('git_pw.api.version', return_value=(1, 2))
[email protected]('git_pw.api.update')
[email protected]('git_pw.api.detail')
[email protected]('git_pw.utils.echo_via_pager')
+class RemoveTestCase(unittest.TestCase):
+
+    @staticmethod
+    def _get_bundle(**kwargs):
+        return ShowTestCase._get_bundle(**kwargs)
+
+    def test_remove(
+        self, mock_echo, mock_detail, mock_update, mock_version,
+    ):
+        """Validate standard behavior."""
+
+        mock_detail.return_value = self._get_bundle(
+            patches=[{'id': 1}, {'id': 2}, {'id': 3}],
+        )
+        mock_update.return_value = self._get_bundle()
+
+        runner = CLIRunner()
+        result = runner.invoke(bundle.remove_cmd, ['1', '1', '2'])
+
+        assert result.exit_code == 0, result
+        mock_detail.assert_called_once_with('bundles', '1')
+        mock_update.assert_called_once_with(
+            'bundles', '1', [('patches', (3,))],
+        )
+
+    @mock.patch('git_pw.bundle.LOG')
+    def test_remove_empty(
+        self, mock_log, mock_echo, mock_detail, mock_update, mock_version,
+    ):
+        """Validate behavior when deleting would remove all patches."""
+
+        mock_detail.return_value = self._get_bundle(
+            patches=[{'id': 1}, {'id': 2}, {'id': 3}],
+        )
+        mock_update.return_value = self._get_bundle()
+
+        runner = CLIRunner()
+        result = runner.invoke(bundle.remove_cmd, ['1', '1', '2', '3'])
+
+        assert result.exit_code == 1, result.output
+        assert mock_log.error.called
+        mock_detail.assert_called_once_with('bundles', '1')
+        mock_update.assert_not_called()
+
+    @mock.patch('git_pw.api.LOG')
+    def test_remove_api_v1_1(
+        self, mock_log, mock_echo, mock_detail, mock_update, mock_version,
+    ):
+        """Validate behavior with API v1.1."""
+
+        mock_version.return_value = (1, 1)
+
+        runner = CLIRunner()
+        result = runner.invoke(bundle.remove_cmd, ['1', '1', '2'])
+
+        assert result.exit_code == 1, result
+        assert mock_log.error.called
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/tests/test_patch.py 
new/git-pw-1.9.0/tests/test_patch.py
--- old/git-pw-1.8.1/tests/test_patch.py        2020-03-31 18:06:27.000000000 
+0200
+++ new/git-pw-1.9.0/tests/test_patch.py        2020-04-18 01:09:43.000000000 
+0200
@@ -2,6 +2,7 @@
 
 import click
 from click.testing import CliRunner as CLIRunner
+from click import utils as click_utils
 import mock
 from packaging import version
 
@@ -92,7 +93,7 @@
 
         assert result.exit_code == 0, result
         mock_detail.assert_called_once_with('patches', 123)
-        mock_download.assert_called_once_with(rsp['mbox'])
+        mock_download.assert_called_once_with(rsp['mbox'], output=None)
         assert mock_log.info.called
 
     def test_download_diff(self, mock_log, mock_download, mock_detail):
@@ -106,42 +107,30 @@
 
         assert result.exit_code == 0, result
         mock_detail.assert_called_once_with('patches', 123)
-        mock_download.assert_called_once_with(rsp['mbox'].replace(
-            'mbox', 'raw'))
+        mock_download.assert_called_once_with(
+            rsp['mbox'].replace('mbox', 'raw'), output=None,
+        )
         assert mock_log.info.called
 
-    @mock.patch('git_pw.api.get')
-    def test_download_to_file(self, mock_get, mock_log, mock_download,
-                              mock_detail):
+    def test_download_to_file(self, mock_log, mock_download, mock_detail):
         """Validate behavior if downloading to a specific file."""
 
-        class MockResponse(object):
-            @property
-            def content(self):
-                return b'alpha-beta'
-
         rsp = {'mbox': 'hello, world', 'diff': 'test'}
         mock_detail.return_value = rsp
-        mock_get.return_value = MockResponse()
 
         runner = CLIRunner()
-        with runner.isolated_filesystem():
-            result = runner.invoke(patch.download_cmd,
-                                   ['123', 'test.patch'])
+        result = runner.invoke(patch.download_cmd, ['123', 'test.patch'])
 
-            assert result.exit_code == 0, result
-
-            with open('test.patch') as output:
-                assert ['alpha-beta'] == output.readlines()
+        assert result.exit_code == 0, result
 
         mock_detail.assert_called_once_with('patches', 123)
-        mock_download.assert_not_called()
-        mock_get.assert_called_once_with(rsp['mbox'])
+        mock_download.assert_called_once_with(rsp['mbox'], output=mock.ANY)
+        assert isinstance(
+            mock_download.call_args[1]['output'], click_utils.LazyFile,
+        )
         assert mock_log.info.called
 
-    @mock.patch('git_pw.api.get')
-    def test_download_diff_to_file(self, mock_get, mock_log, mock_download,
-                                   mock_detail):
+    def test_download_diff_to_file(self, mock_log, mock_download, mock_detail):
         """Validate behavior if downloading a diff to a specific file."""
 
         rsp = {'mbox': 'hello, world', 'diff': b'test'}
@@ -159,7 +148,6 @@
 
         mock_detail.assert_called_once_with('patches', 123)
         mock_download.assert_not_called()
-        mock_get.assert_not_called()
         assert mock_log.info.called
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/tests/test_series.py 
new/git-pw-1.9.0/tests/test_series.py
--- old/git-pw-1.8.1/tests/test_series.py       2020-03-31 18:06:27.000000000 
+0200
+++ new/git-pw-1.9.0/tests/test_series.py       2020-04-18 01:09:43.000000000 
+0200
@@ -1,6 +1,7 @@
 import unittest
 
 from click.testing import CliRunner as CLIRunner
+from click import utils as click_utils
 import mock
 
 from git_pw import series
@@ -45,48 +46,36 @@
 
 @mock.patch('git_pw.api.detail')
 @mock.patch('git_pw.api.download')
[email protected]('git_pw.api.get')
 class DownloadTestCase(unittest.TestCase):
 
-    def test_download(self, mock_get, mock_download, mock_detail):
+    def test_download(self, mock_download, mock_detail):
         """Validate standard behavior."""
 
         rsp = {'mbox': 'http://example.com/api/patches/123/mbox/'}
         mock_detail.return_value = rsp
-        mock_download.return_value = 'test.patch'
 
         runner = CLIRunner()
         result = runner.invoke(series.download_cmd, ['123'])
 
         assert result.exit_code == 0, result
         mock_detail.assert_called_once_with('series', 123)
-        mock_download.assert_called_once_with(rsp['mbox'])
-        mock_get.assert_not_called()
+        mock_download.assert_called_once_with(rsp['mbox'], output=None)
 
-    def test_download_to_file(self, mock_get, mock_download, mock_detail):
+    def test_download_to_file(self, mock_download, mock_detail):
         """Validate downloading to a file."""
 
-        class MockResponse(object):
-            @property
-            def content(self):
-                return b'alpha-beta'
-
         rsp = {'mbox': 'http://example.com/api/patches/123/mbox/'}
         mock_detail.return_value = rsp
-        mock_get.return_value = MockResponse()
 
         runner = CLIRunner()
-        with runner.isolated_filesystem():
-            result = runner.invoke(series.download_cmd, ['123', 'test.patch'])
-
-            assert result.exit_code == 0, result
-
-            with open('test.patch') as output:
-                assert ['alpha-beta'] == output.readlines()
+        result = runner.invoke(series.download_cmd, ['123', 'test.patch'])
 
+        assert result.exit_code == 0, result
         mock_detail.assert_called_once_with('series', 123)
-        mock_get.assert_called_once_with(rsp['mbox'])
-        mock_download.assert_not_called()
+        mock_download.assert_called_once_with(rsp['mbox'], output=mock.ANY)
+        assert isinstance(
+            mock_download.call_args[1]['output'], click_utils.LazyFile,
+        )
 
 
 class ShowTestCase(unittest.TestCase):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/git-pw-1.8.1/tox.ini new/git-pw-1.9.0/tox.ini
--- old/git-pw-1.8.1/tox.ini    2020-03-31 18:06:27.000000000 +0200
+++ new/git-pw-1.9.0/tox.ini    2020-04-18 01:09:43.000000000 +0200
@@ -47,7 +47,7 @@
 
 [testenv:man]
 deps =
-  click-man~=0.3.0
+  click-man~=0.4.0
 commands =
   click-man git-pw
 


Reply via email to