scmrepo 3.3.12__tar.gz → 3.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.

Files changed (73) hide show
  1. {scmrepo-3.3.12 → scmrepo-3.5.0}/.github/workflows/release.yaml +2 -2
  2. {scmrepo-3.3.12 → scmrepo-3.5.0}/.github/workflows/tests.yaml +1 -0
  3. {scmrepo-3.3.12 → scmrepo-3.5.0}/.pre-commit-config.yaml +2 -2
  4. {scmrepo-3.3.12/src/scmrepo.egg-info → scmrepo-3.5.0}/PKG-INFO +3 -3
  5. {scmrepo-3.3.12 → scmrepo-3.5.0}/noxfile.py +5 -1
  6. {scmrepo-3.3.12 → scmrepo-3.5.0}/pyproject.toml +15 -5
  7. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/backend/base.py +1 -1
  8. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/backend/dulwich/__init__.py +33 -6
  9. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/backend/gitpython.py +16 -13
  10. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/backend/pygit2/__init__.py +66 -40
  11. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/backend/pygit2/filter.py +4 -4
  12. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/progress.py +13 -9
  13. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/utils.py +11 -7
  14. {scmrepo-3.3.12 → scmrepo-3.5.0/src/scmrepo.egg-info}/PKG-INFO +3 -3
  15. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo.egg-info/requires.txt +1 -1
  16. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/test_credentials.py +1 -1
  17. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/test_dulwich.py +9 -9
  18. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/test_git.py +162 -11
  19. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/vendor/test_paramiko_vendor.py +6 -6
  20. {scmrepo-3.3.12 → scmrepo-3.5.0}/.coveragerc +0 -0
  21. {scmrepo-3.3.12 → scmrepo-3.5.0}/.cruft.json +0 -0
  22. {scmrepo-3.3.12 → scmrepo-3.5.0}/.gitattributes +0 -0
  23. {scmrepo-3.3.12 → scmrepo-3.5.0}/.github/dependabot.yml +0 -0
  24. {scmrepo-3.3.12 → scmrepo-3.5.0}/.github/workflows/update-template.yaml +0 -0
  25. {scmrepo-3.3.12 → scmrepo-3.5.0}/.gitignore +0 -0
  26. {scmrepo-3.3.12 → scmrepo-3.5.0}/CODE_OF_CONDUCT.rst +0 -0
  27. {scmrepo-3.3.12 → scmrepo-3.5.0}/CONTRIBUTING.rst +0 -0
  28. {scmrepo-3.3.12 → scmrepo-3.5.0}/LICENSE +0 -0
  29. {scmrepo-3.3.12 → scmrepo-3.5.0}/README.rst +0 -0
  30. {scmrepo-3.3.12 → scmrepo-3.5.0}/setup.cfg +0 -0
  31. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/__init__.py +0 -0
  32. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/asyn.py +0 -0
  33. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/base.py +0 -0
  34. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/exceptions.py +0 -0
  35. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/fs.py +0 -0
  36. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/__init__.py +0 -0
  37. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/backend/__init__.py +0 -0
  38. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/backend/dulwich/asyncssh_vendor.py +0 -0
  39. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/backend/dulwich/client.py +0 -0
  40. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/backend/pygit2/callbacks.py +0 -0
  41. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/config.py +0 -0
  42. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/credentials.py +0 -0
  43. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/lfs/__init__.py +0 -0
  44. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/lfs/client.py +0 -0
  45. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/lfs/exceptions.py +0 -0
  46. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/lfs/fetch.py +0 -0
  47. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/lfs/object.py +0 -0
  48. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/lfs/pointer.py +0 -0
  49. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/lfs/progress.py +0 -0
  50. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/lfs/smudge.py +0 -0
  51. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/lfs/storage.py +0 -0
  52. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/objects.py +0 -0
  53. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/git/stash.py +0 -0
  54. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/noscm.py +0 -0
  55. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/py.typed +0 -0
  56. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo/urls.py +0 -0
  57. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo.egg-info/SOURCES.txt +0 -0
  58. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo.egg-info/dependency_links.txt +0 -0
  59. {scmrepo-3.3.12 → scmrepo-3.5.0}/src/scmrepo.egg-info/top_level.txt +0 -0
  60. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/__init__.py +0 -0
  61. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/conftest.py +0 -0
  62. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/docker-compose.yml +0 -0
  63. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/git-init/git.sh +0 -0
  64. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/test_fs.py +0 -0
  65. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/test_lfs.py +0 -0
  66. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/test_noscm.py +0 -0
  67. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/test_pygit2.py +0 -0
  68. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/test_scmrepo.py +0 -0
  69. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/test_stash.py +0 -0
  70. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/test_urls.py +0 -0
  71. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/user.key +0 -0
  72. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/user.key.pub +0 -0
  73. {scmrepo-3.3.12 → scmrepo-3.5.0}/tests/vendor/__init__.py +0 -0
