diff --git a/swh/web/api/views/revision.py b/swh/web/api/views/revision.py index 75f7d30d..4d40d467 100644 --- a/swh/web/api/views/revision.py +++ b/swh/web/api/views/revision.py @@ -1,237 +1,215 @@ # Copyright (C) 2015-2019 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from django.http import HttpResponse from swh.web.api import utils from swh.web.api.apidoc import api_doc, format_docstring from swh.web.api.apiurls import api_route from swh.web.api.views.utils import api_lookup from swh.web.common import archive DOC_RETURN_REVISION = """ :>json object author: information about the author of the revision :>json object committer: information about the committer of the revision :>json string committer_date: ISO representation of the commit date (in UTC) :>json string date: ISO representation of the revision date (in UTC) :>json string directory: the unique identifier that revision points to :>json string directory_url: link to :http:get:`/api/1/directory/(sha1_git)/[(path)/]` to get information about the directory associated to the revision :>json string id: the revision unique identifier :>json boolean merge: whether or not the revision corresponds to a merge commit :>json string message: the message associated to the revision :>json array parents: the parents of the revision, i.e. the previous revisions that head directly to it, each entry of that array contains an unique parent revision identifier but also a link to :http:get:`/api/1/revision/(sha1_git)/` to get more information about it :>json string type: the type of the revision """ DOC_RETURN_REVISION_ARRAY = DOC_RETURN_REVISION.replace(":>json", ":>jsonarr") -def _revision_directory_by(revision, path, request_path, limit=100, with_data=False): - """ - Compute the revision matching criterion's directory or content data. - - Args: - revision: dictionary of criterions representing a revision to lookup - path: directory's path to lookup - request_path: request path which holds the original context to - limit: optional query parameter to limit the revisions log - (default to 100). For now, note that this limit could impede the - transitivity conclusion about sha1_git not being an ancestor of - with_data: indicate to retrieve the content's raw data if path resolves - to a content. - - """ - - def enrich_directory_entry_local(dir, context_url=request_path): - return utils.enrich_directory_entry(dir, context_url) - - rev_id, result = archive.lookup_directory_through_revision( - revision, path, limit=limit, with_data=with_data - ) - - content = result["content"] - if result["type"] == "dir": # dir_entries - result["content"] = list(map(enrich_directory_entry_local, content)) - elif result["type"] == "file": # content - result["content"] = utils.enrich_content(content) - elif result["type"] == "rev": # revision - result["content"] = utils.enrich_revision(content) - - return result - - @api_route( r"/revision/(?P[0-9a-f]+)/", "api-1-revision", checksum_args=["sha1_git"] ) @api_doc("/revision/") @format_docstring(return_revision=DOC_RETURN_REVISION) def api_revision(request, sha1_git): """ .. http:get:: /api/1/revision/(sha1_git)/ Get information about a revision in the archive. Revisions are identified by **sha1** checksums, compatible with Git commit identifiers. See :func:`swh.model.identifiers.revision_identifier` in our data model module for details about how they are computed. :param string sha1_git: hexadecimal representation of the revision **sha1_git** identifier {common_headers} {return_revision} :statuscode 200: no error :statuscode 400: an invalid **sha1_git** value has been provided :statuscode 404: requested revision can not be found in the archive **Example:** .. parsed-literal:: :swh_web_api:`revision/aafb16d69fd30ff58afdd69036a26047f3aebdc6/` """ return api_lookup( archive.lookup_revision, sha1_git, notfound_msg="Revision with sha1_git {} not found.".format(sha1_git), enrich_fn=utils.enrich_revision, request=request, ) @api_route( r"/revision/(?P[0-9a-f]+)/raw/", "api-1-revision-raw-message", checksum_args=["sha1_git"], ) @api_doc("/revision/raw/", tags=["hidden"]) def api_revision_raw_message(request, sha1_git): """Return the raw data of the message of revision identified by sha1_git """ raw = archive.lookup_revision_message(sha1_git) response = HttpResponse(raw["message"], content_type="application/octet-stream") response["Content-disposition"] = "attachment;filename=rev_%s_raw" % sha1_git return response @api_route( r"/revision/(?P[0-9a-f]+)/directory/", "api-1-revision-directory", checksum_args=["sha1_git"], ) @api_route( r"/revision/(?P[0-9a-f]+)/directory/(?P.+)/", "api-1-revision-directory", checksum_args=["sha1_git"], ) @api_doc("/revision/directory/") @format_docstring() def api_revision_directory(request, sha1_git, dir_path=None, with_data=False): """ .. http:get:: /api/1/revision/(sha1_git)/directory/[(path)/] Get information about directory (entry) objects associated to revisions. Each revision is associated to a single "root" directory. This endpoint behaves like :http:get:`/api/1/directory/(sha1_git)/[(path)/]`, but operates on the root directory associated to a given revision. :param string sha1_git: hexadecimal representation of the revision **sha1_git** identifier :param string path: optional parameter to get information about the directory entry pointed by that relative path {common_headers} :>json array content: directory entries as returned by :http:get:`/api/1/directory/(sha1_git)/[(path)/]` :>json string path: path of directory from the revision root one :>json string revision: the unique revision identifier :>json string type: the type of the directory :statuscode 200: no error :statuscode 400: an invalid **sha1_git** value has been provided :statuscode 404: requested revision can not be found in the archive **Example:** .. parsed-literal:: :swh_web_api:`revision/f1b94134a4b879bc55c3dacdb496690c8ebdc03f/directory/` """ - return _revision_directory_by( - {"sha1_git": sha1_git}, dir_path, request.path, with_data=with_data + rev_id, result = archive.lookup_directory_through_revision( + {"sha1_git": sha1_git}, dir_path, with_data=with_data ) + content = result["content"] + if result["type"] == "dir": # dir_entries + result["content"] = [ + utils.enrich_directory_entry(entry, request=request) for entry in content + ] + elif result["type"] == "file": # content + result["content"] = utils.enrich_content(content, request=request) + elif result["type"] == "rev": # revision + result["content"] = utils.enrich_revision(content, request=request) + + return result + @api_route( r"/revision/(?P[0-9a-f]+)/log/", "api-1-revision-log", checksum_args=["sha1_git"], ) @api_doc("/revision/log/") @format_docstring(return_revision_array=DOC_RETURN_REVISION_ARRAY) def api_revision_log(request, sha1_git): """ .. http:get:: /api/1/revision/(sha1_git)/log/ Get a list of all revisions heading to a given one, in other words show the commit log. The revisions are returned in the breadth-first search order while visiting the revision graph. The number of revisions to return is also bounded by the **limit** query parameter. .. warning:: To get the full BFS traversal of the revision graph when the total number of revisions is greater than 1000, it is up to the client to keep track of the multiple branches of history when there's merge revisions in the returned objects. In other words, identify all the continuation points that need to be followed to get the full history through recursion. :param string sha1_git: hexadecimal representation of the revision **sha1_git** identifier :query int limit: maximum number of revisions to return when performing BFS traversal on the revision graph (default to 10, can not exceed 1000) {common_headers} {return_revision_array} :statuscode 200: no error :statuscode 400: an invalid **sha1_git** value has been provided :statuscode 404: head revision can not be found in the archive **Example:** .. parsed-literal:: :swh_web_api:`revision/e1a315fa3fa734e2a6154ed7b5b9ae0eb8987aad/log/` """ limit = int(request.query_params.get("limit", "10")) limit = min(limit, 1000) error_msg = "Revision with sha1_git %s not found." % sha1_git revisions = api_lookup( archive.lookup_revision_log, sha1_git, limit, notfound_msg=error_msg, enrich_fn=utils.enrich_revision, request=request, ) return {"results": revisions} diff --git a/swh/web/tests/api/views/test_revision.py b/swh/web/tests/api/views/test_revision.py index 145cb2a1..734c9764 100644 --- a/swh/web/tests/api/views/test_revision.py +++ b/swh/web/tests/api/views/test_revision.py @@ -1,197 +1,233 @@ # Copyright (C) 2015-2019 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from hypothesis import given -from swh.web.api.utils import enrich_revision -from swh.web.common.exc import NotFoundExc +from swh.model.from_disk import DentryPerms +from swh.model.hashutil import hash_to_bytes, hash_to_hex +from swh.model.model import ( + Directory, + DirectoryEntry, + Revision, + RevisionType, + TimestampWithTimezone, +) +from swh.web.api.utils import enrich_content, enrich_directory_entry, enrich_revision from swh.web.common.utils import reverse from swh.web.tests.data import random_sha1 -from swh.web.tests.strategies import revision +from swh.web.tests.strategies import content, new_person, new_swh_date, revision from swh.web.tests.utils import check_api_get_responses, check_http_get_response @given(revision()) def test_api_revision(api_client, archive_data, revision): url = reverse("api-1-revision", url_args={"sha1_git": revision}) rv = check_api_get_responses(api_client, url, status_code=200) expected_revision = archive_data.revision_get(revision) enrich_revision(expected_revision, rv.wsgi_request) assert rv.data == expected_revision def test_api_revision_not_found(api_client): unknown_revision_ = random_sha1() url = reverse("api-1-revision", url_args={"sha1_git": unknown_revision_}) rv = check_api_get_responses(api_client, url, status_code=404) assert rv.data == { "exception": "NotFoundExc", "reason": "Revision with sha1_git %s not found." % unknown_revision_, } @given(revision()) def test_api_revision_raw_ok(api_client, archive_data, revision): url = reverse("api-1-revision-raw-message", url_args={"sha1_git": revision}) expected_message = archive_data.revision_get(revision)["message"] rv = check_http_get_response(api_client, url, status_code=200) assert rv["Content-Type"] == "application/octet-stream" assert rv.content == expected_message.encode() def test_api_revision_raw_ko_no_rev(api_client): unknown_revision_ = random_sha1() url = reverse( "api-1-revision-raw-message", url_args={"sha1_git": unknown_revision_} ) rv = check_api_get_responses(api_client, url, status_code=404) assert rv.data == { "exception": "NotFoundExc", "reason": "Revision with sha1_git %s not found." % unknown_revision_, } @given(revision()) def test_api_revision_log(api_client, archive_data, revision): limit = 10 url = reverse( "api-1-revision-log", url_args={"sha1_git": revision}, query_params={"limit": limit}, ) rv = check_api_get_responses(api_client, url, status_code=200) expected_log = archive_data.revision_log(revision, limit=limit) expected_log = list( map(enrich_revision, expected_log, [rv.wsgi_request] * len(expected_log)) ) assert rv.data == expected_log def test_api_revision_log_not_found(api_client): unknown_revision_ = random_sha1() url = reverse("api-1-revision-log", url_args={"sha1_git": unknown_revision_}) rv = check_api_get_responses(api_client, url, status_code=404) assert rv.data == { "exception": "NotFoundExc", "reason": "Revision with sha1_git %s not found." % unknown_revision_, } assert not rv.has_header("Link") -def test_api_revision_directory_ko_not_found(api_client, mocker): - mock_rev_dir = mocker.patch("swh.web.api.views.revision._revision_directory_by") - mock_rev_dir.side_effect = NotFoundExc("Not found") - - url = "/api/1/revision/999/directory/some/path/to/dir/" +def test_api_revision_directory_ko_not_found(api_client): + sha1_git = random_sha1() + url = reverse("api-1-revision-directory", {"sha1_git": sha1_git}) rv = check_api_get_responses(api_client, url, status_code=404) + assert rv.data == { + "exception": "NotFoundExc", + "reason": f"Revision with sha1_git {sha1_git} not found.", + } - assert rv.data == {"exception": "NotFoundExc", "reason": "Not found"} - - mock_rev_dir.assert_called_with( - {"sha1_git": "999"}, "some/path/to/dir", url, with_data=False, - ) +@given(revision()) +def test_api_revision_directory_ok_returns_dir_entries( + api_client, archive_data, revision +): + url = reverse("api-1-revision-directory", {"sha1_git": revision}) + rv = check_api_get_responses(api_client, url, status_code=200) -def test_api_revision_directory_ok_returns_dir_entries(api_client, mocker): - mock_rev_dir = mocker.patch("swh.web.api.views.revision._revision_directory_by") - stub_dir = { + rev_data = archive_data.revision_get(revision) + dir_content = archive_data.directory_ls(rev_data["directory"]) + dir_content = [ + enrich_directory_entry(dir_entry, request=rv.wsgi_request) + for dir_entry in dir_content + ] + assert rv.data == { + "content": dir_content, + "path": ".", "type": "dir", - "revision": "999", - "content": [ - { - "sha1_git": "789", - "type": "file", - "target": "101", - "target_url": "/api/1/content/sha1_git:101/", - "name": "somefile", - "file_url": "/api/1/revision/999/directory/some/path/" "somefile/", - }, - { - "sha1_git": "123", - "type": "dir", - "target": "456", - "target_url": "/api/1/directory/456/", - "name": "to-subdir", - "dir_url": "/api/1/revision/999/directory/some/path/" "to-subdir/", - }, - ], + "revision": revision, } - mock_rev_dir.return_value = stub_dir - url = "/api/1/revision/999/directory/some/path/" - rv = check_api_get_responses(api_client, url, status_code=200) - stub_dir["content"][0]["target_url"] = rv.wsgi_request.build_absolute_uri( - stub_dir["content"][0]["target_url"] +@given(content(), new_person(), new_swh_date()) +def test_api_revision_directory_ok_returns_content( + api_client, archive_data, content, person, date +): + content_path = "foo" + _dir = Directory( + entries=( + DirectoryEntry( + name=content_path.encode(), + type="file", + target=hash_to_bytes(content["sha1_git"]), + perms=DentryPerms.content, + ), + ) ) - stub_dir["content"][0]["file_url"] = rv.wsgi_request.build_absolute_uri( - stub_dir["content"][0]["file_url"] + archive_data.directory_add([_dir]) + + revision = Revision( + directory=_dir.id, + author=person, + committer=person, + message=b"commit message", + date=TimestampWithTimezone.from_datetime(date), + committer_date=TimestampWithTimezone.from_datetime(date), + synthetic=False, + type=RevisionType.GIT, ) - stub_dir["content"][1]["target_url"] = rv.wsgi_request.build_absolute_uri( - stub_dir["content"][1]["target_url"] - ) - stub_dir["content"][1]["dir_url"] = rv.wsgi_request.build_absolute_uri( - stub_dir["content"][1]["dir_url"] - ) - - assert rv.data == stub_dir + archive_data.revision_add([revision]) - mock_rev_dir.assert_called_with( - {"sha1_git": "999"}, "some/path", url, with_data=False, + revision_id = hash_to_hex(revision.id) + cnt_data = archive_data.content_get(content["sha1"]) + url = reverse( + "api-1-revision-directory", {"sha1_git": revision_id, "dir_path": content_path}, ) + rv = check_api_get_responses(api_client, url, status_code=200) - -def test_api_revision_directory_ok_returns_content(api_client, mocker): - mock_rev_dir = mocker.patch("swh.web.api.views.revision._revision_directory_by") - stub_content = { + assert rv.data == { + "content": enrich_content(cnt_data, request=rv.wsgi_request), + "path": content_path, "type": "file", - "revision": "999", - "content": { - "sha1_git": "789", - "sha1": "101", - "data_url": "/api/1/content/101/raw/", - }, + "revision": revision_id, } - mock_rev_dir.return_value = stub_content - url = "/api/1/revision/666/directory/some/other/path/" - rv = check_api_get_responses(api_client, url, status_code=200) - - stub_content["content"]["data_url"] = rv.wsgi_request.build_absolute_uri( - stub_content["content"]["data_url"] +@given(revision(), new_person(), new_swh_date()) +def test_api_revision_directory_ok_returns_revision( + api_client, archive_data, revision, person, date +): + rev_path = "foo" + _dir = Directory( + entries=( + DirectoryEntry( + name=rev_path.encode(), + type="rev", + target=hash_to_bytes(revision), + perms=DentryPerms.revision, + ), + ) ) + archive_data.directory_add([_dir]) + + rev = Revision( + directory=_dir.id, + author=person, + committer=person, + message=b"commit message", + date=TimestampWithTimezone.from_datetime(date), + committer_date=TimestampWithTimezone.from_datetime(date), + synthetic=False, + type=RevisionType.GIT, + ) + archive_data.revision_add([rev]) - assert rv.data == stub_content - - mock_rev_dir.assert_called_with( - {"sha1_git": "666"}, "some/other/path", url, with_data=False + revision_id = hash_to_hex(rev.id) + rev_data = archive_data.revision_get(revision) + url = reverse( + "api-1-revision-directory", {"sha1_git": revision_id, "dir_path": rev_path}, ) + rv = check_api_get_responses(api_client, url, status_code=200) + + assert rv.data == { + "content": enrich_revision(rev_data, request=rv.wsgi_request), + "path": rev_path, + "type": "rev", + "revision": revision_id, + } @given(revision()) def test_api_revision_uppercase(api_client, revision): url = reverse( "api-1-revision-uppercase-checksum", url_args={"sha1_git": revision.upper()} ) resp = check_http_get_response(api_client, url, status_code=302) redirect_url = reverse("api-1-revision", url_args={"sha1_git": revision}) assert resp["location"] == redirect_url