diff --git a/swh/fuse/fs/artifact.py b/swh/fuse/fs/artifact.py
index db05133..9118c9a 100644
--- a/swh/fuse/fs/artifact.py
+++ b/swh/fuse/fs/artifact.py
@@ -1,93 +1,178 @@
 # Copyright (C) 2020  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
-from typing import Any, AsyncIterator
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any, AsyncIterator, List
 
 from swh.fuse.fs.entry import EntryMode, FuseEntry
-from swh.model.identifiers import CONTENT, DIRECTORY, SWHID
-
-# Avoid cycling import
-Fuse = "Fuse"
+from swh.fuse.fs.symlink import SymlinkEntry
+from swh.model.from_disk import DentryPerms
+from swh.model.identifiers import CONTENT, DIRECTORY, REVISION, SWHID
 
 
+@dataclass
 class ArtifactEntry(FuseEntry):
     """ FUSE virtual entry for a Software Heritage Artifact
 
     Attributes:
         swhid: Software Heritage persistent identifier
         prefetch: optional prefetched metadata used to set entry attributes
     """
 
-    def __init__(
-        self, name: str, mode: int, fuse: Fuse, swhid: SWHID, prefetch: Any = None
-    ):
-        super().__init__(name, mode, fuse)
-        self.swhid = swhid
-        self.prefetch = prefetch
-
-
-def typify(
-    name: str, mode: int, fuse: Fuse, swhid: SWHID, prefetch: Any = None
-) -> ArtifactEntry:
-    """ Create an artifact entry corresponding to the given artifact type """
-
-    getters = {CONTENT: Content, DIRECTORY: Directory}
-    return getters[swhid.object_type](name, mode, fuse, swhid, prefetch)
+    swhid: SWHID
+    prefetch: Any = None
 
 
 class Content(ArtifactEntry):
     """ Software Heritage content artifact.
 
     Content leaves (AKA blobs) are represented on disks as regular files,
     containing the corresponding bytes, as archived.
 
     Note that permissions are associated to blobs only in the context of
     directories. Hence, when accessing blobs from the top-level `archive/`
     directory, the permissions of the `archive/SWHID` file will be arbitrary and
     not meaningful (e.g., `0x644`). """
 
-    async def content(self) -> bytes:
-        return await self.fuse.get_blob(self.swhid)
+    async def get_content(self) -> bytes:
+        data = await self.fuse.get_blob(self.swhid)
+        self.prefetch["length"] = len(data)
+        return data
 
-    async def length(self) -> int:
-        # When listing entries from a directory, the API already gave us information
+    async def size(self) -> int:
         if self.prefetch:
             return self.prefetch["length"]
-        return len(await self.content())
+        else:
+            return len(await self.get_content())
 
     async def __aiter__(self):
         raise ValueError("Cannot iterate over a content type artifact")
 
 
 class Directory(ArtifactEntry):
     """ Software Heritage directory artifact.
 
     Directory nodes are represented as directories on the file-system,
     containing one entry for each entry of the archived directory. Entry names
     and other metadata, including permissions, will correspond to the archived
     entry metadata.
 
     Note that the FUSE mount is read-only, no matter what the permissions say.
     So it is possible that, in the context of a directory, a file is presented
     as writable, whereas actually writing to it will fail with `EPERM`. """
 
-    async def __aiter__(self) -> AsyncIterator[ArtifactEntry]:
+    async def __aiter__(self) -> AsyncIterator[FuseEntry]:
         metadata = await self.fuse.get_metadata(self.swhid)
         for entry in metadata:
