D7616: exchange: ensure all outgoing subrepo references are present before pushing

mharbison72 (Matt Harbison) phabricator at mercurial-scm.org
Thu Dec 12 13:54:37 EST 2019

mharbison72 created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

  We've run into occasional problems with people committing a repo, and then
  amending or rebasing in the subrepo.  That makes it so that the revision in the
  parent can't be checked out, and the problem gets propagated on push.  Mercurial
  already tries to defend against this sort of dangling reference by pushing *all*
  subrepo revisions first.  This reuses the checks that trigger warnings in
  `hg verify` to bail on the push unless using `--force`.
  I thought about putting this on the server side, but at that point, all of the
  data has been transferred, only to bail out.  Additionally, SCM Manager hosts
  subrepos in a location that isn't nested in the parent, so normal subrepo code
  would complain that the subrepo is missing when run on the server.
  Because the push command pushes subrepos before calling this exchange code, a
  subrepo will be pushed before the parent is verified.  Not great, but no
  dangling references are exchanged, so it solves the problem.  This code isn't in
  the loop that pushes the subrepos because:
  1. the list of outgoing revisions is needed to limit the scope of the check
  2. the loop only accesses the current revision, and therefore can miss subrepos that were dropped in previous commits
  3. this code is called when pushing a subrepo, so the protection is recursive
  I'm not sure if there's a cheap check for the list of files in the outgoing
  bundle.  If there is, that would provide a fast path to bypass this check for
  people not using subrepos (or if no subrepo changes were made).  There's
  probably also room for verifying other references like tags.  But since that
  doesn't break checkouts, it's much less of a problem.

  rHG Mercurial




diff --git a/tests/test-amend-subrepo.t b/tests/test-amend-subrepo.t
--- a/tests/test-amend-subrepo.t
+++ b/tests/test-amend-subrepo.t
@@ -164,4 +164,35 @@
   R .hgsub
   R .hgsubstate
+broken repositories will refuse to push
+#if obsstore-off
+  $ hg up -q -C 2
+  $ hg up -q -C 6
+  $ echo c >> t/b
+  $ hg amend -q -R t
+  $ hg init ../dest
+  $ hg init ../dest/t
+  $ hg init ../dest/s
+  $ hg push -q ../dest
+  abort: subrepo 't' is hidden in revision 04aa62396ec6 (obsstore-on !)
+  abort: subrepo 't' not found in revision 04aa62396ec6 (obsstore-off !)
+  [255]
+... unless forced
+  $ hg push --force -q ../dest
+  $ hg verify -R ../dest
+  checking changesets
+  checking manifests
+  crosschecking files in changesets and manifests
+  checking files
+  checked 5 changesets with 12 changes to 4 files
+  checking subrepo links
+  subrepo 't' not found in revision 04aa62396ec6
+  subrepo 't' not found in revision 6bce99600681
   $ cd ..
diff --git a/mercurial/subrepo.py b/mercurial/subrepo.py
--- a/mercurial/subrepo.py
+++ b/mercurial/subrepo.py
@@ -429,10 +429,12 @@
         convert this repository from shared to normal storage.
-    def verify(self):
-        '''verify the integrity of the repository.  Return 0 on success or
-        warning, 1 on any error.
-        '''
+    def verify(self, onpush=False):
+        """verify the revision of this repository that is held in `_state` is
+        present and not hidden.  Return 0 on success or warning, 1 on any
+        error.  In the case of ``onpush``, warnings or errors will raise an
+        exception if the result of pushing would be a broken remote repository.
+        """
         return 0
@@ -1013,26 +1015,35 @@
         hg.unshare(self.ui, self._repo)
-    def verify(self):
+    def verify(self, onpush=False):
             rev = self._state[1]
             ctx = self._repo.unfiltered()[rev]
             if ctx.hidden():
                 # Since hidden revisions aren't pushed/pulled, it seems worth an
                 # explicit warning.
-                ui = self._repo.ui
-                ui.warn(
-                    _(b"subrepo '%s' is hidden in revision %s\n")
-                    % (self._relpath, node.short(self._ctx.node()))
+                msg = _(b"subrepo '%s' is hidden in revision %s") % (
+                    self._relpath,
+                    node.short(self._ctx.node()),
+                if onpush:
+                    raise error.Abort(msg)
+                else:
+                    self._repo.ui.warn(b'%s\n' % msg)
             return 0
         except error.RepoLookupError:
             # A missing subrepo revision may be a case of needing to pull it, so
-            # don't treat this as an error.
-            self._repo.ui.warn(
-                _(b"subrepo '%s' not found in revision %s\n")
-                % (self._relpath, node.short(self._ctx.node()))
+            # don't treat this as an error for `hg verify`.
+            msg = _(b"subrepo '%s' not found in revision %s") % (
+                self._relpath,
+                node.short(self._ctx.node()),
+            if onpush:
+                raise error.Abort(msg)
+            else:
+                self._repo.ui.warn(b'%s\n' % msg)
             return 0
diff --git a/mercurial/exchange.py b/mercurial/exchange.py
--- a/mercurial/exchange.py
+++ b/mercurial/exchange.py
@@ -646,6 +646,8 @@
+                if not pushop.force:
+                    _checksubrepostate(pushop)
                 if not _forcebundle1(pushop):
@@ -694,6 +696,17 @@
+def _checksubrepostate(pushop):
+    """Ensure all outgoing referenced subrepo revisions are present locally"""
+    for n in pushop.outgoing.missing:
+        ctx = pushop.repo[n]
+        if b'.hgsub' in ctx.manifest() and b'.hgsubstate' in ctx.files():
+            for subpath in sorted(ctx.substate):
+                sub = ctx.sub(subpath)
+                sub.verify(onpush=True)
 def _pushdiscoverychangeset(pushop):
     """discover the changeset that need to be pushed"""

To: mharbison72, #hg-reviewers
Cc: mercurial-devel

More information about the Mercurial-devel mailing list