From 2ce617c642d50f546a8cd287e22b3f4dd3dc8273 Mon Sep 17 00:00:00 2001 From: Ruslan Kuprieiev Date: Sat, 19 Dec 2020 02:44:02 +0200 Subject: [PATCH] git: add pygit2 skeleton (#5138) Implements stuff we need for the tree, but this is more to start filling up pygit2 with something useful. --- dvc/scm/git/__init__.py | 11 +- dvc/scm/git/backend/pygit2.py | 231 ++++++++++++++++++++++++++++++++++ setup.py | 1 + 3 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 dvc/scm/git/backend/pygit2.py diff --git a/dvc/scm/git/__init__.py b/dvc/scm/git/__init__.py index 7373184f7..d01e675d1 100644 --- a/dvc/scm/git/__init__.py +++ b/dvc/scm/git/__init__.py @@ -20,6 +20,7 @@ from dvc.utils.serialize import modify_yaml from .backend.base import NoGitBackendError from .backend.dulwich import DulwichBackend from .backend.gitpython import GitPythonBackend +from .backend.pygit2 import Pygit2Backend from .stash import Stash logger = logging.getLogger(__name__) @@ -31,7 +32,11 @@ class Git(Base): GITIGNORE = ".gitignore" GIT_DIR = ".git" DEFAULT_BACKENDS = OrderedDict( - [("dulwich", DulwichBackend), ("gitpython", GitPythonBackend)] + [ + ("dulwich", DulwichBackend), + ("pygit2", Pygit2Backend), + ("gitpython", GitPythonBackend), + ] ) LOCAL_BRANCH_PREFIX = "refs/heads/" @@ -64,6 +69,10 @@ class Git(Base): def dulwich(self): return self.backends["dulwich"] + @property + def pygit2(self): + return self.backends["pygit2"] + @cached_property def stash(self): return Stash(self) diff --git a/dvc/scm/git/backend/pygit2.py b/dvc/scm/git/backend/pygit2.py new file mode 100644 index 000000000..51c7bfe98 --- /dev/null +++ b/dvc/scm/git/backend/pygit2.py @@ -0,0 +1,231 @@ +import locale +import logging +import os +from io import BytesIO, StringIO +from typing import Callable, Iterable, Optional, Tuple + +from dvc.scm.base import SCMError + +from ..objects import GitObject +from .base import BaseGitBackend + +logger = logging.getLogger(__name__) + + +class Pygit2Object(GitObject): + def __init__(self, obj): + self.obj = obj + + def open(self, mode: str = "r", encoding: str = None): + if not encoding: + encoding = locale.getpreferredencoding(False) + data = self.obj.read_raw() + if mode == "rb": + return BytesIO(data) + return StringIO(data.decode(encoding)) + + @property + def name(self) -> str: + return self.obj.name + + @property + def mode(self): + return self.obj.filemode + + def scandir(self) -> Iterable["Pygit2Object"]: + for entry in self.obj: # noqa: B301 + yield Pygit2Object(entry) + + +class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method + def __init__( # pylint:disable=W0231 + self, root_dir=os.curdir, search_parent_directories=True + ): + import pygit2 + + if search_parent_directories: + ceiling_dirs = "" + else: + ceiling_dirs = os.path.abspath(root_dir) + + # NOTE: discover_repository will return path/.git/ + path = pygit2.discover_repository( # pylint:disable=no-member + root_dir, True, ceiling_dirs + ) + if not path: + raise SCMError(f"{root_dir} is not a git repository") + + self.repo = pygit2.Repository(path) + + self._stashes: dict = {} + + def close(self): + self.repo.free() + + @property + def root_dir(self) -> str: + return self.repo.workdir + + @staticmethod + def clone( + url: str, + to_path: str, + rev: Optional[str] = None, + shallow_branch: Optional[str] = None, + ): + raise NotImplementedError + + @staticmethod + def is_sha(rev: str) -> bool: + raise NotImplementedError + + @property + def dir(self) -> str: + raise NotImplementedError + + def add(self, paths: Iterable[str]): + raise NotImplementedError + + def commit(self, msg: str): + raise NotImplementedError + + def checkout( + self, branch: str, create_new: Optional[bool] = False, **kwargs, + ): + raise NotImplementedError + + def pull(self, **kwargs): + raise NotImplementedError + + def push(self): + raise NotImplementedError + + def branch(self, branch: str): + raise NotImplementedError + + def tag(self, tag: str): + raise NotImplementedError + + def untracked_files(self) -> Iterable[str]: + raise NotImplementedError + + def is_tracked(self, path: str) -> bool: + raise NotImplementedError + + def is_dirty(self, **kwargs) -> bool: + raise NotImplementedError + + def active_branch(self) -> str: + raise NotImplementedError + + def list_branches(self) -> Iterable[str]: + raise NotImplementedError + + def list_tags(self) -> Iterable[str]: + raise NotImplementedError + + def list_all_commits(self) -> Iterable[str]: + raise NotImplementedError + + def get_tree_obj(self, rev: str, **kwargs) -> Pygit2Object: + tree = self.repo[rev].tree + return Pygit2Object(tree) + + def get_rev(self) -> str: + raise NotImplementedError + + def resolve_rev(self, rev: str) -> str: + raise NotImplementedError + + def resolve_commit(self, rev: str) -> str: + raise NotImplementedError + + def branch_revs(self, branch: str, end_rev: Optional[str] = None): + raise NotImplementedError + + def _get_stash(self, ref: str): + raise NotImplementedError + + def is_ignored(self, path): + raise NotImplementedError + + def set_ref( + self, + name: str, + new_ref: str, + old_ref: Optional[str] = None, + message: Optional[str] = None, + symbolic: Optional[bool] = False, + ): + raise NotImplementedError + + def get_ref(self, name, follow: Optional[bool] = True) -> Optional[str]: + raise NotImplementedError + + def remove_ref(self, name: str, old_ref: Optional[str] = None): + raise NotImplementedError + + def iter_refs(self, base: Optional[str] = None): + raise NotImplementedError + + def get_refs_containing(self, rev: str, pattern: Optional[str] = None): + raise NotImplementedError + + def push_refspec( + self, + url: str, + src: Optional[str], + dest: str, + force: bool = False, + on_diverged: Optional[Callable[[str, str], bool]] = None, + ): + raise NotImplementedError + + def fetch_refspecs( + self, + url: str, + refspecs: Iterable[str], + force: Optional[bool] = False, + on_diverged: Optional[Callable[[bytes, bytes], bool]] = None, + ): + raise NotImplementedError + + def _stash_iter(self, ref: str): + raise NotImplementedError + + def _stash_push( + self, + ref: str, + message: Optional[str] = None, + include_untracked: Optional[bool] = False, + ) -> Tuple[Optional[str], bool]: + raise NotImplementedError + + def _stash_apply(self, rev: str): + raise NotImplementedError + + def reflog_delete( + self, ref: str, updateref: bool = False, rewrite: bool = False + ): + raise NotImplementedError + + def describe( + self, + rev: str, + base: Optional[str] = None, + match: Optional[str] = None, + exclude: Optional[str] = None, + ) -> Optional[str]: + raise NotImplementedError + + def diff(self, rev_a: str, rev_b: str, binary=False) -> str: + raise NotImplementedError + + def reset(self, hard: bool = False, paths: Iterable[str] = None): + raise NotImplementedError + + def checkout_paths(self, paths: Iterable[str], force: bool = False): + raise NotImplementedError + + def iter_remote_refs(self, url: str, base: Optional[str] = None): + raise NotImplementedError diff --git a/setup.py b/setup.py index eca0f80bb..fede46905 100644 --- a/setup.py +++ b/setup.py @@ -51,6 +51,7 @@ install_requires = [ "configobj>=5.0.6", "gitpython>3", "dulwich>=0.20.14", + "pygit2>=1.4.0", "setuptools>=34.0.0", "nanotime>=0.5.2", "pyasn1>=0.4.1",