narrow: allow merging non-conflicting change outside of the narrow spec
authorPierre-Yves David <pierre-yves.david@octobus.net>
Thu, 03 Feb 2022 18:14:11 +0100
changeset 48716 f1eb77dceb36
parent 48715 7d073df49a54
child 48717 ae27a0684e75
narrow: allow merging non-conflicting change outside of the narrow spec We use the mergestate to carry information about these merge action and reprocess them at commit time to apply the necessary update. The dirstate itself is never affected and remains "pure", with content only in the narrow-spec. This file involved in such merge are therefor not listed in `hg status`. The current testing is based on a modification of the previous testing, that refused to do such merges. As a result it is a bit simple and more extensive code and testing testing will have to be introduced later. I am planning to do this extra testing, soon. In addition, this only works for flat manifest. Support for tree manifest will need more work. I am not currently planning to do this work. Differential Revision: https://phab.mercurial-scm.org/D12119
mercurial/commit.py
mercurial/merge.py
tests/test-narrow-merge.t
--- a/mercurial/commit.py	Sun Jan 30 06:01:42 2022 +0100
+++ b/mercurial/commit.py	Thu Feb 03 18:14:11 2022 +0100
@@ -134,7 +134,13 @@
     for s in salvaged:
         files.mark_salvaged(s)
 
-    if ctx.manifestnode():
+    narrow_files = {}
+    if not ctx.repo().narrowmatch().always():
+        for f, e in ms.allextras().items():
+            action = e.get(b'outside-narrow-merge-action')
+            if action is not None:
+                narrow_files[f] = action
+    if ctx.manifestnode() and not narrow_files:
         # reuse an existing manifest revision
         repo.ui.debug(b'reusing known manifest\n')
         mn = ctx.manifestnode()
@@ -142,11 +148,11 @@
         if writechangesetcopy:
             files.update_added(ctx.filesadded())
             files.update_removed(ctx.filesremoved())
-    elif not ctx.files():
+    elif not ctx.files() and not narrow_files:
         repo.ui.debug(b'reusing manifest from p1 (no file change)\n')
         mn = p1.manifestnode()
     else:
-        mn = _process_files(tr, ctx, ms, files, error=error)
+        mn = _process_files(tr, ctx, ms, files, narrow_files, error=error)
 
     if origctx and origctx.manifestnode() == mn:
         origfiles = origctx.files()
@@ -177,7 +183,7 @@
     return salvaged
 
 
-def _process_files(tr, ctx, ms, files, error=False):
+def _process_files(tr, ctx, ms, files, narrow_files=None, error=False):
     repo = ctx.repo()
     p1 = ctx.p1()
     p2 = ctx.p2()
@@ -198,8 +204,24 @@
     linkrev = len(repo)
     repo.ui.note(_(b"committing files:\n"))
     uipathfn = scmutil.getuipathfn(repo)
-    for f in sorted(ctx.modified() + ctx.added()):
+    all_files = ctx.modified() + ctx.added()
+    all_files.extend(narrow_files.keys())
+    all_files.sort()
+    for f in all_files:
         repo.ui.note(uipathfn(f) + b"\n")
+        if f in narrow_files:
+            narrow_action = narrow_files.get(f)
+            if narrow_action == mergestate.CHANGE_MODIFIED:
+                files.mark_touched(f)
+                added.append(f)
+                m[f] = m2[f]
+                flags = m2ctx.find(f)[1] or b''
+                m.setflag(f, flags)
+            else:
+                msg = _(b"corrupted mergestate, unknown narrow action: %b")
+                hint = _(b"restart the merge")
+                raise error.Abort(msg, hint=hint)
+            continue
         try:
             fctx = ctx[f]
             if fctx is None:
@@ -239,7 +261,17 @@
             if not rf(f):
                 files.mark_removed(f)
 
-    mn = _commit_manifest(tr, linkrev, ctx, mctx, m, files.touched, added, drop)
+    mn = _commit_manifest(
+        tr,
+        linkrev,
+        ctx,
+        mctx,
+        m,
+        files.touched,
+        added,
+        drop,
+        bool(narrow_files),
+    )
 
     return mn
 
@@ -409,7 +441,17 @@
     return fnode, touched
 
 