-            yield typify(
-                name=entry["name"],
-                # Use default read-only permissions for directories, and
-                # archived permissions for contents
-                mode=(
-                    entry["perms"]
-                    if entry["target"].object_type == CONTENT
-                    else int(EntryMode.RDONLY_DIR)
-                ),
-                fuse=self.fuse,
-                swhid=entry["target"],
-                # The directory API has extra info we can use to set attributes
-                # without additional Software Heritage API call
-                prefetch=entry,
+            name = entry["name"]
+            swhid = entry["target"]
+            mode = (
+                # Archived permissions for directories are always set to
+                # 0o040000 so use a read-only permission instead
+                int(EntryMode.RDONLY_DIR)
+                if swhid.object_type == DIRECTORY
+                else entry["perms"]
+            )
+
+            # 1. Symlinks
+            if mode == DentryPerms.symlink:
+                yield self.create_child(
+                    SymlinkEntry,
+                    name=name,
+                    # Symlink target is stored in the blob content
+                    target=await self.fuse.get_blob(swhid),
+                )
+            # 2. Submodules
+            elif swhid.object_type == REVISION:
+                # Make sure the revision metadata is fetched and create a
+                # symlink to distinguish it with regular directories
+                await self.fuse.get_metadata(swhid)
+                yield self.create_child(
+                    SymlinkEntry,
+                    name=name,
+                    target=Path(self.get_relative_root_path(), f"archive/{swhid}"),
+                )
+            # 3. Regular entries (directories, contents)
+            else:
+                yield self.create_child(
+                    OBJTYPE_GETTERS[swhid.object_type],
+                    name=name,
+                    mode=mode,
+                    swhid=swhid,
+                    # The directory API has extra info we can use to set
+                    # attributes without additional Software Heritage API call
+                    prefetch=entry,
+                )
+
+
+class Revision(ArtifactEntry):
+    """ Software Heritage revision artifact.
+
+    Revision (AKA commit) nodes are represented on the file-system as
+    directories with the following entries:
+
+    - `root`: source tree at the time of the commit, as a symlink pointing into
+      `archive/`, to a SWHID of type `dir`
+    - `parents/` (note the plural): a virtual directory containing entries named
+      `1`, `2`, `3`, etc., one for each parent commit. Each of these entry is a
+      symlink pointing into `archive/`, to the SWHID file for the given parent
+      commit
+    - `parent` (note the singular): present if and only if the current commit
+      has at least one parent commit (which is the most common case). When
+      present it is a symlink pointing into `parents/1/`
+    - `meta.json`: metadata for the current node, as a symlink pointing to the
+      relevant `meta/<SWHID>.json` file """
+
+    async def __aiter__(self) -> AsyncIterator[FuseEntry]:
+        metadata = await self.fuse.get_metadata(self.swhid)
+        directory = metadata["directory"]
+        parents = metadata["parents"]
+
+        # Make sure all necessary metadatas are fetched
+        await self.fuse.get_metadata(directory)
+        for parent in parents:
+            await self.fuse.get_metadata(parent["id"])
+
+        root_path = self.get_relative_root_path()
+
+        yield self.create_child(
+            SymlinkEntry, name="root", target=Path(root_path, f"archive/{directory}"),
+        )
+        yield self.create_child(
+            SymlinkEntry,
+            name="meta.json",
+            target=Path(root_path, f"meta/{self.swhid}.json"),
+        )
+        yield self.create_child(
+            RevisionParents,
+            name="parents",
+            mode=int(EntryMode.RDONLY_DIR),
+            parents=[x["id"] for x in parents],
+        )
+
+        if len(parents) >= 1:
+            yield self.create_child(
+                SymlinkEntry, name="parent", target="parents/1/",
             )
+
+
+@dataclass
+class RevisionParents(FuseEntry):
+    """ Revision virtual `parents/` directory """
+
+    parents: List[SWHID]
+
+    async def __aiter__(self) -> AsyncIterator[FuseEntry]:
+        root_path = self.get_relative_root_path()
+        for i, parent in enumerate(self.parents):
+            yield self.create_child(
+                SymlinkEntry,
+                name=str(i + 1),
+                target=Path(root_path, f"archive/{parent}"),
+            )
+
+
+OBJTYPE_GETTERS = {CONTENT: Content, DIRECTORY: Directory, REVISION: Revision}
diff --git a/swh/fuse/fs/entry.py b/swh/fuse/fs/entry.py
index bb2866a..af4158f 100644
--- a/swh/fuse/fs/entry.py
+++ b/swh/fuse/fs/entry.py
@@ -1,48 +1,75 @@
 # Copyright (C) 2020  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
+from __future__ import annotations
+
+from dataclasses import dataclass, field
 from enum import IntEnum
-from stat import S_IFDIR, S_IFREG
+from pathlib import Path
+from stat import S_IFDIR, S_IFLNK, S_IFREG
+from typing import Any, AsyncIterator, Union
 
 # Avoid cycling import
 Fuse = "Fuse"
 
 
 class EntryMode(IntEnum):
     """ Default entry mode and permissions for the FUSE.
 
     The FUSE mount is always read-only, even if permissions contradict this
     statement (in a context of a directory, entries are listed with permissions
     taken from the archive).
     """
 
     RDONLY_FILE = S_IFREG | 0o444
     RDONLY_DIR = S_IFDIR | 0o555
+    SYMLINK = S_IFLNK | 0o444
 
 
+@dataclass
 class FuseEntry:
     """ Main wrapper class to manipulate virtual FUSE entries
 
     Attributes:
         name: entry filename
         mode: entry permission mode
         fuse: internal reference to the main FUSE class
         inode: unique integer identifying the entry
     """
 
-    def __init__(self, name: str, mode: int, fuse: Fuse):
-        self.name = name
-        self.mode = mode
-        self.fuse = fuse
-        self.inode = fuse._alloc_inode(self)
+    name: str
+    mode: int
+    depth: int
+    fuse: Fuse
+    inode: int = field(init=False)
 
-    async def length(self) -> int:
-        return 0
+    def __post_init__(self):
+        self.inode = self.fuse._alloc_inode(self)
+
+    async def get_content(self) -> bytes:
+        """ Return the content of a file entry """
 
-    async def content(self):
         return None
 
-    async def __aiter__(self):
+    async def size(self) -> int:
+        """ Return the size of a file entry """
+
+        return 0
+
+    async def __aiter__(self) -> AsyncIterator[FuseEntry]:
+        """ Return the child entries of a directory entry """
+
+        yield None
+
+    def get_target(self) -> Union[str, bytes, Path]:
+        """ Return the path target of a symlink entry """
+
         return None
+
+    def get_relative_root_path(self) -> str:
+        return "../" * (self.depth - 1)
+
+    def create_child(self, constructor: Any, **kwargs) -> FuseEntry:
+        return constructor(depth=self.depth + 1, fuse=self.fuse, **kwargs)
diff --git a/swh/fuse/fs/mountpoint.py b/swh/fuse/fs/mountpoint.py
index bc1d7cd..ccaa0f1 100644
--- a/swh/fuse/fs/mountpoint.py
+++ b/swh/fuse/fs/mountpoint.py
@@ -1,77 +1,86 @@
 # Copyright (C) 2020  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
+from dataclasses import dataclass, field
 import json
 from typing import AsyncIterator
 
-from swh.fuse.fs.artifact import typify
+from swh.fuse.fs.artifact import OBJTYPE_GETTERS
 from swh.fuse.fs.entry import EntryMode, FuseEntry
 from swh.model.identifiers import CONTENT, SWHID
 
-# Avoid cycling import
-Fuse = "Fuse"
-
 
+@dataclass
 class Root(FuseEntry):
     """ The FUSE mountpoint, consisting of the archive/ and meta/ directories """
 
-    def __init__(self, fuse: Fuse):
-        super().__init__(name="root", mode=int(EntryMode.RDONLY_DIR), fuse=fuse)
+    name: str = field(init=False, default=None)
+    mode: int = field(init=False, default=int(EntryMode.RDONLY_DIR))
+    depth: int = field(init=False, default=1)
 
     async def __aiter__(self) -> AsyncIterator[FuseEntry]:
-        for entry in [ArchiveDir(self.fuse), MetaDir(self.fuse)]:
-            yield entry
+        yield self.create_child(ArchiveDir)
+        yield self.create_child(MetaDir)
 
 
+@dataclass
 class ArchiveDir(FuseEntry):
     """ The archive/ directory is lazily populated with one entry per accessed
     SWHID, having actual SWHIDs as names """
 
-    def __init__(self, fuse: Fuse):
-        super().__init__(name="archive", mode=int(EntryMode.RDONLY_DIR), fuse=fuse)
+    name: str = field(init=False, default="archive")
+    mode: int = field(init=False, default=int(EntryMode.RDONLY_DIR))
 
     async def __aiter__(self) -> AsyncIterator[FuseEntry]:
         async for swhid in self.fuse.cache.get_cached_swhids():
             if swhid.object_type == CONTENT:
                 mode = EntryMode.RDONLY_FILE
             else:
                 mode = EntryMode.RDONLY_DIR
-            yield typify(str(swhid), int(mode), self.fuse, swhid)
+            yield self.create_child(
+                OBJTYPE_GETTERS[swhid.object_type],
+                name=str(swhid),
+                mode=int(mode),
+                swhid=swhid,
+            )
 
 
+@dataclass
 class MetaDir(FuseEntry):
     """ The meta/ directory contains one SWHID.json file for each SWHID entry
     under archive/. The JSON file contain all available meta information about
     the given SWHID, as returned by the Software Heritage Web API for that
     object. Note that, in case of pagination (e.g., snapshot objects with many
     branches) the JSON file will contain a complete version with all pages
     merged together. """
 
-    def __init__(self, fuse: Fuse):
-        super().__init__(name="meta", mode=int(EntryMode.RDONLY_DIR), fuse=fuse)
+    name: str = field(init=False, default="meta")
+    mode: int = field(init=False, default=int(EntryMode.RDONLY_DIR))
 
     async def __aiter__(self) -> AsyncIterator[FuseEntry]:
         async for swhid in self.fuse.cache.get_cached_swhids():
-            yield MetaEntry(swhid, self.fuse)
+            yield self.create_child(
+                MetaEntry,
+                name=f"{swhid}.json",
+                mode=int(EntryMode.RDONLY_FILE),
+                swhid=swhid,
+            )
 
 
+@dataclass
 class MetaEntry(FuseEntry):
     """ An entry from the meta/ directory, containing for each accessed SWHID a
     corresponding SWHID.json file with all the metadata from the Software
     Heritage archive. """
 
-    def __init__(self, swhid: SWHID, fuse: Fuse):
-        super().__init__(
-            name=str(swhid) + ".json", mode=int(EntryMode.RDONLY_FILE), fuse=fuse
-        )
-        self.swhid = swhid
+    swhid: SWHID
 
-    async def content(self) -> bytes:
+    async def get_content(self) -> bytes:
         # Get raw JSON metadata from API (un-typified)
         metadata = await self.fuse.cache.metadata.get(self.swhid, typify=False)
         return json.dumps(metadata).encode()
 
-    async def length(self) -> int:
-        return len(await self.content())
+    async def size(self) -> int:
+        return len(await self.get_content())
diff --git a/swh/fuse/fs/symlink.py b/swh/fuse/fs/symlink.py
new file mode 100644
index 0000000..2151e9f
--- /dev/null
+++ b/swh/fuse/fs/symlink.py
@@ -0,0 +1,28 @@
+# Copyright (C) 2020  The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Union
+
+from swh.fuse.fs.entry import EntryMode, FuseEntry
+
+
+@dataclass
+class SymlinkEntry(FuseEntry):
+    """ FUSE virtual entry for symlinks
+
+    Attributes:
+        target: path to symlink target
+    """
+
+    mode: int = field(init=False, default=int(EntryMode.SYMLINK))
+    target: Union[str, bytes, Path]
+
+    async def size(self) -> int:
+        return len(str(self.target))
+
+    def get_target(self) -> Union[str, bytes, Path]:
+        return self.target
diff --git a/swh/fuse/fuse.py b/swh/fuse/fuse.py
index 26b827a..086ffb5 100644
--- a/swh/fuse/fuse.py
+++ b/swh/fuse/fuse.py
@@ -1,219 +1,223 @@
 # Copyright (C) 2020  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 import asyncio
 import errno
 import logging
 import os
 from pathlib import Path
 import time
 from typing import Any, Dict, List
 
 import pyfuse3
 import pyfuse3_asyncio
 import requests
 
 from swh.fuse.cache import FuseCache
 from swh.fuse.fs.entry import FuseEntry
 from swh.fuse.fs.mountpoint import Root
 from swh.model.identifiers import CONTENT, SWHID
 from swh.web.client.client import WebAPIClient
 
 
 class Fuse(pyfuse3.Operations):
     """ Software Heritage Filesystem in Userspace (FUSE). Locally mount parts of
     the archive and navigate it as a virtual file system. """
 
     def __init__(
         self, root_path: Path, cache: FuseCache, conf: Dict[str, Any],
     ):
         super(Fuse, self).__init__()
 
         self._next_inode: int = pyfuse3.ROOT_INODE
         self._inode2entry: Dict[int, FuseEntry] = {}
 
         self.root = Root(fuse=self)
 
         self.time_ns: int = time.time_ns()  # start time, used as timestamp
         self.gid = os.getgid()
         self.uid = os.getuid()
 
         self.web_api = WebAPIClient(
             conf["web-api"]["url"], conf["web-api"]["auth-token"]
         )
         self.cache = cache
 
     def shutdown(self) -> None:
         pass
 
     def _alloc_inode(self, entry: FuseEntry) -> int:
         """ Return a unique inode integer for a given entry """
 
         inode = self._next_inode
         self._next_inode += 1
         self._inode2entry[inode] = entry
 
         # TODO add inode recycling with invocation to invalidate_inode when
         # the dicts get too big
 
         return inode
 
     def inode2entry(self, inode: int) -> FuseEntry:
         """ Return the entry matching a given inode """
 
         try:
             return self._inode2entry[inode]
         except KeyError:
             raise pyfuse3.FUSEError(errno.ENOENT)
 
     async def get_metadata(self, swhid: SWHID) -> Any:
         """ Retrieve metadata for a given SWHID using Software Heritage API """
 
         cache = await self.cache.metadata.get(swhid)
         if cache:
             return cache
 
         try:
             # TODO: swh-graph API
             typify = False  # Get the raw JSON from the API
             # TODO: async web API
             loop = asyncio.get_event_loop()
             metadata = await loop.run_in_executor(None, self.web_api.get, swhid, typify)
             await self.cache.metadata.set(swhid, metadata)
             # Retrieve it from cache so it is correctly typed
             return await self.cache.metadata.get(swhid)
         except requests.HTTPError:
             logging.error(f"Unknown SWHID: '{swhid}'")
 
     async def get_blob(self, swhid: SWHID) -> bytes:
         """ Retrieve the blob bytes for a given content SWHID using Software
         Heritage API """
 
         if swhid.object_type != CONTENT:
             raise pyfuse3.FUSEError(errno.EINVAL)
 
         # Make sure the metadata cache is also populated with the given SWHID
         await self.get_metadata(swhid)
 
         cache = await self.cache.blob.get(swhid)
         if cache:
             return cache
 
         loop = asyncio.get_event_loop()
         resp = await loop.run_in_executor(None, self.web_api.content_raw, swhid)
         blob = b"".join(list(resp))
         await self.cache.blob.set(swhid, blob)
         return blob
 
     async def get_attrs(self, entry: FuseEntry) -> pyfuse3.EntryAttributes:
         """ Return entry attributes """
 
         attrs = pyfuse3.EntryAttributes()
         attrs.st_size = 0
         attrs.st_atime_ns = self.time_ns
         attrs.st_ctime_ns = self.time_ns
         attrs.st_mtime_ns = self.time_ns
         attrs.st_gid = self.gid
         attrs.st_uid = self.uid
         attrs.st_ino = entry.inode
         attrs.st_mode = entry.mode
-        attrs.st_size = await entry.length()
+        attrs.st_size = await entry.size()
         return attrs
 
     async def getattr(
         self, inode: int, _ctx: pyfuse3.RequestContext
     ) -> pyfuse3.EntryAttributes:
         """ Get attributes for a given inode """
 
         entry = self.inode2entry(inode)
         return await self.get_attrs(entry)
 
     async def opendir(self, inode: int, _ctx: pyfuse3.RequestContext) -> int:
         """ Open a directory referred by a given inode """
 
         # Re-use inode as directory handle
         return inode
 
     async def readdir(self, fh: int, offset: int, token: pyfuse3.ReaddirToken) -> None:
         """ Read entries in an open directory """
 
         # opendir() uses inode as directory handle
         inode = fh
 
         # TODO: add cache on direntry list?
         direntry = self.inode2entry(inode)
         next_id = offset + 1
         i = 0
         async for entry in direntry:
             if i < offset:
                 i += 1
                 continue
 
             name = os.fsencode(entry.name)
             attrs = await self.get_attrs(entry)
             if not pyfuse3.readdir_reply(token, name, attrs, next_id):
                 break
 
             next_id += 1
             self._inode2entry[attrs.st_ino] = entry
 
     async def open(
         self, inode: int, _flags: int, _ctx: pyfuse3.RequestContext
     ) -> pyfuse3.FileInfo:
         """ Open an inode and return a unique file handle """
 
         # Re-use inode as file handle
         return pyfuse3.FileInfo(fh=inode, keep_cache=True)
 
     async def read(self, fh: int, offset: int, length: int) -> bytes:
         """ Read `length` bytes from file handle `fh` at position `offset` """
 
         # open() uses inode as file handle
         inode = fh
 
         entry = self.inode2entry(inode)
-        data = await entry.content()
+        data = await entry.get_content()
         return data[offset : offset + length]
 
     async def lookup(
         self, parent_inode: int, name: str, _ctx: pyfuse3.RequestContext
     ) -> pyfuse3.EntryAttributes:
         """ Look up a directory entry by name and get its attributes """
 
         name = os.fsdecode(name)
         parent_entry = self.inode2entry(parent_inode)
 
         async for entry in parent_entry:
             if name == entry.name:
                 attr = await self.get_attrs(entry)
                 return attr
 
         logging.error(f"Unknown name during lookup: '{name}'")
         raise pyfuse3.FUSEError(errno.ENOENT)
 
+    async def readlink(self, inode: int, _ctx: pyfuse3.RequestContext) -> bytes:
+        entry = self.inode2entry(inode)
+        return os.fsencode(entry.get_target())
+
 
 async def main(swhids: List[SWHID], root_path: Path, conf: Dict[str, Any]) -> None:
     """ swh-fuse CLI entry-point """
 
     # Use pyfuse3 asyncio layer to match the rest of Software Heritage codebase
     pyfuse3_asyncio.enable()
 
     async with FuseCache(conf["cache"]) as cache:
         fs = Fuse(root_path, cache, conf)
 
         # Initially populate the cache
         for swhid in swhids:
             await fs.get_metadata(swhid)
 
         fuse_options = set(pyfuse3.default_options)
         fuse_options.add("fsname=swhfs")
         fuse_options.add("debug")
         pyfuse3.init(fs, root_path, fuse_options)
 
         try:
             await pyfuse3.main()
         finally:
             fs.shutdown()
             pyfuse3.close(unmount=True)
diff --git a/swh/fuse/tests/api_data.py b/swh/fuse/tests/api_data.py
index 9d1cb97..2285826 100644
--- a/swh/fuse/tests/api_data.py
+++ b/swh/fuse/tests/api_data.py
@@ -1,445 +1,523 @@
 # GENERATED FILE, DO NOT EDIT.
 # Run './gen-api-data.py > api_data.py' instead.
 # fmt: off
 
 API_URL = 'https://invalid-test-only.archive.softwareheritage.org/api/1'
-ROOT_SWHID = 'swh:1:dir:9eb62ef7dd283f7385e7d31af6344d9feedd25de'
-ROOT_URL = 'directory/9eb62ef7dd283f7385e7d31af6344d9feedd25de/'
+ROOTREV_SWHID = 'swh:1:rev:d012a7190fc1fd72ed48911e77ca97ba4521bccd'
+ROOTDIR_SWHID = 'swh:1:dir:9eb62ef7dd283f7385e7d31af6344d9feedd25de'
+ROOTREV_URL = 'revision/d012a7190fc1fd72ed48911e77ca97ba4521bccd/'
+ROOTREV_PARENT_URL = 'revision/cb95712138ec5e480db5160b41172bbc6f6494cc/'
+ROOTDIR_URL = 'directory/9eb62ef7dd283f7385e7d31af6344d9feedd25de/'
 README_URL = 'content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/'
 README_RAW_URL = 'content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/raw/'
 
 MOCK_ARCHIVE = {
+    'revision/d012a7190fc1fd72ed48911e77ca97ba4521bccd/':  # NoQA: E501
+    r"""{
+  "author": {
+    "email": "torvalds@linux-foundation.org",
+    "fullname": "Linus Torvalds <torvalds@linux-foundation.org>",
+    "name": "Linus Torvalds"
+  },
+  "committer": {
+    "email": "torvalds@linux-foundation.org",
+    "fullname": "Linus Torvalds <torvalds@linux-foundation.org>",
+    "name": "Linus Torvalds"
+  },
+  "committer_date": "2020-08-23T14:08:43-07:00",
+  "date": "2020-08-23T14:08:43-07:00",
+  "directory": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
+  "directory_url": "https://archive.softwareheritage.org/api/1/directory/9eb62ef7dd283f7385e7d31af6344d9feedd25de/",
+  "extra_headers": [],
+  "history_url": "https://archive.softwareheritage.org/api/1/revision/d012a7190fc1fd72ed48911e77ca97ba4521bccd/log/",
+  "id": "d012a7190fc1fd72ed48911e77ca97ba4521bccd",
+  "merge": false,
+  "message": "Linux 5.9-rc2\n",
+  "metadata": {},
+  "parents": [
+    {
+      "id": "cb95712138ec5e480db5160b41172bbc6f6494cc",
+      "url": "https://archive.softwareheritage.org/api/1/revision/cb95712138ec5e480db5160b41172bbc6f6494cc/"
+    }
+  ],
+  "synthetic": false,
+  "type": "git",
+  "url": "https://archive.softwareheritage.org/api/1/revision/d012a7190fc1fd72ed48911e77ca97ba4521bccd/"
+}
+""",  # NoQA: E501
+    'revision/cb95712138ec5e480db5160b41172bbc6f6494cc/':  # NoQA: E501
+    r"""{
+  "author": {
+    "email": "torvalds@linux-foundation.org",
+    "fullname": "Linus Torvalds <torvalds@linux-foundation.org>",
+    "name": "Linus Torvalds"
+  },
+  "committer": {
+    "email": "torvalds@linux-foundation.org",
+    "fullname": "Linus Torvalds <torvalds@linux-foundation.org>",
+    "name": "Linus Torvalds"
+  },
+  "committer_date": "2020-08-23T11:37:23-07:00",
+  "date": "2020-08-23T11:37:23-07:00",
+  "directory": "4fa3b43d90ce69b46916cc3fd3ea1d15de70443d",
+  "directory_url": "https://archive.softwareheritage.org/api/1/directory/4fa3b43d90ce69b46916cc3fd3ea1d15de70443d/",
+  "extra_headers": [
+    [
+      "mergetag",
+      "object 64ef8f2c4791940d7f3945507b6a45c20d959260\ntype commit\ntag powerpc-5.9-3\ntagger Michael Ellerman <mpe@ellerman.id.au> 1598185676 +1000\n\npowerpc fixes for 5.9 #3\n\nAdd perf support for emitting extended registers for power10.\n\nA fix for CPU hotplug on pseries, where on large/loaded systems we may not wait\nlong enough for the CPU to be offlined, leading to crashes.\n\nAddition of a raw cputable entry for Power10, which is not required to boot, but\nis required to make our PMU setup work correctly in guests.\n\nThree fixes for the recent changes on 32-bit Book3S to move modules into their\nown segment for strict RWX.\n\nA fix for a recent change in our powernv PCI code that could lead to crashes.\n\nA change to our perf interrupt accounting to avoid soft lockups when using some\nevents, found by syzkaller.\n\nA change in the way we handle power loss events from the hypervisor on pseries.\nWe no longer immediately shut down if we're told we're running on a UPS.\n\nA few other minor fixes.\n\nThanks to:\n  Alexey Kardashevskiy, Andreas Schwab, Aneesh Kumar K.V, Anju T Sudhakar,\n  Athira Rajeev, Christophe Leroy, Frederic Barrat, Greg Kurz, Kajol Jain,\n  Madhavan Srinivasan, Michael Neuling, Michael Roth, Nageswara R Sastry, Oliver\n  O'Halloran, Thiago Jung Bauermann, Vaidyanathan Srinivasan, Vasant Hegde.\n-----BEGIN PGP SIGNATURE-----\n\niQJHBAABCAAxFiEEJFGtCPCthwEv2Y/bUevqPMjhpYAFAl9CYMwTHG1wZUBlbGxl\ncm1hbi5pZC5hdQAKCRBR6+o8yOGlgC/wEACljEVnfHzUObmIgqn9Ru3JlfEI6Hlk\nts7kajCgS/I/bV6DoDMZ8rlZX87QFOwiBkNM1I+vGHSLAuzsmFAnbFPyxw/idxpQ\nXUoNy8OCvbbzCPzChYdiU0PxW2h2i+QxkmktlWSN1SAPudJUWvoPS2Y4+sC4zksk\nB4B6tbW2DT8TFO1kKeZsU9r2t+EH5KwlIOi+uxbH8d76lJINKkBNSnjzMytl7drM\nTZx/HWr8+s/WJo1787x6bv8gxs5tV9b4vIKt2YZNTY2kvYsEDE+fBR1XfCAneXMw\nASYnZV+/xCLIUpRF6DI4RAShLBT/Sfiy1yMTndZgfqAgquokFosszNx2zrk0IzCd\nAgqX93YGbGz/H72W3Y/B0W9+74XyO/u2D9zhNpkCRMpdcsM5MbvOQrQA5Ustu47E\nav5MOaF/nNCd8J+OC4Qjgt5VFb/s0h4FdtrwT80srOa2U6Of9cD/T6xAfOszSJ96\ncWdSb5qhn5wuD9pP32KjwdmWBiUw38/gnRGKpRlOVzyHL/GKZijyaBbWBlkoEmty\n0nbjWW/IVfsOb5Weuiybg541h/QOVuOkb2pOvPClITiH83MY/AciDJ+auo4M//hW\nhaKz9IgV/KctmzDE+v9d0BD8sGmW03YUcQAPdRufI0eGXijDLcnHeuk2B3Nu84Pq\n8mtev+VQ+T6cZA==\n=sdJ1\n-----END PGP SIGNATURE-----"
+    ]
+  ],
+  "history_url": "https://archive.softwareheritage.org/api/1/revision/cb95712138ec5e480db5160b41172bbc6f6494cc/log/",
+  "id": "cb95712138ec5e480db5160b41172bbc6f6494cc",
+  "merge": true,
+  "message": "Merge tag 'powerpc-5.9-3' of git://git.kernel.org/pub/scm/linux/kernel/git/powerpc/linux\n\nPull powerpc fixes from Michael Ellerman:\n\n - Add perf support for emitting extended registers for power10.\n\n - A fix for CPU hotplug on pseries, where on large/loaded systems we\n   may not wait long enough for the CPU to be offlined, leading to\n   crashes.\n\n - Addition of a raw cputable entry for Power10, which is not required\n   to boot, but is required to make our PMU setup work correctly in\n   guests.\n\n - Three fixes for the recent changes on 32-bit Book3S to move modules\n   into their own segment for strict RWX.\n\n - A fix for a recent change in our powernv PCI code that could lead to\n   crashes.\n\n - A change to our perf interrupt accounting to avoid soft lockups when\n   using some events, found by syzkaller.\n\n - A change in the way we handle power loss events from the hypervisor\n   on pseries. We no longer immediately shut down if we're told we're\n   running on a UPS.\n\n - A few other minor fixes.\n\nThanks to Alexey Kardashevskiy, Andreas Schwab, Aneesh Kumar K.V, Anju T\nSudhakar, Athira Rajeev, Christophe Leroy, Frederic Barrat, Greg Kurz,\nKajol Jain, Madhavan Srinivasan, Michael Neuling, Michael Roth,\nNageswara R Sastry, Oliver O'Halloran, Thiago Jung Bauermann,\nVaidyanathan Srinivasan, Vasant Hegde.\n\n* tag 'powerpc-5.9-3' of git://git.kernel.org/pub/scm/linux/kernel/git/powerpc/linux:\n  powerpc/perf/hv-24x7: Move cpumask file to top folder of hv-24x7 driver\n  powerpc/32s: Fix module loading failure when VMALLOC_END is over 0xf0000000\n  powerpc/pseries: Do not initiate shutdown when system is running on UPS\n  powerpc/perf: Fix soft lockups due to missed interrupt accounting\n  powerpc/powernv/pci: Fix possible crash when releasing DMA resources\n  powerpc/pseries/hotplug-cpu: wait indefinitely for vCPU death\n  powerpc/32s: Fix is_module_segment() when MODULES_VADDR is defined\n  powerpc/kasan: Fix KASAN_SHADOW_START on BOOK3S_32\n  powerpc/fixmap: Fix the size of the early debug area\n  powerpc/pkeys: Fix build error with PPC_MEM_KEYS disabled\n  powerpc/kernel: Cleanup machine check function declarations\n  powerpc: Add POWER10 raw mode cputable entry\n  powerpc/perf: Add extended regs support for power10 platform\n  powerpc/perf: Add support for outputting extended regs in perf intr_regs\n  powerpc: Fix P10 PVR revision in /proc/cpuinfo for SMT4 cores\n",
+  "metadata": {},
+  "parents": [
+    {
+      "id": "550c2129d93d5eb198835ac83c05ef672e8c491c",
+      "url": "https://archive.softwareheritage.org/api/1/revision/550c2129d93d5eb198835ac83c05ef672e8c491c/"
+    },
+    {
+      "id": "64ef8f2c4791940d7f3945507b6a45c20d959260",
+      "url": "https://archive.softwareheritage.org/api/1/revision/64ef8f2c4791940d7f3945507b6a45c20d959260/"
+    }
+  ],
+  "synthetic": false,
+  "type": "git",
+  "url": "https://archive.softwareheritage.org/api/1/revision/cb95712138ec5e480db5160b41172bbc6f6494cc/"
+}
+""",  # NoQA: E501
     'directory/9eb62ef7dd283f7385e7d31af6344d9feedd25de/':  # NoQA: E501
     r"""[
   {
     "checksums": {
       "sha1": "39a0a88cd8eae4504e1d33b0c0f88059044d761f",
       "sha1_git": "a0a96088c74f49a961a80bc0851a84214b0a9f83",
       "sha256": "7c0f4eaf45838f26ae951b490beb0d11034a30b21e5c39a54c4223f5c2018890"
     },
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": 16166,
     "name": ".clang-format",
     "perms": 33188,
     "status": "visible",
     "target": "a0a96088c74f49a961a80bc0851a84214b0a9f83",
     "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:a0a96088c74f49a961a80bc0851a84214b0a9f83/",
     "type": "file"
   },
   {
     "checksums": {
       "sha1": "0e31de4130c64f23e9ee5fc761fdbd807dc94360",
       "sha1_git": "43967c6b20151ee126db08e24758e3c789bcb844",
       "sha256": "dbd64d3f532b962d4681d79077cc186340f5f439de7f99c709b01892332af866"
     },
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": 59,
     "name": ".cocciconfig",
     "perms": 33188,
     "status": "visible",
     "target": "43967c6b20151ee126db08e24758e3c789bcb844",
     "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:43967c6b20151ee126db08e24758e3c789bcb844/",
     "type": "file"
   },
   {
     "checksums": {
       "sha1": "52b62d115dfae2ed19561db14e8d10ee22659e7f",
       "sha1_git": "a64d219137455f407a7b1f2c6b156c5575852e9e",
       "sha256": "4c9ba8e0ef521ce01474e98eddfc77afaec8a8e259939a139590c00505646527"
     },
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": 71,
     "name": ".get_maintainer.ignore",
     "perms": 33188,
     "status": "visible",
     "target": "a64d219137455f407a7b1f2c6b156c5575852e9e",
     "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:a64d219137455f407a7b1f2c6b156c5575852e9e/",
     "type": "file"
   },
   {
     "checksums": {
       "sha1": "6cc5a38e6f6ca93e21c78cb9c54794a42c3031c3",
       "sha1_git": "4b32eaa9571e64e47b51c43537063f56b204d8b3",
       "sha256": "dc52a4e1ee3615c87691aca7f667c7e49f6900f36b5c20339ac497366ba9406c"
     },
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": 62,
     "name": ".gitattributes",
     "perms": 33188,
     "status": "visible",
     "target": "4b32eaa9571e64e47b51c43537063f56b204d8b3",
     "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:4b32eaa9571e64e47b51c43537063f56b204d8b3/",
     "type": "file"
   },
   {
     "checksums": {
       "sha1": "1966a794db7d9518f321a8ecb0736c16de59bd91",
       "sha1_git": "162bd2b67bdf6a28be7a361b8418e4e31d542854",
       "sha256": "a9766c936a81df2ed3ea41f506ddaad88a78d9cf47e093df414b3b6f2e6d8e14"
     },
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": 1852,
     "name": ".gitignore",
     "perms": 33188,
     "status": "visible",
     "target": "162bd2b67bdf6a28be7a361b8418e4e31d542854",
     "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:162bd2b67bdf6a28be7a361b8418e4e31d542854/",
     "type": "file"
   },
   {
     "checksums": {
       "sha1": "2bb15bd51c981842b6f710d2e057972e5f22cfcc",
       "sha1_git": "332c7833057f51da02805add9b60161ff31aee71",
       "sha256": "d7b69571529964b3c8444a73ac720bbb883cc70fc4b78a36d1ac277f660c50bb"
     },
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": 17283,
     "name": ".mailmap",
     "perms": 33188,
     "status": "visible",
     "target": "332c7833057f51da02805add9b60161ff31aee71",
     "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:332c7833057f51da02805add9b60161ff31aee71/",
     "type": "file"
   },
   {
     "checksums": {
       "sha1": "0473e748fee37c7b68487fb102c0d563bbc641b3",
       "sha1_git": "a635a38ef9405fdfcfe97f3a435393c1e9cae971",
       "sha256": "fb5a425bd3b3cd6071a3a9aff9909a859e7c1158d54d32e07658398cd67eb6a0"
     },
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": 496,
     "name": "COPYING",
     "perms": 33188,
     "status": "visible",
     "target": "a635a38ef9405fdfcfe97f3a435393c1e9cae971",
     "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:a635a38ef9405fdfcfe97f3a435393c1e9cae971/",
     "type": "file"
   },
   {
     "checksums": {
       "sha1": "f27043aefa4b69b921df3728ee906c3f03087d29",
       "sha1_git": "32ee70a7562eec7345e98841473abb438379a4fd",
       "sha256": "23242c7183ee2815e27fea2346b0e5ad9131b8b611b200ae0418d4027cea2a3d"
     },
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": 99788,
     "name": "CREDITS",
     "perms": 33188,
     "status": "visible",
     "target": "32ee70a7562eec7345e98841473abb438379a4fd",
     "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:32ee70a7562eec7345e98841473abb438379a4fd/",
     "type": "file"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "Documentation",
     "perms": 16384,
     "target": "1ba46735273aa020a173c0ad0c813179530dd117",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/1ba46735273aa020a173c0ad0c813179530dd117/",
     "type": "dir"
   },
   {
     "checksums": {
       "sha1": "2491dd3bed10f6918ed1657ab5a7a8efbddadf5d",
       "sha1_git": "fa441b98c9f6eac1617acf1772ae8b371cfd42aa",
       "sha256": "75df66064f75e91e6458862cd9413b19e65b77eefcc8a95dcbd6bf36fd2e4b59"
     },
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": 1327,
     "name": "Kbuild",
     "perms": 33188,
     "status": "visible",
     "target": "fa441b98c9f6eac1617acf1772ae8b371cfd42aa",
     "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:fa441b98c9f6eac1617acf1772ae8b371cfd42aa/",
     "type": "file"
   },
   {
     "checksums": {
       "sha1": "6b9b12a6bbff219dfbb45ec068af9aa7cf1b7288",
       "sha1_git": "745bc773f567067a85ce6574fb41ce80833247d9",
       "sha256": "a592dae7d067cd8e5dc43e3f9dc363eba9eb1f7cf80c6178b5cd291c0b76d3ec"
     },
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": 555,
     "name": "Kconfig",
     "perms": 33188,
     "status": "visible",
     "target": "745bc773f567067a85ce6574fb41ce80833247d9",
     "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:745bc773f567067a85ce6574fb41ce80833247d9/",
     "type": "file"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "LICENSES",
     "perms": 16384,
     "target": "a49a894ea3684b6c044448c37f812356550d14a2",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/a49a894ea3684b6c044448c37f812356550d14a2/",
     "type": "dir"
   },
   {
     "checksums": {
       "sha1": "eb207b62f2fe0225dc55d9b87d82f6009e864117",
       "sha1_git": "f0068bceeb6158a30c6eee430ca6d2a7e4c4013a",
       "sha256": "3c81b34eaf99d943e4c2fac2548f3d9d740a9e9683ccedbaacfb82796c7965e1"
     },
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": 569101,
     "name": "MAINTAINERS",
     "perms": 33188,
     "status": "visible",
     "target": "f0068bceeb6158a30c6eee430ca6d2a7e4c4013a",
     "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:f0068bceeb6158a30c6eee430ca6d2a7e4c4013a/",
     "type": "file"
   },
   {
     "checksums": {
       "sha1": "9bfa205a4d23b9a60889bd9b59010574670b8b90",
       "sha1_git": "f2116815416091dbfa7dcf58ae179ae3241ec1b1",
       "sha256": "e87fb2b9482b9066b47c1656e55ebc4897bbb226daa122bcd4a2858fef19e597"
     },
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": 63305,
     "name": "Makefile",
     "perms": 33188,
     "status": "visible",
     "target": "f2116815416091dbfa7dcf58ae179ae3241ec1b1",
     "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:f2116815416091dbfa7dcf58ae179ae3241ec1b1/",
     "type": "file"
   },
   {
     "checksums": {
       "sha1": "ca1dc365022dcaa728dfb11bcde40ad3cce0574b",
       "sha1_git": "669ac7c32292798644b21dbb5a0dc657125f444d",
       "sha256": "bad58d396f62102befaf23a8a2ab6b1693fdc8f318de3059b489781f28865612"
     },
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": 727,
     "name": "README",
     "perms": 33188,
     "status": "visible",
     "target": "669ac7c32292798644b21dbb5a0dc657125f444d",
     "target_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/",
     "type": "file"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "arch",
     "perms": 16384,
     "target": "cf12c1ce4de958ab4ddcb008fe89118b82a3c7b7",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/cf12c1ce4de958ab4ddcb008fe89118b82a3c7b7/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "block",
     "perms": 16384,
     "target": "a77c89fa64b8ec37c9aa0fa98add54bfb6075257",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/a77c89fa64b8ec37c9aa0fa98add54bfb6075257/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "certs",
     "perms": 16384,
     "target": "527d8f94235029c6f571414df5f8ed2951a0ca5b",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/527d8f94235029c6f571414df5f8ed2951a0ca5b/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "crypto",
     "perms": 16384,
     "target": "1fb1357e2d22af4332091937ed960a47f78d0b5e",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/1fb1357e2d22af4332091937ed960a47f78d0b5e/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "drivers",
     "perms": 16384,
     "target": "3b5be1ee0216ec59c70e132681be4a5d79e7da9b",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/3b5be1ee0216ec59c70e132681be4a5d79e7da9b/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "fs",
     "perms": 16384,
     "target": "1dbf8d211613db72f5b83b0987023bd5acf866ee",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/1dbf8d211613db72f5b83b0987023bd5acf866ee/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "include",
     "perms": 16384,
     "target": "74991fd1a983c6b3f72c8815f7de81a3abddb255",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/74991fd1a983c6b3f72c8815f7de81a3abddb255/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "init",
     "perms": 16384,
     "target": "c944a589113271d878e27bbc31ae369edecaff90",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/c944a589113271d878e27bbc31ae369edecaff90/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "ipc",
     "perms": 16384,
     "target": "ff553b9398fea6b2e290ea4a95f7a94f1cf3c22c",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/ff553b9398fea6b2e290ea4a95f7a94f1cf3c22c/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "kernel",
     "perms": 16384,
     "target": "8c700fd3589e6d2befa4d9b2cc79471eac37da38",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/8c700fd3589e6d2befa4d9b2cc79471eac37da38/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "lib",
     "perms": 16384,
     "target": "0f2936da43bebe4f26b3be83e8fa392c4f9e82cf",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/0f2936da43bebe4f26b3be83e8fa392c4f9e82cf/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "mm",
     "perms": 16384,
     "target": "e15d954c1ed09e6fc29c184515834696d8e70e7c",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/e15d954c1ed09e6fc29c184515834696d8e70e7c/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "net",
     "perms": 16384,
     "target": "41e1603b37542d265eade0555e0db66668135575",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/41e1603b37542d265eade0555e0db66668135575/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "samples",
     "perms": 16384,
     "target": "9fa649fea3c8ab6b4926f0e7721a21a36b685153",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/9fa649fea3c8ab6b4926f0e7721a21a36b685153/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "scripts",
     "perms": 16384,
     "target": "e4e5b45d7c44d0bd2c6feb1a257fff7303d2c67e",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/e4e5b45d7c44d0bd2c6feb1a257fff7303d2c67e/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "security",
     "perms": 16384,
     "target": "a4a58d89fc506c3660610105a08de60614cdc980",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/a4a58d89fc506c3660610105a08de60614cdc980/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "sound",
     "perms": 16384,
     "target": "bf9e1568b8ce61157a322fddbaab1a0c76be15ef",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/bf9e1568b8ce61157a322fddbaab1a0c76be15ef/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "tools",
     "perms": 16384,
     "target": "83d6279411023bf7edf6bde6ce2e3748912f4936",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/83d6279411023bf7edf6bde6ce2e3748912f4936/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "usr",
     "perms": 16384,
     "target": "aae2ca939e0f7ac6b5e489e4c7835e1a15588cff",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/aae2ca939e0f7ac6b5e489e4c7835e1a15588cff/",
     "type": "dir"
   },
   {
     "dir_id": "9eb62ef7dd283f7385e7d31af6344d9feedd25de",
     "length": null,
     "name": "virt",
     "perms": 16384,
     "target": "d7f6f10a8509839e404d1cc5af51317ac8b26276",
     "target_url": "https://archive.softwareheritage.org/api/1/directory/d7f6f10a8509839e404d1cc5af51317ac8b26276/",
     "type": "dir"
   }
 ]
 """,  # NoQA: E501
     'content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/':  # NoQA: E501
     r"""{
   "checksums": {
     "blake2s256": "746aaa0816ffc8cadf5e7f70b8bb93a47a76299ef263c743dbfef2644c6a0245",
     "sha1": "ca1dc365022dcaa728dfb11bcde40ad3cce0574b",
     "sha1_git": "669ac7c32292798644b21dbb5a0dc657125f444d",
     "sha256": "bad58d396f62102befaf23a8a2ab6b1693fdc8f318de3059b489781f28865612"
   },
   "data_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/raw/",
   "filetype_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/filetype/",
   "language_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/language/",
   "length": 727,
   "license_url": "https://archive.softwareheritage.org/api/1/content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/license/",
   "status": "visible"
 }
 """,  # NoQA: E501
     'content/sha1_git:669ac7c32292798644b21dbb5a0dc657125f444d/raw/':  # NoQA: E501
     r"""Linux kernel
 ============
 
 There are several guides for kernel developers and users. These guides can
 be rendered in a number of formats, like HTML and PDF. Please read
 Documentation/admin-guide/README.rst first.
 
 In order to build the documentation, use ``make htmldocs`` or
 ``make pdfdocs``.  The formatted documentation can also be read online at:
 
     https://www.kernel.org/doc/html/latest/
 
 There are various text files in the Documentation/ subdirectory,
 several of them using the Restructured Text markup notation.
 
 Please read the Documentation/process/changes.rst file, as it contains the
 requirements for building and running the kernel, and information about
 the problems which may result by upgrading your kernel.
 """,  # NoQA: E501
 }
diff --git a/swh/fuse/tests/conftest.py b/swh/fuse/tests/conftest.py
index 03554a8..d81c256 100644
--- a/swh/fuse/tests/conftest.py
+++ b/swh/fuse/tests/conftest.py
@@ -1,67 +1,74 @@
 # Copyright (C) 2020  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 from multiprocessing import Process
 from os import listdir
 from pathlib import Path
 import subprocess
 from tempfile import NamedTemporaryFile, TemporaryDirectory
 import time
 
 from click.testing import CliRunner
 import pytest
 import yaml
 
 from swh.fuse import cli
 
-from .api_data import API_URL, MOCK_ARCHIVE, ROOT_SWHID
+from .api_data import API_URL, MOCK_ARCHIVE, ROOTDIR_SWHID, ROOTREV_SWHID
 
 
 @pytest.fixture
 def web_api_mock(requests_mock):
     for api_call, data in MOCK_ARCHIVE.items():
         requests_mock.get(f"{API_URL}/{api_call}", text=data)
     return requests_mock
 
 
 @pytest.fixture
 def fuse_mntdir(web_api_mock):
     tmpdir = TemporaryDirectory(suffix=".swh-fuse-test")
     tmpfile = NamedTemporaryFile(suffix=".swh-fuse-test.yml")
 
     config = {
         "cache": {"metadata": {"in-memory": True}, "blob": {"in-memory": True}},
         "web-api": {"url": API_URL, "auth-token": None},
     }
 
     # Run FUSE in foreground mode but in a separate process, so it does not
     # block execution and remains easy to kill during teardown
     def fuse_process(tmpdir, tmpfile):
         with tmpdir as mntdir, tmpfile as config_path:
             config_path = Path(config_path.name)
             config_path.write_text(yaml.dump(config))
             CliRunner().invoke(
                 cli.mount,
-                args=[mntdir, ROOT_SWHID, "--foreground", "--config-file", config_path],
+                args=[
+                    mntdir,
+                    ROOTDIR_SWHID,
+                    ROOTREV_SWHID,
+                    "--foreground",
+                    "--config-file",
+                    config_path,
+                ],
             )
 
     fuse = Process(target=fuse_process, args=[tmpdir, tmpfile])
     fuse.start()
     # Wait max 3 seconds for the FUSE to correctly mount
     for i in range(30):
         try:
             root = listdir(tmpdir.name)
             if root:
                 break
         except FileNotFoundError:
             pass
         time.sleep(0.1)
     else:
         raise FileNotFoundError(f"Could not mount FUSE in {tmpdir.name}")
 
     yield tmpdir.name
 
     subprocess.run(["fusermount", "-u", tmpdir.name], check=True)
     fuse.join()
diff --git a/swh/fuse/tests/gen-api-data.py b/swh/fuse/tests/gen-api-data.py
index 08a7ced..5906648 100755
--- a/swh/fuse/tests/gen-api-data.py
+++ b/swh/fuse/tests/gen-api-data.py
@@ -1,51 +1,57 @@
 #!/usr/bin/env python3
 
 # Copyright (C) 2020  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 import json
 
 import requests
 
 API_URL_real = "https://archive.softwareheritage.org/api/1"
 API_URL_test = "https://invalid-test-only.archive.softwareheritage.org/api/1"
 
 # Use the Linux kernel as a testing repository
-ROOT_HASH = "9eb62ef7dd283f7385e7d31af6344d9feedd25de"
+ROOTREV_HASH = "d012a7190fc1fd72ed48911e77ca97ba4521bccd"
+ROOTREV_PARENT_HASH = "cb95712138ec5e480db5160b41172bbc6f6494cc"
+ROOTDIR_HASH = "9eb62ef7dd283f7385e7d31af6344d9feedd25de"
 README_HASH = "669ac7c32292798644b21dbb5a0dc657125f444d"
 
-ROOT_SWHID = f"swh:1:dir:{ROOT_HASH}"
+ROOTREV_SWHID = f"swh:1:rev:{ROOTREV_HASH}"
+ROOTDIR_SWHID = f"swh:1:dir:{ROOTDIR_HASH}"
 
 urls = {
-    "ROOT": f"directory/{ROOT_HASH}/",
+    "ROOTREV": f"revision/{ROOTREV_HASH}/",
+    "ROOTREV_PARENT": f"revision/{ROOTREV_PARENT_HASH}/",
+    "ROOTDIR": f"directory/{ROOTDIR_HASH}/",
     "README": f"content/sha1_git:{README_HASH}/",
     "README_RAW": f"content/sha1_git:{README_HASH}/raw/",
 }
 
 print("# GENERATED FILE, DO NOT EDIT.")
 print("# Run './gen-api-data.py > api_data.py' instead.")
 print("# fmt: off")
 
 print("")
 print(f"API_URL = '{API_URL_test}'")
-print(f"ROOT_SWHID = '{ROOT_SWHID}'")
+print(f"ROOTREV_SWHID = '{ROOTREV_SWHID}'")
+print(f"ROOTDIR_SWHID = '{ROOTDIR_SWHID}'")
 for name, url in urls.items():
     print(f"{name}_URL = '{url}'")
 print("")
 
 print("MOCK_ARCHIVE = {")
 for url in urls.values():
     print(f"    '{url}':  # NoQA: E501")
     print('    r"""', end="")
 
     data = requests.get(f"{API_URL_real}/{url}").text
     if url.endswith("/raw/"):
         print(data, end="")
     else:
         parsed = json.loads(data)
         print(json.dumps(parsed, indent=2, sort_keys=True))
 
     print('""",  # NoQA: E501')
 print("}")
diff --git a/swh/fuse/tests/test_cli.py b/swh/fuse/tests/test_cli.py
index 59290e3..b821c28 100644
--- a/swh/fuse/tests/test_cli.py
+++ b/swh/fuse/tests/test_cli.py
@@ -1,17 +1,17 @@
 # Copyright (C) 2020 The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 from pathlib import Path
 
-from .api_data import ROOT_SWHID
+from .api_data import ROOTDIR_SWHID
 
 
 def test_mountpoint(fuse_mntdir):
     archive_dir = Path(fuse_mntdir, "archive")
     meta_dir = Path(fuse_mntdir, "meta")
-    swhid_dir = Path(fuse_mntdir, "archive", ROOT_SWHID)
+    swhid_dir = Path(fuse_mntdir, "archive", ROOTDIR_SWHID)
     assert archive_dir.is_dir()
     assert meta_dir.is_dir()
     assert swhid_dir.is_dir()
diff --git a/swh/fuse/tests/test_content.py b/swh/fuse/tests/test_content.py
index 8b23dc5..ccf33e5 100644
--- a/swh/fuse/tests/test_content.py
+++ b/swh/fuse/tests/test_content.py
@@ -1,16 +1,16 @@
 from pathlib import Path
 
-from .api_data import MOCK_ARCHIVE, README_RAW_URL, ROOT_SWHID
+from .api_data import MOCK_ARCHIVE, README_RAW_URL, ROOTDIR_SWHID
 
 
 def test_file_exists(fuse_mntdir):
-    readme_path = Path(fuse_mntdir, "archive", ROOT_SWHID, "README")
+    readme_path = Path(fuse_mntdir, "archive", ROOTDIR_SWHID, "README")
     assert readme_path.is_file()
 
 
 def test_cat_file(fuse_mntdir):
-    readme_path = Path(fuse_mntdir, "archive", ROOT_SWHID, "README")
+    readme_path = Path(fuse_mntdir, "archive", ROOTDIR_SWHID, "README")
     expected = MOCK_ARCHIVE[README_RAW_URL]
     with open(readme_path, "r") as f:
         actual = f.read()
         assert actual == expected
diff --git a/swh/fuse/tests/test_directory.py b/swh/fuse/tests/test_directory.py
index 1eabb95..e3cf5af 100644
--- a/swh/fuse/tests/test_directory.py
+++ b/swh/fuse/tests/test_directory.py
@@ -1,14 +1,18 @@
 import json
 from os import listdir
 from pathlib import Path
 
-from .api_data import MOCK_ARCHIVE, ROOT_SWHID, ROOT_URL
+from .api_data import MOCK_ARCHIVE, ROOTDIR_SWHID, ROOTDIR_URL
 
 
-def test_ls_root_swhid(fuse_mntdir):
-    root_resp = json.loads(MOCK_ARCHIVE[ROOT_URL])
-    expected = [entry["name"] for entry in root_resp]
+def get_rootdir_entries():
+    rootdir_resp = json.loads(MOCK_ARCHIVE[ROOTDIR_URL])
+    return [entry["name"] for entry in rootdir_resp]
 
-    swhid_dir = Path(fuse_mntdir, "archive", ROOT_SWHID)
-    actual = listdir(swhid_dir)
+
+def test_ls_rootdir(fuse_mntdir):
+    expected = get_rootdir_entries()
+
+    rootdir_path = Path(fuse_mntdir, "archive", ROOTDIR_SWHID)
+    actual = listdir(rootdir_path)
     assert actual == expected
diff --git a/swh/fuse/tests/test_revision.py b/swh/fuse/tests/test_revision.py
new file mode 100644
index 0000000..b9eb428
--- /dev/null
+++ b/swh/fuse/tests/test_revision.py
@@ -0,0 +1,22 @@
+from os import listdir
+from pathlib import Path
+
+from swh.fuse.tests.test_directory import get_rootdir_entries
+
+from .api_data import ROOTREV_SWHID
+
+
+def test_symlinks_exist(fuse_mntdir):
+    rootrev_dir = Path(fuse_mntdir, "archive", ROOTREV_SWHID)
+    assert Path(rootrev_dir, "root").is_symlink()
+    assert Path(rootrev_dir, "parent").is_symlink()
+    assert Path(rootrev_dir, "parents").is_dir()
+    assert Path(rootrev_dir, "meta.json").is_symlink()
+
+
+def test_ls_rootdir(fuse_mntdir):
+    expected = get_rootdir_entries()
+
+    rootdir_path = Path(fuse_mntdir, "archive", ROOTREV_SWHID, "root")
+    actual = listdir(rootdir_path)
+    assert actual == expected