diff mercurial/merge.py @ 2775:b550cd82f92a

Move merge code to its own module Pull update and merge3 out of localrepo into merge.py s/self/repo/ Add temporary API function in hg.py Convert all users
author Matt Mackall <mpm@selenic.com>
date Thu, 03 Aug 2006 15:24:41 -0500
parents
children 987c31e2a08c
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/merge.py	Thu Aug 03 15:24:41 2006 -0500
@@ -0,0 +1,348 @@
+# merge.py - directory-level update/merge handling for Mercurial
+#
+# Copyright 2006 Matt Mackall <mpm@selenic.com>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+
+from node import *
+from i18n import gettext as _
+from demandload import *
+demandload(globals(), "util os tempfile")
+
+def merge3(repo, fn, my, other, p1, p2):
+    """perform a 3-way merge in the working directory"""
+
+    def temp(prefix, node):
+        pre = "%s~%s." % (os.path.basename(fn), prefix)
+        (fd, name) = tempfile.mkstemp(prefix=pre)
+        f = os.fdopen(fd, "wb")
+        repo.wwrite(fn, fl.read(node), f)
+        f.close()
+        return name
+
+    fl = repo.file(fn)
+    base = fl.ancestor(my, other)
+    a = repo.wjoin(fn)
+    b = temp("base", base)
+    c = temp("other", other)
+
+    repo.ui.note(_("resolving %s\n") % fn)
+    repo.ui.debug(_("file %s: my %s other %s ancestor %s\n") %
+                          (fn, short(my), short(other), short(base)))
+
+    cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge")
+           or "hgmerge")
+    r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root,
+                    environ={'HG_FILE': fn,
+                             'HG_MY_NODE': p1,
+                             'HG_OTHER_NODE': p2,
+                             'HG_FILE_MY_NODE': hex(my),
+                             'HG_FILE_OTHER_NODE': hex(other),
+                             'HG_FILE_BASE_NODE': hex(base)})
+    if r:
+        repo.ui.warn(_("merging %s failed!\n") % fn)
+
+    os.unlink(b)
+    os.unlink(c)
+    return r
+
+def update(repo, node, allow=False, force=False, choose=None,
+           moddirstate=True, forcemerge=False, wlock=None, show_stats=True):
+    pl = repo.dirstate.parents()
+    if not force and pl[1] != nullid:
+        raise util.Abort(_("outstanding uncommitted merges"))
+
+    err = False
+
+    p1, p2 = pl[0], node
+    pa = repo.changelog.ancestor(p1, p2)
+    m1n = repo.changelog.read(p1)[0]
+    m2n = repo.changelog.read(p2)[0]
+    man = repo.manifest.ancestor(m1n, m2n)
+    m1 = repo.manifest.read(m1n)
+    mf1 = repo.manifest.readflags(m1n)
+    m2 = repo.manifest.read(m2n).copy()
+    mf2 = repo.manifest.readflags(m2n)
+    ma = repo.manifest.read(man)
+    mfa = repo.manifest.readflags(man)
+
+    modified, added, removed, deleted, unknown = repo.changes()
+
+    # is this a jump, or a merge?  i.e. is there a linear path
+    # from p1 to p2?
+    linear_path = (pa == p1 or pa == p2)
+
+    if allow and linear_path:
+        raise util.Abort(_("there is nothing to merge, just use "
+                           "'hg update' or look at 'hg heads'"))
+    if allow and not forcemerge:
+        if modified or added or removed:
+            raise util.Abort(_("outstanding uncommitted changes"))
+
+    if not forcemerge and not force:
+        for f in unknown:
+            if f in m2:
+                t1 = repo.wread(f)
+                t2 = repo.file(f).read(m2[f])
+                if cmp(t1, t2) != 0:
+                    raise util.Abort(_("'%s' already exists in the working"
+                                       " dir and differs from remote") % f)
+
+    # resolve the manifest to determine which files
+    # we care about merging
+    repo.ui.note(_("resolving manifests\n"))
+    repo.ui.debug(_(" force %s allow %s moddirstate %s linear %s\n") %
+                  (force, allow, moddirstate, linear_path))
+    repo.ui.debug(_(" ancestor %s local %s remote %s\n") %
+                  (short(man), short(m1n), short(m2n)))
+
+    merge = {}
+    get = {}
+    remove = []
+
+    # construct a working dir manifest
+    mw = m1.copy()
+    mfw = mf1.copy()
+    umap = dict.fromkeys(unknown)
+
+    for f in added + modified + unknown:
+        mw[f] = ""
+        mfw[f] = util.is_exec(repo.wjoin(f), mfw.get(f, False))
+
+    if moddirstate and not wlock:
+        wlock = repo.wlock()
+
+    for f in deleted + removed:
+        if f in mw:
+            del mw[f]
+
+        # If we're jumping between revisions (as opposed to merging),
+        # and if neither the working directory nor the target rev has
+        # the file, then we need to remove it from the dirstate, to
+        # prevent the dirstate from listing the file when it is no
+        # longer in the manifest.
+        if moddirstate and linear_path and f not in m2:
+            repo.dirstate.forget((f,))
+
+    # Compare manifests
+    for f, n in mw.iteritems():
+        if choose and not choose(f):
+            continue
+        if f in m2:
+            s = 0
+
+            # is the wfile new since m1, and match m2?
+            if f not in m1:
+                t1 = repo.wread(f)
+                t2 = repo.file(f).read(m2[f])
+                if cmp(t1, t2) == 0:
+                    n = m2[f]
+                del t1, t2
+
+            # are files different?
+            if n != m2[f]:
+                a = ma.get(f, nullid)
+                # are both different from the ancestor?
+                if n != a and m2[f] != a:
+                    repo.ui.debug(_(" %s versions differ, resolve\n") % f)
+                    # merge executable bits
+                    # "if we changed or they changed, change in merge"
+                    a, b, c = mfa.get(f, 0), mfw[f], mf2[f]
+                    mode = ((a^b) | (a^c)) ^ a
+                    merge[f] = (m1.get(f, nullid), m2[f], mode)
+                    s = 1
+                # are we clobbering?
+                # is remote's version newer?
+                # or are we going back in time?
+                elif force or m2[f] != a or (p2 == pa and mw[f] == m1[f]):
+                    repo.ui.debug(_(" remote %s is newer, get\n") % f)
+                    get[f] = m2[f]
+                    s = 1
+            elif f in umap or f in added:
+                # this unknown file is the same as the checkout
+                # we need to reset the dirstate if the file was added
+                get[f] = m2[f]
+
+            if not s and mfw[f] != mf2[f]:
+                if force:
+                    repo.ui.debug(_(" updating permissions for %s\n") % f)
+                    util.set_exec(repo.wjoin(f), mf2[f])
+                else:
+                    a, b, c = mfa.get(f, 0), mfw[f], mf2[f]
+                    mode = ((a^b) | (a^c)) ^ a
+                    if mode != b:
+                        repo.ui.debug(_(" updating permissions for %s\n")
+                                      % f)
+                        util.set_exec(repo.wjoin(f), mode)
+            del m2[f]
+        elif f in ma:
+            if n != ma[f]:
+                r = _("d")
+                if not force and (linear_path or allow):
+                    r = repo.ui.prompt(
+                        (_(" local changed %s which remote deleted\n") % f) +
+                         _("(k)eep or (d)elete?"), _("[kd]"), _("k"))
+                if r == _("d"):
+                    remove.append(f)
+            else:
+                repo.ui.debug(_("other deleted %s\n") % f)
+                remove.append(f) # other deleted it
+        else:
+            # file is created on branch or in working directory
+            if force and f not in umap:
+                repo.ui.debug(_("remote deleted %s, clobbering\n") % f)
+                remove.append(f)
+            elif n == m1.get(f, nullid): # same as parent
+                if p2 == pa: # going backwards?
+                    repo.ui.debug(_("remote deleted %s\n") % f)
+                    remove.append(f)
+                else:
+                    repo.ui.debug(_("local modified %s, keeping\n") % f)
+            else:
+                repo.ui.debug(_("working dir created %s, keeping\n") % f)
+
+    for f, n in m2.iteritems():
+        if choose and not choose(f):
+            continue
+        if f[0] == "/":
+            continue
+        if f in ma and n != ma[f]:
+            r = _("k")
+            if not force and (linear_path or allow):
+                r = repo.ui.prompt(
+                    (_("remote changed %s which local deleted\n") % f) +
+                     _("(k)eep or (d)elete?"), _("[kd]"), _("k"))
+            if r == _("k"):
+                get[f] = n
+        elif f not in ma:
+            repo.ui.debug(_("remote created %s\n") % f)
+            get[f] = n
+        else:
+            if force or p2 == pa: # going backwards?
+                repo.ui.debug(_("local deleted %s, recreating\n") % f)
+                get[f] = n
+            else:
+                repo.ui.debug(_("local deleted %s\n") % f)
+
+    del mw, m1, m2, ma
+
+    if force:
+        for f in merge:
+            get[f] = merge[f][1]
+        merge = {}
+
+    if linear_path or force:
+        # we don't need to do any magic, just jump to the new rev
+        branch_merge = False
+        p1, p2 = p2, nullid
+    else:
+        if not allow:
+            repo.ui.status(_("this update spans a branch"
+                             " affecting the following files:\n"))
+            fl = merge.keys() + get.keys()
+            fl.sort()
+            for f in fl:
+                cf = ""
+                if f in merge:
+                    cf = _(" (resolve)")
+                repo.ui.status(" %s%s\n" % (f, cf))
+            repo.ui.warn(_("aborting update spanning branches!\n"))
+            repo.ui.status(_("(use 'hg merge' to merge across branches"
+                             " or 'hg update -C' to lose changes)\n"))
+            return 1
+        branch_merge = True
+
+    xp1 = hex(p1)
+    xp2 = hex(p2)
+    if p2 == nullid: xxp2 = ''
+    else: xxp2 = xp2
+
+    repo.hook('preupdate', throw=True, parent1=xp1, parent2=xxp2)
+
+    # get the files we don't need to change
+    files = get.keys()
+    files.sort()
+    for f in files:
+        if f[0] == "/":
+            continue
+        repo.ui.note(_("getting %s\n") % f)
+        t = repo.file(f).read(get[f])
+        repo.wwrite(f, t)
+        util.set_exec(repo.wjoin(f), mf2[f])
+        if moddirstate:
+            if branch_merge:
+                repo.dirstate.update([f], 'n', st_mtime=-1)
+            else:
+                repo.dirstate.update([f], 'n')
+
+    # merge the tricky bits
+    failedmerge = []
+    files = merge.keys()
+    files.sort()
+    for f in files:
+        repo.ui.status(_("merging %s\n") % f)
+        my, other, flag = merge[f]
+        ret = merge3(repo, f, my, other, xp1, xp2)
+        if ret:
+            err = True
+            failedmerge.append(f)
+        util.set_exec(repo.wjoin(f), flag)
+        if moddirstate:
+            if branch_merge:
+                # We've done a branch merge, mark this file as merged
+                # so that we properly record the merger later
+                repo.dirstate.update([f], 'm')
+            else:
+                # We've update-merged a locally modified file, so
+                # we set the dirstate to emulate a normal checkout
+                # of that file some time in the past. Thus our
+                # merge will appear as a normal local file
+                # modification.
+                f_len = len(repo.file(f).read(other))
+                repo.dirstate.update([f], 'n', st_size=f_len, st_mtime=-1)
+
+    remove.sort()
+    for f in remove:
+        repo.ui.note(_("removing %s\n") % f)
+        util.audit_path(f)
+        try:
+            util.unlink(repo.wjoin(f))
+        except OSError, inst:
+            if inst.errno != errno.ENOENT:
+                repo.ui.warn(_("update failed to remove %s: %s!\n") %
+                             (f, inst.strerror))
+    if moddirstate:
+        if branch_merge:
+            repo.dirstate.update(remove, 'r')
+        else:
+            repo.dirstate.forget(remove)
+
+    if moddirstate:
+        repo.dirstate.setparents(p1, p2)
+
+    if show_stats:
+        stats = ((len(get), _("updated")),
+                 (len(merge) - len(failedmerge), _("merged")),
+                 (len(remove), _("removed")),
+                 (len(failedmerge), _("unresolved")))
+        note = ", ".join([_("%d files %s") % s for s in stats])
+        repo.ui.status("%s\n" % note)
+    if moddirstate:
+        if branch_merge:
+            if failedmerge:
+                repo.ui.status(_("There are unresolved merges,"
+                                " you can redo the full merge using:\n"
+                                "  hg update -C %s\n"
+                                "  hg merge %s\n"
+                                % (repo.changelog.rev(p1),
+                                    repo.changelog.rev(p2))))
+            else:
+                repo.ui.status(_("(branch merge, don't forget to commit)\n"))
+        elif failedmerge:
+            repo.ui.status(_("There are unresolved merges with"
+                             " locally modified files.\n"))
+
+    repo.hook('update', parent1=xp1, parent2=xxp2, error=int(err))
+    return err
+