[PATCH RFC] revset: add new predicates for finding merge revisions

Simon Farnsworth simonfar at fb.com
Mon Feb 29 18:14:27 UTC 2016


# HG changeset patch
# User Simon Farnsworth <simonfar at fb.com>
# Date 1456564536 0
#      Sat Feb 27 09:15:36 2016 +0000
# Node ID fbb5d06a6aed2d2e0d9af3a63d3c251dcb72dbcb
# Parent  41dcd754526612c43b9695df8851557c851828ef
revset: add new predicates for finding merge revisions

When you've merged a large stack of changesets in a fast moving repository,
the conflict markers can be apparent nonsense, as they're based on the heads
of the two trees that were merged.

Provide three new revset predicates (conflictbase, conflictother and
conflictlocal) to help you explore the relevant parts of history; these are
the introducing revisions of the "base", "other" and "local" commits for
each file that has conflicts (restricted by a pattern). The user can build
appropriate views of history from these three predicates for their
particular situation.

diff --git a/mercurial/merge.py b/mercurial/merge.py
--- a/mercurial/merge.py
+++ b/mercurial/merge.py
@@ -434,6 +434,25 @@
             if entry[0] == 'u':
                 yield f
 
+    def ancestorfilectx(self, dfile):
+        extras = self.extras(dfile)
+        anccommitnode = extras.get('ancestorlinknode')
+        if anccommitnode:
+            actx = self._repo[anccommitnode]
+        else:
+            actx = None
+
+        entry = self._state[dfile]
+        return self._repo.filectx(entry[3], fileid=entry[4], changeid=actx)
+
+    def otherfilectx(self, dfile):
+        entry = self._state[dfile]
+        return self.otherctx.filectx(entry[5], fileid=entry[6])
+
+    def localfilectx(self, dfile):
+        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,
+    merge as mergemod,
     node,
     obsolete as obsmod,
     parser,
@@ -816,6 +817,93 @@
     getargs(x, 0, 0, _("closed takes no arguments"))
     return subset.filter(lambda r: repo[r].closesbranch())
 
+ at predicate('conflictbase(pattern)')
+def conflictbase(repo, subset, x):
+    """The base revision for any merge conflict matching pattern.
+    See :hg:`help patterns` for information about file patterns.
+
+    The pattern without explicit kind like ``glob:`` is expected to be
+    relative to the current directory and match against a file exactly
+    for efficiency.
+    """
+    # i18n: "conflictbase" is a keyword
+    pat = getstring(x, _("conflictbase requires a pattern"))
+    ms = mergemod.mergestate.read(repo)
+
+    if 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()
+    for f in files:
+        s.add(ms.ancestorfilectx(f).introrev())
+
+    return subset & s
+
+ at predicate('conflictlocal(pattern)')
+def conflictlocal(repo, subset, x):
+    """The local revision for any merge conflict matching pattern.
+    See :hg:`help patterns` for information about file patterns.
+
+    The pattern without explicit kind like ``glob:`` is expected to be
+    relative to the current directory and match against a file exactly
+    for efficiency.
+    """
+    # i18n: "conflictlocal" is a keyword
+    pat = getstring(x, _("conflictlocal requires a pattern"))
+    ms = mergemod.mergestate.read(repo)
+
+    if 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()
+    for f in files:
+        s.add(ms.localfilectx(f).introrev())
+
+    return subset & s
+
+ at predicate('conflictother(pattern)')
+def conflictother(repo, subset, x):
+    """The other revision for any merge conflict matching pattern.
+    See :hg:`help patterns` for information about file patterns.
+
+    The pattern without explicit kind like ``glob:`` is expected to be
+    relative to the current directory and match against a file exactly
+    for efficiency.
+    """
+    # i18n: "conflictother" is a keyword
+    pat = getstring(x, _("conflictother requires a pattern"))
+    ms = mergemod.mergestate.read(repo)
+
+    if 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()
+    for f in files:
+        s.add(ms.otherfilectx(f).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,62 @@
   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
+  $ 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 'conflictbase("glob:*")'
+  0
+  1
+  $ hg debugrevspec 'conflictlocal("glob:*")'
+  5
+  6
+  $ hg debugrevspec 'conflictother("glob:*")'
+  2
+  3
+  $ hg debugrevspec 'conflictbase("file1")'
+  1
+  $ hg debugrevspec 'conflictlocal("file1")'
+  5
+  $ hg debugrevspec 'conflictother("file1")'
+  2
+  $ hg debugrevspec 'conflictbase("file2")'
+  0
+  $ hg debugrevspec 'conflictlocal("file2")'
+  6
+  $ hg debugrevspec 'conflictother("file2")'
+  3
+  $ cd ..


More information about the Mercurial-devel mailing list