Patchwork D2681: [PoC] scmutil: support local only obsolescence

login
register
mail settings
Submitter phabricator
Date March 5, 2018, 12:43 a.m.
Message ID <differential-rev-PHID-DREV-qrshj62iihwvhbumjivh-req@phab.mercurial-scm.org>
Download mbox | patch
Permalink /patch/29031/
State New
Headers show

Comments

phabricator - March 5, 2018, 12:43 a.m.
indygreg created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  Now that we have a config option for enabling local-only obsolescence,
  it is time to do something with it.
  
  This commit teaches cleanupnodes() - which is called after rewrite
  operations - to handle local only obsolescence mode.
  
  In this mode, we create a backup bundle of the obsoleted changesets -
  just like what happens if obsolescence is disabled. But we don't strip
  the repo: we keep the original changesets around in a non-visible state.
  
  The new code verifies that no unstable changesets are introduced in
  local-only obsolescence mode.
  
  The new code hackily only runs if the action is "amend." The intent
  is to make this conditional only on the feature option. However,
  doing that would have significant test fallout. So we limit to "amend"
  for now.
  
  TODO:
  
  - Better test coverage (I think the "(testcase !)" syntax might be subtly wrong by flagging output as optional and not required).
  - Delete obsolescence markers when we pull and unbundle from the bundle.
  - Support pulling locally-hidden heads.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D2681

AFFECTED FILES
  mercurial/scmutil.py
  tests/test-amend.t

CHANGE DETAILS




To: indygreg, #hg-reviewers
Cc: mercurial-devel
phabricator - March 5, 2018, 2:22 a.m.
mharbison72 added a comment.


  re: the "(testcase !)" syntax- I agree that it would be better if "(false !)" means the line is *not* present, instead of optional.
  
  I made an attempt to do that, but somehow I came up with a test in test-run-tests.t that failed there, but the exact same test ran fine in another *.t.  I wondered if it was a case of run-tests.py processing it for a test inside test-run-tests.t, and then the main instance of run-tests.py re-processing it.  If I can find that code, I can post it if you think it might be useful.  But I've long forgotten the nuance in that code.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D2681

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

Patch

diff --git a/tests/test-amend.t b/tests/test-amend.t
--- a/tests/test-amend.t
+++ b/tests/test-amend.t
@@ -1,4 +1,4 @@ 
-#testcases obsstore-off obsstore-on
+#testcases obsstore-off obsstore-on evolution-on
 
   $ cat << EOF >> $HGRCPATH
   > [extensions]
@@ -8,10 +8,17 @@ 
   > git=1
   > EOF
 
-#if obsstore-on
+#if obsstore-off
+  $ cat << EOF >> $HGRCPATH
+  > [ui]
+  > localobsolescence = false
+  > EOF
+#endif
+
+#if evolution-on
   $ cat << EOF >> $HGRCPATH
   > [experimental]
-  > evolution.createmarkers=True
+  > evolution.createmarkers = true
   > EOF
 #endif
 
@@ -49,6 +56,8 @@ 
      +A
      \ No newline at end of file
   
+  $ hg debugobsolete
+
 #else
   $ hg log -p -G --hidden -T '{rev} {node|short} {desc}\n'
   @  2 be169c7e8dbe B
@@ -77,6 +86,95 @@ 
      +A
      \ No newline at end of file
   
