Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions scmrepo/git/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import os
import re
from collections import OrderedDict
from collections.abc import Mapping
from contextlib import contextmanager
from functools import partialmethod
Expand Down Expand Up @@ -30,6 +31,9 @@
BackendCls = Type[BaseGitBackend]


_LOW_PRIO_BACKENDS = ("gitpython",)


class GitBackends(Mapping):
DEFAULT: Dict[str, BackendCls] = {
"dulwich": DulwichBackend,
Expand All @@ -50,7 +54,9 @@ def __init__(
self, selected: Optional[Iterable[str]], *args, **kwargs
) -> None:
selected = selected or list(self.DEFAULT)
self.backends = {key: self.DEFAULT[key] for key in selected}
self.backends = OrderedDict(
((key, self.DEFAULT[key]) for key in selected)
)

self.initialized: Dict[str, BaseGitBackend] = {}

Expand All @@ -71,6 +77,10 @@ def reset_all(self) -> None:
for backend in self.initialized.values():
backend._reset() # pylint: disable=protected-access

def move_to_end(self, key: str, last: bool = True):
Copy link
Contributor Author

@pmrowla pmrowla Mar 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could just make this move_to_head() and get rid of the last option (since that's all we will ever use), but I went with this for now to stay consistent with the OrderedDict call

if key not in _LOW_PRIO_BACKENDS:
self.backends.move_to_end(key, last=last)


class Git(Base):
"""Class for managing Git."""
Expand All @@ -87,6 +97,7 @@ def __init__(
self.backends = GitBackends(backends, *args, **kwargs)
first_ = first(self.backends.values())
super().__init__(first_.root_dir)
self._last_backend: Optional[str] = None

@property
def dir(self):
Expand Down Expand Up @@ -255,11 +266,25 @@ def close(self):
def no_commits(self):
return not bool(self.get_ref("HEAD"))

# Prefer re-using the most recently used backend when possible. When
# changing backends (due to unimplemented calls), we close the previous
# backend to release any open git files/contexts that may cause conflicts
# with the new backend.
#
# See:
# https://github.com/iterative/dvc/issues/5641
# https://github.com/iterative/dvc/issues/7458
def _backend_func(self, name, *args, **kwargs):
for backend in self.backends.values():
for key, backend in self.backends.items():
if self._last_backend is not None and key != self._last_backend:
self.backends[self._last_backend].close()
self._last_backend = None
try:
func = getattr(backend, name)
return func(*args, **kwargs)
result = func(*args, **kwargs)
self._last_backend = key
self.backends.move_to_end(key, last=False)
return result
except NotImplementedError:
pass
raise NoGitBackendError(name)
Expand Down