[PATCH 2 of 2 V2] revset: new predicates to find key merge revisions

Simon Farnsworth simonfar at fb.com
Mon Mar 7 13:27:28 EST 2016


# HG changeset patch
# User Simon Farnsworth <simonfar at fb.com>
# Date 1457374657 0
#      Mon Mar 07 18:17:37 2016 +0000
# Node ID faf1fd7f7079801368317a681f55dbc46e878de0
# Parent  d92485c84727d67bcbf462b26dcfc1b7c746d07d
revset: new predicates to find key merge revisions

When you have tricky merge conflicts, it's useful to look at the history of
the conflict. In a fast moving repository, you're drowned in revisions that
are in the merge but that can't impact on a conflict.

Provide a new revset predicate to help out; conflict(type, pat) allows you
to find the "base", "other" or "local" revision types for the merge
conflicts identified by pat. This, in turn, enables you to write revsets to
find all revisions that are of interest in a conflict situation.

diff --git a/mercurial/mergestate.py b/mercurial/mergestate.py
--- a/mercurial/mergestate.py
+++ b/mercurial/mergestate.py
@@ -73,6 +73,7 @@
         Do not use this directly! Instead call read() or clean()."""
         self._repo = repo
         self._dirty = False
+        self._otherinferred = False
 
     statepathv1 = 'merge/state'
     statepathv2 = 'merge/state2'
@@ -153,6 +154,7 @@
             # we cannot do better than that with v1 of the format
             mctx = self._repo[None].parents()[-1]
             v1records.append(('O', mctx.hex()))
+            self._otherinferred = True
             # add place holder "other" file node information
             # nobody is using it yet so we do no need to fetch the data
             # if mctx was wrong `mctx[bits[-2]]` may fails.
@@ -304,6 +306,26 @@
             if entry[0] == 'u':
                 yield f
 
+    def ancestorchangectx(self, dfile):
+        extras = self.extras(dfile)
+        anccommitnode = extras.get('ancestorlinknode')
+        if anccommitnode:
+            return self._repo[anccommitnode]
+        else:
+            return None
+
+    def otherfilectx(self, dfile):
+        if self._otherinferred:
+            return None
+        entry = self._state[dfile]
+        return self.otherctx.filectx(entry[5], fileid=entry[6])
+
+    def localfilectx(self, dfile):
+        if self._local is None:
+            return None
+        entry = self._state[dfile]
+        return self.localctx.filectx(entry[2])
+
     def driverresolved(self):
         """Obtain the paths of driver-resolved files."""
 
diff --git a/mercurial/revset.py b/mercurial/revset.py
--- a/mercurial/revset.py
+++ b/mercurial/revset.py
@@ -17,6 +17,7 @@
     error,
     hbisect,
     match as matchmod,
+    mergestate as mergestatemod,
     node,
     obsolete as obsmod,
     parser,
@@ -816,6 +817,68 @@
     getargs(x, 0, 0, _("closed takes no arguments"))
     return subset.filter(lambda r: repo[r].closesbranch())
 
+ at predicate('conflict(type,[pattern])')
+def conflict(repo, subset, x):
+    """The type revision for any merge conflict matching pattern.
+    See :hg:`help patterns` for information about file patterns.
+
+    type should be one of "base", "other" or "local", for the three-way
+    merge base, the "other" tree, or the "local" tree.
+
+    The pattern without explicit kind like ``glob:`` is expected to be
+    relative to the current directory and match against a file exactly
+    for efficiency. If pattern is omitted, all conflicts are included.
+    """
+    # i18n: "conflict" is a keyword
+    l = getargs(x, 1, 2, _('conflict takes one or two arguments'))
+    t = getstring(l[0], _("conflict requires a type"))
+    if t not in ["base", "other", "local"]:
+        msg = _('conflict file types are "base", "other" or "local"')
+        raise error.Abort(msg)
+    if len(l) == 2:
+        pat = getstring(l[1], _("conflict takes a string pattern"))
+    else:
+        pat = None
+
+    ms = mergestatemod.mergestatereadonly.read(repo)
+
+    if pat is None:
+        files = ms.files()
+    elif not matchmod.patkind(pat):
+        f = pathutil.canonpath(repo.root, repo.getcwd(), pat)
+        if f in ms:
+            files = [f]
+        else:
+            files = []
+    else:
+        m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=repo[None])
+        files = (f for f in ms if m(f))
+
+    s = set()
+    warning = _("merge {t} commit of file {f} not known - ignoring\n")
+    for f in files:
+        warn = False
+        if t == "base":
+            base = ms.ancestorchangectx(f)
+            if base is None:
+                repo.ui.warn(warning.format(t=t, f=f))
+            else:
+                s.add(base.rev())
+        elif t == "local":
+            local = ms.localfilectx(f)
+            if local is None:
+                repo.ui.warn(warning.format(t=t, f=f))
+            else:
+                s.add(local.introrev())
+        elif t == "other":
+            other = ms.otherfilectx(f)
+            if other is None:
+                repo.ui.warn(warning.format(t=t, f=f))
+            else:
+                s.add(other.introrev())
+
+    return subset & s
+
 @predicate('contains(pattern)')
 def contains(repo, subset, x):
     """The revision's manifest contains a file matching pattern (but might not
