scmrepo 1.4.0__py3-none-any.whl → 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of scmrepo might be problematic. Click here for more details.
- scmrepo/base.py +5 -1
- scmrepo/fs.py +4 -3
- scmrepo/git/__init__.py +13 -0
- scmrepo/git/backend/base.py +43 -0
- scmrepo/git/backend/dulwich/__init__.py +67 -1
- scmrepo/git/backend/gitpython.py +56 -1
- scmrepo/git/backend/pygit2/__init__.py +201 -43
- scmrepo/git/backend/pygit2/callbacks.py +10 -1
- scmrepo/git/backend/pygit2/filter.py +65 -0
- scmrepo/git/config.py +35 -0
- scmrepo/git/lfs/__init__.py +8 -0
- scmrepo/git/lfs/client.py +223 -0
- scmrepo/git/lfs/exceptions.py +5 -0
- scmrepo/git/lfs/fetch.py +162 -0
- scmrepo/git/lfs/object.py +15 -0
- scmrepo/git/lfs/pointer.py +109 -0
- scmrepo/git/lfs/progress.py +61 -0
- scmrepo/git/lfs/smudge.py +51 -0
- scmrepo/git/lfs/storage.py +74 -0
- scmrepo/git/objects.py +3 -2
- {scmrepo-1.4.0.dist-info → scmrepo-1.5.0.dist-info}/METADATA +4 -2
- scmrepo-1.5.0.dist-info/RECORD +37 -0
- {scmrepo-1.4.0.dist-info → scmrepo-1.5.0.dist-info}/WHEEL +1 -1
- scmrepo-1.4.0.dist-info/RECORD +0 -26
- {scmrepo-1.4.0.dist-info → scmrepo-1.5.0.dist-info}/LICENSE +0 -0
- {scmrepo-1.4.0.dist-info → scmrepo-1.5.0.dist-info}/top_level.txt +0 -0
|
@@ -3,13 +3,14 @@ import logging
|
|
|
3
3
|
import os
|
|
4
4
|
import stat
|
|
5
5
|
from contextlib import contextmanager
|
|
6
|
-
from io import BytesIO, StringIO
|
|
6
|
+
from io import BytesIO, StringIO, TextIOWrapper
|
|
7
7
|
from typing import (
|
|
8
8
|
TYPE_CHECKING,
|
|
9
9
|
Callable,
|
|
10
10
|
Dict,
|
|
11
11
|
Generator,
|
|
12
12
|
Iterable,
|
|
13
|
+
Iterator,
|
|
13
14
|
List,
|
|
14
15
|
Mapping,
|
|
15
16
|
Optional,
|
|
@@ -20,8 +21,15 @@ from urllib.parse import urlparse
|
|
|
20
21
|
|
|
21
22
|
from funcy import cached_property, reraise
|
|
22
23
|
|
|
23
|
-
from scmrepo.exceptions import
|
|
24
|
+
from scmrepo.exceptions import (
|
|
25
|
+
CloneError,
|
|
26
|
+
InvalidRemote,
|
|
27
|
+
MergeConflictError,
|
|
28
|
+
RevError,
|
|
29
|
+
SCMError,
|
|
30
|
+
)
|
|
24
31
|
from scmrepo.git.backend.base import BaseGitBackend, SyncStatus
|
|
32
|
+
from scmrepo.git.config import Config
|
|
25
33
|
from scmrepo.git.objects import GitCommit, GitObject, GitTag
|
|
26
34
|
from scmrepo.utils import relpath
|
|
27
35
|
|
|
@@ -29,7 +37,8 @@ logger = logging.getLogger(__name__)
|
|
|
29
37
|
|
|
30
38
|
|
|
31
39
|
if TYPE_CHECKING:
|
|
32
|
-
from pygit2 import Signature
|
|
40
|
+
from pygit2 import Commit, Oid, Signature
|
|
41
|
+
from pygit2.config import Config as _Pygit2Config
|
|
33
42
|
from pygit2.remote import Remote # type: ignore
|
|
34
43
|
from pygit2.repository import Repository
|
|
35
44
|
|
|
@@ -37,13 +46,47 @@ if TYPE_CHECKING:
|
|
|
37
46
|
|
|
38
47
|
|
|
39
48
|
class Pygit2Object(GitObject):
|
|
40
|
-
def __init__(self, obj):
|
|
49
|
+
def __init__(self, obj, backend: Optional["Pygit2Backend"] = None):
|
|
41
50
|
self.obj = obj
|
|
51
|
+
self.backend = backend
|
|
52
|
+
|
|
53
|
+
def open(
|
|
54
|
+
self,
|
|
55
|
+
mode: str = "r",
|
|
56
|
+
encoding: str = None,
|
|
57
|
+
key: Optional[Tuple[str, ...]] = None,
|
|
58
|
+
raw: bool = True,
|
|
59
|
+
rev: Optional[str] = None,
|
|
60
|
+
**kwargs,
|
|
61
|
+
):
|
|
62
|
+
from pygit2 import BlobIO, GitError
|
|
42
63
|
|
|
43
|
-
def open(self, mode: str = "r", encoding: str = None):
|
|
44
64
|
if not encoding:
|
|
45
65
|
encoding = locale.getpreferredencoding(False)
|
|
46
|
-
|
|
66
|
+
if self.backend is not None:
|
|
67
|
+
try:
|
|
68
|
+
if rev:
|
|
69
|
+
# pylint: disable-next=protected-access
|
|
70
|
+
commit, _ref = self.backend._resolve_refish(rev)
|
|
71
|
+
else:
|
|
72
|
+
pass
|
|
73
|
+
if raw:
|
|
74
|
+
blob_kwargs = {}
|
|
75
|
+
else:
|
|
76
|
+
assert key is not None
|
|
77
|
+
path = "/".join(key)
|
|
78
|
+
blob_kwargs = {
|
|
79
|
+
"as_path": path,
|
|
80
|
+
"commit_id": commit.oid,
|
|
81
|
+
}
|
|
82
|
+
blobio = BlobIO(self.obj, **blob_kwargs)
|
|
83
|
+
if mode == "rb":
|
|
84
|
+
return blobio
|
|
85
|
+
return TextIOWrapper(blobio, encoding=encoding)
|
|
86
|
+
except GitError as exc:
|
|
87
|
+
raise SCMError("failed to read git blob") from exc
|
|
88
|
+
else:
|
|
89
|
+
data = self.obj.read_raw()
|
|
47
90
|
if mode == "rb":
|
|
48
91
|
return BytesIO(data)
|
|
49
92
|
return StringIO(data.decode(encoding))
|
|
@@ -74,7 +117,34 @@ class Pygit2Object(GitObject):
|
|
|
74
117
|
|
|
75
118
|
def scandir(self) -> Iterable["Pygit2Object"]:
|
|
76
119
|
for entry in self.obj: # noqa: B301
|
|
77
|
-
yield Pygit2Object(entry)
|
|
120
|
+
yield Pygit2Object(entry, backend=self.backend)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class Pygit2Config(Config):
|
|
124
|
+
def __init__(self, config: "_Pygit2Config"):
|
|
125
|
+
self._config = config
|
|
126
|
+
|
|
127
|
+
def _key(self, section: Tuple[str, ...], name: str) -> str:
|
|
128
|
+
return ".".join(section + (name,))
|
|
129
|
+
|
|
130
|
+
def get(self, section: Tuple[str, ...], name: str) -> str:
|
|
131
|
+
return self._config[self._key(section, name)]
|
|
132
|
+
|
|
133
|
+
def get_bool(self, section: Tuple[str, ...], name: str) -> bool:
|
|
134
|
+
from pygit2 import GitError
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
return self._config.get_bool(self._key(section, name))
|
|
138
|
+
except GitError as exc:
|
|
139
|
+
raise ValueError("invalid boolean config entry") from exc
|
|
140
|
+
|
|
141
|
+
def get_multivar(self, section: Tuple[str, ...], name: str) -> Iterator[str]:
|
|
142
|
+
from pygit2 import GitError
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
yield from self._config.get_multivar(self._key(section, name))
|
|
146
|
+
except GitError as exc:
|
|
147
|
+
raise ValueError("invalid multivar config entry") from exc
|
|
78
148
|
|
|
79
149
|
|
|
80
150
|
class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
|
|
@@ -83,6 +153,8 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
83
153
|
):
|
|
84
154
|
import pygit2
|
|
85
155
|
|
|
156
|
+
from .filter import LFSFilter
|
|
157
|
+
|
|
86
158
|
if search_parent_directories:
|
|
87
159
|
ceiling_dirs = ""
|
|
88
160
|
else:
|
|
@@ -99,6 +171,14 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
99
171
|
|
|
100
172
|
self._stashes: dict = {}
|
|
101
173
|
|
|
174
|
+
try:
|
|
175
|
+
# NOTE: we want this init to be lazy so we do it on backend init.
|
|
176
|
+
# for subsequent backend instances, this call will error out since
|
|
177
|
+
# the filter is already registered
|
|
178
|
+
pygit2.filter_register("lfs", LFSFilter)
|
|
179
|
+
except ValueError:
|
|
180
|
+
pass
|
|
181
|
+
|
|
102
182
|
def close(self):
|
|
103
183
|
if hasattr(self, "_refdb"):
|
|
104
184
|
del self._refdb
|
|
@@ -353,6 +433,15 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
353
433
|
return self.repo.references["HEAD"].target[11:]
|
|
354
434
|
return self.repo.head.shorthand
|
|
355
435
|
|
|
436
|
+
def active_branch_remote(self) -> str:
|
|
437
|
+
try:
|
|
438
|
+
upstream = self.repo.branches[self.active_branch()].upstream
|
|
439
|
+
if upstream:
|
|
440
|
+
return upstream.remote_name
|
|
441
|
+
except (KeyError, ValueError):
|
|
442
|
+
pass
|
|
443
|
+
raise SCMError("No active branch tracking remote")
|
|
444
|
+
|
|
356
445
|
def list_branches(self) -> Iterable[str]:
|
|
357
446
|
base = "refs/heads/"
|
|
358
447
|
return sorted(ref[len(base) :] for ref in self.iter_refs(base))
|
|
@@ -366,7 +455,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
366
455
|
|
|
367
456
|
def get_tree_obj(self, rev: str, **kwargs) -> Pygit2Object:
|
|
368
457
|
tree = self.repo[rev].tree
|
|
369
|
-
return Pygit2Object(tree)
|
|
458
|
+
return Pygit2Object(tree, backend=self)
|
|
370
459
|
|
|
371
460
|
def get_rev(self) -> str:
|
|
372
461
|
raise NotImplementedError
|
|
@@ -551,7 +640,8 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
551
640
|
raise SCMError("Unknown merge analysis result")
|
|
552
641
|
|
|
553
642
|
@contextmanager
|
|
554
|
-
def
|
|
643
|
+
def _get_remote(self, url: str) -> Generator["Remote", None, None]:
|
|
644
|
+
"""Return a pygit2.Remote suitable for the specified Git URL or remote name."""
|
|
555
645
|
try:
|
|
556
646
|
remote = self.repo.remotes[url]
|
|
557
647
|
url = remote.url
|
|
@@ -577,57 +667,84 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
577
667
|
progress: Callable[["GitProgressEvent"], None] = None,
|
|
578
668
|
**kwargs,
|
|
579
669
|
) -> Mapping[str, SyncStatus]:
|
|
670
|
+
import fnmatch
|
|
671
|
+
|
|
580
672
|
from pygit2 import GitError
|
|
581
673
|
|
|
582
674
|
from .callbacks import RemoteCallbacks
|
|
583
675
|
|
|
584
|
-
|
|
585
|
-
refspecs = [refspecs]
|
|
676
|
+
refspecs = self._refspecs_list(refspecs, force=force)
|
|
586
677
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
678
|
+
# libgit2 rejects diverged refs but does not have a callback to notify
|
|
679
|
+
# when a ref was rejected so we have to determine whether no callback
|
|
680
|
+
# means up to date or rejected
|
|
681
|
+
def _default_status(
|
|
682
|
+
src: str, dst: str, remote_refs: Dict[str, "Oid"]
|
|
683
|
+
) -> SyncStatus:
|
|
684
|
+
try:
|
|
685
|
+
if remote_refs[src] != self.repo.references[dst].target:
|
|
686
|
+
return SyncStatus.DIVERGED
|
|
687
|
+
except KeyError:
|
|
688
|
+
# remote_refs lookup is skipped when force is set, refs cannot
|
|
689
|
+
# be diverged on force
|
|
690
|
+
pass
|
|
691
|
+
return SyncStatus.UP_TO_DATE
|
|
692
|
+
|
|
693
|
+
with self._get_remote(url) as remote:
|
|
603
694
|
with reraise(
|
|
604
695
|
GitError,
|
|
605
696
|
SCMError(f"Git failed to fetch ref from '{url}'"),
|
|
606
697
|
):
|
|
607
698
|
with RemoteCallbacks(progress=progress) as cb:
|
|
699
|
+
remote_refs: Dict[str, "Oid"] = (
|
|
700
|
+
{
|
|
701
|
+
head["name"]: head["oid"]
|
|
702
|
+
for head in remote.ls_remotes(callbacks=cb)
|
|
703
|
+
}
|
|
704
|
+
if not force
|
|
705
|
+
else {}
|
|
706
|
+
)
|
|
608
707
|
remote.fetch(
|
|
609
|
-
refspecs=
|
|
708
|
+
refspecs=refspecs,
|
|
610
709
|
callbacks=cb,
|
|
710
|
+
message="fetch",
|
|
611
711
|
)
|
|
612
712
|
|
|
613
713
|
result: Dict[str, "SyncStatus"] = {}
|
|
614
|
-
for refspec in
|
|
615
|
-
|
|
616
|
-
if
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
)
|
|
714
|
+
for refspec in refspecs:
|
|
715
|
+
lh, rh = refspec.split(":")
|
|
716
|
+
if lh.endswith("*"):
|
|
717
|
+
assert rh.endswith("*")
|
|
718
|
+
lh_prefix = lh[:-1]
|
|
719
|
+
rh_prefix = rh[:-1]
|
|
720
|
+
for refname in remote_refs:
|
|
721
|
+
if fnmatch.fnmatch(refname, lh):
|
|
722
|
+
src = refname
|
|
723
|
+
dst = f"{rh_prefix}{refname[len(lh_prefix):]}"
|
|
724
|
+
result[dst] = cb.result.get(
|
|
725
|
+
src, _default_status(src, dst, remote_refs)
|
|
726
|
+
)
|
|
727
|
+
else:
|
|
728
|
+
result[rh] = cb.result.get(lh, _default_status(lh, rh, remote_refs))
|
|
729
|
+
|
|
629
730
|
return result
|
|
630
731
|
|
|
732
|
+
@staticmethod
|
|
733
|
+
def _refspecs_list(
|
|
734
|
+
refspecs: Union[str, Iterable[str]],
|
|
735
|
+
force: bool = False,
|
|
736
|
+
) -> List[str]:
|
|
737
|
+
if isinstance(refspecs, str):
|
|
738
|
+
if force and not refspecs.startswith("+"):
|
|
739
|
+
refspecs = f"+{refspecs}"
|
|
740
|
+
return [refspecs]
|
|
741
|
+
if force:
|
|
742
|
+
return [
|
|
743
|
+
(refspec if refspec.startswith("+") else f"+{refspec}")
|
|
744
|
+
for refspec in refspecs
|
|
745
|
+
]
|
|
746
|
+
return list(refspecs)
|
|
747
|
+
|
|
631
748
|
def _stash_iter(self, ref: str):
|
|
632
749
|
raise NotImplementedError
|
|
633
750
|
|
|
@@ -940,6 +1057,12 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
940
1057
|
def validate_git_remote(self, url: str, **kwargs):
|
|
941
1058
|
raise NotImplementedError
|
|
942
1059
|
|
|
1060
|
+
def get_remote_url(self, remote: str) -> str:
|
|
1061
|
+
try:
|
|
1062
|
+
return self.repo.remotes[remote].url
|
|
1063
|
+
except KeyError as exc:
|
|
1064
|
+
raise InvalidRemote(remote) from exc
|
|
1065
|
+
|
|
943
1066
|
def check_ref_format(self, refname: str):
|
|
944
1067
|
raise NotImplementedError
|
|
945
1068
|
|
|
@@ -968,3 +1091,38 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
968
1091
|
except KeyError:
|
|
969
1092
|
pass
|
|
970
1093
|
return str(ref.target)
|
|
1094
|
+
|
|
1095
|
+
def get_config(self, path: Optional[str] = None) -> "Config":
|
|
1096
|
+
from pygit2.config import Config as _Pygit2Config
|
|
1097
|
+
|
|
1098
|
+
if path:
|
|
1099
|
+
return Pygit2Config(_Pygit2Config(path))
|
|
1100
|
+
return Pygit2Config(self.repo.config)
|
|
1101
|
+
|
|
1102
|
+
def check_attr(
|
|
1103
|
+
self,
|
|
1104
|
+
path: str,
|
|
1105
|
+
attr: str,
|
|
1106
|
+
source: Optional[str] = None,
|
|
1107
|
+
) -> Optional[Union[bool, str]]:
|
|
1108
|
+
from pygit2 import (
|
|
1109
|
+
GIT_ATTR_CHECK_FILE_THEN_INDEX,
|
|
1110
|
+
GIT_ATTR_CHECK_INCLUDE_COMMIT,
|
|
1111
|
+
GIT_ATTR_CHECK_INDEX_ONLY,
|
|
1112
|
+
GitError,
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
commit: Optional["Commit"] = None
|
|
1116
|
+
flags = GIT_ATTR_CHECK_FILE_THEN_INDEX
|
|
1117
|
+
if source:
|
|
1118
|
+
try:
|
|
1119
|
+
commit, _ref = self._resolve_refish(source)
|
|
1120
|
+
flags = GIT_ATTR_CHECK_INDEX_ONLY | GIT_ATTR_CHECK_INCLUDE_COMMIT
|
|
1121
|
+
except (KeyError, GitError) as exc:
|
|
1122
|
+
raise SCMError(f"Invalid commit '{source}'") from exc
|
|
1123
|
+
try:
|
|
1124
|
+
return self.repo.get_attr(
|
|
1125
|
+
path, attr, flags=flags, commit=commit.id if commit else None
|
|
1126
|
+
)
|
|
1127
|
+
except GitError as exc:
|
|
1128
|
+
raise SCMError("Failed to check attribute") from exc
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
from contextlib import AbstractContextManager
|
|
2
2
|
from types import TracebackType
|
|
3
|
-
from typing import TYPE_CHECKING, Callable, Optional, Type, Union
|
|
3
|
+
from typing import TYPE_CHECKING, Callable, Dict, Optional, Type, Union
|
|
4
4
|
|
|
5
5
|
from pygit2 import RemoteCallbacks as _RemoteCallbacks
|
|
6
6
|
|
|
7
|
+
from scmrepo.git.backend.base import SyncStatus
|
|
7
8
|
from scmrepo.git.credentials import Credential, CredentialNotFoundError
|
|
8
9
|
from scmrepo.progress import GitProgressReporter
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
12
|
+
from pygit2 import Oid
|
|
11
13
|
from pygit2.credentials import Keypair, Username, UserPass
|
|
12
14
|
|
|
13
15
|
from scmrepo.progress import GitProgressEvent
|
|
@@ -27,6 +29,7 @@ class RemoteCallbacks(_RemoteCallbacks, AbstractContextManager):
|
|
|
27
29
|
self.progress = GitProgressReporter(progress) if progress else None
|
|
28
30
|
self._store_credentials: Optional["Credential"] = None
|
|
29
31
|
self._tried_credentials = False
|
|
32
|
+
self.result: Dict[str, SyncStatus] = {}
|
|
30
33
|
|
|
31
34
|
def __exit__(
|
|
32
35
|
self,
|
|
@@ -66,3 +69,9 @@ class RemoteCallbacks(_RemoteCallbacks, AbstractContextManager):
|
|
|
66
69
|
def _approve_credentials(self):
|
|
67
70
|
if self._store_credentials:
|
|
68
71
|
self._store_credentials.approve()
|
|
72
|
+
|
|
73
|
+
def update_tips(self, refname: str, old: "Oid", new: "Oid"):
|
|
74
|
+
if old == new:
|
|
75
|
+
self.result[refname] = SyncStatus.UP_TO_DATE
|
|
76
|
+
else:
|
|
77
|
+
self.result[refname] = SyncStatus.SUCCESS
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import logging
|
|
3
|
+
from typing import TYPE_CHECKING, Callable, List, Optional
|
|
4
|
+
|
|
5
|
+
from pygit2 import GIT_FILTER_CLEAN, Filter, Passthrough
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from pygit2 import FilterSource
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LFSFilter(Filter):
|
|
14
|
+
attributes = "filter=*"
|
|
15
|
+
|
|
16
|
+
def __init__(self, *args, **kwargs):
|
|
17
|
+
self._smudge_buf: Optional[io.BytesIO] = None
|
|
18
|
+
self._smudge_root: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
def check(self, src: "FilterSource", attr_values: List[str]):
|
|
21
|
+
if attr_values[0] == "lfs":
|
|
22
|
+
if src.mode != GIT_FILTER_CLEAN:
|
|
23
|
+
self._smudge_buf = io.BytesIO()
|
|
24
|
+
self._smudge_root = src.repo.workdir or src.repo.path
|
|
25
|
+
return
|
|
26
|
+
raise Passthrough
|
|
27
|
+
|
|
28
|
+
def write(
|
|
29
|
+
self, data: bytes, src: "FilterSource", write_next: Callable[[bytes], None]
|
|
30
|
+
):
|
|
31
|
+
if src.mode == GIT_FILTER_CLEAN:
|
|
32
|
+
write_next(data)
|
|
33
|
+
return
|
|
34
|
+
if self._smudge_buf is None:
|
|
35
|
+
self._smudge_buf = io.BytesIO()
|
|
36
|
+
if self._smudge_root is None:
|
|
37
|
+
self._smudge_root = src.repo.workdir or src.repo.path
|
|
38
|
+
self._smudge_buf.write(data)
|
|
39
|
+
|
|
40
|
+
def close(self, write_next: Callable[[bytes], None]):
|
|
41
|
+
if self._smudge_buf is not None:
|
|
42
|
+
assert self._smudge_root
|
|
43
|
+
self._smudge(write_next)
|
|
44
|
+
|
|
45
|
+
def _smudge(self, write_next: Callable[[bytes], None]):
|
|
46
|
+
from scmrepo.exceptions import InvalidRemote
|
|
47
|
+
from scmrepo.git import Git
|
|
48
|
+
from scmrepo.git.lfs import smudge
|
|
49
|
+
from scmrepo.git.lfs.fetch import get_fetch_url
|
|
50
|
+
|
|
51
|
+
assert self._smudge_buf is not None
|
|
52
|
+
self._smudge_buf.seek(0)
|
|
53
|
+
with Git(self._smudge_root) as scm:
|
|
54
|
+
try:
|
|
55
|
+
url = get_fetch_url(scm)
|
|
56
|
+
except InvalidRemote:
|
|
57
|
+
url = None
|
|
58
|
+
fobj = smudge(scm.lfs_storage, self._smudge_buf, url=url)
|
|
59
|
+
data = fobj.read(io.DEFAULT_BUFFER_SIZE)
|
|
60
|
+
try:
|
|
61
|
+
while data:
|
|
62
|
+
write_next(data)
|
|
63
|
+
data = fobj.read(io.DEFAULT_BUFFER_SIZE)
|
|
64
|
+
except KeyboardInterrupt:
|
|
65
|
+
return
|
scmrepo/git/config.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""git config convenience wrapper."""
|
|
2
|
+
import logging
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Iterator, Tuple
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Config(ABC):
|
|
10
|
+
"""Read-only Git config."""
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def get(self, section: Tuple[str, ...], name: str) -> str:
|
|
14
|
+
"""Return the specified setting as a string.
|
|
15
|
+
|
|
16
|
+
Raises:
|
|
17
|
+
KeyError: Option was not set.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def get_bool(self, section: Tuple[str, ...], name: str) -> bool:
|
|
22
|
+
"""Return the specified setting as a boolean.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
KeyError: Option was not set.
|
|
26
|
+
ValueError: Option is not a valid boolean.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def get_multivar(self, section: Tuple[str, ...], name: str) -> Iterator[str]:
|
|
31
|
+
"""Iterate over string values in the specified multivar setting.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
KeyError: Option was not set.
|
|
35
|
+
"""
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .client import LFSClient
|
|
2
|
+
from .exceptions import LFSError
|
|
3
|
+
from .fetch import fetch
|
|
4
|
+
from .pointer import Pointer
|
|
5
|
+
from .smudge import smudge
|
|
6
|
+
from .storage import LFSStorage
|
|
7
|
+
|
|
8
|
+
__all__ = ["LFSClient", "LFSError", "LFSStorage", "Pointer", "fetch", "smudge"]
|