diff --git a/swh/web/browse/snapshot_context.py b/swh/web/browse/snapshot_context.py --- a/swh/web/browse/snapshot_context.py +++ b/swh/web/browse/snapshot_context.py @@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional, Tuple from django.http import HttpRequest, HttpResponse -from django.shortcuts import render +from django.shortcuts import redirect, render from django.utils.html import escape from swh.model.hashutil import hash_to_bytes @@ -65,7 +65,7 @@ snapshot_id, branches_from=branch_name, branches_count=1, - target_types=["revision", "alias"], + target_types=["revision", "alias", "content", "directory"], # pull request branches must be browsable even if they are hidden # by default in branches list branch_name_exclude_prefix=None, @@ -127,7 +127,7 @@ if branch_type == "branch": branch_type = "Branch" branch_type_plural = "branches" - target_type = "revision" + target_type = "branch" else: branch_type = "Release" branch_type_plural = "releases" @@ -202,17 +202,19 @@ continue target_id = target["target"] target_type = target["target_type"] - if target_type == "revision": + if target_type in ("content", "directory", "revision"): branches[branch_name] = SnapshotBranchInfo( name=branch_name, alias=False, - revision=target_id, + target_type=target_type, + target=target_id, date=None, directory=None, message=None, url=None, ) - revision_to_branch[target_id].add(branch_name) + if target_type == "revision": + revision_to_branch[target_id].add(branch_name) elif target_type == "release": release_to_branch[target_id].add(branch_name) elif target_type == "alias": @@ -237,7 +239,8 @@ branches[branch] = SnapshotBranchInfo( name=branch, alias=alias, - revision=revision["id"], + target_type="revision", + target=revision["id"], directory=revision["directory"], date=format_utc_iso_date(revision["date"]), message=revision["message"], @@ -268,7 +271,7 @@ resolved_aliases = {} - for branch_alias, branch_target in branch_aliases.items(): + for branch_alias, _ in branch_aliases.items(): resolved_alias = archive.lookup_snapshot_alias(snapshot["id"], branch_alias) resolved_aliases[branch_alias] = resolved_alias if resolved_alias is None: @@ -283,6 +286,17 @@ elif target_type == "release": release = archive.lookup_release(target) _add_release_info(branch_alias, release, alias=True) + elif target_type in ("content", "directory"): + branches[branch_name] = SnapshotBranchInfo( + name=branch_alias, + alias=True, + target_type=target_type, + target=target, + date=None, + directory=None, + message=None, + url=None, + ) if branch_alias in branches: branches[branch_alias]["name"] = branch_alias @@ -468,7 +482,7 @@ ) visit_url = reverse("browse-origin-directory", query_params=query_params) - visit_info["url"] = directory_url = visit_url + visit_info["url"] = browse_url = visit_url branches_url = reverse("browse-origin-branches", query_params=query_params) @@ -477,7 +491,7 @@ assert snapshot_id is not None branches, releases, aliases = get_snapshot_content(snapshot_id) url_args = {"snapshot_id": snapshot_id} - directory_url = reverse("browse-snapshot-directory", url_args=url_args) + browse_url = reverse("browse-snapshot-directory", url_args=url_args) branches_url = reverse("browse-snapshot-branches", url_args=url_args) releases_url = reverse("browse-snapshot-releases", url_args=url_args) @@ -490,7 +504,11 @@ snapshot_sizes = _get_snapshot_sizes(snapshot_id) - is_empty = (snapshot_sizes["release"] + snapshot_sizes["revision"]) == 0 + snapshot_total_size = sum( + v for k, v in snapshot_sizes.items() if k not in ("alias", "branch") + ) + + is_empty = snapshot_total_size == 0 swh_snp_id = str( CoreSWHID(object_type=ObjectType.SNAPSHOT, object_id=hash_to_bytes(snapshot_id)) @@ -507,8 +525,6 @@ release_id = None root_directory = None - snapshot_total_size = snapshot_sizes["release"] + snapshot_sizes["revision"] - if path is not None: query_params["path"] = path @@ -520,7 +536,8 @@ SnapshotBranchInfo( name=revision_id, alias=False, - revision=revision_id, + target_type="revision", + target=revision_id, directory=root_directory, date=revision["date"], message=revision["message"], @@ -568,17 +585,24 @@ ) else: branch_name = branch["name"] - revision_id = branch["revision"] - root_directory = branch["directory"] + if branch["target_type"] == "revision": + revision_id = branch["target"] + root_directory = branch["directory"] + elif branch["target_type"] == "directory": + root_directory = branch["target"] elif head is not None: # otherwise, browse branch targeted by the HEAD alias if it exists - if head["target_type"] == "revision": - # HEAD alias targets a revision - head_rev = archive.lookup_revision(head["target"]) + if head["target_type"] in ("content", "directory", "revision"): branch_name = "HEAD" - revision_id = head_rev["id"] - root_directory = head_rev["directory"] - else: + if head["target_type"] == "revision": + # HEAD alias targets a revision + head_rev = archive.lookup_revision(head["target"]) + revision_id = head_rev["id"] + root_directory = head_rev["directory"] + elif head["target_type"] == "directory": + # HEAD alias targets a directory + root_directory = head["target"] + elif head["target_type"] == "release": # HEAD alias targets a release release_name = archive.lookup_release(head["target"])["name"] head_rel = _get_release(releases, release_name, snapshot_id) @@ -604,8 +628,13 @@ # fallback to browse first branch otherwise branch = branches[0] branch_name = branch["name"] - revision_id = branch["revision"] - root_directory = branch["directory"] + revision_id = ( + branch["target"] if branch["target_type"] == "revision" else None + ) + if branch["target_type"] == "revision": + root_directory = branch["directory"] + elif branch["target_type"] == "directory": + root_directory = branch["target"] elif releases: # fallback to browse last release otherwise release = releases[-1] @@ -621,7 +650,7 @@ for b in branches: branch_query_params = dict(query_params) branch_query_params.pop("release", None) - if b["name"] != b["revision"]: + if b["name"] != b["target"]: branch_query_params.pop("revision", None) branch_query_params["branch"] = b["name"] b["url"] = reverse( @@ -657,7 +686,7 @@ revision_info["message_header"] = "" snapshot_context = SnapshotContext( - directory_url=directory_url, + browse_url=browse_url, branch=branch_name, branch_alias=branch_name in aliases, branches=branches, @@ -759,6 +788,27 @@ ) root_directory = snapshot_context["root_directory"] + + if root_directory is None and snapshot_context["branch"] is not None: + branch_info = [ + branch + for branch in snapshot_context["branches"] + if branch["name"] == snapshot_context["branch"] + ] + # special case where the branch to browse targets a content instead of a directory + if branch_info and branch_info[0]["target_type"] == "content": + # redirect to browse content view + if "origin_url" not in snapshot_context["query_params"]: + snapshot_id = snapshot_context["snapshot_id"] + snapshot_context["query_params"]["snapshot"] = snapshot_id + return redirect( + reverse( + "browse-content", + url_args={"query_string": f"sha1_git:{branch_info[0]['target']}"}, + query_params=snapshot_context["query_params"], + ) + ) + sha1_git = root_directory error_info: Dict[str, Any] = { "status_code": 200, @@ -1167,7 +1217,7 @@ revision_url = reverse( "browse-revision", - url_args={"sha1_git": branch["revision"]}, + url_args={"sha1_git": branch["target"]}, query_params=query_params, ) diff --git a/swh/web/browse/templates/includes/snapshot-context.html b/swh/web/browse/templates/includes/snapshot-context.html --- a/swh/web/browse/templates/includes/snapshot-context.html +++ b/swh/web/browse/templates/includes/snapshot-context.html @@ -37,12 +37,12 @@ <ul class="nav nav-tabs" id="swh-snapshot-context-nav" style="padding-left: 5px;"> <li class="nav-item"> - <a class="nav-link" id="swh-browse-code-nav-link" href="{{ snapshot_context.directory_url }}"> + <a class="nav-link" id="swh-browse-code-nav-link" href="{{ snapshot_context.browse_url }}"> <i class="mdi mdi-code-tags mdi-fw" aria-hidden="true"></i> Code </a> </li> - {% if not snapshot_context.snapshot_sizes.revision %} + {% if not snapshot_context.snapshot_sizes.branch %} <li class="nav-item"> <a class="nav-link disabled" id="swh-browse-snapshot-branches-nav-link" href="#"> <i class="{{ swh_object_icons.branches }} mdi-fw" aria-hidden="true"></i> @@ -53,7 +53,7 @@ <li class="nav-item"> <a class="nav-link" id="swh-browse-snapshot-branches-nav-link" href="{{ snapshot_context.branches_url }}"> <i class="{{ swh_object_icons.branches }} mdi-fw" aria-hidden="true"></i> - Branches ({{ snapshot_context.snapshot_sizes.revision}}) + Branches ({{ snapshot_context.snapshot_sizes.branch}}) </a> </li> {% endif %} diff --git a/swh/web/browse/templates/includes/top-navigation.html b/swh/web/browse/templates/includes/top-navigation.html --- a/swh/web/browse/templates/includes/top-navigation.html +++ b/swh/web/browse/templates/includes/top-navigation.html @@ -55,11 +55,11 @@ </a> </li> {% endfor %} - {% if snapshot_context.branches|length < snapshot_context.snapshot_sizes.revision %} + {% if snapshot_context.branches|length < snapshot_context.snapshot_sizes.branch %} <li> <i class="mdi mdi-alert mdi-fw" aria-hidden="true"></i> Branches list truncated to {{ snapshot_context.branches|length }} entries, - {{ snapshot_context.branches|length|mul:-1|add:snapshot_context.snapshot_sizes.revision }} + {{ snapshot_context.branches|length|mul:-1|add:snapshot_context.snapshot_sizes.branch }} were omitted. </li> {% endif %} diff --git a/swh/web/tests/browse/test_snapshot_context.py b/swh/web/tests/browse/test_snapshot_context.py --- a/swh/web/tests/browse/test_snapshot_context.py +++ b/swh/web/tests/browse/test_snapshot_context.py @@ -42,7 +42,8 @@ SnapshotBranchInfo( name=branch, alias=alias, - revision=branch_data["target"], + target_type="revision", + target=branch_data["target"], directory=rev_data["directory"], date=format_utc_iso_date(rev_data["date"]), message=rev_data["message"], @@ -108,7 +109,7 @@ root_directory = None for branch in branches: if branch["name"] == "HEAD": - revision_id = branch["revision"] + revision_id = branch["target"] root_directory = branch["directory"] branch["url"] = reverse( f"browse-snapshot-{browse_context}", @@ -151,7 +152,7 @@ snapshot_swhid=snapshot_swhid, url_args=url_args, visit_info=None, - directory_url=directory_url, + browse_url=directory_url, ) if revision_id: @@ -213,7 +214,7 @@ root_directory = None for branch in branches: if branch["name"] == "HEAD": - revision_id = branch["revision"] + revision_id = branch["target"] root_directory = branch["directory"] branch["url"] = reverse( f"browse-origin-{browse_context}", @@ -266,7 +267,7 @@ snapshot_swhid=snapshot_swhid, url_args={}, visit_info=visit_info, - directory_url=directory_url, + browse_url=directory_url, ) if revision_id: @@ -307,14 +308,14 @@ expected_branch = dict(base_expected_context) expected_branch["branch"] = branch["name"] expected_branch["branch_alias"] = branch["alias"] - expected_branch["revision_id"] = branch["revision"] + expected_branch["revision_id"] = branch["target"] expected_branch["revision_info"] = _get_revision_info( - archive_data, branch["revision"] + archive_data, branch["target"] ) expected_branch["root_directory"] = branch["directory"] expected_branch["query_params"] = {"branch": branch["name"], **query_params} expected_branch["revision_info"]["revision_url"] = gen_revision_url( - branch["revision"], expected_branch + branch["target"], expected_branch ) assert snapshot_context == expected_branch @@ -345,7 +346,7 @@ assert snapshot_context == expected_release - revision_log = archive_data.revision_log(branch["revision"]) + revision_log = archive_data.revision_log(branch["target"]) revision = revision_log[-1] snapshot_context = get_snapshot_context( @@ -369,7 +370,8 @@ SnapshotBranchInfo( name=revision["id"], alias=False, - revision=revision["id"], + target_type="revision", + target=revision["id"], directory=revision["directory"], date=revision["date"], message=revision["message"], diff --git a/swh/web/tests/browse/views/test_content.py b/swh/web/tests/browse/views/test_content.py --- a/swh/web/tests/browse/views/test_content.py +++ b/swh/web/tests/browse/views/test_content.py @@ -447,7 +447,7 @@ branches, releases, _ = process_snapshot_branches(snapshot) branch_info = random.choice(branches) - directory = archive_data.revision_get(branch_info["revision"])["directory"] + directory = archive_data.revision_get(branch_info["target"])["directory"] directory_content = archive_data.directory_ls(directory) directory_file = random.choice( [e for e in directory_content if e["type"] == "file"] @@ -480,7 +480,7 @@ metadata={ "origin": origin_url, "visit": gen_swhid(ObjectType.SNAPSHOT, snapshot["id"]), - "anchor": gen_swhid(ObjectType.REVISION, branch_info["revision"]), + "anchor": gen_swhid(ObjectType.REVISION, branch_info["target"]), "path": f"/{directory_file['name']}", }, ) @@ -492,14 +492,14 @@ metadata={ "origin": origin_url, "visit": gen_swhid(ObjectType.SNAPSHOT, snapshot["id"]), - "anchor": gen_swhid(ObjectType.REVISION, branch_info["revision"]), + "anchor": gen_swhid(ObjectType.REVISION, branch_info["target"]), }, ) assert_contains(resp, dir_swhid) rev_swhid = gen_swhid( ObjectType.REVISION, - branch_info["revision"], + branch_info["target"], metadata={ "origin": origin_url, "visit": gen_swhid(ObjectType.SNAPSHOT, snapshot["id"]), diff --git a/swh/web/tests/browse/views/test_directory.py b/swh/web/tests/browse/views/test_directory.py --- a/swh/web/tests/browse/views/test_directory.py +++ b/swh/web/tests/browse/views/test_directory.py @@ -206,7 +206,7 @@ branch for branch in branches if branch["name"] == "refs/heads/master" ) - directory = archive_data.revision_get(branch_info["revision"])["directory"] + directory = archive_data.revision_get(branch_info["target"])["directory"] directory_content = archive_data.directory_ls(directory) directory_subdir = random.choice( [e for e in directory_content if e["type"] == "dir"] @@ -239,7 +239,7 @@ metadata={ "origin": origin_url, "visit": gen_swhid(ObjectType.SNAPSHOT, snapshot["id"]), - "anchor": gen_swhid(ObjectType.REVISION, branch_info["revision"]), + "anchor": gen_swhid(ObjectType.REVISION, branch_info["target"]), "path": "/", }, ) @@ -247,7 +247,7 @@ rev_swhid = gen_swhid( ObjectType.REVISION, - branch_info["revision"], + branch_info["target"], metadata={ "origin": origin_url, "visit": gen_swhid(ObjectType.SNAPSHOT, snapshot["id"]), @@ -357,7 +357,7 @@ branch for branch in branches if branch["name"] == "refs/heads/master" ) - directory = archive_data.revision_get(branch_info["revision"])["directory"] + directory = archive_data.revision_get(branch_info["target"])["directory"] directory_content = archive_data.directory_ls(directory) directory_subdir = random.choice( [e for e in directory_content if e["type"] == "dir"] @@ -369,7 +369,7 @@ query_params={ "origin_url": origin_url, "snapshot": snapshot["id"], - "revision": branch_info["revision"], + "revision": branch_info["target"], "path": directory_subdir["name"], }, ) @@ -378,7 +378,7 @@ client, url, status_code=200, template_used="browse-directory.html" ) - assert_contains(resp, f"Revision: <strong>{branch_info['revision']}</strong>") + assert_contains(resp, f"Revision: <strong>{branch_info['target']}</strong>") def _check_origin_snapshot_related_html( diff --git a/swh/web/tests/browse/views/test_snapshot.py b/swh/web/tests/browse/views/test_snapshot.py --- a/swh/web/tests/browse/views/test_snapshot.py +++ b/swh/web/tests/browse/views/test_snapshot.py @@ -16,6 +16,7 @@ from swh.model.hashutil import hash_to_bytes from swh.model.model import ( ObjectType, + Origin, OriginVisit, OriginVisitStatus, Release, @@ -175,7 +176,7 @@ browse_revision_url = reverse( "browse-revision", - url_args={"sha1_git": branch["revision"]}, + url_args={"sha1_git": branch["target"]}, query_params=query_params, ) assert_contains(resp, '<a href="%s">' % escape(browse_revision_url)) @@ -446,3 +447,205 @@ client, snp_url, status_code=200, template_used="browse-directory.html" ) assert_contains(resp, log_url) + + +@pytest.mark.parametrize( + "aliased,with_origin,with_branch_query_param", + [ + (False, False, False), + (False, False, True), + (False, True, False), + (False, True, True), + (True, False, False), + (True, False, True), + (True, True, False), + (True, True, True), + ], +) +def test_browse_snapshot_single_branch_targeting_content( + client, archive_data, content_text, aliased, with_origin, with_branch_query_param +): + + branch_name = "HEAD" + + if not aliased: + snapshot = Snapshot( + branches={ + branch_name.encode(): SnapshotBranch( + target=hash_to_bytes(content_text["sha1_git"]), + target_type=TargetType.CONTENT, + ), + }, + ) + else: + snapshot = Snapshot( + branches={ + branch_name.encode(): SnapshotBranch( + target=b"content", + target_type=TargetType.ALIAS, + ), + branch_name.encode(): SnapshotBranch( + target=hash_to_bytes(content_text["sha1_git"]), + target_type=TargetType.CONTENT, + ), + }, + ) + + archive_data.snapshot_add([snapshot]) + + query_params = {} + if with_branch_query_param: + query_params["branch"] = branch_name + + if with_origin: + origin_url = "https://git.example.org/user/project" + archive_data.origin_add([Origin(url=origin_url)]) + date = now() + visit = OriginVisit(origin=origin_url, date=date, type="git") + visit = archive_data.origin_visit_add([visit])[0] + visit_status = OriginVisitStatus( + origin=origin_url, + visit=visit.visit, + date=date, + status="full", + snapshot=snapshot.id, + ) + archive_data.origin_visit_status_add([visit_status]) + query_params["origin_url"] = origin_url + url = reverse("browse-origin-directory", query_params=query_params) + else: + url = reverse( + "browse-snapshot-directory", + url_args={"snapshot_id": snapshot.id.hex()}, + query_params=query_params, + ) + + resp = check_html_get_response( + client, + url, + status_code=302, + ) + + if with_origin: + query_params["origin_url"] = origin_url + else: + query_params["snapshot"] = snapshot.id.hex() + + assert resp["location"] == reverse( + "browse-content", + url_args={"query_string": f"sha1_git:{content_text['sha1_git']}"}, + query_params=query_params, + ) + + resp = check_html_get_response( + client, + resp["location"], + status_code=200, + ) + + assert_contains(resp, escape(content_text["raw_data"].decode())) + + if with_origin: + log_url = reverse("browse-origin-log", query_params=query_params) + else: + log_url = reverse( + "browse-snapshot-log", + url_args={"snapshot_id": snapshot.id.hex()}, + query_params=query_params, + ) + + assert_not_contains(resp, log_url) + + +@pytest.mark.parametrize( + "aliased,with_origin,with_branch_query_param", + [ + (False, False, False), + (False, False, True), + (False, True, False), + (False, True, True), + (True, False, False), + (True, False, True), + (True, True, False), + (True, True, True), + ], +) +def test_browse_snapshot_single_branch_targeting_directory( + client, archive_data, directory, aliased, with_origin, with_branch_query_param +): + + branch_name = "HEAD" + + if not aliased: + snapshot = Snapshot( + branches={ + branch_name.encode(): SnapshotBranch( + target=hash_to_bytes(directory), + target_type=TargetType.DIRECTORY, + ), + }, + ) + else: + snapshot = Snapshot( + branches={ + branch_name.encode(): SnapshotBranch( + target=b"directory", + target_type=TargetType.ALIAS, + ), + b"directory": SnapshotBranch( + target=hash_to_bytes(directory), + target_type=TargetType.DIRECTORY, + ), + }, + ) + + archive_data.snapshot_add([snapshot]) + + query_params = {} + if with_branch_query_param: + query_params["branch"] = branch_name + + if with_origin: + origin_url = "https://git.example.org/user/project" + archive_data.origin_add([Origin(url=origin_url)]) + date = now() + visit = OriginVisit(origin=origin_url, date=date, type="git") + visit = archive_data.origin_visit_add([visit])[0] + visit_status = OriginVisitStatus( + origin=origin_url, + visit=visit.visit, + date=date, + status="full", + snapshot=snapshot.id, + ) + archive_data.origin_visit_status_add([visit_status]) + query_params["origin_url"] = origin_url + url = reverse("browse-origin-directory", query_params=query_params) + else: + url = reverse( + "browse-snapshot-directory", + url_args={"snapshot_id": snapshot.id.hex()}, + query_params=query_params, + ) + + resp = check_html_get_response( + client, + url, + status_code=200, + ) + + directory_data = archive_data.directory_get(directory) + + for entry in directory_data["content"]: + assert_contains(resp, entry["name"]) + + if with_origin: + log_url = reverse("browse-origin-log", query_params=query_params) + else: + log_url = reverse( + "browse-snapshot-log", + url_args={"snapshot_id": snapshot.id.hex()}, + query_params=query_params, + ) + + assert_not_contains(resp, log_url) diff --git a/swh/web/tests/conftest.py b/swh/web/tests/conftest.py --- a/swh/web/tests/conftest.py +++ b/swh/web/tests/conftest.py @@ -987,6 +987,10 @@ counts = dict.fromkeys(("alias", "release", "revision"), 0) counts.update(self.storage.snapshot_count_branches(hash_to_bytes(snapshot_id))) counts.pop(None, None) + counts["branch"] = sum( + counts.get(target_type, 0) + for target_type in ("content", "directory", "revision") + ) return counts diff --git a/swh/web/tests/utils/test_archive.py b/swh/web/tests/utils/test_archive.py --- a/swh/web/tests/utils/test_archive.py +++ b/swh/web/tests/utils/test_archive.py @@ -996,13 +996,16 @@ expected_sizes = { "alias": 0, + "branch": 0, "release": 0, "revision": 0, } - for branch_name, branch_info in branches.items(): + for _, branch_info in branches.items(): if branch_info is not None: expected_sizes[branch_info["target_type"]] += 1 + if branch_info["target_type"] in ("content", "directory", "revision"): + expected_sizes["branch"] += 1 assert archive.lookup_snapshot_sizes(snapshot) == expected_sizes @@ -1031,7 +1034,7 @@ ) archive_data.snapshot_add([snapshot]) - expected_sizes = {"alias": 0, "release": 0, "revision": 2} + expected_sizes = {"alias": 0, "branch": 2, "release": 0, "revision": 2} assert ( archive.lookup_snapshot_sizes( diff --git a/swh/web/utils/archive.py b/swh/web/utils/archive.py --- a/swh/web/utils/archive.py +++ b/swh/web/utils/archive.py @@ -1097,6 +1097,12 @@ # when null branches are present in the snapshot branch_counts.pop(None, None) snapshot_sizes.update(branch_counts) + + snapshot_sizes["branch"] = sum( + snapshot_sizes.get(target_type, 0) + for target_type in ("content", "directory", "revision") + ) + return snapshot_sizes diff --git a/swh/web/utils/typing.py b/swh/web/utils/typing.py --- a/swh/web/utils/typing.py +++ b/swh/web/utils/typing.py @@ -55,8 +55,10 @@ """branch name""" alias: bool """define if the branch is an alias""" - revision: str - """branch heading revision""" + target_type: str + """branch target type: content, directory or revision""" + target: str + """branch target id""" url: Optional[str] """optional browse URL (content, directory, ...) scoped to branch""" @@ -127,8 +129,8 @@ """common URL arguments when browsing snapshot content""" visit_info: Optional[OriginVisitInfo] """optional origin visit info associated to the snapshot""" - directory_url: Optional[str] - """optional root directory URL associated to the snapshot""" + browse_url: Optional[str] + """optional browse URL associated to the snapshot""" class SWHObjectInfo(TypedDict):