@@ -21,10 +21,10 @@ jobs:
21
21
  with:
22
22
  fetch-depth: 0
23
23
 
24
- - name: Set up Python 3.10
24
+ - name: Set up Python 3.13
25
25
  uses: actions/setup-python@v5
26
26
  with:
27
- python-version: '3.10'
27
+ python-version: '3.13'
28
28
 
29
29
  - name: Upgrade pip and nox
30
30
  run: |
@@ -33,6 +33,7 @@ jobs:
33
33
  uses: actions/setup-python@v5
34
34
  with:
35
35
  python-version: ${{ matrix.pyv }}
36
+ allow-prereleases: true
36
37
 
37
38
  - name: Upgrade pip and nox
38
39
  run: |
@@ -20,9 +20,9 @@ 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.12.3'
23
+ rev: 'v0.12.5'
24
24
  hooks:
25
- - id: ruff
25
+ - id: ruff-check
26
26
  args: [--fix, --exit-non-zero-on-fix]
27
27
  - id: ruff-format
28
28
  - repo: https://github.com/codespell-project/codespell
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scmrepo
3
- Version: 3.3.12
3
+ Version: 3.5.0
4
4
  Summary: scmrepo
5
5
  Author-email: Iterative <support@dvc.org>
6
- License: Apache-2.0
6
+ License-Expression: Apache-2.0
7
7
  Project-URL: Issues, https://github.com/iterative/scmrepo/issues
8
8
  Project-URL: Source, https://github.com/iterative/scmrepo
9
9
  Classifier: Programming Language :: Python :: 3
@@ -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.23.1
20
+ Requires-Dist: dulwich>=0.24.0
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
@@ -10,7 +10,11 @@ nox.options.sessions = "lint", "tests"
10
10
  locations = "src", "tests"
11
11
 
12
12
 
13
- @nox.session(python=["3.9", "3.10", "3.11", "3.12", "3.13"])
13
+ project = nox.project.load_toml()
14
+ python_versions = nox.project.python_versions(project)
15
+
16
+
17
+ @nox.session(python=python_versions)
14
18
  def tests(session: nox.Session) -> None:
15
19
  session.install(".[tests]")
