scmrepo 3.3.11__tar.gz → 3.4.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 (73) hide show
  1. {scmrepo-3.3.11 → scmrepo-3.4.0}/.pre-commit-config.yaml +1 -1
  2. {scmrepo-3.3.11/src/scmrepo.egg-info → scmrepo-3.4.0}/PKG-INFO +4 -4
  3. {scmrepo-3.3.11 → scmrepo-3.4.0}/pyproject.toml +14 -4
  4. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/asyn.py +1 -3
  5. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/__init__.py +2 -1
  6. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/backend/dulwich/__init__.py +24 -5
  7. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/backend/dulwich/asyncssh_vendor.py +4 -2
  8. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/backend/dulwich/client.py +10 -3
  9. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/backend/pygit2/__init__.py +26 -1
  10. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/backend/pygit2/filter.py +1 -1
  11. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/credentials.py +4 -4
  12. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/lfs/smudge.py +1 -1
  13. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/progress.py +13 -9
  14. {scmrepo-3.3.11 → scmrepo-3.4.0/src/scmrepo.egg-info}/PKG-INFO +4 -4
  15. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo.egg-info/requires.txt +3 -3
  16. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/test_git.py +161 -11
  17. {scmrepo-3.3.11 → scmrepo-3.4.0}/.coveragerc +0 -0
  18. {scmrepo-3.3.11 → scmrepo-3.4.0}/.cruft.json +0 -0
  19. {scmrepo-3.3.11 → scmrepo-3.4.0}/.gitattributes +0 -0
  20. {scmrepo-3.3.11 → scmrepo-3.4.0}/.github/dependabot.yml +0 -0
  21. {scmrepo-3.3.11 → scmrepo-3.4.0}/.github/workflows/release.yaml +0 -0
  22. {scmrepo-3.3.11 → scmrepo-3.4.0}/.github/workflows/tests.yaml +0 -0
  23. {scmrepo-3.3.11 → scmrepo-3.4.0}/.github/workflows/update-template.yaml +0 -0
  24. {scmrepo-3.3.11 → scmrepo-3.4.0}/.gitignore +0 -0
  25. {scmrepo-3.3.11 → scmrepo-3.4.0}/CODE_OF_CONDUCT.rst +0 -0
  26. {scmrepo-3.3.11 → scmrepo-3.4.0}/CONTRIBUTING.rst +0 -0
  27. {scmrepo-3.3.11 → scmrepo-3.4.0}/LICENSE +0 -0
  28. {scmrepo-3.3.11 → scmrepo-3.4.0}/README.rst +0 -0
  29. {scmrepo-3.3.11 → scmrepo-3.4.0}/noxfile.py +0 -0
  30. {scmrepo-3.3.11 → scmrepo-3.4.0}/setup.cfg +0 -0
  31. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/__init__.py +0 -0
  32. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/base.py +0 -0
  33. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/exceptions.py +0 -0
  34. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/fs.py +0 -0
  35. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/backend/__init__.py +0 -0
  36. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/backend/base.py +0 -0
  37. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/backend/gitpython.py +0 -0
  38. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/backend/pygit2/callbacks.py +0 -0
  39. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/config.py +0 -0
  40. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/lfs/__init__.py +0 -0
  41. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/lfs/client.py +0 -0
  42. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/lfs/exceptions.py +0 -0
  43. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/lfs/fetch.py +0 -0
  44. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/lfs/object.py +0 -0
  45. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/lfs/pointer.py +0 -0
  46. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/lfs/progress.py +0 -0
  47. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/lfs/storage.py +0 -0
  48. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/objects.py +0 -0
  49. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/git/stash.py +0 -0
  50. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/noscm.py +0 -0
  51. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/py.typed +0 -0
  52. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/urls.py +0 -0
  53. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo/utils.py +0 -0
  54. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo.egg-info/SOURCES.txt +0 -0
  55. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo.egg-info/dependency_links.txt +0 -0
  56. {scmrepo-3.3.11 → scmrepo-3.4.0}/src/scmrepo.egg-info/top_level.txt +0 -0
  57. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/__init__.py +0 -0
  58. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/conftest.py +0 -0
  59. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/docker-compose.yml +0 -0
  60. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/git-init/git.sh +0 -0
  61. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/test_credentials.py +0 -0
  62. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/test_dulwich.py +0 -0
  63. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/test_fs.py +0 -0
  64. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/test_lfs.py +0 -0
  65. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/test_noscm.py +0 -0
  66. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/test_pygit2.py +0 -0
  67. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/test_scmrepo.py +0 -0
  68. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/test_stash.py +0 -0
  69. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/test_urls.py +0 -0
  70. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/user.key +0 -0
  71. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/user.key.pub +0 -0
  72. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/vendor/__init__.py +0 -0
  73. {scmrepo-3.3.11 → scmrepo-3.4.0}/tests/vendor/test_paramiko_vendor.py +0 -0
