diff --git a/git_pw/api.py b/git_pw/api.py index c62a352..0a90072 100644 --- a/git_pw/api.py +++ b/git_pw/api.py @@ -232,6 +232,11 @@ def version() -> ty.Tuple[int, int]: return (1, 0) +def get(url: str, params: ty.Optional[Filters]) -> ty.Dict: + """Get a JSON document from the API and return it as a dict.""" + return _get(url, params, stream=False).json() + + def download( url: str, params: ty.Optional[Filters] = None, diff --git a/git_pw/series.py b/git_pw/series.py index 1a46bc7..b121c8e 100644 --- a/git_pw/series.py +++ b/git_pw/series.py @@ -25,8 +25,15 @@ ), ) @click.argument('series_id', type=click.INT) +@click.option( + '--deps/--no-deps', + 'deps', + default=False, + help='Download any dependencies this series may have, and apply them' + 'first.', +) @click.argument('args', nargs=-1, type=click.UNPROCESSED) -def apply_cmd(series_id, args): +def apply_cmd(series_id, args, deps): """Apply series. Apply a series locally using the 'git-am' command. Any additional ARGS @@ -35,9 +42,31 @@ def apply_cmd(series_id, args): LOG.debug('Applying series: id=%d, args=%s', series_id, ' '.join(args)) series = api.detail('series', series_id) - mbox = api.download(series['mbox']) - utils.git_am(mbox, args) + # .mbox files are applied in the order they appear in this list. + to_apply = [] + + if deps: + if dependencies := series.get('dependencies'): + to_apply.extend( + map(lambda url: api.get(url)['mbox'], dependencies) + ) + else: + # Notify the user that dependency information could not be found. + LOG.warning( + "Dependency information was not found for this series." + ) + LOG.warning( + "Either dependencies are unsupported by this Patchwork" + "server or the feature is disabled for this project." + ) + LOG.warning("No dependencies will be applied.") + + to_apply.append(series['mbox']) + + for mbox_url in to_apply: + mbox = api.download(mbox_url) + utils.git_am(mbox, args) @click.command(name='download') diff --git a/tests/test_series.py b/tests/test_series.py index 4dd6a5e..5eea227 100644 --- a/tests/test_series.py +++ b/tests/test_series.py @@ -6,11 +6,14 @@ from git_pw import series +@mock.patch('git_pw.api.get') @mock.patch('git_pw.api.detail') @mock.patch('git_pw.api.download') @mock.patch('git_pw.utils.git_am') class ApplyTestCase(unittest.TestCase): - def test_apply_without_args(self, mock_git_am, mock_download, mock_detail): + def test_apply_without_args( + self, mock_git_am, mock_download, mock_detail, mock_api_get + ): """Validate calling with no arguments.""" rsp = {'mbox': 'http://example.com/api/patches/123/mbox/'} @@ -25,7 +28,9 @@ def test_apply_without_args(self, mock_git_am, mock_download, mock_detail): mock_download.assert_called_once_with(rsp['mbox']) mock_git_am.assert_called_once_with(mock_download.return_value, ()) - def test_apply_with_args(self, mock_git_am, mock_download, mock_detail): + def test_apply_with_args( + self, mock_git_am, mock_download, mock_detail, mock_api_get + ): """Validate passthrough of arbitrary arguments to git-am.""" rsp = {'mbox': 'http://example.com/api/patches/123/mbox/'} @@ -42,6 +47,63 @@ def test_apply_with_args(self, mock_git_am, mock_download, mock_detail): mock_download.return_value, ('-3',) ) + def test_apply_with_deps_unsupported( + self, mock_git_am, mock_download, mock_detail, mock_api_get + ): + """ + Validate that a series is applied when dependencies are + requested and dependencies do not appear in the API. + """ + + rsp = {'mbox': 'http://example.com/api/series/123/mbox/'} + mock_detail.return_value = rsp + mock_download.return_value = 'test.patch' + + runner = CLIRunner() + result = runner.invoke(series.apply_cmd, ['123', '--deps']) + + assert result.exit_code == 0, result + mock_detail.assert_called_once_with('series', 123) + mock_download.assert_called_once_with(rsp['mbox']) + mock_git_am.assert_called_once_with(mock_download.return_value, ()) + + def test_apply_with_deps( + self, mock_git_am, mock_download, mock_detail, mock_api_get + ): + """Validate that dependencies are applied when flag is given.""" + dep_ids = [ + '120', + '121', + '122', + ] + dependencies = list( + map(lambda x: f"http://example.com/api/series/{x}/", dep_ids) + ) + dep_details = list(map(lambda x: {"mbox": f"{x}mbox/"}, dependencies)) + mboxes = list(map(lambda x: x["mbox"], dep_details)) + mboxes.append('http://example.com/api/series/123/mbox/') + files = list(map(lambda x: f"series_{x}.mbox", [*dep_ids, '123'])) + + rsp_base = { + 'mbox': 'http://example.com/api/series/123/mbox/', + 'dependencies': dependencies, + } + + mock_detail.return_value = rsp_base + mock_api_get.side_effect = dep_details + mock_download.side_effect = files + + runner = CLIRunner() + result = runner.invoke(series.apply_cmd, ['123', '--deps']) + + assert result.exit_code == 0, result + mock_detail.assert_called_once_with('series', 123) + mock_api_get.assert_has_calls( + map(lambda x: mock.call(x), dependencies) + ) + mock_download.assert_has_calls(map(lambda x: mock.call(x), mboxes)) + mock_git_am.assert_has_calls(map(lambda x: mock.call(x, ()), files)) + @mock.patch('git_pw.api.detail') @mock.patch('git_pw.api.download') @@ -125,6 +187,8 @@ def _get_series(**kwargs): 'received_all': True, 'cover_letter': None, 'patches': [], + 'dependencies': [], + 'dependents': [], } rsp.update(**kwargs)