scmrepo 1.4.1__tar.gz → 1.5.0__tar.gz
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-1.4.1 → scmrepo-1.5.0}/PKG-INFO +4 -2
- {scmrepo-1.4.1 → scmrepo-1.5.0}/pyproject.toml +1 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/setup.cfg +3 -1
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/base.py +5 -1
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/fs.py +4 -3
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/git/__init__.py +13 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/git/backend/base.py +40 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/git/backend/dulwich/__init__.py +67 -1
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/git/backend/gitpython.py +56 -1
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/git/backend/pygit2/__init__.py +138 -8
- scmrepo-1.5.0/src/scmrepo/git/backend/pygit2/filter.py +65 -0
- scmrepo-1.5.0/src/scmrepo/git/config.py +35 -0
- scmrepo-1.5.0/src/scmrepo/git/lfs/__init__.py +8 -0
- scmrepo-1.5.0/src/scmrepo/git/lfs/client.py +223 -0
- scmrepo-1.5.0/src/scmrepo/git/lfs/exceptions.py +5 -0
- scmrepo-1.5.0/src/scmrepo/git/lfs/fetch.py +162 -0
- scmrepo-1.5.0/src/scmrepo/git/lfs/object.py +15 -0
- scmrepo-1.5.0/src/scmrepo/git/lfs/pointer.py +109 -0
- scmrepo-1.5.0/src/scmrepo/git/lfs/progress.py +61 -0
- scmrepo-1.5.0/src/scmrepo/git/lfs/smudge.py +51 -0
- scmrepo-1.5.0/src/scmrepo/git/lfs/storage.py +74 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/git/objects.py +3 -2
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo.egg-info/PKG-INFO +4 -2
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo.egg-info/SOURCES.txt +12 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo.egg-info/requires.txt +3 -1
- {scmrepo-1.4.1 → scmrepo-1.5.0}/tests/test_fs.py +9 -5
- {scmrepo-1.4.1 → scmrepo-1.5.0}/tests/test_git.py +29 -0
- scmrepo-1.5.0/tests/test_lfs.py +76 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/.coveragerc +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/.cruft.json +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/.gitattributes +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/.github/dependabot.yml +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/.github/workflows/release.yaml +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/.github/workflows/tests.yaml +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/.github/workflows/update-template.yaml +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/.gitignore +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/.pre-commit-config.yaml +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/CODE_OF_CONDUCT.rst +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/CONTRIBUTING.rst +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/LICENSE +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/README.rst +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/noxfile.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/__init__.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/asyn.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/exceptions.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/git/backend/__init__.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/git/backend/dulwich/asyncssh_vendor.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/git/backend/dulwich/client.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/git/backend/pygit2/callbacks.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/git/credentials.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/git/stash.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/noscm.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/progress.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/py.typed +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo/utils.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo.egg-info/dependency_links.txt +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo.egg-info/not-zip-safe +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/src/scmrepo.egg-info/top_level.txt +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/tests/__init__.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/tests/conftest.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/tests/docker-compose.yml +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/tests/git-init/git.sh +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/tests/test_credentials.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/tests/test_dulwich.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/tests/test_noscm.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/tests/test_pygit2.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/tests/test_scmrepo.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/tests/test_stash.py +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/tests/user.key +0 -0
- {scmrepo-1.4.1 → scmrepo-1.5.0}/tests/user.key.pub +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: scmrepo
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: SCM wrapper and fsspec filesystem for Git for use in DVC
|
|
5
5
|
Home-page: https://github.com/iterative/scmrepo
|
|
6
6
|
Maintainer-email: support@dvc.org
|
|
@@ -17,13 +17,15 @@ Description-Content-Type: text/x-rst
|
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: gitpython>3
|
|
19
19
|
Requires-Dist: dulwich>=0.21.6
|
|
20
|
-
Requires-Dist: pygit2>=1.13.
|
|
20
|
+
Requires-Dist: pygit2>=1.13.3
|
|
21
21
|
Requires-Dist: pygtrie>=2.3.2
|
|
22
22
|
Requires-Dist: fsspec>=2021.7.0
|
|
23
23
|
Requires-Dist: pathspec>=0.9.0
|
|
24
24
|
Requires-Dist: asyncssh<3,>=2.13.1
|
|
25
25
|
Requires-Dist: funcy>=1.14
|
|
26
26
|
Requires-Dist: shortuuid>=0.5.0
|
|
27
|
+
Requires-Dist: dvc-objects<2,>=1.0.1
|
|
28
|
+
Requires-Dist: dvc-http>=2.29.0
|
|
27
29
|
Provides-Extra: tests
|
|
28
30
|
Requires-Dist: pytest==7.2.0; extra == "tests"
|
|
29
31
|
Requires-Dist: pytest-sugar==0.9.5; extra == "tests"
|
|
@@ -26,13 +26,15 @@ packages = find:
|
|
|
26
26
|
install_requires =
|
|
27
27
|
gitpython>3
|
|
28
28
|
dulwich>=0.21.6
|
|
29
|
-
pygit2>=1.13.
|
|
29
|
+
pygit2>=1.13.3
|
|
30
30
|
pygtrie>=2.3.2
|
|
31
31
|
fsspec>=2021.7.0
|
|
32
32
|
pathspec>=0.9.0
|
|
33
33
|
asyncssh>=2.13.1,<3
|
|
34
34
|
funcy>=1.14
|
|
35
35
|
shortuuid>=0.5.0
|
|
36
|
+
dvc-objects>=1.0.1,<2
|
|
37
|
+
dvc-http>=2.29.0
|
|
36
38
|
|
|
37
39
|
[options.extras_require]
|
|
38
40
|
tests =
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Manages source control systems (e.g. Git) in DVC."""
|
|
2
|
+
from contextlib import AbstractContextManager
|
|
2
3
|
|
|
3
4
|
|
|
4
|
-
class Base:
|
|
5
|
+
class Base(AbstractContextManager):
|
|
5
6
|
"""Base class for source control management driver implementations."""
|
|
6
7
|
|
|
7
8
|
def __init__(self, root_dir=None):
|
|
@@ -18,6 +19,9 @@ class Base:
|
|
|
18
19
|
class_name=type(self).__name__, directory=self.dir
|
|
19
20
|
)
|
|
20
21
|
|
|
22
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
23
|
+
self.close()
|
|
24
|
+
|
|
21
25
|
@property
|
|
22
26
|
def dir(self):
|
|
23
27
|
"""Path to a directory with SCM specific information."""
|
|
@@ -187,9 +187,10 @@ class GitFileSystem(AbstractFileSystem):
|
|
|
187
187
|
self,
|
|
188
188
|
path: str,
|
|
189
189
|
mode: str = "rb",
|
|
190
|
-
block_size: int = None,
|
|
190
|
+
block_size: Optional[int] = None,
|
|
191
191
|
autocommit: bool = True,
|
|
192
|
-
cache_options: Dict = None,
|
|
192
|
+
cache_options: Optional[Dict] = None,
|
|
193
|
+
raw: bool = False,
|
|
193
194
|
**kwargs: Any,
|
|
194
195
|
) -> BinaryIO:
|
|
195
196
|
if mode != "rb":
|
|
@@ -197,7 +198,7 @@ class GitFileSystem(AbstractFileSystem):
|
|
|
197
198
|
|
|
198
199
|
key = self._get_key(path)
|
|
199
200
|
try:
|
|
200
|
-
obj = self.trie.open(key, mode=mode)
|
|
201
|
+
obj = self.trie.open(key, mode=mode, raw=raw)
|
|
201
202
|
obj.size = bytesio_len(obj)
|
|
202
203
|
return obj
|
|
203
204
|
except KeyError as exc:
|
|
@@ -171,6 +171,13 @@ class Git(Base):
|
|
|
171
171
|
def ignore_file(self):
|
|
172
172
|
return self.GITIGNORE
|
|
173
173
|
|
|
174
|
+
@cached_property
|
|
175
|
+
def lfs_storage(self):
|
|
176
|
+
from .lfs import LFSStorage
|
|
177
|
+
from .lfs.storage import get_storage_path
|
|
178
|
+
|
|
179
|
+
return LFSStorage(get_storage_path(self))
|
|
180
|
+
|
|
174
181
|
def _get_gitignore(self, path):
|
|
175
182
|
ignore_file_dir = os.path.dirname(path)
|
|
176
183
|
|
|
@@ -267,6 +274,8 @@ class Git(Base):
|
|
|
267
274
|
|
|
268
275
|
def close(self):
|
|
269
276
|
self.backends.close_initialized()
|
|
277
|
+
if "lfs_storage" in self.__dict__:
|
|
278
|
+
self.lfs_storage.close()
|
|
270
279
|
|
|
271
280
|
@property
|
|
272
281
|
def no_commits(self):
|
|
@@ -358,6 +367,7 @@ class Git(Base):
|
|
|
358
367
|
is_tracked = partialmethod(_backend_func, "is_tracked")
|
|
359
368
|
is_dirty = partialmethod(_backend_func, "is_dirty")
|
|
360
369
|
active_branch = partialmethod(_backend_func, "active_branch")
|
|
370
|
+
active_branch_remote = partialmethod(_backend_func, "active_branch_remote")
|
|
361
371
|
list_branches = partialmethod(_backend_func, "list_branches")
|
|
362
372
|
list_tags = partialmethod(_backend_func, "list_tags")
|
|
363
373
|
list_all_commits = partialmethod(_backend_func, "list_all_commits")
|
|
@@ -383,8 +393,11 @@ class Git(Base):
|
|
|
383
393
|
status = partialmethod(_backend_func, "status")
|
|
384
394
|
merge = partialmethod(_backend_func, "merge")
|
|
385
395
|
validate_git_remote = partialmethod(_backend_func, "validate_git_remote")
|
|
396
|
+
get_remote_url = partialmethod(_backend_func, "get_remote_url")
|
|
386
397
|
check_ref_format = partialmethod(_backend_func, "check_ref_format")
|
|
387
398
|
get_tag = partialmethod(_backend_func, "get_tag")
|
|
399
|
+
get_config = partialmethod(_backend_func, "get_config")
|
|
400
|
+
check_attr = partialmethod(_backend_func, "check_attr")
|
|
388
401
|
|
|
389
402
|
get_tree_obj = partialmethod(_backend_func, "get_tree_obj")
|
|
390
403
|
|
|
@@ -10,6 +10,7 @@ from ..objects import GitObject
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
from scmrepo.progress import GitProgressEvent
|
|
12
12
|
|
|
13
|
+
from ..config import Config
|
|
13
14
|
from ..objects import GitCommit, GitTag
|
|
14
15
|
|
|
15
16
|
|
|
@@ -135,6 +136,10 @@ class BaseGitBackend(ABC):
|
|
|
135
136
|
def active_branch(self) -> str:
|
|
136
137
|
pass
|
|
137
138
|
|
|
139
|
+
@abstractmethod
|
|
140
|
+
def active_branch_remote(self) -> str:
|
|
141
|
+
"""Return the fetch remote name for the current branch."""
|
|
142
|
+
|
|
138
143
|
@abstractmethod
|
|
139
144
|
def list_branches(self) -> Iterable[str]:
|
|
140
145
|
pass
|
|
@@ -397,6 +402,10 @@ class BaseGitBackend(ABC):
|
|
|
397
402
|
def validate_git_remote(self, url: str, **kwargs):
|
|
398
403
|
"""Verify that url is a valid git URL or remote name."""
|
|
399
404
|
|
|
405
|
+
@abstractmethod
|
|
406
|
+
def get_remote_url(self, remote: str) -> str:
|
|
407
|
+
"""Return URL for the specified remote."""
|
|
408
|
+
|
|
400
409
|
@abstractmethod
|
|
401
410
|
def check_ref_format(self, refname: str) -> bool:
|
|
402
411
|
"""Check if a reference name is well formed."""
|
|
@@ -413,3 +422,34 @@ class BaseGitBackend(ABC):
|
|
|
413
422
|
String SHA for the target object if the tag is a lightweight tag.
|
|
414
423
|
GitTag object if the tag is an annotated tag.
|
|
415
424
|
"""
|
|
425
|
+
|
|
426
|
+
@abstractmethod
|
|
427
|
+
def get_config(self, path: Optional[str] = None) -> "Config":
|
|
428
|
+
"""Return a Git config object.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
path: If set, a config object for the specified config file will be
|
|
432
|
+
returned. By default, the standard Git system/global/repo config
|
|
433
|
+
stack object will be returned.
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
@abstractmethod
|
|
437
|
+
def check_attr(
|
|
438
|
+
self,
|
|
439
|
+
path: str,
|
|
440
|
+
attr: str,
|
|
441
|
+
source: Optional[str] = None,
|
|
442
|
+
) -> Optional[Union[bool, str]]:
|
|
443
|
+
"""Return the value of the specified attribute for a pathname.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
path: Pathname to check.
|
|
447
|
+
attr: Attribute to check.
|
|
448
|
+
source: Optional tree-ish source to check.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
None when the attribute is not defined for the path (unspecified).
|
|
452
|
+
True when the attribute is defined as true (set).
|
|
453
|
+
False when the attribute is defined as false (unset).
|
|
454
|
+
The value of the attribute when a value has been assigned.
|
|
455
|
+
"""
|
|
@@ -27,11 +27,13 @@ from scmrepo.exceptions import AuthError, CloneError, InvalidRemote, RevError, S
|
|
|
27
27
|
from scmrepo.progress import GitProgressReporter
|
|
28
28
|
from scmrepo.utils import relpath
|
|
29
29
|
|
|
30
|
+
from ...config import Config
|
|
30
31
|
from ...objects import GitObject, GitTag
|
|
31
32
|
from ..base import BaseGitBackend, SyncStatus
|
|
32
33
|
|
|
33
34
|
if TYPE_CHECKING:
|
|
34
35
|
from dulwich.client import SSHVendor
|
|
36
|
+
from dulwich.config import ConfigFile, StackedConfig
|
|
35
37
|
from dulwich.repo import Repo
|
|
36
38
|
|
|
37
39
|
from scmrepo.progress import GitProgressEvent
|
|
@@ -49,7 +51,16 @@ class DulwichObject(GitObject):
|
|
|
49
51
|
self._mode = mode
|
|
50
52
|
self._sha = sha
|
|
51
53
|
|
|
52
|
-
def open(
|
|
54
|
+
def open( # pylint: disable=unused-argument
|
|
55
|
+
self,
|
|
56
|
+
mode: str = "r",
|
|
57
|
+
encoding: Optional[str] = None,
|
|
58
|
+
raw: bool = True,
|
|
59
|
+
rev: Optional[str] = None,
|
|
60
|
+
**kwargs,
|
|
61
|
+
):
|
|
62
|
+
if not raw:
|
|
63
|
+
raise NotImplementedError
|
|
53
64
|
if not encoding:
|
|
54
65
|
encoding = locale.getpreferredencoding(False)
|
|
55
66
|
# NOTE: we didn't load the object before as Dulwich will also try to
|
|
@@ -130,6 +141,35 @@ def _get_ssh_vendor() -> "SSHVendor":
|
|
|
130
141
|
return AsyncSSHVendor()
|
|
131
142
|
|
|
132
143
|
|
|
144
|
+
class DulwichConfig(Config):
|
|
145
|
+
def __init__(self, config: Union["ConfigFile", "StackedConfig"]):
|
|
146
|
+
self._config = config
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def encoding(self) -> str:
|
|
150
|
+
from dulwich.config import ConfigFile
|
|
151
|
+
|
|
152
|
+
if isinstance(self._config, ConfigFile):
|
|
153
|
+
return self._config.encoding
|
|
154
|
+
return self._config.backends[0].encoding
|
|
155
|
+
|
|
156
|
+
def get(self, section: Tuple[str, ...], name: str) -> str:
|
|
157
|
+
"""Return the specified setting as a string."""
|
|
158
|
+
return self._config.get(section, name).decode(self.encoding)
|
|
159
|
+
|
|
160
|
+
def get_bool(self, section: Tuple[str, ...], name: str) -> bool:
|
|
161
|
+
"""Return the specified setting as a boolean."""
|
|
162
|
+
value = self._config.get_boolean(section, name)
|
|
163
|
+
if value is None:
|
|
164
|
+
raise ValueError("setting is not a valid boolean")
|
|
165
|
+
return value
|
|
166
|
+
|
|
167
|
+
def get_multivar(self, section: Tuple[str, ...], name: str) -> Iterator[str]:
|
|
168
|
+
"""Iterate over string values in the specified multivar setting."""
|
|
169
|
+
for value in self._config.get_multivar(section, name):
|
|
170
|
+
yield value.decode(self.encoding)
|
|
171
|
+
|
|
172
|
+
|
|
133
173
|
class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
|
|
134
174
|
"""Dulwich Git backend."""
|
|
135
175
|
|
|
@@ -430,6 +470,9 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
430
470
|
def active_branch(self) -> str:
|
|
431
471
|
raise NotImplementedError
|
|
432
472
|
|
|
473
|
+
def active_branch_remote(self) -> str:
|
|
474
|
+
raise NotImplementedError
|
|
475
|
+
|
|
433
476
|
def list_branches(self) -> Iterable[str]:
|
|
434
477
|
base = "refs/heads/"
|
|
435
478
|
return sorted(ref[len(base) :] for ref in self.iter_refs(base))
|
|
@@ -884,6 +927,14 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
884
927
|
):
|
|
885
928
|
raise InvalidRemote(url)
|
|
886
929
|
|
|
930
|
+
def get_remote_url(self, remote: str) -> str:
|
|
931
|
+
from dulwich.porcelain import get_remote_repo
|
|
932
|
+
|
|
933
|
+
remote_name, location = get_remote_repo(self.repo, remote)
|
|
934
|
+
if not remote_name:
|
|
935
|
+
raise InvalidRemote(remote)
|
|
936
|
+
return location
|
|
937
|
+
|
|
887
938
|
def check_ref_format(self, refname: str) -> bool:
|
|
888
939
|
from dulwich.refs import check_ref_format
|
|
889
940
|
|
|
@@ -913,6 +964,21 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
913
964
|
)
|
|
914
965
|
return os.fsdecode(ref)
|
|
915
966
|
|
|
967
|
+
def get_config(self, path: Optional[str] = None) -> "Config":
|
|
968
|
+
from dulwich.config import ConfigFile
|
|
969
|
+
|
|
970
|
+
if path:
|
|
971
|
+
return DulwichConfig(ConfigFile.from_path(path))
|
|
972
|
+
return DulwichConfig(self.repo.get_config_stack())
|
|
973
|
+
|
|
974
|
+
def check_attr(
|
|
975
|
+
self,
|
|
976
|
+
path: str,
|
|
977
|
+
attr: str,
|
|
978
|
+
source: Optional[str] = None,
|
|
979
|
+
) -> Optional[Union[bool, str]]:
|
|
980
|
+
raise NotImplementedError
|
|
981
|
+
|
|
916
982
|
|
|
917
983
|
_IDENTITY_RE = re.compile(r"(?P<name>.+)\s+<(?P<email>.+)>")
|
|
918
984
|
|
|
@@ -2,6 +2,7 @@ import io
|
|
|
2
2
|
import locale
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
5
6
|
import sys
|
|
6
7
|
from functools import partial, wraps
|
|
7
8
|
from typing import (
|
|
@@ -21,6 +22,7 @@ from funcy import ignore
|
|
|
21
22
|
|
|
22
23
|
from scmrepo.exceptions import (
|
|
23
24
|
CloneError,
|
|
25
|
+
InvalidRemote,
|
|
24
26
|
MergeConflictError,
|
|
25
27
|
RevError,
|
|
26
28
|
SCMError,
|
|
@@ -34,6 +36,8 @@ from .base import BaseGitBackend, SyncStatus
|
|
|
34
36
|
if TYPE_CHECKING:
|
|
35
37
|
from scmrepo.progress import GitProgressEvent
|
|
36
38
|
|
|
39
|
+
from ..config import Config
|
|
40
|
+
|
|
37
41
|
|
|
38
42
|
logger = logging.getLogger(__name__)
|
|
39
43
|
|
|
@@ -80,7 +84,15 @@ class GitPythonObject(GitObject):
|
|
|
80
84
|
def __init__(self, obj):
|
|
81
85
|
self.obj = obj
|
|
82
86
|
|
|
83
|
-
def open(
|
|
87
|
+
def open(
|
|
88
|
+
self,
|
|
89
|
+
mode: str = "r",
|
|
90
|
+
encoding: str = None,
|
|
91
|
+
raw: bool = True,
|
|
92
|
+
**kwargs,
|
|
93
|
+
):
|
|
94
|
+
if not raw:
|
|
95
|
+
raise NotImplementedError
|
|
84
96
|
if not encoding:
|
|
85
97
|
encoding = locale.getpreferredencoding(False)
|
|
86
98
|
# GitPython's obj.data_stream is a fragile thing, it is better to
|
|
@@ -341,6 +353,12 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
341
353
|
except TypeError as exc:
|
|
342
354
|
raise SCMError("No active branch") from exc
|
|
343
355
|
|
|
356
|
+
def active_branch_remote(self) -> str:
|
|
357
|
+
try:
|
|
358
|
+
return self.repo.active_branch.tracking_branch()
|
|
359
|
+
except (TypeError, ValueError) as exc:
|
|
360
|
+
raise SCMError("No active branch tracking remote") from exc
|
|
361
|
+
|
|
344
362
|
def list_branches(self):
|
|
345
363
|
return [h.name for h in self.repo.heads]
|
|
346
364
|
|
|
@@ -714,6 +732,14 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
714
732
|
def validate_git_remote(self, url: str, **kwargs):
|
|
715
733
|
raise NotImplementedError
|
|
716
734
|
|
|
735
|
+
def get_remote_url(self, remote: str) -> str:
|
|
736
|
+
from git.exc import GitCommandError
|
|
737
|
+
|
|
738
|
+
try:
|
|
739
|
+
return self.repo.remotes[remote].url
|
|
740
|
+
except (KeyError, GitCommandError) as exc:
|
|
741
|
+
raise InvalidRemote(remote) from exc
|
|
742
|
+
|
|
717
743
|
def check_ref_format(self, refname: str):
|
|
718
744
|
raise NotImplementedError
|
|
719
745
|
|
|
@@ -736,3 +762,32 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
736
762
|
except IndexError:
|
|
737
763
|
pass
|
|
738
764
|
return None
|
|
765
|
+
|
|
766
|
+
def get_config(self, path: Optional[str] = None) -> "Config":
|
|
767
|
+
raise NotImplementedError
|
|
768
|
+
|
|
769
|
+
def check_attr(
|
|
770
|
+
self,
|
|
771
|
+
path: str,
|
|
772
|
+
attr: str,
|
|
773
|
+
source: Optional[str] = None,
|
|
774
|
+
) -> Optional[Union[bool, str]]:
|
|
775
|
+
from git.exc import GitCommandError
|
|
776
|
+
|
|
777
|
+
try:
|
|
778
|
+
result = self.git.check_attr(attr, "--", path, source=source)
|
|
779
|
+
except GitCommandError as exc:
|
|
780
|
+
raise SCMError("Failed to check attribute") from exc
|
|
781
|
+
escaped_path = re.escape(path)
|
|
782
|
+
escaped_attr = re.escape(attr)
|
|
783
|
+
m = re.match(f"{escaped_path}: {escaped_attr}: (?P<info>.*)", result)
|
|
784
|
+
if not m or not m.group("info"):
|
|
785
|
+
raise SCMError("Failed to check attribute")
|
|
786
|
+
info = m.group("info")
|
|
787
|
+
if info == "unspecified":
|
|
788
|
+
return None
|
|
789
|
+
if info == "set":
|
|
790
|
+
return True
|
|
791
|
+
if info == "unset":
|
|
792
|
+
return False
|
|
793
|
+
return info
|
|
@@ -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 Oid, 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
|
|
@@ -968,6 +1057,12 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
968
1057
|
def validate_git_remote(self, url: str, **kwargs):
|
|
969
1058
|
raise NotImplementedError
|
|
970
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
|
+
|
|
971
1066
|
def check_ref_format(self, refname: str):
|
|
972
1067
|
raise NotImplementedError
|
|
973
1068
|
|
|
@@ -996,3 +1091,38 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
|
|
|
996
1091
|
except KeyError:
|
|
997
1092
|
pass
|
|
998
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
|