mercurial/merge.py
changeset 2775 b550cd82f92a
child 2803 987c31e2a08c
equal deleted inserted replaced
2774:8cd3e19bf4a5 2775:b550cd82f92a
       
     1 # merge.py - directory-level update/merge handling for Mercurial
       
     2 #
       
     3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
       
     4 #
       
     5 # This software may be used and distributed according to the terms
       
     6 # of the GNU General Public License, incorporated herein by reference.
       
     7 
       
     8 from node import *
       
     9 from i18n import gettext as _
       
    10 from demandload import *
       
    11 demandload(globals(), "util os tempfile")
       
    12 
       
    13 def merge3(repo, fn, my, other, p1, p2):
       
    14     """perform a 3-way merge in the working directory"""
       
    15 
       
    16     def temp(prefix, node):
       
    17         pre = "%s~%s." % (os.path.basename(fn), prefix)
       
    18         (fd, name) = tempfile.mkstemp(prefix=pre)
       
    19         f = os.fdopen(fd, "wb")
       
    20         repo.wwrite(fn, fl.read(node), f)
       
    21         f.close()
       
    22         return name
       
    23 
       
    24     fl = repo.file(fn)
       
    25     base = fl.ancestor(my, other)
       
    26     a = repo.wjoin(fn)
       
    27     b = temp("base", base)
       
    28     c = temp("other", other)
       
    29 
       
    30     repo.ui.note(_("resolving %s\n") % fn)
       
    31     repo.ui.debug(_("file %s: my %s other %s ancestor %s\n") %
       
    32                           (fn, short(my), short(other), short(base)))
       
    33 
       
    34     cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge")
       
    35            or "hgmerge")
       
    36     r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root,
       
    37                     environ={'HG_FILE': fn,
       
    38                              'HG_MY_NODE': p1,
       
    39                              'HG_OTHER_NODE': p2,
       
    40                              'HG_FILE_MY_NODE': hex(my),
       
    41                              'HG_FILE_OTHER_NODE': hex(other),
       
    42                              'HG_FILE_BASE_NODE': hex(base)})
       
    43     if r:
       
    44         repo.ui.warn(_("merging %s failed!\n") % fn)
       
    45 
       
    46     os.unlink(b)
       
    47     os.unlink(c)
       
    48     return r
       
    49 
       
    50 def update(repo, node, allow=False, force=False, choose=None,
       
    51            moddirstate=True, forcemerge=False, wlock=None, show_stats=True):
       
    52     pl = repo.dirstate.parents()
       
    53     if not force and pl[1] != nullid:
       
    54         raise util.Abort(_("outstanding uncommitted merges"))
       
    55 
       
    56     err = False
       
    57 
       
    58     p1, p2 = pl[0], node
       
    59     pa = repo.changelog.ancestor(p1, p2)
       
    60     m1n = repo.changelog.read(p1)[0]
       
    61     m2n = repo.changelog.read(p2)[0]
       
    62     man = repo.manifest.ancestor(m1n, m2n)
       
    63     m1 = repo.manifest.read(m1n)
       
    64     mf1 = repo.manifest.readflags(m1n)
       
    65     m2 = repo.manifest.read(m2n).copy()
       
    66     mf2 = repo.manifest.readflags(m2n)
       
    67     ma = repo.manifest.read(man)
       
    68     mfa = repo.manifest.readflags(man)
       
    69 
       
    70     modified, added, removed, deleted, unknown = repo.changes()
       
    71 
       
    72     # is this a jump, or a merge?  i.e. is there a linear path
       
    73     # from p1 to p2?
       
    74     linear_path = (pa == p1 or pa == p2)
       
    75 
       
    76     if allow and linear_path:
       
    77         raise util.Abort(_("there is nothing to merge, just use "
       
    78                            "'hg update' or look at 'hg heads'"))
       
    79     if allow and not forcemerge:
       
    80         if modified or added or removed:
       
    81             raise util.Abort(_("outstanding uncommitted changes"))
       
    82 
       
    83     if not forcemerge and not force:
       
    84         for f in unknown:
       
    85             if f in m2:
       
    86                 t1 = repo.wread(f)
       
    87                 t2 = repo.file(f).read(m2[f])
       
    88                 if cmp(t1, t2) != 0:
       
    89                     raise util.Abort(_("'%s' already exists in the working"
       
    90                                        " dir and differs from remote") % f)
       
    91 
       
    92     # resolve the manifest to determine which files
       
    93     # we care about merging
       
    94     repo.ui.note(_("resolving manifests\n"))
       
    95     repo.ui.debug(_(" force %s allow %s moddirstate %s linear %s\n") %
       
    96                   (force, allow, moddirstate, linear_path))
       
    97     repo.ui.debug(_(" ancestor %s local %s remote %s\n") %
       
    98                   (short(man), short(m1n), short(m2n)))
       
    99 
       
   100     merge = {}
       
   101     get = {}
       
   102     remove = []
       
   103 
       
   104     # construct a working dir manifest
       
   105     mw = m1.copy()
       
   106     mfw = mf1.copy()
       
   107     umap = dict.fromkeys(unknown)
       
   108 
       
   109     for f in added + modified + unknown:
       
   110         mw[f] = ""
       
   111         mfw[f] = util.is_exec(repo.wjoin(f), mfw.get(f, False))
       
   112 
       
   113     if moddirstate and not wlock:
       
   114         wlock = repo.wlock()
       
   115 
       
   116     for f in deleted + removed:
       
   117         if f in mw:
       
   118             del mw[f]
       
   119 
       
   120         # If we're jumping between revisions (as opposed to merging),
       
   121         # and if neither the working directory nor the target rev has
       
   122         # the file, then we need to remove it from the dirstate, to
       
   123         # prevent the dirstate from listing the file when it is no
       
   124         # longer in the manifest.
       
   125         if moddirstate and linear_path and f not in m2:
       
   126             repo.dirstate.forget((f,))
       
   127 
       
   128     # Compare manifests
       
   129     for f, n in mw.iteritems():
       
   130         if choose and not choose(f):
       
   131             continue
       
   132         if f in m2:
       
   133             s = 0
       
   134 
       
   135             # is the wfile new since m1, and match m2?
       
   136             if f not in m1:
       
   137                 t1 = repo.wread(f)
       
   138                 t2 = repo.file(f).read(m2[f])
       
   139                 if cmp(t1, t2) == 0:
       
   140                     n = m2[f]
       
   141                 del t1, t2
       
   142 
       
   143             # are files different?
       
   144             if n != m2[f]:
       
   145                 a = ma.get(f, nullid)
       
   146                 # are both different from the ancestor?
       
   147                 if n != a and m2[f] != a:
       
   148                     repo.ui.debug(_(" %s versions differ, resolve\n") % f)
       
   149                     # merge executable bits
       
   150                     # "if we changed or they changed, change in merge"
       
   151                     a, b, c = mfa.get(f, 0), mfw[f], mf2[f]
       
   152                     mode = ((a^b) | (a^c)) ^ a
       
   153                     merge[f] = (m1.get(f, nullid), m2[f], mode)
       
   154                     s = 1
       
   155                 # are we clobbering?
       
   156                 # is remote's version newer?
       
   157                 # or are we going back in time?
       
   158                 elif force or m2[f] != a or (p2 == pa and mw[f] == m1[f]):
       
   159                     repo.ui.debug(_(" remote %s is newer, get\n") % f)
       
   160                     get[f] = m2[f]
       
   161                     s = 1
       
   162             elif f in umap or f in added:
       
   163                 # this unknown file is the same as the checkout
       
   164                 # we need to reset the dirstate if the file was added
       
   165                 get[f] = m2[f]
       
   166 
       
   167             if not s and mfw[f] != mf2[f]:
       
   168                 if force:
       
   169                     repo.ui.debug(_(" updating permissions for %s\n") % f)
       
   170                     util.set_exec(repo.wjoin(f), mf2[f])
       
   171                 else:
       
   172                     a, b, c = mfa.get(f, 0), mfw[f], mf2[f]
       
   173                     mode = ((a^b) | (a^c)) ^ a
       
   174                     if mode != b:
       
   175                         repo.ui.debug(_(" updating permissions for %s\n")
       
   176                                       % f)
       
   177                         util.set_exec(repo.wjoin(f), mode)
       
   178             del m2[f]
       
   179         elif f in ma:
       
   180             if n != ma[f]:
       
   181                 r = _("d")
       
   182                 if not force and (linear_path or allow):
       
   183                     r = repo.ui.prompt(
       
   184                         (_(" local changed %s which remote deleted\n") % f) +
       
   185                          _("(k)eep or (d)elete?"), _("[kd]"), _("k"))
       
   186                 if r == _("d"):
       
   187                     remove.append(f)
       
   188             else:
       
   189                 repo.ui.debug(_("other deleted %s\n") % f)
       
   190                 remove.append(f) # other deleted it
       
   191         else:
       
   192             # file is created on branch or in working directory
       
   193             if force and f not in umap:
       
   194                 repo.ui.debug(_("remote deleted %s, clobbering\n") % f)
       
   195                 remove.append(f)
       
   196             elif n == m1.get(f, nullid): # same as parent
       
   197                 if p2 == pa: # going backwards?
       
   198                     repo.ui.debug(_("remote deleted %s\n") % f)
       
   199                     remove.append(f)
       
   200                 else:
       
   201                     repo.ui.debug(_("local modified %s, keeping\n") % f)
       
   202             else:
       
   203                 repo.ui.debug(_("working dir created %s, keeping\n") % f)
       
   204 
       
   205     for f, n in m2.iteritems():
       
   206         if choose and not choose(f):
       
   207             continue
       
   208         if f[0] == "/":
       
   209             continue
       
   210         if f in ma and n != ma[f]:
       
   211             r = _("k")
       
   212             if not force and (linear_path or allow):
       
   213                 r = repo.ui.prompt(
       
   214                     (_("remote changed %s which local deleted\n") % f) +
       
   215                      _("(k)eep or (d)elete?"), _("[kd]"), _("k"))
       
   216             if r == _("k"):
       
   217                 get[f] = n
       
   218         elif f not in ma:
       
   219             repo.ui.debug(_("remote created %s\n") % f)
       
   220             get[f] = n
       
   221         else:
       
   222             if force or p2 == pa: # going backwards?
       
   223                 repo.ui.debug(_("local deleted %s, recreating\n") % f)
       
   224                 get[f] = n
       
   225             else:
       
   226                 repo.ui.debug(_("local deleted %s\n") % f)
       
   227 
       
   228     del mw, m1, m2, ma
       
   229 
       
   230     if force:
       
   231         for f in merge:
       
   232             get[f] = merge[f][1]
       
   233         merge = {}
       
   234 
       
   235     if linear_path or force:
       
   236         # we don't need to do any magic, just jump to the new rev
       
   237         branch_merge = False
       
   238         p1, p2 = p2, nullid
       
   239     else:
       
   240         if not allow:
       
   241             repo.ui.status(_("this update spans a branch"
       
   242                              " affecting the following files:\n"))
       
   243             fl = merge.keys() + get.keys()
       
   244             fl.sort()
       
   245             for f in fl:
       
   246                 cf = ""
       
   247                 if f in merge:
       
   248                     cf = _(" (resolve)")
       
   249                 repo.ui.status(" %s%s\n" % (f, cf))
       
   250             repo.ui.warn(_("aborting update spanning branches!\n"))
       
   251             repo.ui.status(_("(use 'hg merge' to merge across branches"
       
   252                              " or 'hg update -C' to lose changes)\n"))
       
   253             return 1
       
   254         branch_merge = True
       
   255 
       
   256     xp1 = hex(p1)
       
   257     xp2 = hex(p2)
       
   258     if p2 == nullid: xxp2 = ''
       
   259     else: xxp2 = xp2
       
   260 
       
   261     repo.hook('preupdate', throw=True, parent1=xp1, parent2=xxp2)
       
   262 
       
   263     # get the files we don't need to change
       
   264     files = get.keys()
       
   265     files.sort()
       
   266     for f in files:
       
   267         if f[0] == "/":
       
   268             continue
       
   269         repo.ui.note(_("getting %s\n") % f)
       
   270         t = repo.file(f).read(get[f])
       
   271         repo.wwrite(f, t)
       
   272         util.set_exec(repo.wjoin(f), mf2[f])
       
   273         if moddirstate:
       
   274             if branch_merge:
       
   275                 repo.dirstate.update([f], 'n', st_mtime=-1)
       
   276             else:
       
   277                 repo.dirstate.update([f], 'n')
       
   278 
       
   279     # merge the tricky bits
       
   280     failedmerge = []
       
   281     files = merge.keys()
       
   282     files.sort()
       
   283     for f in files:
       
   284         repo.ui.status(_("merging %s\n") % f)
       
   285         my, other, flag = merge[f]
       
   286         ret = merge3(repo, f, my, other, xp1, xp2)
       
   287         if ret:
       
   288             err = True
       
   289             failedmerge.append(f)
       
   290         util.set_exec(repo.wjoin(f), flag)
       
   291         if moddirstate:
       
   292             if branch_merge:
       
   293                 # We've done a branch merge, mark this file as merged
       
   294                 # so that we properly record the merger later
       
   295                 repo.dirstate.update([f], 'm')
       
   296             else:
       
   297                 # We've update-merged a locally modified file, so
       
   298                 # we set the dirstate to emulate a normal checkout
       
   299                 # of that file some time in the past. Thus our
       
   300                 # merge will appear as a normal local file
       
   301                 # modification.
       
   302                 f_len = len(repo.file(f).read(other))
       
   303                 repo.dirstate.update([f], 'n', st_size=f_len, st_mtime=-1)
       
   304 
       
   305     remove.sort()
       
   306     for f in remove:
       
   307         repo.ui.note(_("removing %s\n") % f)
       
   308         util.audit_path(f)
       
   309         try:
       
   310             util.unlink(repo.wjoin(f))
       
   311         except OSError, inst:
       
   312             if inst.errno != errno.ENOENT:
       
   313                 repo.ui.warn(_("update failed to remove %s: %s!\n") %
       
   314                              (f, inst.strerror))
       
   315     if moddirstate:
       
   316         if branch_merge:
       
   317             repo.dirstate.update(remove, 'r')
       
   318         else:
       
   319             repo.dirstate.forget(remove)
       
   320 
       
   321     if moddirstate:
       
   322         repo.dirstate.setparents(p1, p2)
       
   323 
       
   324     if show_stats:
       
   325         stats = ((len(get), _("updated")),
       
   326                  (len(merge) - len(failedmerge), _("merged")),
       
   327                  (len(remove), _("removed")),
       
   328                  (len(failedmerge), _("unresolved")))
       
   329         note = ", ".join([_("%d files %s") % s for s in stats])
       
   330         repo.ui.status("%s\n" % note)
       
   331     if moddirstate:
       
   332         if branch_merge:
       
   333             if failedmerge:
       
   334                 repo.ui.status(_("There are unresolved merges,"
       
   335                                 " you can redo the full merge using:\n"
       
   336                                 "  hg update -C %s\n"
       
   337                                 "  hg merge %s\n"
       
   338                                 % (repo.changelog.rev(p1),
       
   339                                     repo.changelog.rev(p2))))
       
   340             else:
       
   341                 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
       
   342         elif failedmerge:
       
   343             repo.ui.status(_("There are unresolved merges with"
       
   344                              " locally modified files.\n"))
       
   345 
       
   346     repo.hook('update', parent1=xp1, parent2=xxp2, error=int(err))
       
   347     return err
       
   348