@@ -20,7 +20,7 @@ repos:
20
20
  - id: sort-simple-yaml
21
21
  - id: trailing-whitespace
22
22
  - repo: https://github.com/astral-sh/ruff-pre-commit
23
- rev: 'v0.11.7'
23
+ rev: 'v0.12.3'
24
24
  hooks:
25
25
  - id: ruff
26
26
  args: [--fix, --exit-non-zero-on-fix]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scmrepo
3
- Version: 3.3.11
3
+ Version: 3.4.0
4
4
  Summary: scmrepo
5
5
  Author-email: Iterative <support@dvc.org>
6
6
  License: Apache-2.0
@@ -17,7 +17,7 @@ Requires-Python: >=3.9
17
17
  Description-Content-Type: text/x-rst
18
18
  License-File: LICENSE
19
19
  Requires-Dist: gitpython>3
20
- Requires-Dist: dulwich>=0.22.1
20
+ Requires-Dist: dulwich>=0.23.1
21
21
  Requires-Dist: pygit2>=1.14.0
22
22
  Requires-Dist: pygtrie>=2.3.2
23
23
  Requires-Dist: fsspec[tqdm]>=2024.2.0
@@ -30,7 +30,7 @@ Provides-Extra: tests
30
30
  Requires-Dist: aioresponses<0.8,>=0.7; extra == "tests"
31
31
  Requires-Dist: paramiko<4,>=3.4.0; extra == "tests"
32
32
  Requires-Dist: pytest<9,>=7; extra == "tests"
33
- Requires-Dist: pytest-asyncio<1,>=0.23.2; extra == "tests"
33
+ Requires-Dist: pytest-asyncio<2,>=0.23.2; extra == "tests"
34
34
  Requires-Dist: pytest-cov>=4.1.0; extra == "tests"
35
35
  Requires-Dist: pytest-docker<4,>=1; extra == "tests"
36
36
  Requires-Dist: pytest-mock; extra == "tests"
@@ -38,7 +38,7 @@ Requires-Dist: pytest-sugar; extra == "tests"
38
38
  Requires-Dist: pytest-test-utils<0.2,>=0.1.0; extra == "tests"
39
39
  Requires-Dist: proxy.py; extra == "tests"
40
40
  Provides-Extra: dev
41
- Requires-Dist: mypy==1.15.0; extra == "dev"
41
+ Requires-Dist: mypy==1.17.0; extra == "dev"
42
42
  Requires-Dist: scmrepo[tests]; extra == "dev"
43
43
  Requires-Dist: types-certifi; extra == "dev"
44
44
  Requires-Dist: types-mock; extra == "dev"
@@ -23,7 +23,7 @@ requires-python = ">=3.9"
23
23
  dynamic = ["version"]