+
+  $ hg debugobsolete
+  112478962961147124edd43549aedd1a335e44bf be169c7e8dbe21cd10b3d79691cbe7f241e3c21c 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '8', 'operation': 'amend', 'user': 'test'}
+
+#endif
+
+Unbundling from backup bundle restores visibility
+
+#if obsstore-on
+
+  $ cd ..
+  $ cp -r repo1 repo1-unbundle-backup
+  $ cd repo1-unbundle-backup
+
+  $ hg unbundle .hg/strip-backup/112478962961-7e959a55-amend.hg
+  adding changesets
+  adding manifests
+  adding file changes
+  added 0 changesets with 0 changes to 1 files
+  (run 'hg update' to get a working copy)
+
+TODO obsmarker should be deleted, both versions should be visible
+
+  $ hg debugobsolete
+  112478962961147124edd43549aedd1a335e44bf be169c7e8dbe21cd10b3d79691cbe7f241e3c21c 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '8', 'operation': 'amend', 'user': 'test'}
+
+  $ hg log -G
+  @  changeset:   2:be169c7e8dbe
+  |  tag:         tip
+  |  parent:      0:426bada5c675
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     B
+  |
+  | x  changeset:   1:112478962961
+  |/   tag:         B
+  |    user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    obsolete:    rewritten using amend as 2:be169c7e8dbe
+  |    summary:     B
+  |
+  o  changeset:   0:426bada5c675
+     tag:         A
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     A
+  
+
+  $ cd ..
+
+Pulling from a backup bundle restores visibility
+
+  $ cp -r repo1 repo1-pull-backup
+  $ cd repo1-pull-backup
+
+  $ hg pull .hg/strip-backup/112478962961-7e959a55-amend.hg
+  pulling from .hg/strip-backup/112478962961-7e959a55-amend.hg
+  searching for changes
+  no changes found
+
+TODO obsmarker should be deleted, both versions should be visible
+
+  $ hg debugobsolete
+  112478962961147124edd43549aedd1a335e44bf be169c7e8dbe21cd10b3d79691cbe7f241e3c21c 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '8', 'operation': 'amend', 'user': 'test'}
+
+  $ hg log -G
+  @  changeset:   2:be169c7e8dbe
+  |  tag:         tip
+  |  parent:      0:426bada5c675
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     B
+  |
+  | x  changeset:   1:112478962961
+  |/   tag:         B
+  |    user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    obsolete:    rewritten using amend as 2:be169c7e8dbe
+  |    summary:     B
+  |
+  o  changeset:   0:426bada5c675
+     tag:         A
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     A
+  
+
+  $ cd ../repo1
+
 #endif
 
 Nothing changed
@@ -174,7 +272,7 @@ 
   abort: cannot amend changeset with children
   [255]
 
-#if obsstore-on
+#if evolution-on
 
 With allowunstable, amend could work in the middle of a stack
 
diff --git a/mercurial/scmutil.py b/mercurial/scmutil.py
--- a/mercurial/scmutil.py
+++ b/mercurial/scmutil.py
@@ -702,8 +702,13 @@ 
         if bmarkchanges:
             bmarks.applychanges(repo, tr, bmarkchanges)
 
-        # Obsolete or strip nodes
-        if obsolete.isenabled(repo, obsolete.createmarkersopt):
+        # We either create obsolescence markers or strip.
+        # If we create markers and we're in local only mode, we create a backup
+        # bundle.
+        obsopts = obsolete.getoptions(repo)
+
+        if (obsopts[obsolete.createmarkersopt] or
+            obsopts[obsolete.localonlymodeopt]):
             # If a node is already obsoleted, and we want to obsolete it
             # without a successor, skip that obssolete request since it's
             # unnecessary. That's the "if s or not isobs(n)" check below.
@@ -717,8 +722,36 @@ 
                     for n, s in sorted(replacements.items(), key=sortfunc)
                     if s or not isobs(n)]
             if rels:
+                # For now, explicitly limit which operations opt in to this
+                # behavior. Also, we need to run this code before markers are
+                # created, otherwise visibility changes would impact operation.
+                if operation == 'amend' and obsopts[obsolete.localonlymodeopt]:
+                    from . import repair # avoid import cycle
+
+                    # We should not be creating unstable changesets in
+                    # local only mode. Verify that.
+                    oldrevs = set(torev(n) for n in replacements)
+                    tobundle = set(repo.revs('%ld::', oldrevs))
+
+                    if len(tobundle) > len(oldrevs):
+                        raise error.Abort(_('cannot create unstable changesets '
+                                            'in local-only obsolescence mode'))
+
+                    roots = set(ctx.node()
+                                for ctx in repo.set('roots(%ld)', oldrevs))
+                    heads = set(ctx.node()
+                                for ctx in repo.set('heads(%ld)', oldrevs))
+
+                    backupfile = repair.backupbundle(
+                        repo, roots, heads, repo[min(roots)].node(), operation)
+                    repo.ui.status(_('saved backup bundle to %s\n') %
+                                   repo.vfs.join(backupfile))
+                    repo.ui.log('backupbundle', 'saved backup bundle to %s\n',
+                                repo.vfs.join(backupfile))
+
                 obsolete.createmarkers(repo, rels, operation=operation,
                                        metadata=metadata)
+
         else:
             from . import repair # avoid import cycle
             tostrip = list(replacements)