scmrepo 1.4.1__tar.gz → 1.6.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.

Files changed (72) hide show
  1. {scmrepo-1.4.1 → scmrepo-1.6.0}/.cruft.json +1 -1
  2. {scmrepo-1.4.1 → scmrepo-1.6.0}/.gitignore +2 -0
  3. {scmrepo-1.4.1 → scmrepo-1.6.0}/.pre-commit-config.yaml +6 -23
  4. {scmrepo-1.4.1/src/scmrepo.egg-info → scmrepo-1.6.0}/PKG-INFO +9 -21
  5. {scmrepo-1.4.1 → scmrepo-1.6.0}/noxfile.py +0 -1
  6. scmrepo-1.6.0/pyproject.toml +126 -0
  7. scmrepo-1.6.0/setup.cfg +4 -0
  8. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/base.py +5 -1
  9. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/fs.py +4 -3
  10. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/git/__init__.py +13 -0
  11. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/git/backend/base.py +40 -0
  12. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/git/backend/dulwich/__init__.py +67 -1
  13. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/git/backend/gitpython.py +56 -1
  14. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/git/backend/pygit2/__init__.py +138 -8
  15. scmrepo-1.6.0/src/scmrepo/git/backend/pygit2/filter.py +65 -0
  16. scmrepo-1.6.0/src/scmrepo/git/config.py +35 -0
  17. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/git/credentials.py +2 -1
  18. scmrepo-1.6.0/src/scmrepo/git/lfs/__init__.py +8 -0
  19. scmrepo-1.6.0/src/scmrepo/git/lfs/client.py +218 -0
  20. scmrepo-1.6.0/src/scmrepo/git/lfs/exceptions.py +5 -0
  21. scmrepo-1.6.0/src/scmrepo/git/lfs/fetch.py +162 -0
  22. scmrepo-1.6.0/src/scmrepo/git/lfs/object.py +15 -0
  23. scmrepo-1.6.0/src/scmrepo/git/lfs/pointer.py +109 -0
  24. scmrepo-1.6.0/src/scmrepo/git/lfs/progress.py +61 -0
  25. scmrepo-1.6.0/src/scmrepo/git/lfs/smudge.py +51 -0
  26. scmrepo-1.6.0/src/scmrepo/git/lfs/storage.py +74 -0
  27. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/git/objects.py +3 -2
  28. {scmrepo-1.4.1 → scmrepo-1.6.0/src/scmrepo.egg-info}/PKG-INFO +9 -21
  29. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo.egg-info/SOURCES.txt +12 -2
  30. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo.egg-info/requires.txt +4 -18
  31. {scmrepo-1.4.1 → scmrepo-1.6.0}/tests/test_credentials.py +6 -12
  32. {scmrepo-1.4.1 → scmrepo-1.6.0}/tests/test_fs.py +9 -5
  33. {scmrepo-1.4.1 → scmrepo-1.6.0}/tests/test_git.py +29 -0
  34. scmrepo-1.6.0/tests/test_lfs.py +76 -0
  35. scmrepo-1.4.1/pyproject.toml +0 -108
  36. scmrepo-1.4.1/setup.cfg +0 -80
  37. scmrepo-1.4.1/src/scmrepo.egg-info/not-zip-safe +0 -1
  38. {scmrepo-1.4.1 → scmrepo-1.6.0}/.coveragerc +0 -0
  39. {scmrepo-1.4.1 → scmrepo-1.6.0}/.gitattributes +0 -0
  40. {scmrepo-1.4.1 → scmrepo-1.6.0}/.github/dependabot.yml +0 -0
  41. {scmrepo-1.4.1 → scmrepo-1.6.0}/.github/workflows/release.yaml +0 -0
  42. {scmrepo-1.4.1 → scmrepo-1.6.0}/.github/workflows/tests.yaml +0 -0
  43. {scmrepo-1.4.1 → scmrepo-1.6.0}/.github/workflows/update-template.yaml +0 -0
  44. {scmrepo-1.4.1 → scmrepo-1.6.0}/CODE_OF_CONDUCT.rst +0 -0
  45. {scmrepo-1.4.1 → scmrepo-1.6.0}/CONTRIBUTING.rst +0 -0
  46. {scmrepo-1.4.1 → scmrepo-1.6.0}/LICENSE +0 -0
  47. {scmrepo-1.4.1 → scmrepo-1.6.0}/README.rst +0 -0
  48. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/__init__.py +0 -0
  49. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/asyn.py +0 -0
  50. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/exceptions.py +0 -0
  51. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/git/backend/__init__.py +0 -0
  52. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/git/backend/dulwich/asyncssh_vendor.py +0 -0
  53. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/git/backend/dulwich/client.py +0 -0
  54. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/git/backend/pygit2/callbacks.py +0 -0
  55. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/git/stash.py +0 -0
  56. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/noscm.py +0 -0
  57. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/progress.py +0 -0
  58. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/py.typed +0 -0
  59. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo/utils.py +0 -0
  60. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo.egg-info/dependency_links.txt +0 -0
  61. {scmrepo-1.4.1 → scmrepo-1.6.0}/src/scmrepo.egg-info/top_level.txt +0 -0
  62. {scmrepo-1.4.1 → scmrepo-1.6.0}/tests/__init__.py +0 -0
  63. {scmrepo-1.4.1 → scmrepo-1.6.0}/tests/conftest.py +0 -0
  64. {scmrepo-1.4.1 → scmrepo-1.6.0}/tests/docker-compose.yml +0 -0
  65. {scmrepo-1.4.1 → scmrepo-1.6.0}/tests/git-init/git.sh +0 -0
  66. {scmrepo-1.4.1 → scmrepo-1.6.0}/tests/test_dulwich.py +0 -0
  67. {scmrepo-1.4.1 → scmrepo-1.6.0}/tests/test_noscm.py +0 -0
  68. {scmrepo-1.4.1 → scmrepo-1.6.0}/tests/test_pygit2.py +0 -0
  69. {scmrepo-1.4.1 → scmrepo-1.6.0}/tests/test_scmrepo.py +0 -0
  70. {scmrepo-1.4.1 → scmrepo-1.6.0}/tests/test_stash.py +0 -0
  71. {scmrepo-1.4.1 → scmrepo-1.6.0}/tests/user.key +0 -0
  72. {scmrepo-1.4.1 → scmrepo-1.6.0}/tests/user.key.pub +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "template": "https://github.com/iterative/py-template",
