Patchwork [2,of,2] revset: new predicates to find key merge revisions

login
register
mail settings
Submitter Simon Farnsworth
Date March 1, 2016, 4:07 p.m.
Message ID <9d6c19c8413a039aff83.1456848468@simonfar-macbookpro.local>
Download mbox | patch
Permalink /patch/13506/
State Changes Requested
Headers show

Comments

Simon Farnsworth - March 1, 2016, 4:07 p.m.
# HG changeset patch
# User Simon Farnsworth <simonfar@fb.com>
# Date 1456846307 0
#      Tue Mar 01 15:31:47 2016 +0000
# Node ID 9d6c19c8413a039aff8399d6b1db573cb6610fab
# Parent  37fe1f9d08245f7540cb6137c312ae30dbcde688
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 revset predicates to help out; each of conflictbase(pat),
conflictlocal(pat) and conflictother(pat) find conflict base, local and
other revisions 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.
Simon Farnsworth - March 3, 2016, 5:59 p.m.
I'm going to send a V2 of this patch. Patch 1 in this series won't change, though.

Two things I intend to change:

 1. The "ancestor" commit is as far back as I need to go - there's no need to go back in history to the introducing revision, as that's not actually relevant to the merge conflict.

 2. I'm going to change from three operators to one (conflict("which", "pattern")), as I'm also looking at extending merge state to allow rebase to tell you which of "other" and "local" is "source" and "destination" in the command line parameters - I don't want to proliferate predicates.

Simon



On 01/03/2016, 16:07, "Mercurial-devel on behalf of Simon Farnsworth" <mercurial-devel-bounces@mercurial-scm.org on behalf of simonfar@fb.com> wrote:

># HG changeset patch

># User Simon Farnsworth <simonfar@fb.com>

># Date 1456846307 0

>#      Tue Mar 01 15:31:47 2016 +0000

># Node ID 9d6c19c8413a039aff8399d6b1db573cb6610fab

># Parent  37fe1f9d08245f7540cb6137c312ae30dbcde688

>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 revset predicates to help out; each of conflictbase(pat),

>conflictlocal(pat) and conflictother(pat) find conflict base, local and

>other revisions 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

>@@ -304,6 +304,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].rev()

>+        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,

>+    mergestate as mergestatemod,

>     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())

> 

>+@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 = mergestatemod.mergestatereadonly.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

>+

>+@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 = mergestatemod.mergestatereadonly.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

>+

>+@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 = mergestatemod.mergestatereadonly.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,91 @@

>   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 'conflictbase("glob:*")'

>+  $ hg debugrevspec 'conflictlocal("glob:*")'

>+  $ hg debugrevspec 'conflictother("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 '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

>+

>+There are no markers on files not in conflict

>+  $ hg debugrevspec 'conflictbase("file4")'

>+  $ hg debugrevspec 'conflictlocal("file4")'

>+  $ hg debugrevspec 'conflictother("file4")'

>+

>+It's possible to get interesting sets from markers

>+

>+  $ hg debugrevspec 'conflictbase("glob:*"):conflictlocal("file1")'

>+  0

>+  1

>+  2

>+  3

>+  4

>+  5

>+  $ hg debugrevspec 'conflictbase("file2"):conflictother("re:file[1234]")'

>+  0

>+  1

>+  2

>+  3

>+  $ cd ..

>_______________________________________________

>Mercurial-devel mailing list

>Mercurial-devel@mercurial-scm.org

>https://urldefense.proofpoint.com/v2/url?u=https-3A__www.mercurial-2Dscm.org_mailman_listinfo_mercurial-2Ddevel&d=CwIGaQ&c=5VD0RTtNlTh3ycd41b3MUw&r=mEgSWILcY4c4W3zjApBQLA&m=mvVmLG96hpa4zqNsmvZB04mUp8RSn7DYqtFfTxrWsFU&s=oE7qZHBK0YZAGg6pw2XWM3Ou7BACqagT4yj5E28xtew&e=

Patch

diff --git a/mercurial/mergestate.py b/mercurial/mergestate.py
--- a/mercurial/mergestate.py
+++ b/mercurial/mergestate.py
@@ -304,6 +304,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].rev()
+        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,
+    mergestate as mergestatemod,
     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())
 
+@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 = mergestatemod.mergestatereadonly.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
+
+@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 = mergestatemod.mergestatereadonly.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
+
+@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 = mergestatemod.mergestatereadonly.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,91 @@ 
   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 'conflictbase("glob:*")'
+  $ hg debugrevspec 'conflictlocal("glob:*")'
+  $ hg debugrevspec 'conflictother("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 '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
+
+There are no markers on files not in conflict
+  $ hg debugrevspec 'conflictbase("file4")'
+  $ hg debugrevspec 'conflictlocal("file4")'
+  $ hg debugrevspec 'conflictother("file4")'
+
+It's possible to get interesting sets from markers
+
+  $ hg debugrevspec 'conflictbase("glob:*"):conflictlocal("file1")'
+  0
+  1
+  2
+  3
+  4
+  5
+  $ hg debugrevspec 'conflictbase("file2"):conflictother("re:file[1234]")'
+  0
+  1
+  2
+  3
+  $ cd ..