Skip to content

Commit 2ce617c

Browse files
authored
git: add pygit2 skeleton (#5138)
Implements stuff we need for the tree, but this is more to start filling up pygit2 with something useful.
1 parent f5d46c2 commit 2ce617c

File tree

3 files changed

+242
-1
lines changed

3 files changed

+242
-1
lines changed

dvc/scm/git/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .backend.base import NoGitBackendError
2121
from .backend.dulwich import DulwichBackend
2222
from .backend.gitpython import GitPythonBackend
23+
from .backend.pygit2 import Pygit2Backend
2324
from .stash import Stash
2425

2526
logger = logging.getLogger(__name__)
@@ -31,7 +32,11 @@ class Git(Base):
3132
GITIGNORE = ".gitignore"
3233
GIT_DIR = ".git"
3334
DEFAULT_BACKENDS = OrderedDict(
34-
[("dulwich", DulwichBackend), ("gitpython", GitPythonBackend)]
35+
[
36+
("dulwich", DulwichBackend),
37+
("pygit2", Pygit2Backend),
38+
("gitpython", GitPythonBackend),
39+
]
3540
)
3641
LOCAL_BRANCH_PREFIX = "refs/heads/"
3742

@@ -64,6 +69,10 @@ def gitpython(self):
6469
def dulwich(self):
6570
return self.backends["dulwich"]
6671

72+
@property
73+
def pygit2(self):
74+
return self.backends["pygit2"]
75+
6776
@cached_property
6877
def stash(self):
6978
return Stash(self)

dvc/scm/git/backend/pygit2.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import locale
2+
import logging
3+
import os
4+
from io import BytesIO, StringIO
5+
from typing import Callable, Iterable, Optional, Tuple
6+
7+
from dvc.scm.base import SCMError
8+
9+
from ..objects import GitObject
10+
from .base import BaseGitBackend
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class Pygit2Object(GitObject):
16+
def __init__(self, obj):
17+
self.obj = obj
18+
19+
def open(self, mode: str = "r", encoding: str = None):
20+
if not encoding:
21+
encoding = locale.getpreferredencoding(False)
22+
data = self.obj.read_raw()
23+
if mode == "rb":
24+
return BytesIO(data)
25+
return StringIO(data.decode(encoding))
26+
27+
@property
28+
def name(self) -> str:
29+
return self.obj.name
30+
31+
@property
32+
def mode(self):
33+
return self.obj.filemode
34+
35+
def scandir(self) -> Iterable["Pygit2Object"]:
36+
for entry in self.obj: # noqa: B301
37+
yield Pygit2Object(entry)
38+
39+
40+
class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
41+
def __init__( # pylint:disable=W0231
42+
self, root_dir=os.curdir, search_parent_directories=True
43+
):
44+
import pygit2
45+
46+
if search_parent_directories:
47+
ceiling_dirs = ""
48+
else:
49+
ceiling_dirs = os.path.abspath(root_dir)
50+
51+
# NOTE: discover_repository will return path/.git/
52+
path = pygit2.discover_repository( # pylint:disable=no-member
53+
root_dir, True, ceiling_dirs
54+
)
55+
if not path:
56+
raise SCMError(f"{root_dir} is not a git repository")
57+
58+
self.repo = pygit2.Repository(path)
59+
60+
self._stashes: dict = {}
61+
62+
def close(self):
63+
self.repo.free()
64+
65+
@property
66+
def root_dir(self) -> str:
67+
return self.repo.workdir
68+
69+
@staticmethod
70+
def clone(
71+
url: str,
72+
to_path: str,
73+
rev: Optional[str] = None,
74+
shallow_branch: Optional[str] = None,
75+
):
76+
raise NotImplementedError
77+
78+
@staticmethod
79+
def is_sha(rev: str) -> bool:
80+
raise NotImplementedError
81+
82+
@property
83+
def dir(self) -> str:
84+
raise NotImplementedError
85+
86+
def add(self, paths: Iterable[str]):
87+
raise NotImplementedError
88+
89+
def commit(self, msg: str):
90+
raise NotImplementedError
91+
92+
def checkout(
93+
self, branch: str, create_new: Optional[bool] = False, **kwargs,
94+
):
95+
raise NotImplementedError
96+
97+
def pull(self, **kwargs):
98+
raise NotImplementedError
99+
100+
def push(self):
101+
raise NotImplementedError
102+
103+
def branch(self, branch: str):
104+
raise NotImplementedError
105+
106+
def tag(self, tag: str):
107+
raise NotImplementedError
108+
109+
def untracked_files(self) -> Iterable[str]:
110+
raise NotImplementedError
111+
112+
def is_tracked(self, path: str) -> bool:
113+
raise NotImplementedError
114+
115+
def is_dirty(self, **kwargs) -> bool:
116+
raise NotImplementedError
117+
118+
def active_branch(self) -> str:
119+
raise NotImplementedError
120+
121+
def list_branches(self) -> Iterable[str]:
122+
raise NotImplementedError
123+
124+
def list_tags(self) -> Iterable[str]:
125+
raise NotImplementedError
126+
127+
def list_all_commits(self) -> Iterable[str]:
128+
raise NotImplementedError
129+
130+
def get_tree_obj(self, rev: str, **kwargs) -> Pygit2Object:
131+
tree = self.repo[rev].tree
132+
return Pygit2Object(tree)
133+
134+
def get_rev(self) -> str:
135+
raise NotImplementedError
136+
137+
def resolve_rev(self, rev: str) -> str:
138+
raise NotImplementedError
139+
140+
def resolve_commit(self, rev: str) -> str:
141+
raise NotImplementedError
142+
143+
def branch_revs(self, branch: str, end_rev: Optional[str] = None):
144+
raise NotImplementedError
145+
146+
def _get_stash(self, ref: str):
147+
raise NotImplementedError
148+
149+
def is_ignored(self, path):
150+
raise NotImplementedError
151+
152+
def set_ref(
153+
self,
154+
name: str,
155+
new_ref: str,
156+
old_ref: Optional[str] = None,
157+
message: Optional[str] = None,
158+
symbolic: Optional[bool] = False,
159+
):
160+
raise NotImplementedError
161+
162+
def get_ref(self, name, follow: Optional[bool] = True) -> Optional[str]:
163+
raise NotImplementedError
164+
165+
def remove_ref(self, name: str, old_ref: Optional[str] = None):
166+
raise NotImplementedError
167+
168+
def iter_refs(self, base: Optional[str] = None):
169+
raise NotImplementedError
170+
171+
def get_refs_containing(self, rev: str, pattern: Optional[str] = None):
172+
raise NotImplementedError
173+
174+
def push_refspec(
175+
self,
176+
url: str,
177+
src: Optional[str],
178+
dest: str,
179+
force: bool = False,
180+
on_diverged: Optional[Callable[[str, str], bool]] = None,
181+
):
182+
raise NotImplementedError
183+
184+
def fetch_refspecs(
185+
self,
186+
url: str,
187+
refspecs: Iterable[str],
188+
force: Optional[bool] = False,
189+
on_diverged: Optional[Callable[[bytes, bytes], bool]] = None,
190+
):
191+
raise NotImplementedError
192+
193+
def _stash_iter(self, ref: str):
194+
raise NotImplementedError
195+
196+
def _stash_push(
197+
self,
198+
ref: str,
199+
message: Optional[str] = None,
200+
include_untracked: Optional[bool] = False,
201+
) -> Tuple[Optional[str], bool]:
202+
raise NotImplementedError
203+
204+
def _stash_apply(self, rev: str):
205+
raise NotImplementedError
206+
207+
def reflog_delete(
208+
self, ref: str, updateref: bool = False, rewrite: bool = False
209+
):
210+
raise NotImplementedError
211+
212+
def describe(
213+
self,
214+
rev: str,
215+
base: Optional[str] = None,
216+
match: Optional[str] = None,
217+
exclude: Optional[str] = None,
218+
) -> Optional[str]:
219+
raise NotImplementedError
220+
221+
def diff(self, rev_a: str, rev_b: str, binary=False) -> str:
222+
raise NotImplementedError
223+
224+
def reset(self, hard: bool = False, paths: Iterable[str] = None):
225+
raise NotImplementedError
226+
227+
def checkout_paths(self, paths: Iterable[str], force: bool = False):
228+
raise NotImplementedError
229+
230+
def iter_remote_refs(self, url: str, base: Optional[str] = None):
231+
raise NotImplementedError

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def run(self):
5151
"configobj>=5.0.6",
5252
"gitpython>3",
5353
"dulwich>=0.20.14",
54+
"pygit2>=1.4.0",
5455
"setuptools>=34.0.0",
5556
"nanotime>=0.5.2",
5657
"pyasn1>=0.4.1",

0 commit comments

Comments
 (0)