24
24
  dependencies = [
25
25
  "gitpython>3",
26
- "dulwich>=0.22.1",
26
+ "dulwich>=0.23.1",
27
27
  "pygit2>=1.14.0",
28
28
  "pygtrie>=2.3.2",
29
29
  "fsspec[tqdm]>=2024.2.0",
@@ -43,7 +43,7 @@ tests = [
43
43
  "aioresponses>=0.7,<0.8",
44
44
  "paramiko>=3.4.0,<4",
45
45
  "pytest>=7,<9",
46
- "pytest-asyncio>=0.23.2,<1",
46
+ "pytest-asyncio>=0.23.2,<2",
47
47
  "pytest-cov>=4.1.0",
48
48
  "pytest-docker>=1,<4",
49
49
  "pytest-mock",
@@ -52,7 +52,7 @@ tests = [
52
52
  "proxy.py",
53
53
  ]
54
54
  dev = [
55
- "mypy==1.15.0",
55
+ "mypy==1.17.0",
56
56
  "scmrepo[tests]",
57
57
  "types-certifi",
58
58
  "types-mock",
@@ -147,6 +147,7 @@ ignore = [
147
147
  "RET503", # implicit-return
148
148
  "SIM117", # multiple-with-statements
149
149
  "N818", # error-suffix-on-exception-name
150
+ "PLC0415", # import-outside-top-level
150
151
  ]
151
152
  select = [
152
153
  "A", # flake8-buitlins
@@ -191,8 +192,9 @@ select = [
191
192
 
192
193
  [tool.ruff.lint.per-file-ignores]
193
194
  "noxfile.py" = ["D", "PTH"]
194
- "tests/**" = ["S", "ARG001", "ARG002", "ANN"]
195
+ "tests/**" = ["S", "ARG001", "ARG002", "ANN", "TID251", "TID253"]
195
196
  "docs/**" = ["INP"]
197
+ "src/scmrepo/git/backend/gitpython.py" = ["TID251"]
196
198
 
197
199
  [tool.ruff.lint.flake8-pytest-style]
198
200
  fixture-parentheses = false
@@ -202,6 +204,14 @@ parametrize-names-type = "csv"
202
204
  [tool.ruff.lint.flake8-type-checking]
203
205
  strict = true
204
206
 
207
+ [tool.ruff.lint.flake8-tidy-imports.banned-api]
208
+ "git".msg = "importing from 'git' is not allowed except inside `gitpython` backend"
209
+
210
+ [tool.ruff.lint.flake8-tidy-imports]
211
+ # Ban certain modules from being imported at module level, instead requiring
212
+ # that they're imported lazily (e.g., within a function definition).
213
+ banned-module-level-imports = ["git"]
214
+
205
215
  [tool.ruff.lint.isort]
206
216
  known-first-party = ["scmrepo"]
207
217
 
@@ -6,7 +6,6 @@ import threading
6
6
  from typing import Any, Optional
7
7
 
8
8
  from fsspec.asyn import ( # noqa: F401, pylint:disable=unused-import
9
- _selector_policy,
10
9
  sync,
11
10
  sync_wrapper,
12
11
  )
@@ -23,8 +22,7 @@ def get_loop() -> asyncio.AbstractEventLoop:
23
22
  if default_loop[0] is None:
24
23
  with lock:
25
24
  if default_loop[0] is None:
26
- with _selector_policy():
27
- default_loop[0] = asyncio.new_event_loop()
25
+ default_loop[0] = asyncio.new_event_loop()
28
26
  loop = default_loop[0]
29
27
  th = threading.Thread(
30
28
  target=loop.run_forever, # type: ignore[attr-defined]
@@ -10,6 +10,7 @@ from contextlib import contextmanager
10
10
  from functools import partialmethod
11
11
  from typing import (
12
12
  TYPE_CHECKING,
13
+ Any,
13
14
  Callable,
14
15
  ClassVar,
15
16
  Optional,
@@ -295,7 +296,7 @@ class Git(Base):
295
296
  # See:
296
297
  # https://github.com/iterative/dvc/issues/5641
297
298
  # https://github.com/iterative/dvc/issues/7458
298
- def _backend_func(self, name, *args, **kwargs):
299
+ def _backend_func(self, name, *args, **kwargs) -> Any:
299
300
  backends: Iterable[str] = kwargs.pop("backends", self.backends)
300
301
  for key in backends:
301
302
  if self._last_backend is not None and key != self._last_backend:
@@ -17,6 +17,7 @@ from typing import (
17
17
  )
18
18
 
19
19
  from dulwich.config import ConfigFile, StackedConfig
20
+ from dulwich.walk import ORDER_DATE
20
21
  from funcy import cached_property, reraise
21
22
 
22
23
  from scmrepo.exceptions import AuthError, CloneError, InvalidRemote, RevError, SCMError
@@ -39,7 +40,7 @@ logger = logging.getLogger(__name__)
39
40
 
40
41
 
41
42
  class DulwichObject(GitObject):
42
- def __init__(self, repo, name, mode, sha):
43
+ def __init__(self, repo, name, mode, sha) -> None:
43
44
  self.repo = repo
44
45
  self._name = name
45
46
  self._mode = mode
@@ -136,7 +137,7 @@ def _get_ssh_vendor() -> "SSHVendor":
136
137
 
137
138
 
138
139
  class DulwichConfig(Config):
139
- def __init__(self, config: Union["ConfigFile", "StackedConfig"]):
140
+ def __init__(self, config: Union["ConfigFile", "StackedConfig"]) -> None:
140
141
  self._config = config
141
142
 
142
143
  @property
@@ -183,7 +184,7 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
183
184
 
184
185
  def __init__( # pylint:disable=W0231
185
186
  self, root_dir=os.curdir, search_parent_directories=True
186
- ):
187
+ ) -> None:
187
188
  from dulwich.errors import NotGitRepository
188
189
  from dulwich.repo import Repo
189
190
 
@@ -473,7 +474,25 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
473
474
  return sorted(ref[len(base) :] for ref in self.iter_refs(base))
474
475
 
475
476
  def list_all_commits(self) -> Iterable[str]:
476
- raise NotImplementedError
477
+ from dulwich.objects import Tag
478
+
479
+ repo = self.repo
480
+ starting_points: list[bytes] = []
481
+
482
+ # HEAD
483
+ head_rev = self.get_ref("HEAD")
484
+ if head_rev:
485
+ starting_points.append(head_rev.encode("utf-8"))
486
+
487
+ # Branches and remotes
488
+ for ref in repo.refs:
489
+ if ref.startswith((b"refs/heads/", b"refs/remotes/", b"refs/tags/")):
490
+ if isinstance(repo.refs[ref], Tag):
491
+ ref = self.repo.get_peeled(repo.refs[ref])
492
+ starting_points.append(repo.refs[ref])
493
+
494
+ walker = self.repo.get_walker(include=starting_points, order=ORDER_DATE)
495
+ return [e.commit.id.decode() for e in walker]
477
496
 
478
497
  def get_tree_obj(self, rev: str, **kwargs) -> DulwichObject:
479
498
  from dulwich.objectspec import parse_tree
@@ -990,7 +1009,7 @@ def ls_remote(url: str) -> dict[str, str]:
990
1009
  from dulwich.client import HTTPUnauthorized
991
1010
 
992
1011
  try:
993
- refs = porcelain.ls_remote(url)
1012
+ refs = porcelain.ls_remote(url).refs
994
1013
  return {os.fsdecode(ref): sha.decode("ascii") for ref, sha in refs.items()}
995
1014
  except HTTPUnauthorized as exc:
996
1015
  raise AuthError(url) from exc
@@ -71,7 +71,9 @@ class _StderrWrapper:
71
71
 
72
72
 
73
73
  class AsyncSSHWrapper(BaseAsyncObject):
74
- def __init__(self, conn: "SSHClientConnection", proc: "SSHClientProcess", **kwargs):
74
+ def __init__(
75
+ self, conn: "SSHClientConnection", proc: "SSHClientProcess", **kwargs
76
+ ) -> None:
75
77
  super().__init__(**kwargs)
76
78
  self.conn: SSHClientConnection = conn
77
79
  self.proc: SSHClientProcess = proc
@@ -109,7 +111,7 @@ class InteractiveSSHClient(SSHClient):
109
111
  _keys_to_try: Optional[list["FilePath"]] = None
110
112
  _passphrases: dict[str, str]
111
113
 
112
- def __init__(self, *args, **kwargs):
114
+ def __init__(self, *args, **kwargs) -> None:
113
115
  super(*args, **kwargs)
114
116
  self._passphrases: dict[str, str] = {}
115
117
 
@@ -14,7 +14,7 @@ class GitCredentialsHTTPClient(Urllib3HttpGitClient): # pylint: disable=abstrac
14
14
  password=None,
15
15
  config=None,
16
16
  **kwargs,
17
- ):
17
+ ) -> None:
18
18
  super().__init__(
19
19
  base_url=base_url,
20
20
  username=username,
@@ -29,6 +29,7 @@ class GitCredentialsHTTPClient(Urllib3HttpGitClient): # pylint: disable=abstrac
29
29
  url: str,
30
30
  headers: Optional[dict[str, str]] = None,
31
31
  data: Optional[Union[bytes, Iterator[bytes]]] = None,
32
+ raise_for_status: bool = True,
32
33
  ):
33
34
  cached_chunks: list[bytes] = []
34
35
 
@@ -48,7 +49,10 @@ class GitCredentialsHTTPClient(Urllib3HttpGitClient): # pylint: disable=abstrac
48
49
 
49
50
  try:
50
51
  result = super()._http_request(
51
- url, headers=headers, data=None if data is None else _cached_data()
52
+ url,
53
+ headers=headers,
54
+ data=None if data is None else _cached_data(),
55
+ raise_for_status=raise_for_status,
52
56
  )
53
57
  except HTTPUnauthorized:
54
58
  auth_header = self._get_auth()
@@ -59,7 +63,10 @@ class GitCredentialsHTTPClient(Urllib3HttpGitClient): # pylint: disable=abstrac
59
63
  else:
60
64
  headers = auth_header
61
65
  result = super()._http_request(
62
- url, headers=headers, data=None if data is None else _cached_data()
66
+ url,
67
+ headers=headers,
68
+ data=None if data is None else _cached_data(),
69
+ raise_for_status=raise_for_status,
63
70
  )
64
71
  if self._store_credentials is not None:
65
72
  self._store_credentials.approve()
@@ -452,7 +452,32 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
452
452
  return sorted(ref[len(base) :] for ref in self.iter_refs(base))
453
453
 
454
454
  def list_all_commits(self) -> Iterable[str]:
455
- raise NotImplementedError
455
+ import pygit2
456
+ from pygit2.enums import SortMode
457
+
458
+ # Add HEAD
459
+ starting_points: list[Union[Oid, str]] = []
460
+ if not self.repo.head_is_unborn:
461
+ starting_points.append(self.repo.head.target)
462
+
463
+ # Add all branches, remotes, and tags
464
+ for ref in self.repo.references:
465
+ if ref.startswith(("refs/heads/", "refs/remotes/")):
466
+ oid = self.repo.revparse_single(ref).id
467
+ starting_points.append(oid)
468
+ elif ref.startswith("refs/tags/"):
469
+ tag_obj = self.repo.revparse_single(ref)
470
+ if isinstance(tag_obj, pygit2.Tag):
471
+ starting_points.append(tag_obj.target)
472
+ else:
473
+ starting_points.append(tag_obj.id)
474
+
475
+ # Walk all commits
476
+ walker = self.repo.walk(None)
477
+ for oid in starting_points:
478
+ walker.push(oid)
479
+ walker.sort(SortMode.TIME)
480
+ return [str(commit.id) for commit in walker]
456
481
 
457
482
  def get_tree_obj(self, rev: str, **kwargs) -> Pygit2Object:
458
483
  tree = self.repo[rev].tree
@@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
13
13
  class LFSFilter(Filter):
14
14
  attributes = "filter=*"
15
15
 
16
- def __init__(self, *args, **kwargs):
16
+ def __init__(self, *args, **kwargs) -> None:
17
17
  self._smudge_buf: Optional[io.BytesIO] = None
18
18
  self._smudge_root: Optional[str] = None
19
19
 
@@ -105,7 +105,7 @@ class GitCredentialHelper(CredentialHelper):
105
105
  >>> password = credentials.password
106
106
  """
107
107
 
108
- def __init__(self, command: str, use_http_path: bool = False):
108
+ def __init__(self, command: str, use_http_path: bool = False) -> None:
109
109
  super().__init__()
110
110
  self._command = command
111
111
  self._run_kwargs: dict[str, Any] = {}
@@ -136,7 +136,7 @@ class GitCredentialHelper(CredentialHelper):
136
136
  if not shutil.which(executable) and shutil.which("git"):
137
137
  # If the helper cannot be found in PATH, it might be
138
138
  # a C git helper in GIT_EXEC_PATH
139
- git_exec_path = subprocess.check_output( # noqa: S603
139
+ git_exec_path = subprocess.check_output(
140
140
  ("git", "--exec-path"),
141
141
  text=True,
142
142
  ).strip()
@@ -353,7 +353,7 @@ def _input_tty(prompt: str = "Username: ") -> str:
353
353
  class MemoryCredentialHelper(CredentialHelper):
354
354
  """Memory credential helper that supports optional interactive input."""
355
355
 
356
- def __init__(self):
356
+ def __init__(self) -> None:
357
357
  super().__init__()
358
358
  self._credentials: dict[_CredentialKey, Credential] = {}
359
359
 
@@ -535,7 +535,7 @@ class Credential(Mapping[str, str]):
535
535
  path: Optional[str] = None,
536
536
  username: Optional[str] = None,
537
537
  password: Optional[str] = None,
538
- password_expiry_utc: Optional[int] = None,
538
+ password_expiry_utc: Optional[str] = None,
539
539
  url: Optional[str] = None,
540
540
  ):
541
541
  self.protocol = protocol
@@ -17,7 +17,7 @@ def smudge(
17
17
  batch_size: Optional[int] = None,
18
18
  ) -> BinaryIO:
19
19
  """Wrap the specified binary IO stream and run LFS smudge if necessary."""
20
- reader = io.BufferedReader(fobj) # type: ignore[arg-type]
20
+ reader = io.BufferedReader(fobj) # type: ignore[type-var]
21
21
  data = reader.peek(100)
22
22
  if any(data.startswith(header) for header in HEADERS):
23
23
  # read the pointer data into memory since the raw stream is unseekable
@@ -4,7 +4,7 @@ from funcy import compose
4
4
 
5
5
 
6
6
  def code2desc(op_code):
7
- from git import RootUpdateProgress as OP # noqa: N814
7
+ from git import RootUpdateProgress as OP # noqa: N814, TID251
8
8
 
9
9
  ops = {
10
10
  OP.COUNTING: "Counting",
@@ -44,16 +44,20 @@ class GitProgressEvent(NamedTuple):
44
44
 
45
45
  class GitProgressReporter:
46
46
  def __init__(self, fn) -> None:
47
- from git.util import CallableRemoteProgress
48
-
49
- self._reporter = CallableRemoteProgress(self.wrap_fn(fn))
47
+ try:
48
+ from git.util import CallableRemoteProgress # noqa: TID251
49
+ except ImportError:
50
+ self._reporter = None
51
+ else:
52
+ self._reporter = CallableRemoteProgress(self.wrap_fn(fn))
50
53
 
51
54
  def __call__(self, msg: Union[str, bytes]) -> None:
52
- self._reporter._parse_progress_line(
53
- msg.decode("utf-8", errors="replace").strip()
54
- if isinstance(msg, bytes)
55
- else msg
56
- )
55
+ if self._reporter is not None:
56
+ self._reporter._parse_progress_line(
57
+ msg.decode("utf-8", errors="replace").strip()
58
+ if isinstance(msg, bytes)
59
+ else msg
60
+ )
57
61
 
58
62
  @staticmethod
59
63
  def wrap_fn(fn):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scmrepo
3
- Version: 3.3.11
3
+ Version: 3.4.0
4
4
  Summary: scmrepo
5
5
  Author-email: Iterative <support@dvc.org>
6
6
  License: Apache-2.0
@@ -17,7 +17,7 @@ Requires-Python: >=3.9
17
17
  Description-Content-Type: text/x-rst
18
18
  License-File: LICENSE
19
19
  Requires-Dist: gitpython>3
20
- Requires-Dist: dulwich>=0.22.1
20
+ Requires-Dist: dulwich>=0.23.1
21
21
  Requires-Dist: pygit2>=1.14.0
22
22
  Requires-Dist: pygtrie>=2.3.2
23
23
  Requires-Dist: fsspec[tqdm]>=2024.2.0
@@ -30,7 +30,7 @@ Provides-Extra: tests
30
30
  Requires-Dist: aioresponses<0.8,>=0.7; extra == "tests"
31
31
  Requires-Dist: paramiko<4,>=3.4.0; extra == "tests"
32
32
  Requires-Dist: pytest<9,>=7; extra == "tests"
33
- Requires-Dist: pytest-asyncio<1,>=0.23.2; extra == "tests"
33
+ Requires-Dist: pytest-asyncio<2,>=0.23.2; extra == "tests"
34
34
  Requires-Dist: pytest-cov>=4.1.0; extra == "tests"
35
35
  Requires-Dist: pytest-docker<4,>=1; extra == "tests"
36
36
  Requires-Dist: pytest-mock; extra == "tests"
@@ -38,7 +38,7 @@ Requires-Dist: pytest-sugar; extra == "tests"
38
38
  Requires-Dist: pytest-test-utils<0.2,>=0.1.0; extra == "tests"
39
39
  Requires-Dist: proxy.py; extra == "tests"
40
40
  Provides-Extra: dev
41
- Requires-Dist: mypy==1.15.0; extra == "dev"
41
+ Requires-Dist: mypy==1.17.0; extra == "dev"
42
42
  Requires-Dist: scmrepo[tests]; extra == "dev"
43
43
  Requires-Dist: types-certifi; extra == "dev"
44
44
  Requires-Dist: types-mock; extra == "dev"
@@ -1,5 +1,5 @@
1
1
  gitpython>3
2
- dulwich>=0.22.1
2
+ dulwich>=0.23.1
3
3
  pygit2>=1.14.0
4
4
  pygtrie>=2.3.2
5
5
  fsspec[tqdm]>=2024.2.0
@@ -10,7 +10,7 @@ aiohttp-retry>=2.5.0
10
10
  tqdm
11
11
 
12
12
  [dev]
13
- mypy==1.15.0
13
+ mypy==1.17.0
14
14
  scmrepo[tests]
15
15
  types-certifi
16
16
  types-mock
@@ -21,7 +21,7 @@ types-tqdm
21
21
  aioresponses<0.8,>=0.7
22
22
  paramiko<4,>=3.4.0
23
23
  pytest<9,>=7
24
- pytest-asyncio<1,>=0.23.2
24
+ pytest-asyncio<2,>=0.23.2
25
25
  pytest-cov>=4.1.0
26
26
  pytest-docker<4,>=1
27
27
  pytest-mock
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import shutil
3
+ import time
3
4
  from pathlib import Path
4
5
  from typing import Any, Optional
5
6
 
@@ -422,21 +423,170 @@ def test_iter_remote_refs(
422
423
  } == set(git.iter_remote_refs(remote))
423
424
 
424
425
 
425
- @pytest.mark.skip_git_backend("dulwich", "pygit2")
426
+ def _gen(scm: Git, s: str, commit_timestamp: Optional[float] = None) -> str:
427
+ with open(s, mode="w") as f:
428
+ f.write(s)
429
+ scm.dulwich.add([s])
430
+ scm.dulwich.repo.do_commit(
431
+ message=s.encode("utf-8"), commit_timestamp=commit_timestamp
432
+ )
433
+ return scm.get_rev()
434
+
435
+
426
436
  def test_list_all_commits(tmp_dir: TmpDir, scm: Git, git: Git, matcher: type[Matcher]):
427
- def _gen(s):
428
- tmp_dir.gen(s, s)
429
- scm.add_commit(s, message=s)
430
- return scm.get_rev()
437
+ assert git.list_all_commits() == []
438
+ # https://github.com/libgit2/libgit2/issues/6336
439
+ now = time.time()
440
+
441
+ rev_a = _gen(scm, "a", commit_timestamp=now - 10)
442
+ rev_b = _gen(scm, "b", commit_timestamp=now - 8)
443
+ rev_c = _gen(scm, "c", commit_timestamp=now - 5)
444
+ rev_d = _gen(scm, "d", commit_timestamp=now - 2)
445
+
446
+ assert git.list_all_commits() == [rev_d, rev_c, rev_b, rev_a]
447
+
448
+ scm.gitpython.git.reset(rev_b, hard=True)
449
+ assert git.list_all_commits() == [rev_b, rev_a]
431
450
 
432
- rev_a = _gen("a")
433
- rev_b = _gen("b")
451
+
452
+ def test_list_all_commits_branch(
453
+ tmp_dir: TmpDir, scm: Git, git: Git, matcher: type[Matcher]
454
+ ):
455
+ revs = {}
456
+ now = time.time()
457
+
458
+ revs["1"] = _gen(scm, "a", commit_timestamp=now - 10)
459
+
460
+ scm.checkout("branch", create_new=True)
461
+ revs["3"] = _gen(scm, "c", commit_timestamp=now - 9)
462
+
463
+ scm.checkout("master")
464
+ revs["2"] = _gen(scm, "b", commit_timestamp=now - 7)
465
+
466
+ scm.checkout("branch")
467
+ revs["5"] = _gen(scm, "e", commit_timestamp=now - 6)
468
+
469
+ scm.checkout("master")
470
+ revs["4"] = _gen(scm, "d", commit_timestamp=now - 5)
471
+
472
+ scm.checkout("branch")
473
+ revs["6"] = _gen(scm, "f", commit_timestamp=now - 4)
474
+
475
+ scm.checkout("master")
476
+ revs["7"] = _gen(scm, "g", commit_timestamp=now - 3)
477
+ revs["8"] = scm.merge("branch", msg="merge branch")
478
+
479
+ inv_map = {v: k for k, v in revs.items()}
480
+ assert [inv_map[k] for k in git.list_all_commits()] == [
481
+ "8",
482
+ "7",
483
+ "6",
484
+ "4",
485
+ "5",
486
+ "2",
487
+ "3",
488
+ "1",
489
+ ]
490
+
491
+
492
+ def test_list_all_tags(tmp_dir: TmpDir, scm: Git, git: Git, matcher: type[Matcher]):
493
+ rev_a = _gen(scm, "a")
434
494
  scm.tag("tag")
435
- rev_c = _gen("c")
495
+ rev_b = _gen(scm, "b")
496
+ scm.tag("annotated", annotated=True, message="Annotated Tag")
497
+ rev_c = _gen(scm, "c")
498
+ rev_d = _gen(scm, "d")
499
+ assert git.list_all_commits() == matcher.unordered(rev_d, rev_c, rev_b, rev_a)
500
+
501
+ rev_e = _gen(scm, "e")
502
+ scm.tag(
503
+ "annotated2",
504
+ target="refs/tags/annotated",
505
+ annotated=True,
506
+ message="Annotated Tag",
507
+ )
508
+ assert git.list_all_commits() == matcher.unordered(
509
+ rev_e, rev_d, rev_c, rev_b, rev_a
510
+ )
511
+
512
+ rev_f = _gen(scm, "f")
513
+ scm.tag(
514
+ "annotated3",
515
+ target="refs/tags/annotated2",
516
+ annotated=True,
517
+ message="Annotated Tag 3",
518
+ )
519
+ assert git.list_all_commits() == matcher.unordered(
520
+ rev_f, rev_e, rev_d, rev_c, rev_b, rev_a
521
+ )
522
+
436
523
  scm.gitpython.git.reset(rev_a, hard=True)
437
- scm.set_ref("refs/foo/bar", rev_c)
524
+ assert git.list_all_commits() == matcher.unordered(rev_b, rev_a)
525
+
526
+
527
+ def test_list_all_commits_dangling_annotated_tag(tmp_dir: TmpDir, scm: Git, git: Git):
528
+ rev_a = _gen(scm, "a")
529
+ scm.tag("annotated", annotated=True, message="Annotated Tag")
530
+
531
+ _gen(scm, "b")
438
532
 
439
- assert git.list_all_commits() == matcher.unordered(rev_a, rev_b)
533
+ # Delete branch pointing to rev_a
534
+ scm.checkout(rev_a)
535
+ scm.gitpython.repo.delete_head("master", force=True)
536
+
537
+ assert git.list_all_commits() == [rev_a] # Only reachable via the tag
538
+
539
+
540
+ def test_list_all_commits_orphan(
541
+ tmp_dir: TmpDir, scm: Git, git: Git, matcher: type[Matcher]
542
+ ):
543
+ rev_a = _gen(scm, "a")
544
+
545
+ # Make an orphan branch
546
+ scm.gitpython.git.checkout("--orphan", "orphan-branch")
547
+ rev_orphan = _gen(scm, "orphanfile")
548
+
549
+ assert rev_orphan != rev_a
550
+ assert git.list_all_commits() == matcher.unordered(rev_orphan, rev_a)
551
+
552
+
553
+ def test_list_all_commits_refs(
554
+ tmp_dir: TmpDir, scm: Git, git: Git, matcher: type[Matcher]
555
+ ):
556
+ assert git.list_all_commits() == []
557
+
558
+ rev_a = _gen(scm, "a")
559
+
560
+ assert git.list_all_commits() == [rev_a]
561
+ rev_b = _gen(scm, "b")
562
+ scm.set_ref("refs/remotes/origin/feature", rev_b)
563
+ assert git.list_all_commits() == matcher.unordered(rev_b, rev_a)
564
+
565
+ # also add refs/exps/foo/bar
566
+ rev_c = _gen(scm, "c")
567
+ scm.set_ref("refs/exps/foo/bar", rev_c)
568
+ assert git.list_all_commits() == matcher.unordered(rev_c, rev_b, rev_a)
569
+
570
+ # Dangling/broken ref ---
571
+ scm.set_ref("refs/heads/bad-ref", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
572
+ with pytest.raises(Exception): # noqa: B017, PT011
573
+ git.list_all_commits()
574
+ scm.remove_ref("refs/heads/bad-ref")
575
+
576
+ scm.gitpython.git.reset(rev_a, hard=True)
577
+ assert git.list_all_commits() == matcher.unordered(rev_b, rev_a)
578
+
579
+
580
+ def test_list_all_commits_detached_head(
581
+ tmp_dir: TmpDir, scm: Git, git: Git, matcher: type[Matcher]
582
+ ):
583
+ rev_a = _gen(scm, "a")
584
+ rev_b = _gen(scm, "b")
585
+ rev_c = _gen(scm, "c")
586
+ scm.checkout(rev_b)
587
+
588
+ assert scm.pygit2.repo.head_is_detached
589
+ assert git.list_all_commits() == matcher.unordered(rev_c, rev_b, rev_a)
440
590
 
441
591
 
442
592
  @pytest.mark.skip_git_backend("pygit2")
@@ -771,7 +921,7 @@ def test_ignored(tmp_dir: TmpDir, scm: Git, git: Git, git_backend: str):
771
921
  assert not git.is_ignored(tmp_dir / "dir1" / "file2.txt")
772
922
 
773
923
 
774
- @pytest.mark.skip_git_backend("pygit2", "gitpython")
924
+ @pytest.mark.skip_git_backend("pygit2", "gitpython", "dulwich")
775
925
  def test_ignored_dir_unignored_subdirs(tmp_dir: TmpDir, scm: Git, git: Git):
776
926
  tmp_dir.gen({".gitignore": "data/**\n!data/**/\n!data/**/*.csv"})
777
927
  scm.add([".gitignore"])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes