changeset 20595:710c2755e66a

merge with stable
author Matt Mackall <mpm@selenic.com>
date Thu, 27 Feb 2014 18:57:03 -0600
parents cb18fe3461b1 (current diff) ba619c50a355 (diff)
children 004a1744088d
files mercurial/commands.py
diffstat 3 files changed, 230 insertions(+), 19 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/commands.py	Thu Feb 13 09:18:16 2014 -0800
+++ b/mercurial/commands.py	Thu Feb 27 18:57:03 2014 -0600
@@ -4954,7 +4954,6 @@
                 ms.mark(f, "u")
             else:
                 wctx = repo[None]
-                mctx = wctx.parents()[-1]
 
                 # backup pre-resolve (merge uses .orig for its own purposes)
                 a = repo.wjoin(f)
@@ -4963,7 +4962,7 @@
                 try:
                     # resolve file
                     ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
-                    if ms.resolve(f, wctx, mctx):
+                    if ms.resolve(f, wctx):
                         ret = 1
                 finally:
                     ui.setconfig('ui', 'forcemerge', '')
--- a/mercurial/merge.py	Thu Feb 13 09:18:16 2014 -0800
+++ b/mercurial/merge.py	Thu Feb 27 18:57:03 2014 -0600
@@ -5,52 +5,172 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
+import struct
+
 from node import nullid, nullrev, hex, bin
 from i18n import _
 from mercurial import obsolete
 import error, util, filemerge, copies, subrepo, worker, dicthelpers
 import errno, os, shutil
 
+_pack = struct.pack
+_unpack = struct.unpack
+
+def _droponode(data):
+    # used for compatibility for v1
+    bits = data.split("\0")
+    bits = bits[:-2] + bits[-1:]
+    return "\0".join(bits)
+
 class mergestate(object):
-    '''track 3-way merge state of individual files'''
+    '''track 3-way merge state of individual files
+
+    it is stored on disk when needed. Two file are used, one with an old
+    format, one with a new format. Both contains similar data, but the new
+    format can store new kind of field.
+
+    Current new format is a list of arbitrary record of the form:
+
+        [type][length][content]
+
+    Type is a single character, length is a 4 bytes integer, content is an
+    arbitrary suites of bytes of lenght `length`.
+
+    Type should be a letter. Capital letter are mandatory record, Mercurial
+    should abort if they are unknown. lower case record can be safely ignored.
+
+    Currently known record:
+
+    L: the node of the "local" part of the merge (hexified version)
+    O: the node of the "other" part of the merge (hexified version)
+    F: a file to be merged entry
+    '''
+    statepathv1 = "merge/state"
+    statepathv2 = "merge/state2"
     def __init__(self, repo):
         self._repo = repo
         self._dirty = False
         self._read()
-    def reset(self, node=None):
+    def reset(self, node=None, other=None):
         self._state = {}
         if node:
             self._local = node
+            self._other = other
         shutil.rmtree(self._repo.join("merge"), True)
         self._dirty = False
     def _read(self):
         self._state = {}
+        records = self._readrecords()
+        for rtype, record in records:
+            if rtype == 'L':
+                self._local = bin(record)
+            elif rtype == 'O':
+                self._other = bin(record)
+            elif rtype == "F":
+                bits = record.split("\0")
+                self._state[bits[0]] = bits[1:]
+            elif not rtype.islower():
+                raise util.Abort(_('unsupported merge state record:'
+                                   % rtype))
+        self._dirty = False
+    def _readrecords(self):
+        v1records = self._readrecordsv1()
+        v2records = self._readrecordsv2()
+        oldv2 = set() # old format version of v2 record
+        for rec in v2records:
+            if rec[0] == 'L':
+                oldv2.add(rec)
+            elif rec[0] == 'F':
+                # drop the onode data (not contained in v1)
+                oldv2.add(('F', _droponode(rec[1])))
+        for rec in v1records:
+            if rec not in oldv2:
+                # v1 file is newer than v2 file, use it
+                # we have to infer the "other" changeset of the merge
+                # we cannot do better than that with v1 of the format
+                mctx = self._repo[None].parents()[-1]
+                v1records.append(('O', mctx.hex()))
+                # 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.
+                for idx, r in enumerate(v1records):
+                    if r[0] == 'F':
+                        bits = r[1].split("\0")
+                        bits.insert(-2, '')
+                        v1records[idx] = (r[0], "\0".join(bits))
+                return v1records
+        else:
+            return v2records
+    def _readrecordsv1(self):
+        records = []
         try:
-            f = self._repo.opener("merge/state")
+            f = self._repo.opener(self.statepathv1)
             for i, l in enumerate(f):
                 if i == 0:
-                    self._local = bin(l[:-1])
+                    records.append(('L', l[:-1]))
                 else:
-                    bits = l[:-1].split("\0")
-                    self._state[bits[0]] = bits[1:]
+                    records.append(('F', l[:-1]))
             f.close()
         except IOError, err:
             if err.errno != errno.ENOENT:
                 raise
-        self._dirty = False
+        return records
+    def _readrecordsv2(self):
+        records = []
+        try:
+            f = self._repo.opener(self.statepathv2)
+            data = f.read()
+            off = 0
+            end = len(data)
+            while off < end:
+                rtype = data[off]
+                off += 1
+                lenght = _unpack('>I', data[off:(off + 4)])[0]
+                off += 4
+                record = data[off:(off + lenght)]
+                off += lenght
+                records.append((rtype, record))
+            f.close()
+        except IOError, err:
+            if err.errno != errno.ENOENT:
+                raise
+        return records
     def commit(self):
         if self._dirty:
-            f = self._repo.opener("merge/state", "w")
-            f.write(hex(self._local) + "\n")
+            records = []
+            records.append(("L", hex(self._local)))
+            records.append(("O", hex(self._other)))
             for d, v in self._state.iteritems():
-                f.write("\0".join([d] + v) + "\n")
-            f.close()
+                records.append(("F", "\0".join([d] + v)))
+            self._writerecords(records)
             self._dirty = False
+    def _writerecords(self, records):
+        self._writerecordsv1(records)
+        self._writerecordsv2(records)
+    def _writerecordsv1(self, records):
+        f = self._repo.opener(self.statepathv1, "w")
+        irecords = iter(records)
+        lrecords = irecords.next()
+        assert lrecords[0] == 'L'
+        f.write(hex(self._local) + "\n")
+        for rtype, data in irecords:
+            if rtype == "F":
+                f.write("%s\n" % _droponode(data))
+        f.close()
+    def _writerecordsv2(self, records):
+        f = self._repo.opener(self.statepathv2, "w")
+        for key, data in records:
+            assert len(key) == 1
+            format = ">sI%is" % len(data)
+            f.write(_pack(format, key, len(data), data))
+        f.close()
     def add(self, fcl, fco, fca, fd):
         hash = util.sha1(fcl.path()).hexdigest()
         self._repo.opener.write("merge/" + hash, fcl.data())
-        self._state[fd] = ['u', hash, fcl.path(), fca.path(),
-                           hex(fca.filenode()), fco.path(), fcl.flags()]
+        self._state[fd] = ['u', hash, fcl.path(),
+                           fca.path(), hex(fca.filenode()),
+                           fco.path(), hex(fco.filenode()),
+                           fcl.flags()]
         self._dirty = True
     def __contains__(self, dfile):
         return dfile in self._state
@@ -66,10 +186,12 @@
     def mark(self, dfile, state):
         self._state[dfile][0] = state
         self._dirty = True
-    def resolve(self, dfile, wctx, octx):
+    def resolve(self, dfile, wctx):
         if self[dfile] == 'r':
             return 0
-        state, hash, lfile, afile, anode, ofile, flags = self._state[dfile]
+        stateentry = self._state[dfile]
+        state, hash, lfile, afile, anode, ofile, onode, flags = stateentry
+        octx = self._repo[self._other]
         fcd = wctx[dfile]
         fco = octx[ofile]
         fca = self._repo.filectx(afile, fileid=anode)
@@ -440,7 +562,7 @@
 
     updated, merged, removed, unresolved = 0, 0, 0, 0
     ms = mergestate(repo)
-    ms.reset(wctx.p1().node())
+    ms.reset(wctx.p1().node(), mctx.node())
     moves = []
     actions.sort(key=actionkey)
 
@@ -520,7 +642,7 @@
                                  overwrite)
                 continue
             audit(fd)
-            r = ms.resolve(fd, wctx, mctx)
+            r = ms.resolve(fd, wctx)
             if r is not None and r > 0:
                 unresolved += 1
             else:
--- a/tests/test-backout.t	Thu Feb 13 09:18:16 2014 -0800
+++ b/tests/test-backout.t	Thu Feb 27 18:57:03 2014 -0600
@@ -408,3 +408,93 @@
   update: (current)
 
   $ cd ..
+
+
+Test usage of `hg resolve` in case of conflict
+(issue4163)
+
+  $ hg init issue4163
+  $ cd issue4163
+  $ touch foo
+  $ hg add foo
+  $ cat > foo << EOF
+  > one
+  > two
+  > three
+  > four
+  > five
+  > six
+  > seven
+  > height
+  > nine
+  > ten
+  > EOF
+  $ hg ci -m 'initial'
+  $ cat > foo << EOF
+  > one
+  > two
+  > THREE
+  > four
+  > five
+  > six
+  > seven
+  > height
+  > nine
+  > ten
+  > EOF
+  $ hg ci -m 'capital three'
+  $ cat > foo << EOF
+  > one
+  > two
+  > THREE
+  > four
+  > five
+  > six
+  > seven
+  > height
+  > nine
+  > TEN
+  > EOF
+  $ hg ci -m 'capital ten'
+  $ hg backout -r 'desc("capital three")' --tool internal:fail
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges
+  [1]
+  $ hg status
+  $ hg resolve -l  # still unresolved
+  U foo
+  $ hg summary
+  parent: 2:b71750c4b0fd tip
+   capital ten
+  branch: default
+  commit: 1 unresolved (clean)
+  update: (current)
+  $ hg resolve --all --debug
+  picked tool 'internal:merge' for foo (binary False symlink False)
+  merging foo
+  my foo@b71750c4b0fd+ other foo@a30dd8addae3 ancestor foo@913609522437
+   premerge successful
+  $ hg status
+  M foo
+  ? foo.orig
+  $ hg resolve -l
+  R foo
+  $ hg summary
+  parent: 2:b71750c4b0fd tip
+   capital ten
+  branch: default
+  commit: 1 modified, 1 unknown
+  update: (current)
+  $ cat foo
+  one
+  two
+  three
+  four
+  five
+  six
+  seven
+  height
+  nine
+  TEN
+
+