diff --git a/tests/test-revset.t b/tests/test-revset.t
--- a/tests/test-revset.t
+++ b/tests/test-revset.t
@@ -2230,3 +2230,105 @@
   2
 
   $ cd ..
+
+Test merge conflict predicates
+
+  $ hg init conflictrepo
+  $ cd conflictrepo
+  $ echo file1 > file1
+  $ echo file2 > file2
+  $ hg commit -qAm first
+  $ echo line2 >> file1
+  $ hg commit -qAm second
+  $ hg bookmark base
+  $ hg bookmark tree1
+  $ echo line1 > file1
+  $ hg commit -qAm tree1-file1
+  $ echo tree1-file2 > file2
+  $ hg commit -qAm tree1-file2
+  $ echo file3 > file3
+  $ hg commit -qAm tree1-file3
+  $ hg update base
+  2 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  (activating bookmark base)
+  $ hg bookmark tree2
+  $ echo lineA > file1
+  $ echo line2 >> file1
+  $ hg commit -qAm tree2
+  $ hg bookmark tree2
+  $ echo tree2-file2 > file2
+  $ hg commit -qAm tree2-file2
+  $ echo file4 > file4
+  $ hg commit -qAm tree2-file4
+
+There are no markers before a merge conflict exists
+
+  $ hg debugrevspec 'conflict("base","glob:*")'
+  $ hg debugrevspec 'conflict("local","glob:*")'
+  $ hg debugrevspec 'conflict("other","glob:*")'
+
+Merge and test that the expected set of markers exist and work with patterns
+
+  $ hg merge --rev tree1 --tool :fail
+  1 files updated, 0 files merged, 0 files removed, 2 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+  [1]
+  $ hg resolve --list
+  U file1
+  U file2
+  $ hg debugrevspec 'conflict("base","glob:*")'
+  1
+  $ hg debugrevspec 'conflict("local","glob:*")'
+  5
+  6
+  $ hg debugrevspec 'conflict("other","glob:*")'
+  2
+  3
+  $ hg debugrevspec 'conflict("base", "file1")'
+  1
+  $ hg debugrevspec 'conflict("local", "file1")'
+  5
+  $ hg debugrevspec 'conflict("other", "file1")'
+  2
+  $ hg debugrevspec 'conflict("base", "file2")'
+  1
+  $ hg debugrevspec 'conflict("local", "file2")'
+  6
+  $ hg debugrevspec 'conflict("other", "file2")'
+  3
+
+There are no markers on files not in conflict
+  $ hg debugrevspec 'conflict("base", "file4")'
+  $ hg debugrevspec 'conflict("local", "file4")'
+  $ hg debugrevspec 'conflict("other","file4")'
+
+It's possible to get interesting sets from markers
+
+  $ hg debugrevspec 'conflict("base", "glob:*"):conflict("local", "file1")'
+  1
+  2
+  3
+  4
+  5
+  $ hg debugrevspec 'conflict("base", "file2"):conflict("other", "re:file[1234]")'
+  1
+  2
+  3
+
+If we only have a V1 state file, "base" and "other" markers can't be found,
+but local can.
+
+  $ rm .hg/merge/state2
+  $ hg debugrevspec 'conflict("base", "file1")'
+  merge base commit of file file1 not known - ignoring
+  $ hg debugrevspec 'conflict("local", "file1")'
+  5
+  $ hg debugrevspec 'conflict("other", "file1")'
+  merge other commit of file file1 not known - ignoring
+
+We get a nice error if we ask for a non-existent marker
+
+  $ hg debugrevspec 'conflict("mercurial-versus-sccs", "glob:*")'
+  abort: conflict file types are "base", "other" or "local"
+  [255]
+  $ cd ..


More information about the Mercurial-devel mailing list