Changeset View
Standalone View
swh/loader/bzr/loader.py
- This file was added.
# Copyright (C) 2019 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 | ||||||||||||||||||||||
"""This document contains a SWH loader for ingesting repository data | ||||||||||||||||||||||
from Bazaar or Breezy. | ||||||||||||||||||||||
""" | ||||||||||||||||||||||
from collections import deque | ||||||||||||||||||||||
from datetime import datetime | ||||||||||||||||||||||
from typing import Deque, Dict, Iterator, NewType, Optional, TypeVar, Union | ||||||||||||||||||||||
from attr import dataclass | ||||||||||||||||||||||
from breezy import errors | ||||||||||||||||||||||
from breezy import graph as graphmod | ||||||||||||||||||||||
from breezy import repository | ||||||||||||||||||||||
from breezy.bzr import bzrdir | ||||||||||||||||||||||
from breezy.bzr.inventory import Inventory, InventoryEntry | ||||||||||||||||||||||
from breezy.revision import NULL_REVISION | ||||||||||||||||||||||
from breezy.revision import Revision as BzrRevision | ||||||||||||||||||||||
from swh.loader.core.loader import BaseLoader | ||||||||||||||||||||||
from swh.loader.core.utils import clean_dangling_folders | ||||||||||||||||||||||
from swh.model import identifiers | ||||||||||||||||||||||
from swh.model.from_disk import Content, DentryPerms, Directory | ||||||||||||||||||||||
from swh.model.model import ( | ||||||||||||||||||||||
ExtID, | ||||||||||||||||||||||
Origin, | ||||||||||||||||||||||
Person, | ||||||||||||||||||||||
Revision, | ||||||||||||||||||||||
RevisionType, | ||||||||||||||||||||||
Sha1Git, | ||||||||||||||||||||||
Snapshot, | ||||||||||||||||||||||
SnapshotBranch, | ||||||||||||||||||||||
TargetType, | ||||||||||||||||||||||
TimestampWithTimezone, | ||||||||||||||||||||||
) | ||||||||||||||||||||||
from swh.model.model import Content as ModelContent | ||||||||||||||||||||||
from swh.storage.interface import StorageInterface | ||||||||||||||||||||||
TEMPORARY_DIR_PREFIX_PATTERN = "swh.loader.bzr.from_disk" | ||||||||||||||||||||||
EXTID_TYPE = "bzr-nodeid" | ||||||||||||||||||||||
vlorentz: This is going to be confusing, because `swh.model.model` also has types `Content` and… | ||||||||||||||||||||||
EXTID_VERSION: int = 1 | ||||||||||||||||||||||
BzrRevisionId = NewType("BzrRevisionId", bytes) | ||||||||||||||||||||||
T = TypeVar("T") | ||||||||||||||||||||||
@dataclass | ||||||||||||||||||||||
class BranchingInfo: | ||||||||||||||||||||||
branches: Dict[bytes, BzrRevisionId] | ||||||||||||||||||||||
"""All branches of the repository, and their head revision id""" | ||||||||||||||||||||||
default_branch_alias: Optional[bytes] # We may not want a default branch alias | ||||||||||||||||||||||
"""The default snapshot branch to show in the UI""" | ||||||||||||||||||||||
class BzrDirectory(Directory): | ||||||||||||||||||||||
"""A more practical directory. | ||||||||||||||||||||||
- creates missing parent directories | ||||||||||||||||||||||
- removes empty directories | ||||||||||||||||||||||
""" | ||||||||||||||||||||||
def __setitem__(self, path: bytes, value: Union[Content, "BzrDirectory"]) -> None: | ||||||||||||||||||||||
if b"/" in path: | ||||||||||||||||||||||
head, tail = path.split(b"/", 1) | ||||||||||||||||||||||
directory = self.get(head) | ||||||||||||||||||||||
if directory is None or isinstance(directory, Content): | ||||||||||||||||||||||
directory = BzrDirectory() | ||||||||||||||||||||||
self[head] = directory | ||||||||||||||||||||||
directory[tail] = value | ||||||||||||||||||||||
else: | ||||||||||||||||||||||
super().__setitem__(path, value) | ||||||||||||||||||||||
def __delitem__(self, path: bytes) -> None: | ||||||||||||||||||||||
super().__delitem__(path) | ||||||||||||||||||||||
while b"/" in path: # remove empty parent directories | ||||||||||||||||||||||
path = path.rsplit(b"/", 1)[0] | ||||||||||||||||||||||
if len(self[path]) == 0: | ||||||||||||||||||||||
super().__delitem__(path) | ||||||||||||||||||||||
else: | ||||||||||||||||||||||
break | ||||||||||||||||||||||
def get( | ||||||||||||||||||||||
Done Inline ActionsCould you add tests for this? vlorentz: Could you add tests for this? | ||||||||||||||||||||||
self, path: bytes, default: Optional[T] = None | ||||||||||||||||||||||
) -> Optional[Union[Content, "BzrDirectory", T]]: | ||||||||||||||||||||||
# TODO move to swh.model.from_disk.Directory | ||||||||||||||||||||||
try: | ||||||||||||||||||||||
return self[path] | ||||||||||||||||||||||
except KeyError: | ||||||||||||||||||||||
return default | ||||||||||||||||||||||
class BazaarLoaderFromDisk(BaseLoader): | ||||||||||||||||||||||
"""Loads a Bazaar repository from disk""" | ||||||||||||||||||||||
visit_type = "bzr" | ||||||||||||||||||||||
def __init__( | ||||||||||||||||||||||
self, | ||||||||||||||||||||||
storage: StorageInterface, | ||||||||||||||||||||||
url: str, | ||||||||||||||||||||||
directory: Optional[str] = None, | ||||||||||||||||||||||
logging_class: str = "swh.loader.bzr.LoaderFromDisk", | ||||||||||||||||||||||
visit_date: Optional[datetime] = None, | ||||||||||||||||||||||
temp_directory: str = "/tmp", | ||||||||||||||||||||||
clone_timeout_seconds: int = 7200, | ||||||||||||||||||||||
max_content_size: Optional[int] = None, | ||||||||||||||||||||||
): | ||||||||||||||||||||||
super().__init__( | ||||||||||||||||||||||
storage=storage, | ||||||||||||||||||||||
logging_class=logging_class, | ||||||||||||||||||||||
max_content_size=max_content_size, | ||||||||||||||||||||||
) | ||||||||||||||||||||||
self._temp_directory = temp_directory | ||||||||||||||||||||||
self._clone_timeout = clone_timeout_seconds | ||||||||||||||||||||||
self._revision_id_to_sha1git: Dict[BzrRevisionId, Sha1Git] = {} | ||||||||||||||||||||||
self._last_root = BzrDirectory() | ||||||||||||||||||||||
self._branching_info: Optional[BranchingInfo] = None | ||||||||||||||||||||||
self.origin_url = url | ||||||||||||||||||||||
self.visit_date = visit_date | ||||||||||||||||||||||
self.directory = directory | ||||||||||||||||||||||
self.repo: Optional[repository.Repository] = None | ||||||||||||||||||||||
def pre_cleanup(self) -> None: | ||||||||||||||||||||||
"""As a first step, will try and check for dangling data to cleanup. | ||||||||||||||||||||||
This should do its best to avoid raising issues. | ||||||||||||||||||||||
""" | ||||||||||||||||||||||
clean_dangling_folders( | ||||||||||||||||||||||
self._temp_directory, | ||||||||||||||||||||||
pattern_check=TEMPORARY_DIR_PREFIX_PATTERN, | ||||||||||||||||||||||
log=self.log, | ||||||||||||||||||||||
) | ||||||||||||||||||||||
def prepare_origin_visit(self) -> None: | ||||||||||||||||||||||
"""First step executed by the loader to prepare origin and visit | ||||||||||||||||||||||
references. Set/update self.origin, and | ||||||||||||||||||||||
optionally self.origin_url, self.visit_date. | ||||||||||||||||||||||
""" | ||||||||||||||||||||||
self.origin = Origin(url=self.origin_url) | ||||||||||||||||||||||
def prepare(self) -> None: | ||||||||||||||||||||||
"""Second step executed by the loader to prepare some state needed by | ||||||||||||||||||||||
the loader. | ||||||||||||||||||||||
""" | ||||||||||||||||||||||
self.repo = bzrdir.BzrDir.open(self.directory).open_repository() | ||||||||||||||||||||||
self.repo.lock_read() | ||||||||||||||||||||||
def cleanup(self) -> None: | ||||||||||||||||||||||
assert self.repo is not None | ||||||||||||||||||||||
self.repo.unlock() | ||||||||||||||||||||||
def fetch_data(self) -> bool: | ||||||||||||||||||||||
"""Fetch the data from the source the loader is currently loading | ||||||||||||||||||||||
Returns: | ||||||||||||||||||||||
a value that is interpreted as a boolean. If True, fetch_data needs | ||||||||||||||||||||||
to be called again to complete loading. | ||||||||||||||||||||||
""" | ||||||||||||||||||||||
self.branching_info # set the property | ||||||||||||||||||||||
return False | ||||||||||||||||||||||
def get_bzr_revs_to_load(self) -> Iterator[BzrRevision]: | ||||||||||||||||||||||
assert self.repo is not None | ||||||||||||||||||||||
repo: repository.Repository = self.repo | ||||||||||||||||||||||
graph: graphmod.Graph = repo.get_graph() | ||||||||||||||||||||||
# TODO should we do an external parent mapping algorithm? | ||||||||||||||||||||||
heads = sorted(self.branching_info.branches.values()) | ||||||||||||||||||||||
for head in heads: | ||||||||||||||||||||||
graph_iter = reversed( | ||||||||||||||||||||||
list(graph.iter_lefthand_ancestry(head, (NULL_REVISION,))) | ||||||||||||||||||||||
) | ||||||||||||||||||||||
while True: | ||||||||||||||||||||||
try: | ||||||||||||||||||||||
revision_id = next(graph_iter) | ||||||||||||||||||||||
except errors.RevisionNotPresent as e: | ||||||||||||||||||||||
# Oops, a ghost. TODO what should we do with ghosts? | ||||||||||||||||||||||
yield e.revision_id | ||||||||||||||||||||||
break | ||||||||||||||||||||||
except StopIteration: | ||||||||||||||||||||||
break | ||||||||||||||||||||||
else: | ||||||||||||||||||||||
yield revision_id | ||||||||||||||||||||||
def store_data(self): | ||||||||||||||||||||||
"""Store fetched data in the database.""" | ||||||||||||||||||||||
# Insert revisions using a topological sorting | ||||||||||||||||||||||
revs = self.get_bzr_revs_to_load() | ||||||||||||||||||||||
for rev in revs: | ||||||||||||||||||||||
Not Done Inline Actionsthis should probably match some sort of delimiter after the format version (eg. if a future version uses "Bazaar repository format 2abc") Also, it looks like this would fail with "needs upgrade" in the presence of repositories newer than this. vlorentz: this should probably match some sort of delimiter after the format version (eg. if a future… | ||||||||||||||||||||||
Done Inline Actions
Sure, good point
This is on purpose since it's very unlikely that Breezy will get a new repository format. In the case that it does, we will need to update this code anyway. Should the exception carry more detail in its docstring to encourage checking for a newer format if brz upgrade does work? Alphare: > this should probably match some sort of delimiter after the format version (eg. if a future… | ||||||||||||||||||||||
Done Inline ActionsI would expect something like this: if format is older: raise RepositoryNeedsUpgrade() elif format is newer: assert False, "oh no, we need to update this loader for format XXX" vlorentz: I would expect something like this:
```
if format is older:
raise RepositoryNeedsUpgrade()… | ||||||||||||||||||||||
revision = self.repo.get_revision(rev) | ||||||||||||||||||||||
self.store_revision(revision) | ||||||||||||||||||||||
# TODO tags, (maybe other things?) | ||||||||||||||||||||||
snapshot_branches: Dict[bytes, SnapshotBranch] = {} | ||||||||||||||||||||||
branching_info = self.branching_info | ||||||||||||||||||||||
for branch_name, head in branching_info.branches.items(): | ||||||||||||||||||||||
target = self.get_revision_id_from_bzr_id(head) | ||||||||||||||||||||||
snapshot_branches[branch_name] = SnapshotBranch( | ||||||||||||||||||||||
target=target, target_type=TargetType.REVISION | ||||||||||||||||||||||
) | ||||||||||||||||||||||
default_branch_head = branching_info.default_branch_alias | ||||||||||||||||||||||
if default_branch_head is not None: | ||||||||||||||||||||||
snapshot_branches[b"HEAD"] = SnapshotBranch( | ||||||||||||||||||||||
target=default_branch_head, target_type=TargetType.ALIAS, | ||||||||||||||||||||||
) | ||||||||||||||||||||||
snapshot = Snapshot(branches=snapshot_branches) | ||||||||||||||||||||||
self.storage.snapshot_add([snapshot]) | ||||||||||||||||||||||
self.flush() | ||||||||||||||||||||||
self.loaded_snapshot_id = snapshot.id | ||||||||||||||||||||||
def store_content( | ||||||||||||||||||||||
self, bzr_rev: BzrRevision, file_path: bytes, entry: InventoryEntry | ||||||||||||||||||||||
) -> Content: | ||||||||||||||||||||||
repo: repository.Repository = self.repo | ||||||||||||||||||||||
Not Done Inline ActionsWe would probably want to store a dangling branch for this. vlorentz: We would probably want to store a dangling branch for this. | ||||||||||||||||||||||
Done Inline ActionsI'm not 100% sure how to do that. Is the target just the empty string as git_objects.py explains? Alphare: I'm not 100% sure how to do that. Is the target just the empty string as `git_objects.py`… | ||||||||||||||||||||||
Not Done Inline Actionser, I think so vlorentz: er, I think so | ||||||||||||||||||||||
Done Inline ActionsLooking at the code in swh-model/swh/model.git_objects.py, dangling branches are not exposed as a TargetType variant but hardcoded bytes, so there appears to be no easy way for me to do it from the outside. Is there? Alphare: Looking at the code in `swh-model/swh/model.git_objects.py`, dangling branches are not exposed… | ||||||||||||||||||||||
Not Done Inline ActionsSnapshotBranch(target=b"", target_type=TargetType.REVISION) vlorentz: `SnapshotBranch(target=b"", target_type=TargetType.REVISION)` | ||||||||||||||||||||||
Done Inline ActionsI had tried this, and it gives the following error: `swh.storage.exc.StorageArgumentException: value for domain sha1_git violates check constraint "sha1_git_check" Alphare: I had tried this, and it gives the following error: `swh.storage.exc.StorageArgumentException… | ||||||||||||||||||||||
Not Done Inline Actionsheh, alright, it's not worth the trouble then vlorentz: heh, alright, it's not worth the trouble then | ||||||||||||||||||||||
Not Done Inline ActionsA dangling branch is stored in the Snapshot as a None instead of the SnapshotBranch object. olasd: A dangling branch is stored in the Snapshot as a `None` instead of the `SnapshotBranch` object. | ||||||||||||||||||||||
Done Inline ActionsAaah, that's what I was missing, thanks vlorentz: Aaah, that's what I was missing, thanks | ||||||||||||||||||||||
if entry.executable: | ||||||||||||||||||||||
perms = DentryPerms.executable_content | ||||||||||||||||||||||
elif entry.kind == "directory": | ||||||||||||||||||||||
perms = DentryPerms.directory | ||||||||||||||||||||||
elif entry.kind == "symlink": | ||||||||||||||||||||||
perms = DentryPerms.symlink | ||||||||||||||||||||||
elif entry.kind == "file": | ||||||||||||||||||||||
perms = DentryPerms.content | ||||||||||||||||||||||
else: | ||||||||||||||||||||||
raise RuntimeError("Hit unreachable condition") | ||||||||||||||||||||||
Done Inline Actionsditto vlorentz: ditto | ||||||||||||||||||||||
data = b"" | ||||||||||||||||||||||
if entry.has_text(): | ||||||||||||||||||||||
rev_tree = repo.revision_tree(bzr_rev.revision_id) | ||||||||||||||||||||||
data = rev_tree.get_file(file_path).read() | ||||||||||||||||||||||
assert len(data) == entry.text_size | ||||||||||||||||||||||
content = ModelContent.from_data(data) | ||||||||||||||||||||||
self.storage.content_add([content]) | ||||||||||||||||||||||
return Content({"sha1_git": content.sha1_git, "perms": perms}) | ||||||||||||||||||||||
Not Done Inline Actionswhat is a branch nick? vlorentz: what is a branch nick? | ||||||||||||||||||||||
Done Inline ActionsThe name associated with the branch in the clone source. It's usually the name of the project, but not always. It's not really meaningful in our case since we're keeping metadata about the project including its name anyway and we only have one branch per repository. Alphare: The name associated with the branch in the clone source. It's usually the name of the project… | ||||||||||||||||||||||
def store_directories(self, bzr_rev: BzrRevision) -> Sha1Git: | ||||||||||||||||||||||
repo: repository.Repository = self.repo | ||||||||||||||||||||||
inventory: Inventory = repo.get_inventory(bzr_rev.revision_id) | ||||||||||||||||||||||
# TODO store from diff instead of everything each revision | ||||||||||||||||||||||
for path, entry in inventory.iter_entries(): | ||||||||||||||||||||||
if path == "": | ||||||||||||||||||||||
# root repo is created by default (TODO check this) | ||||||||||||||||||||||
continue | ||||||||||||||||||||||
content = self.store_content(bzr_rev, path, entry) | ||||||||||||||||||||||
self._last_root[path.encode()] = content | ||||||||||||||||||||||
directories: Deque[Directory] = deque([self._last_root]) | ||||||||||||||||||||||
while directories: | ||||||||||||||||||||||
directory = directories.pop() | ||||||||||||||||||||||
self.storage.directory_add([directory.to_model()]) | ||||||||||||||||||||||
Not Done Inline Actions
brand-new method, added last week :) vlorentz: brand-new method, added last week :) | ||||||||||||||||||||||
Done Inline ActionsThis does not seems to be valid, and I'm not sure exactly how to use the new method to be honest. Alphare: This does not seems to be valid, and I'm not sure exactly how to use the new method to be… | ||||||||||||||||||||||
Not Done Inline Actionswhy not? vlorentz: why not? | ||||||||||||||||||||||
directories.extend( | ||||||||||||||||||||||
[item for item in directory.values() if isinstance(item, Directory)] | ||||||||||||||||||||||
Not Done Inline ActionsWe need to discuss this format. Could you open a task and ping olasd and I? We are working on changes to TimestampWithTimezone, which makes it more extensible. vlorentz: We need to discuss this format. Could you open a task and ping olasd and I?
We are working on… | ||||||||||||||||||||||
Not Done Inline Actionsactually, I just did: T3886 vlorentz: actually, I just did: T3886 | ||||||||||||||||||||||
) | ||||||||||||||||||||||
return self._last_root.hash | ||||||||||||||||||||||
Done Inline Actionsanother task for this. We could settle on GitHub's ad-hoc Co-authored-by format: https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/creating-a-commit-with-multiple-authors but this needs a discussion vlorentz: another task for this. We could settle on GitHub's ad-hoc `Co-authored-by` format: https://docs. | ||||||||||||||||||||||
Done Inline ActionsAlphare: Done in T3887 and T3888 | ||||||||||||||||||||||
def store_revision(self, bzr_rev: BzrRevision): | ||||||||||||||||||||||
date = TimestampWithTimezone.from_dict(int(bzr_rev.timestamp)) | ||||||||||||||||||||||
extra_headers = [ | ||||||||||||||||||||||
(b"time_offset_seconds", str(bzr_rev.timezone).encode(),), | ||||||||||||||||||||||
] | ||||||||||||||||||||||
directory = self.store_directories(bzr_rev) | ||||||||||||||||||||||
revision = Revision( | ||||||||||||||||||||||
author=Person.from_fullname(bzr_rev.get_apparent_authors()[0].encode()), | ||||||||||||||||||||||
date=date, | ||||||||||||||||||||||
committer=Person.from_fullname(bzr_rev.committer.encode()), | ||||||||||||||||||||||
committer_date=date, | ||||||||||||||||||||||
type=RevisionType.BAZAAR, | ||||||||||||||||||||||
directory=directory, | ||||||||||||||||||||||
message=bzr_rev.message.encode(), | ||||||||||||||||||||||
extra_headers=extra_headers, # todo check those | ||||||||||||||||||||||
synthetic=False, | ||||||||||||||||||||||
parents=self.get_revision_parents(bzr_rev), | ||||||||||||||||||||||
) | ||||||||||||||||||||||
self._revision_id_to_sha1git[bzr_rev.revision_id] = revision.id | ||||||||||||||||||||||
self.storage.revision_add([revision]) | ||||||||||||||||||||||
# Save the mapping from SWHID to bzr id | ||||||||||||||||||||||
revision_swhid = identifiers.CoreSWHID( | ||||||||||||||||||||||
object_type=identifiers.ObjectType.REVISION, object_id=revision.id, | ||||||||||||||||||||||
) | ||||||||||||||||||||||
self.storage.extid_add( | ||||||||||||||||||||||
[ | ||||||||||||||||||||||
ExtID( | ||||||||||||||||||||||
Done Inline Actions
vlorentz: | ||||||||||||||||||||||
extid_type=EXTID_TYPE, | ||||||||||||||||||||||
extid_version=EXTID_VERSION, | ||||||||||||||||||||||
extid=bzr_rev.revision_id, | ||||||||||||||||||||||
target=revision_swhid, | ||||||||||||||||||||||
) | ||||||||||||||||||||||
] | ||||||||||||||||||||||
) | ||||||||||||||||||||||
def get_revision_parents(self, bzr_rev: BzrRevision): | ||||||||||||||||||||||
parents = [] | ||||||||||||||||||||||
for parent_id in bzr_rev.parent_ids: | ||||||||||||||||||||||
if parent_id == NULL_REVISION: | ||||||||||||||||||||||
continue | ||||||||||||||||||||||
revision_id = self.get_revision_id_from_bzr_id(parent_id) | ||||||||||||||||||||||
parents.append(revision_id) | ||||||||||||||||||||||
return tuple(parents) | ||||||||||||||||||||||
def get_revision_id_from_bzr_id(self, bzr_id: BzrRevisionId) -> Sha1Git: | ||||||||||||||||||||||
"""Return the git sha1 of a revision given its bazaar revision id. | ||||||||||||||||||||||
""" | ||||||||||||||||||||||
from_cache = self._revision_id_to_sha1git.get(bzr_id) | ||||||||||||||||||||||
if from_cache is not None: | ||||||||||||||||||||||
return from_cache | ||||||||||||||||||||||
# The parent was not loaded in this run, get it from storage | ||||||||||||||||||||||
from_storage = [ | ||||||||||||||||||||||
extid | ||||||||||||||||||||||
for extid in self.storage.extid_get_from_extid(EXTID_TYPE, ids=[bzr_id]) | ||||||||||||||||||||||
if extid.extid_version == EXTID_VERSION | ||||||||||||||||||||||
] | ||||||||||||||||||||||
msg = "Expected 1 match from storage for bzr node %r, got %d" | ||||||||||||||||||||||
assert len(from_storage) == 1, msg % (bzr_id, len(from_storage)) | ||||||||||||||||||||||
return from_storage[0].target.object_id | ||||||||||||||||||||||
@property | ||||||||||||||||||||||
def branching_info(self) -> BranchingInfo: | ||||||||||||||||||||||
assert self.repo is not None | ||||||||||||||||||||||
if self._branching_info is None: | ||||||||||||||||||||||
heads = { | ||||||||||||||||||||||
b.nick.encode(): b.last_revision() | ||||||||||||||||||||||
for b in self.repo.find_branches(using=True) | ||||||||||||||||||||||
} | ||||||||||||||||||||||
default = heads.get(b"trunk") | ||||||||||||||||||||||
if default is None and len(heads) == 1: | ||||||||||||||||||||||
# TODO Maybe this doesn't make sense? | ||||||||||||||||||||||
default = list(heads.keys())[0] | ||||||||||||||||||||||
Not Done Inline Actionswhat does it mean when entry.has_text() is falsy? vlorentz: what does it mean when `entry.has_text()` is falsy? | ||||||||||||||||||||||
Done Inline ActionsIt means it's from an old tree that did not have the "weave index" for all inventory entries (something to help with merges IIUC), basically this is for backwards compat and the text is functionally empty. Alphare: It means it's from an old tree that did not have the "weave index" for all inventory entries… | ||||||||||||||||||||||
branching_info = BranchingInfo( | ||||||||||||||||||||||
branches=heads, default_branch_alias=default, | ||||||||||||||||||||||
) | ||||||||||||||||||||||
self._branching_info = branching_info | ||||||||||||||||||||||
return self._branching_info | ||||||||||||||||||||||
Done Inline Actionsshould be faster with a regular list, since you're only using it as a stack vlorentz: should be faster with a regular list, since you're only using it as a stack | ||||||||||||||||||||||
Not Done Inline Actionsdead code? vlorentz: dead code? | ||||||||||||||||||||||
Done Inline ActionsAh this will only be useful with the incremental loader which (most likely) will arrive in the next patch. I should probably remove the whole storage fallback for now. Alphare: Ah this will only be useful with the incremental loader which (most likely) will arrive in the… | ||||||||||||||||||||||
Done Inline Actionscould you add a docstring? vlorentz: could you add a docstring? |
This is going to be confusing, because swh.model.model also has types Content and Directory.
Could you replace from swh.model.from_disk import Content, DentryPerms, Directory with from swh.model import from_disk?