-def _commit_manifest(tr, linkrev, ctx, mctx, manifest, files, added, drop):
+def _commit_manifest(
+    tr,
+    linkrev,
+    ctx,
+    mctx,
+    manifest,
+    files,
+    added,
+    drop,
+    has_some_narrow_action=False,
+):
     """make a new manifest entry (or reuse a new one)
 
     given an initialised manifest context and precomputed list of
@@ -451,6 +493,10 @@
         # at this point is merges, and we already error out in the
         # case where the merge has files outside of the narrowspec,
         # so this is safe.
+        if has_some_narrow_action:
+            match = None
+        else:
+            match = repo.narrowmatch()
         mn = mctx.write(
             tr,
             linkrev,
@@ -458,7 +504,7 @@
             p2.manifestnode(),
             added,
             drop,
-            match=repo.narrowmatch(),
+            match=match,
         )
     else:
         repo.ui.debug(
--- a/mercurial/merge.py	Sun Jan 30 06:01:42 2022 +0100
+++ b/mercurial/merge.py	Thu Feb 03 18:14:11 2022 +0100
@@ -518,13 +518,23 @@
             mresult.removefile(f)  # just updating, ignore changes outside clone
         elif action[0].no_op:
             mresult.removefile(f)  # merge does not affect file
-        elif action[0].narrow_safe:  # TODO: handle these cases
-            msg = _(
-                b'merge affects file \'%s\' outside narrow, '
-                b'which is not yet supported'
-            )
-            hint = _(b'merging in the other direction may work')
-            raise error.Abort(msg % f, hint=hint)
+        elif action[0].narrow_safe:
+            if (
+                not f.endswith(b'/')
+                and action[0].changes == mergestatemod.CHANGE_MODIFIED
+            ):
+                mresult.removefile(f)  # merge won't affect on-disk files
+
+                mresult.addcommitinfo(
+                    f, b'outside-narrow-merge-action', action[0].changes
+                )
+            else:  # TODO: handle the tree case
+                msg = _(
+                    b'merge affects file \'%s\' outside narrow, '
+                    b'which is not yet supported'
+                )
+                hint = _(b'merging in the other direction may work')
+                raise error.Abort(msg % f, hint=hint)
         else:
             msg = _(b'conflict in file \'%s\' is outside narrow clone')
             raise error.StateError(msg % f)
--- a/tests/test-narrow-merge.t	Sun Jan 30 06:01:42 2022 +0100
+++ b/tests/test-narrow-merge.t	Thu Feb 03 18:14:11 2022 +0100
@@ -83,12 +83,67 @@
 TODO: Can merge non-conflicting changes outside narrow spec
 
   $ hg update -q 'desc("modify inside/f1")'
+
+#if flat
+
   $ hg merge 'desc("modify outside/f1")'
-  abort: merge affects file 'outside/f1' outside narrow, which is not yet supported (flat !)
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+
+status should be clean
+
+  $ hg status
+  ? inside/f1.orig
+
+file out of the spec should still not be in the dirstate at all
+
+  $ hg debugdirstate | grep outside/f1
+  [1]
+
+Commit that merge
+
+  $ hg ci -m 'merge from outside to inside'
+
+status should be clean
+
+  $ hg status
+  ? inside/f1.orig
+
+file out of the spec should not be in the mergestate anymore
+
+  $ hg debugmergestate | grep outside/f1
+  [1]
+
+file out of the spec should still not be in the dirstate at all
+
+  $ hg debugdirstate | grep outside/f1
+  [1]
+
+The filenode used should come from p2
+
+  $ hg manifest --debug --rev . | grep outside/f1
+  83cd11431a3b2aff8a3995e5f27bcf33cdb5be98 644   outside/f1
+  $ hg manifest --debug --rev 'p1(.)' | grep outside/f1
+  c6b956c48be2cd4fa94be16002aba311143806fa 644   outside/f1
+  $ hg manifest --debug --rev 'p2(.)' | grep outside/f1
+  83cd11431a3b2aff8a3995e5f27bcf33cdb5be98 644   outside/f1
+
+
+remove the commit to get in the previous situation again
+
+  $ hg debugstrip -r .
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  saved backup bundle to $TESTTMP/narrow/.hg/strip-backup/48eb25338b19-a1bb8350-backup.hg
+
+#else
+
+  $ hg merge 'desc("modify outside/f1")'
   abort: merge affects file 'outside/' outside narrow, which is not yet supported (tree !)
   (merging in the other direction may work)
   [255]
 
+#endif
+
   $ hg update -q 'desc("modify outside/f1")'
   $ hg merge 'desc("modify inside/f1")'
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved