Skip to content

Commit 41c20c7

Browse files
Dafna Hirschfeldstephenfin
andauthored
Allow downloading of series patches to separate files
* Add '--separate' option to download each patch separately Add '--separate' and '--combined' option to download command of a series to download each patch in a separate file or all into one file. 'combined' is the default. * Change the allow 'output' arg to be a directory when downloading. In the download commands, allow 'output' argument to be a directory. In that case, the file name will be set by the patch/series/bundle subject and wiil be downloaded into the given 'output' directory. Signed-off-by: Dafna Hirschfeld <dafna.hirschfeld@collabora.com> Co-authored-by: Stephen Finucane <stephen@that.guru>
1 parent e45cc67 commit 41c20c7

File tree

7 files changed

+111
-39
lines changed

7 files changed

+111
-39
lines changed

git_pw/api.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from functools import update_wrapper
66
import logging
77
import os.path
8-
import pty
98
import re
109
import sys
1110
import tempfile
@@ -209,7 +208,7 @@ def version() -> ty.Tuple[int, int]:
209208

210209

211210
def download(
212-
url: str, params: Filters = None, output: ty.IO = None,
211+
url: str, params: Filters = None, output: ty.Optional[str] = None,
213212
) -> ty.Optional[str]:
214213
"""Retrieve a specific API resource and save it to a file/stdout.
215214
@@ -219,8 +218,10 @@ def download(
219218
Arguments:
220219
url: The resource URL.
221220
params: Additional parameters.
222-
output: The output file. If provided, the caller is responsible for
223-
closing. If None, a temporary file will be used.
221+
output: The output file. If output is a directory then
222+
the file name will be according to the patch subject and
223+
will be downloaded into the output directory.
224+
If None, a temporary file will be used.
224225
225226
Returns:
226227
A path to an output file containing the content, else None if stdout
@@ -236,24 +237,27 @@ def download(
236237
LOG.error('Filename was expected but was not provided in response')
237238
sys.exit(1)
238239

239-
if output:
240-
output_path = None
241-
if output.fileno() != pty.STDOUT_FILENO:
242-
LOG.debug('Saving to %s', output.name)
243-
output_path = output.name
240+
if output == '-':
241+
output_path = output
242+
output_file = sys.stdout.buffer
243+
else:
244+
if output:
245+
output_path = output
246+
if os.path.isdir(output):
247+
output_path = os.path.join(output, header.group(1))
248+
else:
249+
output_path = os.path.join(
250+
tempfile.mkdtemp(prefix='git-pw'), header.group(1),
251+
)
252+
LOG.debug('Saving to %s', output_path)
253+
output_file = open(output_path, 'wb')
244254

255+
try:
245256
# we use iter_content because patches can be binary
246257
for block in rsp.iter_content(1024):
247-
output.write(block)
248-
else:
249-
output_path = os.path.join(
250-
tempfile.mkdtemp(prefix='git-pw'), header.group(1),
251-
)
252-
with open(output_path, 'wb') as output_file:
253-
LOG.debug('Saving to %s', output_path)
254-
# we use iter_content because patches can be binary
255-
for block in rsp.iter_content(1024):
256-
output_file.write(block)
258+
output_file.write(block)
259+
finally:
260+
output_file.close()
257261

258262
return output_path
259263

git_pw/bundle.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,17 @@ def apply_cmd(bundle_id: str, args: ty.Tuple[str]) -> None:
6060

6161
@click.command(name='download')
6262
@click.argument('bundle_id')
63-
@click.argument('output', type=click.File('wb'), required=False)
64-
def download_cmd(bundle_id: str, output: ty.IO) -> None:
63+
@click.argument(
64+
'output',
65+
type=click.Path(file_okay=True, writable=True, readable=True),
66+
required=False,
67+
)
68+
def download_cmd(bundle_id: str, output: ty.Optional[str]) -> None:
6569
"""Download bundle in mbox format.
6670
6771
Download a bundle but do not apply it. ``OUTPUT`` is optional and can be an
68-
output path or ``-`` to output to ``stdout``. If ``OUTPUT`` is not
69-
provided, the output path will be automatically chosen.
72+
output full file path or a directory or ``-`` to output to ``stdout``. If
73+
``OUTPUT`` is not provided, the output path will be automatically chosen.
7074
"""
7175
LOG.debug('Downloading bundle: id=%s', bundle_id)
7276

git_pw/patch.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
"""
44

55
import logging
6-
import pty
76
import sys
7+
import os
88

99
import arrow
1010
import click
@@ -58,7 +58,11 @@ def apply_cmd(patch_id, series, deps, args):
5858