16
20
  session.run(
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["setuptools>=48", "setuptools_scm[toml]>=6.3.1"]
2
+ requires = ["setuptools>=77", "setuptools_scm[toml]>=8"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [tool.setuptools_scm]
@@ -8,7 +8,8 @@ build-backend = "setuptools.build_meta"
8
8
  name = "scmrepo"
9
9
  description = "scmrepo"
10
10
  readme = "README.rst"
11
- license = {text = "Apache-2.0"}
11
+ license = "Apache-2.0"
12
+ license-files = ["LICENSE"]
12
13
  authors = [{ name = "Iterative", email = "support@dvc.org" }]
13
14
  classifiers = [
14
15
  "Programming Language :: Python :: 3",
@@ -23,7 +24,7 @@ requires-python = ">=3.9"
23
24
  dynamic = ["version"]
24
25
  dependencies = [
25
26
  "gitpython>3",
26
- "dulwich>=0.23.1",
27
+ "dulwich>=0.24.0",
27
28
  "pygit2>=1.14.0",
28
29
  "pygtrie>=2.3.2",
29
30
  "fsspec[tqdm]>=2024.2.0",
@@ -102,7 +103,7 @@ show_error_codes = true
102
103
  show_error_context = true
103
104
  show_traceback = true
104
105
  pretty = true
105
- check_untyped_defs = false
106
+ check_untyped_defs = true
106
107
  # Warnings
107
108
  warn_no_return = true
108
109
  warn_redundant_casts = true
@@ -192,8 +193,9 @@ select = [
192
193
 
193
194
  [tool.ruff.lint.per-file-ignores]
194
195
  "noxfile.py" = ["D", "PTH"]
195
- "tests/**" = ["S", "ARG001", "ARG002", "ANN"]
196
+ "tests/**" = ["S", "ARG001", "ARG002", "ANN", "TID251", "TID253"]
196
197
  "docs/**" = ["INP"]
198
+ "src/scmrepo/git/backend/gitpython.py" = ["TID251"]
197
199
 
198
200
  [tool.ruff.lint.flake8-pytest-style]
199
201
  fixture-parentheses = false
@@ -203,6 +205,14 @@ parametrize-names-type = "csv"
203
205
  [tool.ruff.lint.flake8-type-checking]
204
206
  strict = true
205
207
 
208
+ [tool.ruff.lint.flake8-tidy-imports.banned-api]
209
+ "git".msg = "importing from 'git' is not allowed except inside `gitpython` backend"
210
+
211
+ [tool.ruff.lint.flake8-tidy-imports]
212
+ # Ban certain modules from being imported at module level, instead requiring
213
+ # that they're imported lazily (e.g., within a function definition).
214
+ banned-module-level-imports = ["git"]
215
+
206
216
  [tool.ruff.lint.isort]
207
217
  known-first-party = ["scmrepo"]
208
218
 
@@ -40,7 +40,7 @@ class BaseGitBackend(ABC):
40
40
 
41
41
  @property
42
42
  @abstractmethod
43
- def root_dir(self) -> str:
43
+ def root_dir(self) -> Optional[str]:
44
44
  pass
45
45
 
46
46
  @staticmethod
@@ -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
@@ -203,9 +204,11 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
203
204
 
204
205
  Submodule paths will be relative to Git repo root.
205
206
  """
207
+
206
208
  from dulwich.config import ConfigFile, parse_submodules
207
209
 
208
210
  submodules: dict[str, str] = {}
211
+ assert self.root_dir
209
212
  config_path = os.path.join(self.root_dir, ".gitmodules")
210
213
  if os.path.isfile(config_path):
211
214
  config = ConfigFile.from_path(config_path)
@@ -217,7 +220,7 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
217
220
  self.repo.close()
218
221
 
219
222
  @property
220
- def root_dir(self) -> str:
223
+ def root_dir(self) -> Optional[str]:
221
224
  return self.repo.path
222
225
 
223
226
  @classmethod
@@ -354,6 +357,7 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
354
357
  # path relative to the submodule root.
355
358
  fs_path = relpath(path, self.root_dir)
356
359
  for sm_path in self._submodules.values():
360
+ assert self.root_dir
357
361
  if fs_path.startswith(sm_path):
358
362
  path = os.path.join(
359
363
  self.root_dir,
@@ -378,7 +382,7 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
378
382
 
379
383
  with reraise((Error, CommitError), SCMError("Git commit failed")):
380
384
  try:
381
- commit(self.root_dir, message=msg, no_verify=no_verify)
385
+ commit(self.repo, message=msg, no_verify=no_verify)
382
386
  except InvalidUserIdentity as exc:
383
387
  raise SCMError("Git username and email must be configured") from exc
384
388
  except TimezoneFormatError as exc:
@@ -421,7 +425,7 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
421
425
  from dulwich.porcelain import Error, branch_create
422
426
 
423
427
  try:
424
- branch_create(self.root_dir, branch)
428
+ branch_create(self.repo, branch)
425
429
  except Error as exc:
426
430
  raise SCMError(f"Failed to create branch '{branch}'") from exc
427
431
 
@@ -473,7 +477,25 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
473
477
  return sorted(ref[len(base) :] for ref in self.iter_refs(base))
474
478
 
475
479
  def list_all_commits(self) -> Iterable[str]:
476
- raise NotImplementedError
480
+ from dulwich.objects import Tag
481
+
482
+ repo = self.repo
483
+ starting_points: list[bytes] = []
484
+
485
+ # HEAD
486
+ head_rev = self.get_ref("HEAD")
487
+ if head_rev:
488
+ starting_points.append(head_rev.encode("utf-8"))
489
+
490
+ # Branches and remotes
491
+ for ref in repo.refs:
492
+ if ref.startswith((b"refs/heads/", b"refs/remotes/", b"refs/tags/")):
493
+ if isinstance(repo.refs[ref], Tag):
494
+ ref = self.repo.get_peeled(repo.refs[ref])
495
+ starting_points.append(repo.refs[ref])
496
+
497
+ walker = self.repo.get_walker(include=starting_points, order=ORDER_DATE)
498
+ return [e.commit.id.decode() for e in walker]
477
499
 
478
500
  def get_tree_obj(self, rev: str, **kwargs) -> DulwichObject:
479
501
  from dulwich.objectspec import parse_tree
@@ -629,8 +651,13 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
629
651
  def update_refs(refs):
630
652
  from dulwich.objects import ZERO_SHA
631
653
 
654
+ _refspecs = (
655
+ os.fsencode(refspecs)
656
+ if isinstance(refspecs, str)
657
+ else [os.fsencode(refspec) for refspec in refspecs]
658
+ )
632
659
  selected_refs.extend(
633
- parse_reftuples(self.repo.refs, refs, refspecs, force=force)
660
+ parse_reftuples(self.repo.refs, refs, _refspecs, force=force)
634
661
  )
635
662
  new_refs = {}
636
663
  for lh, rh, _ in selected_refs:
@@ -882,7 +909,7 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
882
909
 
883
910
  with reraise(Error, SCMError("Git status failed")):
884
911
  staged, unstaged, untracked = git_status(
885
- self.root_dir, ignored=ignored, untracked_files=untracked_files
912
+ self.repo, ignored=ignored, untracked_files=untracked_files
886
913
  )
887
914
 
888
915
  return (
@@ -161,8 +161,9 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
161
161
  return bool(func(str(path)))
162
162
 
163
163
  @property
164
- def root_dir(self) -> str:
165
- return self.repo.working_tree_dir
164
+ def root_dir(self) -> Optional[str]:
165
+ d = self.repo.working_tree_dir
166
+ return os.fspath(d) if d is not None else d
166
167
 
167
168
  @staticmethod
168
169
  @requires_git
@@ -241,7 +242,7 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
241
242
 
242
243
  @property
243
244
  def dir(self) -> str:
244
- return self.repo.git_dir
245
+ return os.fspath(self.repo.git_dir)
245
246
 
246
247
  def add(
247
248
  self,
@@ -262,7 +263,7 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
262
263
  paths = [path for path in paths if not self.is_ignored(path)]
263
264
  self.git.add(*paths, **kwargs)
264
265
  else:
265
- self.repo.index.add(paths)
266
+ self.repo.index.add(paths if isinstance(paths, str) else list(paths))
266
267
  except AssertionError as exc:
267
268
  # NOTE: GitPython is not currently able to handle index version >= 3.
268
269
  # See https://github.com/iterative/dvc/issues/610 for more details.
@@ -301,7 +302,7 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
301
302
  kwargs["force"] = True
302
303
  if unshallow:
303
304
  kwargs["unshallow"] = True
304
- infos = self.repo.remote(name=remote).fetch(**kwargs)
305
+ infos = self.repo.remote(name=remote).fetch(**kwargs) # type: ignore[arg-type]
305
306
  for info in infos:
306
307
  if info.flags & info.ERROR:
307
308
  raise SCMError(f"fetch failed: {info.note}")
@@ -350,7 +351,7 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
350
351
 
351
352
  def active_branch_remote(self) -> str:
352
353
  try:
353
- return self.repo.active_branch.tracking_branch()
354
+ return self.repo.active_branch.tracking_branch() # type: ignore[return-value]
354
355
  except (TypeError, ValueError) as exc:
355
356
  raise SCMError("No active branch tracking remote") from exc
356
357
 
@@ -428,15 +429,15 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
428
429
  return GitCommit(
429
430
  commit.hexsha,
430
431
  commit.committed_date,
431
- commit.committer_tz_offset,
432
- commit.message,
432
+ commit.committer_tz_offset, # type: ignore[arg-type]
433
+ commit.message, # type: ignore[arg-type]
433
434
  [str(parent) for parent in commit.parents],
434
- commit.committer.name,
435
- commit.committer.email,
436
- commit.author.name,
437
- commit.author.email,
435
+ commit.committer.name, # type: ignore[arg-type]
436
+ commit.committer.email, # type: ignore[arg-type]
437
+ commit.author.name, # type: ignore[arg-type]
438
+ commit.author.email, # type: ignore[arg-type]
438
439
  commit.authored_date,
439
- commit.author_tz_offset,
440
+ commit.author_tz_offset, # type: ignore[arg-type]
440
441
  )
441
442
 
442
443
  def set_ref(
@@ -744,6 +745,8 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
744
745
  if not ref.tag:
745
746
  return ref.commit.hexsha
746
747
  tag = ref.tag
748
+ assert tag.tagger.email
749
+ assert tag.tagger.name
747
750
  return GitTag(
748
751
  tag.tag,
749
752
  tag.hexsha,
@@ -171,7 +171,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
171
171
  # NOTE: we want this init to be lazy so we do it on backend init.
172
172
  # for subsequent backend instances, this call will error out since
173
173
  # the filter is already registered
174
- pygit2.filter_register("lfs", LFSFilter)
174
+ pygit2.filter_register("lfs", LFSFilter) # type: ignore[attr-defined]
175
175
  except ValueError:
176
176
  pass
177
177
 
@@ -181,7 +181,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
181
181
  self.repo.free()
182
182
 
183
183
  @property
184
- def root_dir(self) -> str:
184
+ def root_dir(self) -> Optional[str]:
185
185
  return self.repo.workdir
186
186
 
187
187
  @cached_property
@@ -194,10 +194,10 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
194
194
  from pygit2 import Tag
195
195
  from pygit2.enums import ObjectType
196
196
 
197
- commit, ref = self.repo.resolve_refish(refish)
197
+ commit, ref = self.repo.resolve_refish(refish) # type: ignore[attr-defined]
198
198
  if isinstance(commit, Tag):
199
199
  ref = commit
200
- commit = commit.peel(ObjectType.COMMIT)
200
+ commit = commit.peel(ObjectType.COMMIT) # type: ignore[call-overload]
201
201
  return commit, ref
202
202
 
203
203
  @property
@@ -354,8 +354,8 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
354
354
  with self.release_odb_handles():
355
355
  if create_new:
356
356
  commit = self.repo.revparse_single("HEAD")
357
- new_branch = self.repo.branches.local.create(branch, commit)
358
- self.repo.checkout(new_branch, strategy=strategy)
357
+ new_branch = self.repo.branches.local.create(branch, commit) # type: ignore[arg-type]
358
+ self.repo.checkout(new_branch, strategy=strategy) # type: ignore[attr-defined]
359
359
  else:
360
360
  if branch == "-":
361
361
  branch = "@{-1}"
@@ -363,12 +363,12 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
363
363
  commit, ref = self._resolve_refish(branch)
364
364
  except (KeyError, GitError) as exc:
365
365
  raise RevError(f"unknown Git revision '{branch}'") from exc
366
- self.repo.checkout_tree(commit, strategy=strategy)
366
+ self.repo.checkout_tree(commit, strategy=strategy) # type: ignore[attr-defined]
367
367
  detach = kwargs.get("detach", False)
368
368
  if ref and not detach:
369
- self.repo.set_head(ref.name)
369
+ self.repo.set_head(ref.name) # type: ignore[attr-defined]
370
370
  else:
371
- self.repo.set_head(commit.id)
371
+ self.repo.set_head(commit.id) # type: ignore[attr-defined]
372
372
 
373
373
  def fetch(
374
374
  self,
@@ -389,7 +389,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
389
389
 
390
390
  try:
391
391
  commit = self.repo[self.repo.head.target]
392
- self.repo.create_branch(branch, commit)
392
+ self.repo.create_branch(branch, commit) # type: ignore[arg-type]
393
393
  except GitError as exc:
394
394
  raise SCMError(f"Failed to create branch '{branch}'") from exc
395
395
 
@@ -431,7 +431,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
431
431
  # if HEAD points to a nonexistent branch we still return the
432
432
  # branch name (without "refs/heads/" prefix) to match gitpython's
433
433
  # behavior
434
- return self.repo.references["HEAD"].target[11:]
434
+ return self.repo.references["HEAD"].target[11:] # type: ignore[index]
435
435
  return self.repo.head.shorthand
436
436
 
437
437
  def active_branch_remote(self) -> str:
@@ -452,10 +452,35 @@ 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 o in starting_points:
478
+ walker.push(o)
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
- tree = self.repo[rev].tree
483
+ tree = self.repo[rev].tree # type: ignore[attr-defined]
459
484
  return Pygit2Object(tree, backend=self)
460
485
 
461
486
  def get_rev(self) -> str:
@@ -546,7 +571,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
546
571
  try:
547
572
  obj = self.repo[ref.target]
548
573
  if isinstance(obj, Tag):
549
- return str(obj.peel(ObjectType.COMMIT).id)
574
+ return str(obj.peel(ObjectType.COMMIT).id) # type: ignore[call-overload]
550
575
  except ValueError:
551
576
  pass
552
577
 
@@ -759,7 +784,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
759
784
  from scmrepo.git import Stash
760
785
 
761
786
  try:
762
- oid = self.repo.stash(
787
+ oid = self.repo.stash( # type: ignore[attr-defined]
763
788
  self.committer,
764
789
  message=message,
765
790
  include_untracked=include_untracked,
@@ -770,8 +795,8 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
770
795
  commit = self.repo[oid]
771
796
 
772
797
  if ref != Stash.DEFAULT_STASH:
773
- self.set_ref(ref, commit.id, message=commit.message)
774
- self.repo.stash_drop()
798
+ self.set_ref(ref, commit.id, message=commit.message) # type: ignore[attr-defined,arg-type]
799
+ self.repo.stash_drop() # type: ignore[attr-defined]
775
800
  return str(oid), False
776
801
 
777
802
  def _stash_apply(
@@ -788,11 +813,11 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
788
813
 
789
814
  def _apply(index):
790
815
  try:
791
- self.repo.index.read(False)
816
+ self.repo.index.read(False) # type: ignore[attr-defined]
792
817
  strategy = self._get_checkout_strategy()
793
818
  if skip_conflicts:
794
819
  strategy |= CheckoutStrategy.ALLOW_CONFLICTS
795
- self.repo.stash_apply(
820
+ self.repo.stash_apply( # type: ignore[attr-defined]
796
821
  index, strategy=strategy, reinstate_index=reinstate_index
797
822
  )
798
823
  except GitError as exc:
@@ -814,7 +839,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
814
839
  try:
815
840
  _apply(0)
816
841
  finally:
817
- self.repo.stash_drop()
842
+ self.repo.stash_drop() # type: ignore[attr-defined]
818
843
 
819
844
  def _stash_drop(self, ref: str, index: int):
820
845
  from scmrepo.git import Stash
@@ -822,7 +847,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
822
847
  if ref != Stash.DEFAULT_STASH:
823
848
  raise NotImplementedError
824
849
 
825
- self.repo.stash_drop(index)
850
+ self.repo.stash_drop(index) # type: ignore[attr-defined]
826
851
 
827
852
  def _describe(
828
853
  self,
@@ -840,16 +865,16 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
840
865
  from pygit2 import IndexEntry
841
866
  from pygit2.enums import ResetMode
842
867
 
843
- self.repo.index.read(False)
868
+ self.repo.index.read(False) # type: ignore[attr-defined]
844
869
  if paths is not None:
845
- tree = self.repo.revparse_single("HEAD").tree
870
+ tree = self.repo.revparse_single("HEAD").tree # type: ignore[attr-defined]
846
871
  for path in paths:
847
872
  rel = relpath(path, self.root_dir)
848
873
  if os.name == "nt":
849
874
  rel = rel.replace("\\", "/")
850
875
  obj = tree[rel]
851
- self.repo.index.add(IndexEntry(rel, obj.id, obj.filemode))
852
- self.repo.index.write()
876
+ self.repo.index.add(IndexEntry(rel, obj.id, obj.filemode)) # type: ignore[attr-defined]
877
+ self.repo.index.write() # type: ignore[attr-defined]
853
878
  elif hard:
854
879
  self.repo.reset(self.repo.head.target, ResetMode.HARD)
855
880
  else:
@@ -875,7 +900,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
875
900
  strategy |= CheckoutStrategy.ALLOW_CONFLICTS
876
901
  strategy = self._get_checkout_strategy(strategy)
877
902
 
878
- index = self.repo.index
903
+ index = self.repo.index # type: ignore[attr-defined]
879
904
  if paths:
880
905
  path_list: Optional[list[str]] = [
881
906
  relpath(path, self.root_dir) for path in paths
@@ -889,7 +914,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
889
914
  path_list = None
890
915
 
891
916
  with self.release_odb_handles():
892
- self.repo.checkout_index(index=index, paths=path_list, strategy=strategy)
917
+ self.repo.checkout_index(index=index, paths=path_list, strategy=strategy) # type: ignore[attr-defined]
893
918
 
894
919
  if index.conflicts and (ours or theirs):
895
920
  for ancestor, ours_entry, theirs_entry in index.conflicts:
@@ -900,9 +925,10 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
900
925
  index.add(ours_entry)
901
926
  else:
902
927
  entry = theirs_entry
928
+ assert self.root_dir
903
929
  path = os.path.join(self.root_dir, entry.path)
904
930
  with open(path, "wb") as fobj:
905
- fobj.write(self.repo.get(entry.id).read_raw())
931
+ fobj.write(self.repo.get(entry.id).read_raw()) # type: ignore[attr-defined]
906
932
  index.add(entry.path)
907
933
  index.write()
908
934
 
@@ -965,8 +991,8 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
965
991
  raise SCMError("Cannot merge with 'squash' and 'commit'")
966
992
 
967
993
  with self.release_odb_handles():
968
- self.repo.index.read(False)
969
- obj, _ref = self.repo.resolve_refish(rev)
994
+ self.repo.index.read(False) # type: ignore[attr-defined]
995
+ obj, _ref = self.repo.resolve_refish(rev) # type: ignore[attr-defined]
970
996
  try:
971
997
  analysis, ff_pref = self.repo.merge_analysis(obj.id)
972
998
  except GitError as exc:
@@ -978,12 +1004,12 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
978
1004
  return None
979
1005
 
980
1006
  try:
981
- self.repo.merge(obj.id)
982
- self.repo.index.write()
1007
+ self.repo.merge(obj.id) # type: ignore[attr-defined]
1008
+ self.repo.index.write() # type: ignore[attr-defined]
983
1009
  except GitError as exc:
984
1010
  raise SCMError("Merge failed") from exc
985
1011
 
986
- if self.repo.index.conflicts:
1012
+ if self.repo.index.conflicts: # type: ignore[attr-defined]
987
1013
  raise MergeConflictError("Merge contained conflicts")
988
1014
 
989
1015
  try:
@@ -992,7 +1018,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
992
1018
  return self._merge_ff(rev, obj)
993
1019
 
994
1020
  if analysis & MergeAnalysis.UNBORN:
995
- self.repo.set_head(obj.id)
1021
+ self.repo.set_head(obj.id) # type: ignore[attr-defined]
996
1022
  return str(obj.id)
997
1023
 
998
1024
  if ff_pref & MergePreference.FASTFORWARD_ONLY:
@@ -1005,12 +1031,12 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
1005
1031
  # HEAD is not moved and merge changes stay in index
1006
1032
  return None
1007
1033
  finally:
1008
- self.repo.state_cleanup()
1009
- self.repo.index.write()
1034
+ self.repo.state_cleanup() # type: ignore[attr-defined]
1035
+ self.repo.index.write() # type: ignore[attr-defined]
1010
1036
 
1011
1037
  def _merge_ff(self, rev: str, obj) -> str:
1012
1038
  if self.repo.head_is_detached:
1013
- self.repo.set_head(obj.id)
1039
+ self.repo.set_head(obj.id) # type: ignore[attr-defined]
1014
1040
  else:
1015
1041
  branch = self.get_ref("HEAD", follow=False)
1016
1042
  assert branch
@@ -1024,7 +1050,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
1024
1050
  def _merge_commit(self, msg: Optional[str], obj) -> str:
1025
1051
  if not msg:
1026
1052
  raise SCMError("Merge commit message is required")
1027
- tree = self.repo.index.write_tree()
1053
+ tree = self.repo.index.write_tree() # type: ignore[attr-defined]
1028
1054
  merge_commit = self.repo.create_commit(
1029
1055
  "HEAD",
1030
1056
  self.author,
@@ -1078,7 +1104,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
1078
1104
 
1079
1105
  if path:
1080
1106
  return Pygit2Config(_Pygit2Config(path))
1081
- return Pygit2Config(self.repo.config)
1107
+ return Pygit2Config(self.repo.config) # type: ignore[attr-defined]
1082
1108
 
1083
1109
  def check_attr(
1084
1110
  self,
@@ -1098,7 +1124,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
1098
1124
  except (KeyError, GitError) as exc:
1099
1125
  raise SCMError(f"Invalid commit '{source}'") from exc
1100
1126
  try:
1101
- return self.repo.get_attr(
1127
+ return self.repo.get_attr( # type: ignore[attr-defined]
1102
1128
  path, attr, flags=flags, commit=commit.id if commit else None
1103
1129
  )
1104
1130
  except GitError as exc:
@@ -18,22 +18,22 @@ class LFSFilter(Filter):
18
18
  self._smudge_root: Optional[str] = None
19
19
 
20
20
  def check(self, src: "FilterSource", attr_values: list[Optional[str]]):
21
- if attr_values[0] == "lfs" and src.mode != GIT_FILTER_CLEAN:
21
+ if attr_values[0] == "lfs" and src.mode != GIT_FILTER_CLEAN: # type: ignore[attr-defined]
22
22
  self._smudge_buf = io.BytesIO()
23
- self._smudge_root = src.repo.workdir or src.repo.path
23
+ self._smudge_root = src.repo.workdir or src.repo.path # type: ignore[attr-defined]
24
24
  return
25
25
  raise Passthrough
26
26
 
27
27
  def write(
28
28
  self, data: bytes, src: "FilterSource", write_next: Callable[[bytes], None]
29
29
  ):
30
- if src.mode == GIT_FILTER_CLEAN:
30
+ if src.mode == GIT_FILTER_CLEAN: # type: ignore[attr-defined]
31
31
  write_next(data)
32
32
  return
33
33
  if self._smudge_buf is None:
34
34
  self._smudge_buf = io.BytesIO()
35
35
  if self._smudge_root is None:
36
- self._smudge_root = src.repo.workdir or src.repo.path
36
+ self._smudge_root = src.repo.workdir or src.repo.path # type: ignore[attr-defined]
37
37
  self._smudge_buf.write(data)
38
38
 
39
39
  def close(self, write_next: Callable[[bytes], None]):
@@ -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,9 +1,13 @@
1
1
  import os
2
- from collections.abc import MutableMapping
2
+ from collections.abc import Iterator, MutableMapping
3
+ from typing import Callable, TypeVar, Union
3
4
 
5
+ K = TypeVar("K")
6
+ V = TypeVar("V")
4
7
 
5
- class LazyDict(MutableMapping):
6
- def __init__(self, values):
8
+
9
+ class LazyDict(MutableMapping[K, V]):
10
+ def __init__(self, values: dict[K, Union[V, Callable[[], V]]]):
7
11
  self._values = values
8
12
 
9
13
  def __getitem__(self, item):
@@ -13,16 +17,16 @@ class LazyDict(MutableMapping):
13
17
  self._values[item] = value
14
18
  return value
15
19
 
16
- def __setitem__(self, key, value):
20
+ def __setitem__(self, key: K, value: Union[V, Callable[[], V]]) -> None:
17
21
  self._values[key] = value
18
22
 
19
- def __delitem__(self, key):
23
+ def __delitem__(self, key: K) -> None:
20
24
  del self._values[key]
21
25
 
22
- def __iter__(self):
26
+ def __iter__(self) -> Iterator[K]:
23
27
  return iter(self._values)
24
28
 
25
- def __len__(self):
29
+ def __len__(self) -> int:
26
30
  return len(self._values)
27
31
 
28
32
 
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scmrepo
3
- Version: 3.3.12
3
+ Version: 3.5.0
4
4
  Summary: scmrepo
5
5
  Author-email: Iterative <support@dvc.org>
6
- License: Apache-2.0
6
+ License-Expression: Apache-2.0
7
7
  Project-URL: Issues, https://github.com/iterative/scmrepo/issues
8
8
  Project-URL: Source, https://github.com/iterative/scmrepo
9
9
  Classifier: Programming Language :: Python :: 3
@@ -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.23.1
20
+ Requires-Dist: dulwich>=0.24.0
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
@@ -1,5 +1,5 @@
1
1
  gitpython>3
2
- dulwich>=0.23.1
2
+ dulwich>=0.24.0
3
3
  pygit2>=1.14.0
4
4
  pygtrie>=2.3.2
5
5
  fsspec[tqdm]>=2024.2.0
@@ -73,7 +73,7 @@ def test_subprocess_strip_trailing_garbage_bytes(git_helper, mocker):
73
73
  "subprocess.run",
74
74
  # Simulate git-credential-osxkeychain (version 2.45), assuming initial 0-byte
75
75
  return_value=mocker.Mock(
76
- stdout=f"username=foo\npassword=bar{chr(0)}{random.randbytes(15)}"
76
+ stdout=f"username=foo\npassword=bar{chr(0)}{random.randbytes(15)}" # type: ignore[str-bytes-safe]
77
77
  ),
78
78
  )
79
79
  creds = git_helper.get(Credential(protocol="https", host="foo.com", path="foo.git"))
@@ -68,24 +68,24 @@ class Server(paramiko.ServerInterface):
68
68
 
69
69
  def check_auth_interactive_response(self, responses):
70
70
  if responses[0] == PASSWORD:
71
- return paramiko.AUTH_SUCCESSFUL
72
- return paramiko.AUTH_FAILED
71
+ return paramiko.AUTH_SUCCESSFUL # type: ignore[attr-defined]
72
+ return paramiko.AUTH_FAILED # type: ignore[attr-defined]
73
73
 
74
74
  def check_auth_password(self, username, password):
75
75
  if username == USER and password == PASSWORD:
76
- return paramiko.AUTH_SUCCESSFUL
77
- return paramiko.AUTH_FAILED
76
+ return paramiko.AUTH_SUCCESSFUL # type: ignore[attr-defined]
77
+ return paramiko.AUTH_FAILED # type: ignore[attr-defined]
78
78
 
79
79
  def check_auth_publickey(self, username, key):
80
80
  pubkey = paramiko.RSAKey.from_private_key(StringIO(CLIENT_KEY))
81
81
  if username == USER and key == pubkey:
82
- return paramiko.AUTH_SUCCESSFUL
83
- return paramiko.AUTH_FAILED
82
+ return paramiko.AUTH_SUCCESSFUL # type: ignore[attr-defined]
83
+ return paramiko.AUTH_FAILED # type: ignore[attr-defined]
84
84
 
85
85
  def check_channel_request(self, kind, chanid):
86
86
  if kind == "session":
87
- return paramiko.OPEN_SUCCEEDED
88
- return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
87
+ return paramiko.OPEN_SUCCEEDED # type: ignore[attr-defined]
88
+ return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED # type: ignore[attr-defined]
89
89
 
90
90
  def get_allowed_auths(self, username):
91
91
  return self.allowed_auths
@@ -111,7 +111,7 @@ def ssh_conn(request: pytest.FixtureRequest) -> dict[str, Any]:
111
111
  conn, _ = sock.accept()
112
112
  except OSError:
113
113
  return False
114
- server.transport = transport = paramiko.Transport(conn)
114
+ server.transport = transport = paramiko.Transport(conn) # type: ignore[attr-defined]
115
115
  request.addfinalizer(transport.close)
116
116
  host_key = paramiko.RSAKey.from_private_key(StringIO(CLIENT_KEY))
117
117
  transport.add_server_key(host_key)
@@ -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", "dulwich")
924
+ @pytest.mark.skip_git_backend("pygit2", "gitpython")
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"])
@@ -961,6 +1111,7 @@ def proxy_server():
961
1111
  pass
962
1112
 
963
1113
  _ProxyServer.setUpClass()
1114
+ assert _ProxyServer.PROXY
964
1115
  yield f"http://{_ProxyServer.PROXY.flags.hostname}:{_ProxyServer.PROXY.flags.port}"
965
1116
  _ProxyServer.tearDownClass()
966
1117
 
@@ -41,19 +41,19 @@ else:
41
41
 
42
42
  def check_auth_password(self, username, password):
43
43
  if username == USER and password == PASSWORD:
44
- return paramiko.AUTH_SUCCESSFUL
45
- return paramiko.AUTH_FAILED
44
+ return paramiko.AUTH_SUCCESSFUL # type: ignore[attr-defined]
45
+ return paramiko.AUTH_FAILED # type: ignore[attr-defined]
46
46
 
47
47
  def check_auth_publickey(self, username, key):
48
48
  pubkey = paramiko.RSAKey.from_private_key(StringIO(CLIENT_KEY))
49
49
  if username == USER and key == pubkey:
50
- return paramiko.AUTH_SUCCESSFUL
51
- return paramiko.AUTH_FAILED
50
+ return paramiko.AUTH_SUCCESSFUL # type: ignore[attr-defined]
51
+ return paramiko.AUTH_FAILED # type: ignore[attr-defined]
52
52
 
53
53
  def check_channel_request(self, kind, chanid):
54
54
  if kind == "session":
55
- return paramiko.OPEN_SUCCEEDED
56
- return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
55
+ return paramiko.OPEN_SUCCEEDED # type: ignore[attr-defined]
56
+ return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED # type: ignore[attr-defined]
57
57
 
58
58
  def get_allowed_auths(self, username):
59
59
  return "password,publickey"
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