Commits

Brodie Rao committed 43714bf

Initial import

Comments (0)

Files changed (1)

+"""Utilities for carrying out merges in memory"""
+
+import os
+from contextlib import contextmanager
+
+from mercurial import merge, node, node as hgnode, util
+from mercurial.context import memctx, memfilectx
+from mercurial.simplemerge import Merge3Text
+
+class memworkingctx(memctx):
+    """Like memctx, but tries to blend in as a workingctx"""
+    def node(self):
+        return None
+
+@contextmanager
+def setworkingctx(repo, ctx):
+    """Context manager that sets repo's working context to ctx"""
+    class mergerepo(repo.__class__):
+        def __getitem__(self, changeid):
+            # repo[None] should return our custom working context
+            if changeid is None:
+                return ctx
+            return super(mergerepo, self).__getitem__(changeid)
+    oldcls = repo.__class__
+    if ctx is not None:
+        repo.__class__ = mergerepo
+    try:
+        yield
+    finally:
+        repo.__class__ = oldcls
+
+def memfilemerge(fcd, fco, fca, **mergeopts):
+    """Perform a 3-way merge in memory
+
+    For now, we ignore binaries (which simplemerge doesn't support).
+    """
+    def isbin(ctx):
+        try:
+            return util.binary(ctx.data())
+        except IOError:
+            return False
+
+    if not fco.cmp(fcd): # files identical?
+        return fco.data(), False
+
+    binary = isbin(fcd) or isbin(fco) or isbin(fca)
+    if not binary:
+        m3 = Merge3Text(fca.data(), fcd.data(), fco.data())
+        out = list(m3.merge_lines(**mergeopts))
+        return out, m3.conflicts
+    else:
+        return [], None
+
+def memmerge(repo, node1, node2, text='', user=None, date=None, extra=None,
+             diff3=False, local_marker='yours', remote_marker='theirs',
+             limit=None):
+    """Merge node2 into node1 in repo.
+
+    The merge is done in-memory, and a 4-tuple of (memctx, status,
+    merged, conflict) is returned. status is a list containing 3 lists
+    listing modified, added, and removed files. merged is a set
+    containing filenames that required merging. conflict is a set
+    containing filenames that had merge conflicts.
+
+    To commit the merge, call ctx.commit(). Note that if repo is a
+    bundlerepo, committing isn't possible.
+
+    To get a diff of the merge:
+
+    >>> ctx, status, conflicts = memmerge(repo, node1, node2)
+    >>> with setworkingctx(repo, ctx):
+    >>>     patch.diff(repo, p1.node(), ctx.node(), changes=status)
+
+    This works with both regular repos and bundlerepos.
+    """
+    p1 = repo[node1]
+    p2 = repo[node2]
+    pa = p1.ancestor(p2)
+    # If the common ancestor is the same as p1, that means history is
+    # linear; if pa is the same as p2, that means we're trying to do
+    # backwards merge, which we don't support (only hg update does
+    # that).
+    if pa == p2:
+        # Nothing to merge
+        return None, None, set(), set()
+
+    if isinstance(local_marker, unicode):
+        local_marker = local_marker.encode('utf-8')
+    if isinstance(remote_marker, unicode):
+        remote_marker = remote_marker.encode('utf-8')
+
+    def mergeopts(f, f2):
+        opts = {'name_a': '%s:%s' % (local_marker, f),
+                'name_b': '%s:%s' % (remote_marker, f2)}
+        if diff3:
+            opts['base_marker'] ='|||||||'
+        return opts
+
+    for p in (p1, p2, pa):
+        # Ignore subrepo data
+        p.substate = {}
+
+    modified, added, removed = [], [], []
+    merged, conflicted = set(), set()
+    actions = merge.manifestmerge(repo, p1, p2, pa, False, False)
+
+    if limit is not None and len(actions) > limit:
+        raise OverflowError('too many files to merge')
+
+    actions.sort(key=merge.actionkey)
+
+    output = {}
+    def getfilectx(repo_, mctx, f):
+        if f not in output:
+            raise IOError
+        fctx, data, mode, copied = output[f]
+        if data is None:
+            data = fctx.data()
+        fctx = memfilectx(f, data, 'l' in mode, 'x' in mode, copied)
+        # FIXME: Hack to appease patch.diff(), which doesn't work
+        # with memfilectxes.
+        fctx.filelog = lambda: None
+        return fctx
+
+    # manifestmerge() generates a list of actions that we have to
+    # carry out to do the merge. The actions are as follows:
+    #
+    # - 'r': A file (f) was removed in the other head.
+    #
+    # - 'm': A 3-way merge needs to happen. f is the source file, f2
+    #        is the file in the other head, fd is the destination
+    #        file. If f and f2 differ, there was a copy/rename. If the
+    #        move flag is True, it was a rename. flags contains
+    #        link/exec bits (same as filectx.flags()).
+    #
+    # - 'd': A file was renamed as the result of a directory being
+    #        renamed. If f is non-empty, it's a rename of f to fd; if
+    #        f2 is non-empty, it's a copy of f2 to fd.
+    #
+    # - 'g': A file (f) was added or modified in the other head, but
+    #        there were no changes to it in our line of history. If f
+    #        is in our head's manifest, it was a modification; if it's
+    #        only in the other head's manifest, it was an addition.
+    #
+    # - 'e': Just the exec bit of a file (f) was changed in the other
+    #        head.
+    for a in actions:
+        f, m = a[:2]
+        if f and f[0] == '/':
+            continue
+        if m == 'r': # remove file
+            removed.append(f)
+        elif m == 'm': # merge file changes between heads
+            f2, fd, flags, move = a[2:]
+            if f != f2:
+                if move:
+                    # file rename
+                    removed.append(f)
+                # file copy
+                if f != fd:
+                    copied = f
+                    fcd = p1[f] # local destination file (doesn't exist)
+                else:
+                    copied = f2
+                    fcd = p2[f2] # local destination file (doesn't exist)
+                added.append(fd)
+            else:
+                copied = None
+                modified.append(f)
+                fcd = p1[fd] # local destination file (exists)
+
+            fcl = p1[f]  # local source file
+            fco = p2[f2] # other destination file
+            fca = fcl.ancestor(p2[f2], pa) # common ancestor
+            if not fca:
+                # no common ancestor
+                fca = repo.filectx(f, fileid=node.nullrev)
+            afile = fca.path() # ancestor file path
+            anode = node.hex(fca.filenode()) # ancestor file node
+            fca = repo.filectx(afile, fileid=anode) # base file
+
+            out, conflicts = memfilemerge(fcd, fco, fca, **mergeopts(f, f2))
+            merged.add(fd)
+            if conflicts:
+                conflicted.add(fd)
+            # merge result
+            output[fd] = (None, ''.join(out), flags, copied)
+        elif m == 'd': # rename file as part of a directory rename
+            f2, fd, flags = a[2:]
+            if f:
+                # file rename
+                removed.append(f)
+                added.append(fd)
+                fctx = p1[f]
+                output[fd] = (fctx, None, flags, f)
+            if f2:
+                # file copy
+                added.append(fd)
+                fctx = p2[f2]
+                output[fd] = (fctx, None, flags, f)
+        elif m == 'g': # update file (added/modified in other head)
+            flags = a[2]
+            # was it added or modified?
+            if f in p1.manifest():
+                modified.append(f)
+            else:
+                added.append(f)
+            fctx = p2.filectx(f)
+            output[f] = (fctx, None, flags, None)
+        elif m == 'e': # update file execute bit
+            flags = a[2]
+            modified.append(f)
+            fctx = p2.filectx(f)
+            output[f] = (fctx, None, flags, None)
+
+    status = [modified, added, removed]
+
+    # create a memory context containing the potential merge
+    ctx = memworkingctx(repo, (p1.node(), p2.node()), text,
+                        [fn for st in status for fn in st],
+                        getfilectx, user, date, extra)
+    return ctx, status, merged, conflicted
+
+def memupdate(repo, node, branchmerge, force, partial):
+    """Experimental equivalent of hg's merge.update().
+
+    For cases that we can't handle, we shell out to
+    merge.update(). This requires a repository with a working copy to
+    work properly.
+
+    For now, this is primarily meant to be plugged into hg so we can
+    run its test suite with our merge algorithm to verify the
+    algorithm's correctness.
+    """
+    onode = node
+    wlock = repo.wlock()
+    try:
+        wc = repo[None]
+        if node is None:
+            try:
+                node = repo.branchtags()[wc.branch()]
+            except KeyError:
+                if wc.branch() == 'default':
+                    node = repo.lookup('tip')
+                else:
+                    return merge.update(repo, onode, branchmerge, force,
+                                        partial)
+
+        pl = wc.parents()
+        p1, p2 = pl[0], repo[node]
+        pa = p1.ancestor(p2)
+        xp1, xp2 = str(p1), str(p2)
+        if len(pl) > 1 or pa in (p1, p2):
+            return merge.update(repo, onode, branchmerge, force, partial)
+
+        ctx, status, merged, conflicts = memmerge(repo, p1.node(), p2.node())
+        # Since we don't modify the working copy, we need to make note
+        # of where we merged so memstatus() and memcommit() can pick
+        # up where we left off.
+        with open(repo.join('memmerge'), 'wb') as f:
+            f.write(hgnode.hex(p1.node()))
+            f.write(' ')
+            f.write(hgnode.hex(p2.node()))
+        stats = (len(status[0]) + len(status[1]), len(merged),
+                 len(status[2]), len(conflicts))
+    finally:
+        wlock.release()
+
+    if not partial:
+        repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
+    return stats
+
+def memstatus(repo, node1, node2, match, a1, a2, a3, a4):
+    """Experimental version of hg's repo.status() that uses memmerge()
+    data.
+
+    For now, this is primarily meant for testing our merge algorithm.
+    """
+    if not os.path.exists(repo.join('memmerge')):
+        return repo.status(node1, node2, match, a1, a2, a3, a4)
+    with open(repo.join('memmerge'), 'rb') as f:
+        p1, p2 = f.read().strip().split()
+    ctx, status, merged, conflicts = memmerge(repo, p1, p2)
+    return status + [[], [], [], []]
+
+def memcommit(repo, message, user, date, match, editor, extra):
+    """Experimental version of hg's repo.commit() that uses
+    memmerge() data.
+
+    For now, this is primarily meant for testing our merge algorithm.
+    """
+    if not os.path.exists(repo.join('memmerge')):
+        return repo.commit(message, user, date, match, editor=editor,
+                           extra=extra)
+    with open(repo.join('memmerge'), 'rb') as f:
+        p1, p2 = f.read().strip().split()
+    ctx, status, merged, conflicts = memmerge(repo, p1, p2, message, user,
+                                              date, extra)
+    branch = repo[None].branch()
+    repo.dirstate.branch = lambda: branch
+    n = ctx.commit()
+    os.unlink(repo.join('memmerge'))
+    return n