3
- "commit": "0aac03d10cbc7b2f7e144e728de5043b7a3458cb",
3
+ "commit": "15ee26df315020399731c6291d61bef81a3fc5d3",
4
4
  "context": {
5
5
  "cookiecutter": {
6
6
  "project_name": "scmrepo",
@@ -136,3 +136,5 @@ dmypy.json
136
136
 
137
137
  # Cython debug symbols
138
138
  cython_debug/
139
+
140
+ .DS_Store
@@ -1,10 +1,6 @@
1
1
  default_language_version:
2
2
  python: python3
3
3
  repos:
4
- - repo: https://github.com/psf/black
5
- rev: 23.9.1
6
- hooks:
7
- - id: black
8
4
  - repo: https://github.com/pre-commit/pre-commit-hooks
9
5
  rev: v4.4.0
10
6
  hooks:
@@ -24,6 +20,12 @@ repos:
24
20
  args: ['--fix=lf']
25
21
  - id: sort-simple-yaml
26
22
  - id: trailing-whitespace
23
+ - repo: https://github.com/astral-sh/ruff-pre-commit
24
+ rev: 'v0.1.5'
25
+ hooks:
26
+ - id: ruff
27
+ args: [--fix, --exit-non-zero-on-fix]
28
+ - id: ruff-format
27
29
  - repo: https://github.com/codespell-project/codespell
28
30
  rev: v2.2.5
29
31
  hooks:
@@ -34,22 +36,3 @@ repos:
34
36
  hooks:
35
37
  - id: pyupgrade
36
38
  args: [--py38-plus]
37
- - repo: https://github.com/PyCQA/isort
38
- rev: 5.12.0
39
- hooks:
40
- - id: isort
41
- - repo: https://github.com/pycqa/flake8
42
- rev: 6.1.0
43
- hooks:
44
- - id: flake8
45
- additional_dependencies:
46
- - flake8-bugbear==23.7.10
47
- - flake8-comprehensions==3.14.0
48
- - flake8-debugger==4.1.2
49
- - flake8-string-format==0.3.0
50
- - repo: https://github.com/pycqa/bandit
51
- rev: 1.7.5
52
- hooks:
53
- - id: bandit
54
- args: ["-c", "pyproject.toml"]
55
- additional_dependencies: [".[toml]"]
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: scmrepo
3
- Version: 1.4.1
4
- Summary: SCM wrapper and fsspec filesystem for Git for use in DVC
5
- Home-page: https://github.com/iterative/scmrepo
6
- Maintainer-email: support@dvc.org
3
+ Version: 1.6.0
4
+ Summary: scmrepo
5
+ Author-email: Iterative <support@dvc.org>
7
6
  License: Apache-2.0
8
- Platform: any
7
+ Project-URL: Issues, https://github.com/iterative/scmrepo/issues
8
+ Project-URL: Source, https://github.com/iterative/scmrepo
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Programming Language :: Python :: 3.8
11
11
  Classifier: Programming Language :: Python :: 3.9
@@ -17,19 +17,20 @@ 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.0
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<3,>=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"
30
32
  Requires-Dist: pytest-cov==3.0.0; extra == "tests"
31
33
  Requires-Dist: pytest-mock==3.8.2; extra == "tests"
32
- Requires-Dist: pylint==2.15.0; extra == "tests"
33
34
  Requires-Dist: mypy==0.971; extra == "tests"
34
35
  Requires-Dist: pytest-test-utils==0.0.8; extra == "tests"
35
36
  Requires-Dist: pytest-asyncio==0.18.3; extra == "tests"
@@ -40,20 +41,7 @@ Requires-Dist: types-certifi==2021.10.8.3; extra == "tests"
40
41
  Requires-Dist: types-mock==5.1.0.2; extra == "tests"
41
42
  Requires-Dist: types-paramiko==3.3.0.0; extra == "tests"
42
43
  Provides-Extra: dev
43
- Requires-Dist: pytest==7.2.0; extra == "dev"
44
- Requires-Dist: pytest-sugar==0.9.5; extra == "dev"
45
- Requires-Dist: pytest-cov==3.0.0; extra == "dev"
46
- Requires-Dist: pytest-mock==3.8.2; extra == "dev"
47
- Requires-Dist: pylint==2.15.0; extra == "dev"
48
- Requires-Dist: mypy==0.971; extra == "dev"
49
- Requires-Dist: pytest-test-utils==0.0.8; extra == "dev"
50
- Requires-Dist: pytest-asyncio==0.18.3; extra == "dev"
51
- Requires-Dist: pytest-docker==0.12.0; (python_version < "3.10" and implementation_name != "pypy") and extra == "dev"
52
- Requires-Dist: mock==5.1.0; extra == "dev"
53
- Requires-Dist: paramiko==3.3.1; extra == "dev"
54
- Requires-Dist: types-certifi==2021.10.8.3; extra == "dev"
55
- Requires-Dist: types-mock==5.1.0.2; extra == "dev"
56
- Requires-Dist: types-paramiko==3.3.0.0; extra == "dev"
44
+ Requires-Dist: scmrepo[tests]; extra == "dev"
57
45
 
58
46
  scmrepo
59
47
  =======
@@ -29,7 +29,6 @@ def lint(session: nox.Session) -> None:
29
29
  args = *(session.posargs or ("--show-diff-on-failure",)), "--all-files"
30
30
  session.run("pre-commit", "run", *args)
31
31
  session.run("python", "-m", "mypy")
32
- session.run("python", "-m", "pylint", *locations)
33
32
 
34
33
 
35
34
  @nox.session
@@ -0,0 +1,126 @@
1
+ [build-system]
2
+ requires = ["setuptools>=48", "setuptools_scm[toml]>=6.3.1"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.setuptools_scm]
6
+
7
+ [project]
8
+ name = "scmrepo"
9
+ description = "scmrepo"
10
+ readme = "README.rst"
11
+ license = {text = "Apache-2.0"}
12
+ authors = [{ name = "Iterative", email = "support@dvc.org" }]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.8",
16
+ "Programming Language :: Python :: 3.9",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Development Status :: 4 - Beta",
20
+ ]
21
+ requires-python = ">=3.8"
22
+ dynamic = ["version"]
23
+ dependencies = [
24
+ "gitpython>3",
25
+ "dulwich>=0.21.6",
26
+ "pygit2>=1.13.3",
27
+ "pygtrie>=2.3.2",
28
+ "fsspec>=2021.7.0",
29
+ "pathspec>=0.9.0",
30
+ "asyncssh>=2.13.1,<3",
31
+ "funcy>=1.14",
32
+ "shortuuid>=0.5.0",
33
+ "dvc-objects>=1.0.1,<3",
34
+ "dvc-http>=2.29.0",
35
+ ]
36
+
37
+ [project.urls]
38
+ Issues = "https://github.com/iterative/scmrepo/issues"
39
+ Source = "https://github.com/iterative/scmrepo"
40
+
41
+ [project.optional-dependencies]
42
+ tests = [
43
+ "pytest==7.2.0",
44
+ "pytest-sugar==0.9.5",
45
+ "pytest-cov==3.0.0",
46
+ "pytest-mock==3.8.2",
47
+ "mypy==0.971",
48
+ "pytest-test-utils==0.0.8",
49
+ "pytest-asyncio==0.18.3",
50
+ # https://github.com/docker/docker-py/issues/2902
51
+ "pytest-docker==0.12.0; python_version < '3.10' and implementation_name != 'pypy'",
52
+ "mock==5.1.0",
53
+ "paramiko==3.3.1",
54
+ "types-certifi==2021.10.8.3",
55
+ "types-mock==5.1.0.2",
56
+ "types-paramiko==3.3.0.0",
57
+ ]
58
+ dev = [
59
+ "scmrepo[tests]",
60
+ ]
61
+
62
+ [tool.setuptools.package-data]
63
+ dvc_objects = ["py.typed"]
64
+
65
+ [tool.setuptools.packages.find]
66
+ where = ["src"]
67
+ namespaces = false
68
+
69
+ [tool.pytest.ini_options]
70
+ addopts = "-ra"
71
+ markers = [
72
+ "skip_git_backend: skip tests for given backend",
73
+ "slow: mark test as slow to run",
74
+ ]
75
+
76
+ [tool.coverage.run]
77
+ branch = true
78
+ source = ["scmrepo", "tests"]
79
+
80
+ [tool.coverage.paths]
81
+ source = ["src", "*/site-packages"]
82
+
83
+ [tool.coverage.report]
84
+ show_missing = true
85
+ exclude_lines = [
86
+ "pragma: no cover",
87
+ "if __name__ == .__main__.:",
88
+ "if typing.TYPE_CHECKING:",
89
+ "if TYPE_CHECKING:",
90
+ "raise NotImplementedError",
91
+ "raise AssertionError",
92
+ "@overload",
93
+ ]
94
+
95
+ [tool.mypy]
96
+ # Error output
97
+ show_column_numbers = true
98
+ show_error_codes = true
99
+ show_error_context = true
100
+ show_traceback = true
101
+ pretty = true
102
+ check_untyped_defs = false
103
+ # Warnings
104
+ warn_no_return = true
105
+ warn_redundant_casts = true
106
+ warn_unreachable = true
107
+ files = ["src", "tests"]
108
+
109
+ [[tool.mypy.overrides]]
110
+ module = [
111
+ "pygtrie",
112
+ "dvc_http.*",
113
+ "funcy",
114
+ "git",
115
+ "gitdb.*",
116
+ "fsspec.*",
117
+ "pathspec.patterns",
118
+ "asyncssh.*",
119
+ "pygit2.*",
120
+ "pytest_docker.plugin",
121
+ "urllib3.*",
122
+ ]
123
+ ignore_missing_imports = true
124
+
125
+ [tool.codespell]
126
+ ignore-words-list = "cachable, keypair"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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(self, mode: str = "r", encoding: str = None):
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(self, mode: str = "r", encoding: str = None):
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