changeset 39622:c5e6c1ba1c79

hg: don't reuse repo instance after unshare() Unsharing a repository is a pretty invasive procedure and fundamentally changes the behavior of the repository. Currently, hg.unshare() calls into localrepository.__init__ to re-initialize the repository instance. This is a bit hacky. And future commits that refactor how localrepository instances are constructed will make this difficult to support. This commit changes unshare() so it constructs a new repo instance once the unshare I/O has completed. It then poisons the old repo instance so any further use will result in error. Surprisingly, nothing in core appears to access a repo instance after it has been unshared! .. api:: ``hg.unshare()`` now poisons the repo instance so it can't be used. It also returns a new repo instance suitable for interacting with the unshared repository. Differential Revision: https://phab.mercurial-scm.org/D4557
author Gregory Szorc <gregory.szorc@gmail.com>
date Wed, 12 Sep 2018 19:00:46 -0700
parents 23f2299e9e53
children 5362c96f2feb
files mercurial/hg.py mercurial/localrepo.py
diffstat 2 files changed, 39 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/hg.py	Tue Sep 11 20:06:39 2018 -0700
+++ b/mercurial/hg.py	Wed Sep 12 19:00:46 2018 -0700
@@ -307,6 +307,11 @@
     """convert a shared repository to a normal one
 
     Copy the store data to the repo and remove the sharedpath data.
+
+    Returns a new repository object representing the unshared repository.
+
+    The passed repository object is not usable after this function is
+    called.
     """
 
     destlock = lock = None
@@ -329,16 +334,22 @@
         destlock and destlock.release()
         lock and lock.release()
 
-    # update store, spath, svfs and sjoin of repo
-    repo.unfiltered().__init__(repo.baseui, repo.root)
+    # Removing share changes some fundamental properties of the repo instance.
+    # So we instantiate a new repo object and operate on it rather than
+    # try to keep the existing repo usable.
+    newrepo = repository(repo.baseui, repo.root, create=False)
 
     # TODO: figure out how to access subrepos that exist, but were previously
     #       removed from .hgsub
-    c = repo['.']
+    c = newrepo['.']
     subs = c.substate
     for s in sorted(subs):
         c.sub(s).unshare()
 
+    localrepo.poisonrepository(repo)
+
+    return newrepo
+
 def postshare(sourcerepo, destrepo, bookmarks=True, defaultpath=None):
     """Called after a new shared repo is created.
 
--- a/mercurial/localrepo.py	Tue Sep 11 20:06:39 2018 -0700
+++ b/mercurial/localrepo.py	Wed Sep 12 19:00:46 2018 -0700
@@ -2503,3 +2503,28 @@
                      b'layout')
 
     scmutil.writerequires(hgvfs, requirements)
+
+def poisonrepository(repo):
+    """Poison a repository instance so it can no longer be used."""
+    # Perform any cleanup on the instance.
+    repo.close()
+
+    # Our strategy is to replace the type of the object with one that
+    # has all attribute lookups result in error.
+    #
+    # But we have to allow the close() method because some constructors
+    # of repos call close() on repo references.
+    class poisonedrepository(object):
+        def __getattribute__(self, item):
+            if item == r'close':
+                return object.__getattribute__(self, item)
+
+            raise error.ProgrammingError('repo instances should not be used '
+                                         'after unshare')
+
+        def close(self):
+            pass
+
+    # We may have a repoview, which intercepts __setattr__. So be sure
+    # we operate at the lowest level possible.
+    object.__setattr__(repo, r'__class__', poisonedrepository)