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 @@ -183,6 +183,36 @@ return main.storage().revision_log([sha1_git_bin], limit) +def revision_log_by(origin_id, branch_name, ts, limit=100): + """Return information about the revision matching the timestamp + ts, from origin origin_id, in branch branch_name. + + Args: + origin_id: origin of the revision + - branch_name: revision's branch. + - timestamp: revision's time frame. + + Returns: + Information for the revision matching the criterions. + + """ + # Disable pending RemoteStorage opening revision_log_by + """ + if not ts and branch_name == 'refs/heads/master': + return main.storage().revision_log_by(origin_id) + """ + + rev = main.storage().revision_get_by(origin_id, + branch_name, + timestamp=ts, + limit=1) + if not rev: + return None + + rev_sha1s_bin = [revision['id'] for revision in rev] + return main.storage().revision_log(rev_sha1s_bin, limit) + + def stat_counters(): """Return the stat counters for Software Heritage 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 @@ -295,6 +295,35 @@ return map(converters.from_revision, revision_entries) +def lookup_revision_log_by(origin_id, branch_name, timestamp): + """Return information about the revision with sha1 revision_sha1_git. + + Args: + origin_id: origin of the revision + branch_name: revision's branch + timestamp: revision's time frame + limit: the maximum number of revisions returned + + Returns: + Revision information as dict. + + Raises: + NotFoundExc if no revision corresponds to the criterion + NotFoundExc if the corresponding revision has no log + + """ + revision_entries = backend.revision_log_by(origin_id, + branch_name, + timestamp) + error_msg = 'No revision matching origin %s ' % origin_id + error_msg += ', branch name %s' % branch_name + error_msg += (' and time stamp %s.' % timestamp) if timestamp else '.' + + if not revision_entries: + raise NotFoundExc(error_msg) + return map(converters.from_revision, revision_entries) + + def lookup_revision_with_context_by(origin_id, branch_name, ts, sha1_git, limit=100): """Return information about revision sha1_git, limited to the 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 @@ -454,6 +454,62 @@ self.storage.revision_log.assert_called_with([sha1_bin], 100) @istest + def revision_log_by(self): + # given + # given + sha1_bin = hashutil.hex_to_hash( + '28d8be353ed3480476f032475e7c233eff7371d5') + stub_revision_log = [{ + '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': [], + }] + + self.storage.revision_get_by = MagicMock(return_value=[ + {'id': sha1_bin}]) + self.storage.revision_log = MagicMock(return_value=stub_revision_log) + + # when + actual_log = backend.revision_log_by(1, 'refs/heads/master', None) + + # then + self.assertEqual(actual_log, stub_revision_log) + self.storage.revision_log.assert_called_with([sha1_bin], 100) + + @istest + def revision_log_by_norev(self): + # given + # given + sha1_bin = hashutil.hex_to_hash( + '28d8be353ed3480476f032475e7c233eff7371d5') + + self.storage.revision_get_by = MagicMock(return_value=None) + + # when + actual_log = backend.revision_log_by(1, 'refs/heads/master', None) + + # then + self.assertEqual(actual_log, None) + self.storage.revision_log.assert_called_with([sha1_bin], 100) + + @istest def stat_counters(self): # given input_stats = { 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 @@ -1126,6 +1126,91 @@ @patch('swh.web.ui.service.backend') @istest + def lookup_revision_log_by(self, mock_backend): + # given + stub_revision_log = [{ + 'id': hex_to_hash('28d8be353ed3480476f032475e7c233eff7371d5'), + 'directory': 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': { + 'timestamp': datetime.datetime( + 2000, 1, 17, 11, 23, 54, + tzinfo=datetime.timezone.utc, + ).timestamp(), + 'offset': 0, + 'negative_utc': False, + }, + 'committer_date': { + 'timestamp': datetime.datetime( + 2000, 1, 17, 11, 23, 54, + tzinfo=datetime.timezone.utc, + ).timestamp(), + 'offset': 0, + 'negative_utc': False, + }, + 'synthetic': False, + 'type': 'git', + 'parents': [], + 'metadata': [], + }] + mock_backend.revision_log_by = MagicMock( + return_value=stub_revision_log) + + # when + actual_log = service.lookup_revision_log_by( + 1, 'refs/heads/master', None) + # then + self.assertEqual(list(actual_log), [{ + 'id': '28d8be353ed3480476f032475e7c233eff7371d5', + '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", + 'committer_date': "2000-01-17T11:23:54+00:00", + 'synthetic': False, + 'type': 'git', + 'parents': [], + 'metadata': [], + }]) + + mock_backend.revision_log_by.assert_called_with( + 1, 'refs/heads/master', None) + + @patch('swh.web.ui.service.backend') + @istest + def lookup_revision_log_by_nolog(self, mock_backend): + # given + mock_backend.revision_log_by = MagicMock(return_value=None) + + with self.assertRaises(NotFoundExc) as nfe: + # when + service.lookup_revision_log_by( + 1, 'refs/heads/master', None) + # then + self.assertEquals('No revision matching origin 1, branch ' + 'name refs/heads/master.', + nfe.exception.args[0]) + mock_backend.revision_log_by.assert_called_with( + 1, 'refs/heads/master', None) + + @patch('swh.web.ui.service.backend') + @istest def lookup_content_raw_not_found(self, mock_backend): # given mock_backend.content_find = MagicMock(return_value=None) 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 @@ -1452,6 +1452,85 @@ @patch('swh.web.ui.views.api.service') @istest + def api_revision_log_by(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_by.return_value = stub_revisions + + expected_revisions = [{ + 'id': '18d8be353ed3480476f032475e7c233eff7371d5', + 'url': '/api/1/revision/18d8be353ed3480476f032475e7c233eff7371d5/', + 'history_url': '/api/1/revision/18d8be353ed3480476f032475e7c233ef' + 'f7371d5/log/', + 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6', + 'directory_url': '/api/1/directory/7834ef7e7c357ce2af928115c6c6a' + '42b7e2a44e6/', + '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, + }] + + # when + rv = self.app.get('/api/1/revision/origin/1/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, expected_revisions) + + mock_service.lookup_revision_log_by.assert_called_once_with( + 1, 'refs/heads/master', None) + + @patch('swh.web.ui.views.api.service') + @istest + def api_revision_log_by_norev(self, mock_service): + # given + mock_service.lookup_revision_log_by.side_effect = NotFoundExc( + 'No revision') + + # when + rv = self.app.get('/api/1/revision/origin/1/log/') + + # then + self.assertEquals(rv.status_code, 404) + self.assertEquals(rv.mimetype, 'application/json') + + response_data = json.loads(rv.data.decode('utf-8')) + self.assertEquals(response_data, {'error': 'No revision'}) + + mock_service.lookup_revision_log_by.assert_called_once_with( + 1, 'refs/heads/master', None) + + @patch('swh.web.ui.views.api.service') + @istest def api_revision_history_not_found(self, mock_service): # given mock_service.lookup_revision_with_context.return_value = None 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 @@ -675,6 +675,38 @@ enrich_fn=utils.enrich_revision) +@app.route('/api/1/revision' + '/origin/log/') +@app.route('/api/1/revision' + '/origin//log/') +@app.route('/api/1/revision' + '/origin/' + '/branch//log/') +@app.route('/api/1/revision' + '/origin/' + '/branch/' + '/ts//log/') +@app.route('/api/1/revision' + '/origin/' + '/ts//log/') +def api_revision_log_by(origin_id, + branch_name='refs/heads/master', + ts=None): + if ts: + ts = utils.parse_timestamp(ts) + + return _api_lookup( + origin_id, + service.lookup_revision_log_by, + 'Revision with (origin_id: %s, branch_name: %s' + ', ts: %s) not found.' % (origin_id, + branch_name, + ts), + utils.enrich_revision, + branch_name, + ts) + + @app.route('/api/1/directory/') @app.route('/api/1/directory//') @app.route('/api/1/directory///')