diff --git a/swh/web/ui/backend.py b/swh/web/ui/backend.py --- a/swh/web/ui/backend.py +++ b/swh/web/ui/backend.py @@ -166,6 +166,25 @@ return None +def revision_get_multiple(sha1_git_bin_list): + """Return information about the revisions in sha1_git_bin_list + + Args: + sha1_git_bin_list: The revisions' sha1s as a list of bytes. + + Returns: + Revisions' information as an iterable of dicts if any found, + an empty list otherwise + + Raises: + ValueError if the identifier provided is not of sha1 nature. + """ + res = main.storage().revision_get(sha1_git_bin_list) + if res and len(res) >= 1: + return res + return [] + + def revision_log(sha1_git_bin, limit=100): """Return information about the revision with sha1 sha1_git_bin. diff --git a/swh/web/ui/service.py b/swh/web/ui/service.py --- a/swh/web/ui/service.py +++ b/swh/web/ui/service.py @@ -233,6 +233,31 @@ return converters.from_revision(revision) +def lookup_revision_multiple(sha1_git_list): + """Return information about the revision with sha1 revision_sha1_git. + + Args: + revision_sha1_git: The revision's sha1 as hexadecimal + + Returns: + Revision information as dict. + + Raises: + ValueError if the identifier provided is not of sha1 nature. + + """ + def to_sha1_bin(sha1_hex): + _, sha1_git_bin = query.parse_hash_with_algorithms_or_throws( + sha1_hex, + ['sha1'], + 'Only sha1_git is supported.') + return sha1_git_bin + + sha1_bin_list = map(to_sha1_bin, sha1_git_list) + revisions = backend.revision_get_multiple(sha1_bin_list) + return map(converters.from_revision, revisions) + + def lookup_revision_message(rev_sha1_git): """Return the raw message of the revision with sha1 revision_sha1_git. diff --git a/swh/web/ui/tests/test_backend.py b/swh/web/ui/tests/test_backend.py --- a/swh/web/ui/tests/test_backend.py +++ b/swh/web/ui/tests/test_backend.py @@ -417,6 +417,92 @@ self.storage.revision_get.assert_called_with([sha1_bin]) @istest + def revision_get_multiple(self): + # given + sha1_bin = hashutil.hex_to_hash( + '18d8be353ed3480476f032475e7c233eff7371d5') + sha1_other = hashutil.hex_to_hash( + 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc') + + stub_revisions = [ + { + 'id': sha1_bin, + 'directory': hashutil.hex_to_hash( + '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6'), + 'author': { + 'name': b'bill & boule', + 'email': b'bill@boule.org', + }, + 'committer': { + 'name': b'boule & bill', + 'email': b'boule@bill.org', + }, + 'message': b'elegant fix for bug 31415957', + 'date': datetime.datetime(2000, 1, 17, 11, 23, 54), + 'date_offset': 0, + 'committer_date': datetime.datetime(2000, 1, 17, 11, 23, 54), + 'committer_date_offset': 0, + 'synthetic': False, + 'type': 'git', + 'parents': [], + 'metadata': [], + }, + { + 'id': sha1_other, + 'directory': hashutil.hex_to_hash( + '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6'), + 'author': { + 'name': b'name', + 'email': b'name@surname.org', + }, + 'committer': { + 'name': b'name', + 'email': b'name@surname.org', + }, + 'message': b'ugly fix for bug 42', + 'date': datetime.datetime(2000, 1, 12, 5, 23, 54), + 'date_offset': 0, + 'committer_date': datetime.datetime(2000, 1, 12, 5, 23, 54), + 'committer_date_offset': 0, + 'synthetic': False, + 'type': 'git', + 'parents': [], + 'metadata': [], + } + ] + self.storage.revision_get = MagicMock( + return_value=stub_revisions) + + # when + actual_revision = backend.revision_get_multiple([sha1_bin, sha1_other]) + + # then + self.assertEqual(actual_revision, stub_revisions) + + self.storage.revision_get.assert_called_with( + [sha1_bin, sha1_other]) + + @istest + def revision_get_multiple_none_found(self): + # given + sha1_bin = hashutil.hex_to_hash( + '18d8be353ed3480476f032475e7c233eff7371d5') + sha1_other = hashutil.hex_to_hash( + 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc') + + self.storage.revision_get = MagicMock( + return_value=[]) + + # when + actual_revision = backend.revision_get_multiple([sha1_bin, sha1_other]) + + # then + self.assertEqual(actual_revision, []) + + self.storage.revision_get.assert_called_with( + [sha1_bin, sha1_other]) + + @istest def revision_log(self): # given sha1_bin = hashutil.hex_to_hash( diff --git a/swh/web/ui/tests/test_service.py b/swh/web/ui/tests/test_service.py --- a/swh/web/ui/tests/test_service.py +++ b/swh/web/ui/tests/test_service.py @@ -1167,6 +1167,164 @@ @patch('swh.web.ui.service.backend') @istest + def lookup_revision_multiple(self, mock_backend): + # given + + sha1_bin = '18d8be353ed3480476f032475e7c233eff7371d5' + sha1_other = 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc' + + stub_revisions = [ + { + 'id': hex_to_hash(sha1_bin), + 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', + 'author': { + 'name': b'bill & boule', + 'email': b'bill@boule.org', + }, + 'committer': { + 'name': b'boule & bill', + 'email': b'boule@bill.org', + }, + 'message': b'elegant fix for bug 31415957', + 'date': { + 'timestamp': datetime.datetime( + 2000, 1, 17, 11, 23, 54, + tzinfo=datetime.timezone.utc).timestamp(), + 'offset': 0, + 'negative_utc': False + }, + 'date_offset': 0, + 'committer_date': { + 'timestamp': datetime.datetime( + 2000, 1, 17, 11, 23, 54, + tzinfo=datetime.timezone.utc).timestamp(), + 'offset': 0, + 'negative_utc': False + }, + 'committer_date_offset': 0, + 'synthetic': False, + 'type': 'git', + 'parents': [], + 'metadata': [], + }, + { + 'id': hex_to_hash(sha1_other), + 'directory': 'abcdbe353ed3480476f032475e7c233eff7371d5', + 'author': { + 'name': b'name', + 'email': b'name@surname.org', + }, + 'committer': { + 'name': b'name', + 'email': b'name@surname.org', + }, + 'message': b'ugly fix for bug 42', + 'date': { + 'timestamp': datetime.datetime( + 2000, 1, 12, 5, 23, 54, + tzinfo=datetime.timezone.utc).timestamp(), + 'offset': 0, + 'negative_utc': False + }, + 'date_offset': 0, + 'committer_date': { + 'timestamp': datetime.datetime( + 2000, 1, 12, 5, 23, 54, + tzinfo=datetime.timezone.utc).timestamp(), + 'offset': 0, + 'negative_utc': False + }, + 'committer_date_offset': 0, + 'synthetic': False, + 'type': 'git', + 'parents': [], + 'metadata': [], + } + ] + + mock_backend.revision_get_multiple.return_value = stub_revisions + + # when + actual_revisions = service.lookup_revision_multiple( + [sha1_bin, sha1_other]) + + # then + self.assertEqual(list(actual_revisions), [ + { + 'id': sha1_bin, + 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', + 'author': { + 'name': 'bill & boule', + 'email': 'bill@boule.org', + }, + 'committer': { + 'name': 'boule & bill', + 'email': 'boule@bill.org', + }, + 'message': 'elegant fix for bug 31415957', + 'date': '2000-01-17T11:23:54+00:00', + 'date_offset': 0, + 'committer_date': '2000-01-17T11:23:54+00:00', + 'committer_date_offset': 0, + 'synthetic': False, + 'type': 'git', + 'parents': [], + 'metadata': [], + }, + { + 'id': sha1_other, + 'directory': 'abcdbe353ed3480476f032475e7c233eff7371d5', + 'author': { + 'name': 'name', + 'email': 'name@surname.org', + }, + 'committer': { + 'name': 'name', + 'email': 'name@surname.org', + }, + 'message': 'ugly fix for bug 42', + 'date': '2000-01-12T05:23:54+00:00', + 'date_offset': 0, + 'committer_date': '2000-01-12T05:23:54+00:00', + 'committer_date_offset': 0, + 'synthetic': False, + 'type': 'git', + 'parents': [], + 'metadata': [], + } + ]) + + self.assertEqual( + list(mock_backend.revision_get_multiple.call_args[0][0]), + [hex_to_hash( + '18d8be353ed3480476f032475e7c233eff7371d5'), + hex_to_hash( + 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc')]) + + @patch('swh.web.ui.service.backend') + @istest + def lookup_revision_multiple_none_found(self, mock_backend): + # given + sha1_bin = '18d8be353ed3480476f032475e7c233eff7371d5' + sha1_other = 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc' + + mock_backend.revision_get_multiple.return_value = [] + + # then + actual_revisions = service.lookup_revision_multiple( + [sha1_bin, sha1_other]) + + self.assertEqual(list(actual_revisions), []) + + self.assertEqual( + list(mock_backend.revision_get_multiple.call_args[0][0]), + [hex_to_hash( + '18d8be353ed3480476f032475e7c233eff7371d5'), + hex_to_hash( + 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc')]) + + @patch('swh.web.ui.service.backend') + @istest def lookup_revision_log(self, mock_backend): # given stub_revision_log = [{ diff --git a/swh/web/ui/tests/views/test_api.py b/swh/web/ui/tests/views/test_api.py --- a/swh/web/ui/tests/views/test_api.py +++ b/swh/web/ui/tests/views/test_api.py @@ -1461,6 +1461,104 @@ @patch('swh.web.ui.views.api.service') @istest + def api_revision_log_context(self, mock_service): + # given + stub_revisions = [{ + 'id': '18d8be353ed3480476f032475e7c233eff7371d5', + 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', + 'author_name': 'Software Heritage', + 'author_email': 'robot@softwareheritage.org', + 'committer_name': 'Software Heritage', + 'committer_email': 'robot@softwareheritage.org', + 'message': 'synthetic revision message', + 'date_offset': 0, + 'committer_date_offset': 0, + 'parents': ['7834ef7e7c357ce2af928115c6c6a42b7e2a4345'], + 'type': 'tar', + 'synthetic': True, + }] + + mock_service.lookup_revision_log.return_value = stub_revisions + mock_service.lookup_revision_multiple.return_value = [{ + 'id': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', + 'directory': '18d8be353ed3480476f032475e7c233eff7371d5', + 'author_name': 'Name Surname', + 'author_email': 'name@surname.com', + 'committer_name': 'Name Surname', + 'committer_email': 'name@surname.com', + 'message': 'amazing revision message', + 'date_offset': 0, + 'committer_date_offset': 0, + 'parents': ['adc83b19e793491b1c6ea0fd8b46cd9f32e592fc'], + 'type': 'tar', + 'synthetic': True, + }] + + # when + rv = self.app.get('/api/1/revision/18d8be353ed3480476f0' + '32475e7c233eff7371d5/prev/prev-rev/log/') + + # then + self.assertEquals(rv.status_code, 200) + self.assertEquals(rv.mimetype, 'application/json') + response_data = json.loads(rv.data.decode('utf-8')) + self.assertEquals(response_data, [ + { + 'url': '/api/1/revision/' + '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6/', + 'history_url': '/api/1/revision/' + '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6/log/', + 'id': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', + 'directory': '18d8be353ed3480476f032475e7c233eff7371d5', + 'directory_url': '/api/1/directory/' + '18d8be353ed3480476f032475e7c233eff7371d5/', + 'author_name': 'Name Surname', + 'author_email': 'name@surname.com', + 'committer_name': 'Name Surname', + 'committer_email': 'name@surname.com', + 'message': 'amazing revision message', + 'date_offset': 0, + 'committer_date_offset': 0, + 'parents': ['adc83b19e793491b1c6ea0fd8b46cd9f32e592fc'], + 'parent_urls': [ + '/api/1/revision/7834ef7e7c357ce2af928115c6c6a42b7e2a44e6' + '/history/adc83b19e793491b1c6ea0fd8b46cd9f32e592fc/' + ], + 'type': 'tar', + 'synthetic': True, + }, + { + 'url': '/api/1/revision/' + '18d8be353ed3480476f032475e7c233eff7371d5/', + 'history_url': '/api/1/revision/' + '18d8be353ed3480476f032475e7c233eff7371d5/log/', + 'id': '18d8be353ed3480476f032475e7c233eff7371d5', + 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', + 'directory_url': '/api/1/directory/' + '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6/', + 'author_name': 'Software Heritage', + 'author_email': 'robot@softwareheritage.org', + 'committer_name': 'Software Heritage', + 'committer_email': 'robot@softwareheritage.org', + 'message': 'synthetic revision message', + 'date_offset': 0, + 'committer_date_offset': 0, + 'parents': ['7834ef7e7c357ce2af928115c6c6a42b7e2a4345'], + 'parent_urls': [ + '/api/1/revision/18d8be353ed3480476f032475e7c233eff7371d5' + '/history/7834ef7e7c357ce2af928115c6c6a42b7e2a4345/' + ], + 'type': 'tar', + 'synthetic': True, + }]) + + mock_service.lookup_revision_log.assert_called_once_with( + '18d8be353ed3480476f032475e7c233eff7371d5', 100) + mock_service.lookup_revision_multiple.assert_called_once_with( + ['prev-rev']) + + @patch('swh.web.ui.views.api.service') + @istest def api_revision_log_by(self, mock_service): # given stub_revisions = [{ diff --git a/swh/web/ui/views/api.py b/swh/web/ui/views/api.py --- a/swh/web/ui/views/api.py +++ b/swh/web/ui/views/api.py @@ -689,17 +689,20 @@ @app.route('/api/1/revision//log/') -def api_revision_log(sha1_git): +@app.route('/api/1/revision//prev//log/') +def api_revision_log(sha1_git, prev_sha1s=None): """Show all revisions (~git log) starting from sha1_git. The first element returned is the given sha1_git. Args: sha1_git: the revision's hash. + prev_sha1s: the navigation breadcrumb limit: optional query parameter to limit the revisions log (default to 100). Returns: - Information on the revision if found. + Information on the revision if found, complemented with the revision's + children if we have navigation breadcrumbs for them. Raises: BadInputExc in case of unknown algo_hash or bad hash. @@ -712,10 +715,20 @@ return service.lookup_revision_log(s, limit) error_msg = 'Revision with sha1_git %s not found.' % sha1_git - return _api_lookup(sha1_git, - lookup_fn=lookup_revision_log_with_limit, - error_msg_if_not_found=error_msg, - enrich_fn=utils.enrich_revision) + rev_backward = _api_lookup(sha1_git, + lookup_fn=lookup_revision_log_with_limit, + error_msg_if_not_found=error_msg, + enrich_fn=utils.enrich_revision) + + if not prev_sha1s: # no nav breadcrumbs, so we're done + return rev_backward + + rev_forward_ids = prev_sha1s.split('/') + rev_forward = _api_lookup(rev_forward_ids, + lookup_fn=service.lookup_revision_multiple, + error_msg_if_not_found=error_msg, + enrich_fn=utils.enrich_revision) + return rev_forward + rev_backward @app.route('/api/1/revision'