eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/mercurial/merge.py
changeset 69 c6bca38c1cbf
equal deleted inserted replaced
68:5ff1fc726848 69:c6bca38c1cbf
       
     1 # merge.py - directory-level update/merge handling for Mercurial
       
     2 #
       
     3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
       
     4 #
       
     5 # This software may be used and distributed according to the terms of the
       
     6 # GNU General Public License version 2 or any later version.
       
     7 
       
     8 from node import nullid, nullrev, hex, bin
       
     9 from i18n import _
       
    10 import util, filemerge, copies, subrepo
       
    11 import errno, os, shutil
       
    12 
       
    13 class mergestate(object):
       
    14     '''track 3-way merge state of individual files'''
       
    15     def __init__(self, repo):
       
    16         self._repo = repo
       
    17         self._dirty = False
       
    18         self._read()
       
    19     def reset(self, node=None):
       
    20         self._state = {}
       
    21         if node:
       
    22             self._local = node
       
    23         shutil.rmtree(self._repo.join("merge"), True)
       
    24         self._dirty = False
       
    25     def _read(self):
       
    26         self._state = {}
       
    27         try:
       
    28             f = self._repo.opener("merge/state")
       
    29             for i, l in enumerate(f):
       
    30                 if i == 0:
       
    31                     self._local = bin(l[:-1])
       
    32                 else:
       
    33                     bits = l[:-1].split("\0")
       
    34                     self._state[bits[0]] = bits[1:]
       
    35         except IOError, err:
       
    36             if err.errno != errno.ENOENT:
       
    37                 raise
       
    38         self._dirty = False
       
    39     def commit(self):
       
    40         if self._dirty:
       
    41             f = self._repo.opener("merge/state", "w")
       
    42             f.write(hex(self._local) + "\n")
       
    43             for d, v in self._state.iteritems():
       
    44                 f.write("\0".join([d] + v) + "\n")
       
    45             self._dirty = False
       
    46     def add(self, fcl, fco, fca, fd, flags):
       
    47         hash = util.sha1(fcl.path()).hexdigest()
       
    48         self._repo.opener("merge/" + hash, "w").write(fcl.data())
       
    49         self._state[fd] = ['u', hash, fcl.path(), fca.path(),
       
    50                            hex(fca.filenode()), fco.path(), flags]
       
    51         self._dirty = True
       
    52     def __contains__(self, dfile):
       
    53         return dfile in self._state
       
    54     def __getitem__(self, dfile):
       
    55         return self._state[dfile][0]
       
    56     def __iter__(self):
       
    57         l = self._state.keys()
       
    58         l.sort()
       
    59         for f in l:
       
    60             yield f
       
    61     def mark(self, dfile, state):
       
    62         self._state[dfile][0] = state
       
    63         self._dirty = True
       
    64     def resolve(self, dfile, wctx, octx):
       
    65         if self[dfile] == 'r':
       
    66             return 0
       
    67         state, hash, lfile, afile, anode, ofile, flags = self._state[dfile]
       
    68         f = self._repo.opener("merge/" + hash)
       
    69         self._repo.wwrite(dfile, f.read(), flags)
       
    70         fcd = wctx[dfile]
       
    71         fco = octx[ofile]
       
    72         fca = self._repo.filectx(afile, fileid=anode)
       
    73         r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca)
       
    74         if not r:
       
    75             self.mark(dfile, 'r')
       
    76         return r
       
    77 
       
    78 def _checkunknown(wctx, mctx):
       
    79     "check for collisions between unknown files and files in mctx"
       
    80     for f in wctx.unknown():
       
    81         if f in mctx and mctx[f].cmp(wctx[f]):
       
    82             raise util.Abort(_("untracked file in working directory differs"
       
    83                                " from file in requested revision: '%s'") % f)
       
    84 
       
    85 def _checkcollision(mctx):
       
    86     "check for case folding collisions in the destination context"
       
    87     folded = {}
       
    88     for fn in mctx:
       
    89         fold = fn.lower()
       
    90         if fold in folded:
       
    91             raise util.Abort(_("case-folding collision between %s and %s")
       
    92                              % (fn, folded[fold]))
       
    93         folded[fold] = fn
       
    94 
       
    95 def _forgetremoved(wctx, mctx, branchmerge):
       
    96     """
       
    97     Forget removed files
       
    98 
       
    99     If we're jumping between revisions (as opposed to merging), and if
       
   100     neither the working directory nor the target rev has the file,
       
   101     then we need to remove it from the dirstate, to prevent the
       
   102     dirstate from listing the file when it is no longer in the
       
   103     manifest.
       
   104 
       
   105     If we're merging, and the other revision has removed a file
       
   106     that is not present in the working directory, we need to mark it
       
   107     as removed.
       
   108     """
       
   109 
       
   110     action = []
       
   111     state = branchmerge and 'r' or 'f'
       
   112     for f in wctx.deleted():
       
   113         if f not in mctx:
       
   114             action.append((f, state))
       
   115 
       
   116     if not branchmerge:
       
   117         for f in wctx.removed():
       
   118             if f not in mctx:
       
   119                 action.append((f, "f"))
       
   120 
       
   121     return action
       
   122 
       
   123 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
       
   124     """
       
   125     Merge p1 and p2 with ancestor pa and generate merge action list
       
   126 
       
   127     overwrite = whether we clobber working files
       
   128     partial = function to filter file lists
       
   129     """
       
   130 
       
   131     def fmerge(f, f2, fa):
       
   132         """merge flags"""
       
   133         a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2)
       
   134         if m == n: # flags agree
       
   135             return m # unchanged
       
   136         if m and n and not a: # flags set, don't agree, differ from parent
       
   137             r = repo.ui.promptchoice(
       
   138                 _(" conflicting flags for %s\n"
       
   139                   "(n)one, e(x)ec or sym(l)ink?") % f,
       
   140                 (_("&None"), _("E&xec"), _("Sym&link")), 0)
       
   141             if r == 1:
       
   142                 return "x" # Exec
       
   143             if r == 2:
       
   144                 return "l" # Symlink
       
   145             return ""
       
   146         if m and m != a: # changed from a to m
       
   147             return m
       
   148         if n and n != a: # changed from a to n
       
   149             return n
       
   150         return '' # flag was cleared
       
   151 
       
   152     def act(msg, m, f, *args):
       
   153         repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
       
   154         action.append((f, m) + args)
       
   155 
       
   156     action, copy = [], {}
       
   157 
       
   158     if overwrite:
       
   159         pa = p1
       
   160     elif pa == p2: # backwards
       
   161         pa = p1.p1()
       
   162     elif pa and repo.ui.configbool("merge", "followcopies", True):
       
   163         dirs = repo.ui.configbool("merge", "followdirs", True)
       
   164         copy, diverge = copies.copies(repo, p1, p2, pa, dirs)
       
   165         for of, fl in diverge.iteritems():
       
   166             act("divergent renames", "dr", of, fl)
       
   167 
       
   168     repo.ui.note(_("resolving manifests\n"))
       
   169     repo.ui.debug(" overwrite %s partial %s\n" % (overwrite, bool(partial)))
       
   170     repo.ui.debug(" ancestor %s local %s remote %s\n" % (pa, p1, p2))
       
   171 
       
   172     m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
       
   173     copied = set(copy.values())
       
   174 
       
   175     if '.hgsubstate' in m1:
       
   176         # check whether sub state is modified
       
   177         for s in p1.substate:
       
   178             if p1.sub(s).dirty():
       
   179                 m1['.hgsubstate'] += "+"
       
   180                 break
       
   181 
       
   182     # Compare manifests
       
   183     for f, n in m1.iteritems():
       
   184         if partial and not partial(f):
       
   185             continue
       
   186         if f in m2:
       
   187             rflags = fmerge(f, f, f)
       
   188             a = ma.get(f, nullid)
       
   189             if n == m2[f] or m2[f] == a: # same or local newer
       
   190                 # is file locally modified or flags need changing?
       
   191                 # dirstate flags may need to be made current
       
   192                 if m1.flags(f) != rflags or n[20:]:
       
   193                     act("update permissions", "e", f, rflags)
       
   194             elif n == a: # remote newer
       
   195                 act("remote is newer", "g", f, rflags)
       
   196             else: # both changed
       
   197                 act("versions differ", "m", f, f, f, rflags, False)
       
   198         elif f in copied: # files we'll deal with on m2 side
       
   199             pass
       
   200         elif f in copy:
       
   201             f2 = copy[f]
       
   202             if f2 not in m2: # directory rename
       
   203                 act("remote renamed directory to " + f2, "d",
       
   204                     f, None, f2, m1.flags(f))
       
   205             else: # case 2 A,B/B/B or case 4,21 A/B/B
       
   206                 act("local copied/moved to " + f2, "m",
       
   207                     f, f2, f, fmerge(f, f2, f2), False)
       
   208         elif f in ma: # clean, a different, no remote
       
   209             if n != ma[f]:
       
   210                 if repo.ui.promptchoice(
       
   211                     _(" local changed %s which remote deleted\n"
       
   212                       "use (c)hanged version or (d)elete?") % f,
       
   213                     (_("&Changed"), _("&Delete")), 0):
       
   214                     act("prompt delete", "r", f)
       
   215                 else:
       
   216                     act("prompt keep", "a", f)
       
   217             elif n[20:] == "a": # added, no remote
       
   218                 act("remote deleted", "f", f)
       
   219             elif n[20:] != "u":
       
   220                 act("other deleted", "r", f)
       
   221 
       
   222     for f, n in m2.iteritems():
       
   223         if partial and not partial(f):
       
   224             continue
       
   225         if f in m1 or f in copied: # files already visited
       
   226             continue
       
   227         if f in copy:
       
   228             f2 = copy[f]
       
   229             if f2 not in m1: # directory rename
       
   230                 act("local renamed directory to " + f2, "d",
       
   231                     None, f, f2, m2.flags(f))
       
   232             elif f2 in m2: # rename case 1, A/A,B/A
       
   233                 act("remote copied to " + f, "m",
       
   234                     f2, f, f, fmerge(f2, f, f2), False)
       
   235             else: # case 3,20 A/B/A
       
   236                 act("remote moved to " + f, "m",
       
   237                     f2, f, f, fmerge(f2, f, f2), True)
       
   238         elif f not in ma:
       
   239             act("remote created", "g", f, m2.flags(f))
       
   240         elif n != ma[f]:
       
   241             if repo.ui.promptchoice(
       
   242                 _("remote changed %s which local deleted\n"
       
   243                   "use (c)hanged version or leave (d)eleted?") % f,
       
   244                 (_("&Changed"), _("&Deleted")), 0) == 0:
       
   245                 act("prompt recreating", "g", f, m2.flags(f))
       
   246 
       
   247     return action
       
   248 
       
   249 def actionkey(a):
       
   250     return a[1] == 'r' and -1 or 0, a
       
   251 
       
   252 def applyupdates(repo, action, wctx, mctx, actx):
       
   253     """apply the merge action list to the working directory
       
   254 
       
   255     wctx is the working copy context
       
   256     mctx is the context to be merged into the working copy
       
   257     actx is the context of the common ancestor
       
   258     """
       
   259 
       
   260     updated, merged, removed, unresolved = 0, 0, 0, 0
       
   261     ms = mergestate(repo)
       
   262     ms.reset(wctx.parents()[0].node())
       
   263     moves = []
       
   264     action.sort(key=actionkey)
       
   265     substate = wctx.substate # prime
       
   266 
       
   267     # prescan for merges
       
   268     u = repo.ui
       
   269     for a in action:
       
   270         f, m = a[:2]
       
   271         if m == 'm': # merge
       
   272             f2, fd, flags, move = a[2:]
       
   273             if f == '.hgsubstate': # merged internally
       
   274                 continue
       
   275             repo.ui.debug("preserving %s for resolve of %s\n" % (f, fd))
       
   276             fcl = wctx[f]
       
   277             fco = mctx[f2]
       
   278             if mctx == actx: # backwards, use working dir parent as ancestor
       
   279                 if fcl.parents():
       
   280                     fca = fcl.parents()[0]
       
   281                 else:
       
   282                     fca = repo.filectx(f, fileid=nullrev)
       
   283             else:
       
   284                 fca = fcl.ancestor(fco, actx)
       
   285             if not fca:
       
   286                 fca = repo.filectx(f, fileid=nullrev)
       
   287             ms.add(fcl, fco, fca, fd, flags)
       
   288             if f != fd and move:
       
   289                 moves.append(f)
       
   290 
       
   291     # remove renamed files after safely stored
       
   292     for f in moves:
       
   293         if os.path.lexists(repo.wjoin(f)):
       
   294             repo.ui.debug("removing %s\n" % f)
       
   295             os.unlink(repo.wjoin(f))
       
   296 
       
   297     audit_path = util.path_auditor(repo.root)
       
   298 
       
   299     numupdates = len(action)
       
   300     for i, a in enumerate(action):
       
   301         f, m = a[:2]
       
   302         u.progress(_('updating'), i + 1, item=f, total=numupdates,
       
   303                    unit=_('files'))
       
   304         if f and f[0] == "/":
       
   305             continue
       
   306         if m == "r": # remove
       
   307             repo.ui.note(_("removing %s\n") % f)
       
   308             audit_path(f)
       
   309             if f == '.hgsubstate': # subrepo states need updating
       
   310                 subrepo.submerge(repo, wctx, mctx, wctx)
       
   311             try:
       
   312                 util.unlink(repo.wjoin(f))
       
   313             except OSError, inst:
       
   314                 if inst.errno != errno.ENOENT:
       
   315                     repo.ui.warn(_("update failed to remove %s: %s!\n") %
       
   316                                  (f, inst.strerror))
       
   317             removed += 1
       
   318         elif m == "m": # merge
       
   319             if f == '.hgsubstate': # subrepo states need updating
       
   320                 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx))
       
   321                 continue
       
   322             f2, fd, flags, move = a[2:]
       
   323             r = ms.resolve(fd, wctx, mctx)
       
   324             if r is not None and r > 0:
       
   325                 unresolved += 1
       
   326             else:
       
   327                 if r is None:
       
   328                     updated += 1
       
   329                 else:
       
   330                     merged += 1
       
   331             util.set_flags(repo.wjoin(fd), 'l' in flags, 'x' in flags)
       
   332             if f != fd and move and os.path.lexists(repo.wjoin(f)):
       
   333                 repo.ui.debug("removing %s\n" % f)
       
   334                 os.unlink(repo.wjoin(f))
       
   335         elif m == "g": # get
       
   336             flags = a[2]
       
   337             repo.ui.note(_("getting %s\n") % f)
       
   338             t = mctx.filectx(f).data()
       
   339             repo.wwrite(f, t, flags)
       
   340             t = None
       
   341             updated += 1
       
   342             if f == '.hgsubstate': # subrepo states need updating
       
   343                 subrepo.submerge(repo, wctx, mctx, wctx)
       
   344         elif m == "d": # directory rename
       
   345             f2, fd, flags = a[2:]
       
   346             if f:
       
   347                 repo.ui.note(_("moving %s to %s\n") % (f, fd))
       
   348                 t = wctx.filectx(f).data()
       
   349                 repo.wwrite(fd, t, flags)
       
   350                 util.unlink(repo.wjoin(f))
       
   351             if f2:
       
   352                 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
       
   353                 t = mctx.filectx(f2).data()
       
   354                 repo.wwrite(fd, t, flags)
       
   355             updated += 1
       
   356         elif m == "dr": # divergent renames
       
   357             fl = a[2]
       
   358             repo.ui.warn(_("note: possible conflict - %s was renamed "
       
   359                            "multiple times to:\n") % f)
       
   360             for nf in fl:
       
   361                 repo.ui.warn(" %s\n" % nf)
       
   362         elif m == "e": # exec
       
   363             flags = a[2]
       
   364             util.set_flags(repo.wjoin(f), 'l' in flags, 'x' in flags)
       
   365     ms.commit()
       
   366     u.progress(_('updating'), None, total=numupdates, unit=_('files'))
       
   367 
       
   368     return updated, merged, removed, unresolved
       
   369 
       
   370 def recordupdates(repo, action, branchmerge):
       
   371     "record merge actions to the dirstate"
       
   372 
       
   373     for a in action:
       
   374         f, m = a[:2]
       
   375         if m == "r": # remove
       
   376             if branchmerge:
       
   377                 repo.dirstate.remove(f)
       
   378             else:
       
   379                 repo.dirstate.forget(f)
       
   380         elif m == "a": # re-add
       
   381             if not branchmerge:
       
   382                 repo.dirstate.add(f)
       
   383         elif m == "f": # forget
       
   384             repo.dirstate.forget(f)
       
   385         elif m == "e": # exec change
       
   386             repo.dirstate.normallookup(f)
       
   387         elif m == "g": # get
       
   388             if branchmerge:
       
   389                 repo.dirstate.otherparent(f)
       
   390             else:
       
   391                 repo.dirstate.normal(f)
       
   392         elif m == "m": # merge
       
   393             f2, fd, flag, move = a[2:]
       
   394             if branchmerge:
       
   395                 # We've done a branch merge, mark this file as merged
       
   396                 # so that we properly record the merger later
       
   397                 repo.dirstate.merge(fd)
       
   398                 if f != f2: # copy/rename
       
   399                     if move:
       
   400                         repo.dirstate.remove(f)
       
   401                     if f != fd:
       
   402                         repo.dirstate.copy(f, fd)
       
   403                     else:
       
   404                         repo.dirstate.copy(f2, fd)
       
   405             else:
       
   406                 # We've update-merged a locally modified file, so
       
   407                 # we set the dirstate to emulate a normal checkout
       
   408                 # of that file some time in the past. Thus our
       
   409                 # merge will appear as a normal local file
       
   410                 # modification.
       
   411                 if f2 == fd: # file not locally copied/moved
       
   412                     repo.dirstate.normallookup(fd)
       
   413                 if move:
       
   414                     repo.dirstate.forget(f)
       
   415         elif m == "d": # directory rename
       
   416             f2, fd, flag = a[2:]
       
   417             if not f2 and f not in repo.dirstate:
       
   418                 # untracked file moved
       
   419                 continue
       
   420             if branchmerge:
       
   421                 repo.dirstate.add(fd)
       
   422                 if f:
       
   423                     repo.dirstate.remove(f)
       
   424                     repo.dirstate.copy(f, fd)
       
   425                 if f2:
       
   426                     repo.dirstate.copy(f2, fd)
       
   427             else:
       
   428                 repo.dirstate.normal(fd)
       
   429                 if f:
       
   430                     repo.dirstate.forget(f)
       
   431 
       
   432 def update(repo, node, branchmerge, force, partial):
       
   433     """
       
   434     Perform a merge between the working directory and the given node
       
   435 
       
   436     node = the node to update to, or None if unspecified
       
   437     branchmerge = whether to merge between branches
       
   438     force = whether to force branch merging or file overwriting
       
   439     partial = a function to filter file lists (dirstate not updated)
       
   440 
       
   441     The table below shows all the behaviors of the update command
       
   442     given the -c and -C or no options, whether the working directory
       
   443     is dirty, whether a revision is specified, and the relationship of
       
   444     the parent rev to the target rev (linear, on the same named
       
   445     branch, or on another named branch).
       
   446 
       
   447     This logic is tested by test-update-branches.t.
       
   448 
       
   449     -c  -C  dirty  rev  |  linear   same  cross
       
   450      n   n    n     n   |    ok     (1)     x
       
   451      n   n    n     y   |    ok     ok     ok
       
   452      n   n    y     *   |   merge   (2)    (2)
       
   453      n   y    *     *   |    ---  discard  ---
       
   454      y   n    y     *   |    ---    (3)    ---
       
   455      y   n    n     *   |    ---    ok     ---
       
   456      y   y    *     *   |    ---    (4)    ---
       
   457 
       
   458     x = can't happen
       
   459     * = don't-care
       
   460     1 = abort: crosses branches (use 'hg merge' or 'hg update -c')
       
   461     2 = abort: crosses branches (use 'hg merge' to merge or
       
   462                  use 'hg update -C' to discard changes)
       
   463     3 = abort: uncommitted local changes
       
   464     4 = incompatible options (checked in commands.py)
       
   465     """
       
   466 
       
   467     onode = node
       
   468     wlock = repo.wlock()
       
   469     try:
       
   470         wc = repo[None]
       
   471         if node is None:
       
   472             # tip of current branch
       
   473             try:
       
   474                 node = repo.branchtags()[wc.branch()]
       
   475             except KeyError:
       
   476                 if wc.branch() == "default": # no default branch!
       
   477                     node = repo.lookup("tip") # update to tip
       
   478                 else:
       
   479                     raise util.Abort(_("branch %s not found") % wc.branch())
       
   480         overwrite = force and not branchmerge
       
   481         pl = wc.parents()
       
   482         p1, p2 = pl[0], repo[node]
       
   483         pa = p1.ancestor(p2)
       
   484         fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
       
   485         fastforward = False
       
   486 
       
   487         ### check phase
       
   488         if not overwrite and len(pl) > 1:
       
   489             raise util.Abort(_("outstanding uncommitted merges"))
       
   490         if branchmerge:
       
   491             if pa == p2:
       
   492                 raise util.Abort(_("merging with a working directory ancestor"
       
   493                                    " has no effect"))
       
   494             elif pa == p1:
       
   495                 if p1.branch() != p2.branch():
       
   496                     fastforward = True
       
   497                 else:
       
   498                     raise util.Abort(_("nothing to merge (use 'hg update'"
       
   499                                        " or check 'hg heads')"))
       
   500             if not force and (wc.files() or wc.deleted()):
       
   501                 raise util.Abort(_("outstanding uncommitted changes "
       
   502                                    "(use 'hg status' to list changes)"))
       
   503         elif not overwrite:
       
   504             if pa == p1 or pa == p2: # linear
       
   505                 pass # all good
       
   506             elif wc.files() or wc.deleted():
       
   507                 raise util.Abort(_("crosses branches (merge branches or use"
       
   508                                    " --clean to discard changes)"))
       
   509             elif onode is None:
       
   510                 raise util.Abort(_("crosses branches (merge branches or use"
       
   511                                    " --check to force update)"))
       
   512             else:
       
   513                 # Allow jumping branches if clean and specific rev given
       
   514                 overwrite = True
       
   515 
       
   516         ### calculate phase
       
   517         action = []
       
   518         wc.status(unknown=True) # prime cache
       
   519         if not force:
       
   520             _checkunknown(wc, p2)
       
   521         if not util.checkcase(repo.path):
       
   522             _checkcollision(p2)
       
   523         action += _forgetremoved(wc, p2, branchmerge)
       
   524         action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
       
   525 
       
   526         ### apply phase
       
   527         if not branchmerge: # just jump to the new rev
       
   528             fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
       
   529         if not partial:
       
   530             repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
       
   531 
       
   532         stats = applyupdates(repo, action, wc, p2, pa)
       
   533 
       
   534         if not partial:
       
   535             repo.dirstate.setparents(fp1, fp2)
       
   536             recordupdates(repo, action, branchmerge)
       
   537             if not branchmerge and not fastforward:
       
   538                 repo.dirstate.setbranch(p2.branch())
       
   539     finally:
       
   540         wlock.release()
       
   541 
       
   542     if not partial:
       
   543         repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
       
   544     return stats