5959
@click.command(name='download')
6060
@click.argument('patch_id', type=click.INT)
61-
@click.argument('output', type=click.File('wb'), required=False)
61+
@click.argument(
62+
'output',
63+
type=click.Path(file_okay=True, writable=True, readable=True),
64+
required=False
65+
)
6266
@click.option('--diff', 'fmt', flag_value='diff',
6367
help='Show patch in diff format.')
6468
@click.option('--mbox', 'fmt', flag_value='mbox', default=True,
@@ -67,19 +71,24 @@ def download_cmd(patch_id, output, fmt):
6771
"""Download patch in diff or mbox format.
6872
6973
Download a patch but do not apply it. ``OUTPUT`` is optional and can be an
70-
output path or ``-`` to output to ``stdout``. If ``OUTPUT`` is not
71-
provided, the output path will be automatically chosen.
74+
output file path or a directory or ``-`` to output to ``stdout``. If
75+
``OUTPUT`` is not provided, the output path will be automatically chosen.
7276
"""
7377
LOG.debug('Downloading patch: id=%d, format=%s', patch_id, fmt)
7478

7579
path = None
7680
patch = api.detail('patches', patch_id)
7781

7882
if fmt == 'diff':
79-
if output:
80-
output.write(patch['diff'])
81-
if output.fileno() != pty.STDOUT_FILENO:
82-
path = output.name
83+
if output and not os.path.isdir(output):
84+
if output == '-':
85+
output_path = 0 # stdout fd
86+
else:
87+
output_path = output
88+
path = output
89+
90+
with open(output_path, 'w') as output_file:
91+
output_file.write(utils.ensure_str(patch['diff']))
8392
else:
8493
# TODO(stephenfin): We discard the 'diff' field so we can get the
8594
# filename and save to the correct file. We should expose this

git_pw/series.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
from git_pw import api
1111
from git_pw import utils
12+
import os.path
13+
import sys
1214

1315
LOG = logging.getLogger(__name__)
1416

@@ -37,8 +39,20 @@ def apply_cmd(series_id, args):
3739

3840
@click.command(name='download')
3941
@click.argument('series_id', type=click.INT)
40-
@click.argument('output', type=click.File('wb'), required=False)
41-
def download_cmd(series_id, output):
42+
@click.argument(
43+
'output',
44+
type=click.Path(file_okay=True, writable=True, readable=True),
45+
required=False,
46+
)
47+
@click.option(
48+
'--separate', 'fmt', flag_value='separate',
49+
help='Download each series patch to a separate file',
50+
)
51+
@click.option(
52+
'--combined', 'fmt', flag_value='combined', default=True,
53+
help='Download all series patches to one file',
54+
)
55+
def download_cmd(series_id, output, fmt):
4256
"""Download series in mbox format.
4357
4458
Download a series but do not apply it. ``OUTPUT`` is optional and can be an
@@ -50,6 +64,23 @@ def download_cmd(series_id, output):
5064
path = None
5165
series = api.detail('series', series_id)
5266

67+
if fmt == 'separate':
68+
if output and not os.path.isdir(output):
69+
LOG.error(
70+
'When downloading into separate files, OUTPUT can only be a '
71+
'directoy'
72+
)
73+
sys.exit(1)
74+
75+
for patch in series.get('patches'):
76+
path = api.download(patch['mbox'], output=output)
77+
if path:
78+
LOG.info(
79+
'Downloaded patch %s from series %s to %s',
80+
patch.get('id'), series.get('id'), path,
81+
)
82+
return
83+
5384
path = api.download(series['mbox'], output=output)
5485

5586
if path:

tests/test_bundle.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import unittest
22

33
from click.testing import CliRunner as CLIRunner
4-
from click import utils as click_utils
54
import mock
65

76
from git_pw import bundle
@@ -125,7 +124,7 @@ def test_download_to_file(self, mock_download, mock_get_bundle):
125124
mock_get_bundle.assert_called_once_with('123')
126125
mock_download.assert_called_once_with(rsp['mbox'], output=mock.ANY)
127126
assert isinstance(
128-
mock_download.call_args[1]['output'], click_utils.LazyFile,
127+
mock_download.call_args[1]['output'], str,
129128
)
130129

131130

tests/test_patch.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import click
44
from click.testing import CliRunner as CLIRunner
5-
from click import utils as click_utils
65
import mock
76
from packaging import version
87

@@ -126,7 +125,7 @@ def test_download_to_file(self, mock_log, mock_download, mock_detail):
126125
mock_detail.assert_called_once_with('patches', 123)
127126
mock_download.assert_called_once_with(rsp['mbox'], output=mock.ANY)
128127
assert isinstance(
129-
mock_download.call_args[1]['output'], click_utils.LazyFile,
128+
mock_download.call_args[1]['output'], str,
130129
)
131130
assert mock_log.info.called
132131

tests/test_series.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import unittest
22

33
from click.testing import CliRunner as CLIRunner
4-
from click import utils as click_utils
54
import mock
65

76
from git_pw import series
@@ -74,7 +73,34 @@ def test_download_to_file(self, mock_download, mock_detail):
7473
mock_detail.assert_called_once_with('series', 123)
7574
mock_download.assert_called_once_with(rsp['mbox'], output=mock.ANY)
7675
assert isinstance(
77-
mock_download.call_args[1]['output'], click_utils.LazyFile,
76+
mock_download.call_args[1]['output'], str,
77+
)
78+
79+
def test_download_separate_to_dir(self, mock_download, mock_detail):
80+
"""Validate downloading seperate to a directory."""
81+
82+
rsp = {
83+
'mbox': 'http://example.com/api/patches/123/mbox/',
84+
'patches': [
85+
{
86+
'id': 10539359,
87+
'mbox': 'https://example.com/project/foo/patch/123/mbox/',
88+
}
89+
90+
]
91+
}
92+
mock_detail.return_value = rsp
93+
94+
runner = CLIRunner()
95+
result = runner.invoke(series.download_cmd, ['123', '--separate', '.'])
96+
97+
assert result.exit_code == 0, result
98+
mock_detail.assert_called_once_with('series', 123)
99+
mock_download.assert_called_once_with(
100+
rsp['patches'][0]['mbox'], output=mock.ANY,
101+
)
102+
assert isinstance(
103+
mock_download.call_args[1]['output'], str,
78104
)
79105

80106

0 commit comments

Comments
 (0)