33import logging
44import os
55import re
6+ from collections import OrderedDict
67from collections .abc import Mapping
78from contextlib import contextmanager
89from functools import partialmethod
3031BackendCls = Type [BaseGitBackend ]
3132
3233
34+ _LOW_PRIO_BACKENDS = ("gitpython" ,)
35+
36+
3337class GitBackends (Mapping ):
3438 DEFAULT : Dict [str , BackendCls ] = {
3539 "dulwich" : DulwichBackend ,
@@ -50,7 +54,9 @@ def __init__(
5054 self , selected : Optional [Iterable [str ]], * args , ** kwargs
5155 ) -> None :
5256 selected = selected or list (self .DEFAULT )
53- self .backends = {key : self .DEFAULT [key ] for key in selected }
57+ self .backends = OrderedDict (
58+ ((key , self .DEFAULT [key ]) for key in selected )
59+ )
5460
5561 self .initialized : Dict [str , BaseGitBackend ] = {}
5662
@@ -71,6 +77,10 @@ def reset_all(self) -> None:
7177 for backend in self .initialized .values ():
7278 backend ._reset () # pylint: disable=protected-access
7379
80+ def move_to_end (self , key : str , last : bool = True ):
81+ if key not in _LOW_PRIO_BACKENDS :
82+ self .backends .move_to_end (key , last = last )
83+
7484
7585class Git (Base ):
7686 """Class for managing Git."""
@@ -87,6 +97,7 @@ def __init__(
8797 self .backends = GitBackends (backends , * args , ** kwargs )
8898 first_ = first (self .backends .values ())
8999 super ().__init__ (first_ .root_dir )
100+ self ._last_backend : Optional [str ] = None
90101
91102 @property
92103 def dir (self ):
@@ -255,11 +266,25 @@ def close(self):
255266 def no_commits (self ):
256267 return not bool (self .get_ref ("HEAD" ))
257268
269+ # Prefer re-using the most recently used backend when possible. When
270+ # changing backends (due to unimplemented calls), we close the previous
271+ # backend to release any open git files/contexts that may cause conflicts
272+ # with the new backend.
273+ #
274+ # See:
275+ # https://github.com/iterative/dvc/issues/5641
276+ # https://github.com/iterative/dvc/issues/7458
258277 def _backend_func (self , name , * args , ** kwargs ):
259- for backend in self .backends .values ():
278+ for key , backend in self .backends .items ():
279+ if self ._last_backend is not None and key != self ._last_backend :
280+ self .backends [self ._last_backend ].close ()
281+ self ._last_backend = None
260282 try :
261283 func = getattr (backend , name )
262- return func (* args , ** kwargs )
284+ result = func (* args , ** kwargs )
285+ self ._last_backend = key
286+ self .backends .move_to_end (key , last = False )
287+ return result
263288 except NotImplementedError :
264289 pass
265290 raise NoGitBackendError (name )
0 commit comments