def manifestmerge(repo, wctx, p2, pa, branchmerge, force, partial, acceptremote=False): """ Merge p1 and p2 with ancestor pa and generate merge action list branchmerge and force are as passed in to update partial = function to filter file lists acceptremote = accept the incoming changes without prompting """ overwrite = force and not branchmerge actions, copy, movewithdir = [], {}, {} followcopies = False if overwrite: pa = wctx elif pa == p2: # backwards pa = wctx.p1() elif not branchmerge and not wctx.dirty(missing=True): pass elif pa and repo.ui.configbool("merge", "followcopies", True): followcopies = True # manifests fetched in order are going to be faster, so prime the caches [x.manifest() for x in sorted(wctx.parents() + [p2, pa], key=lambda x: x.rev())] if followcopies: ret = copies.mergecopies(repo, wctx, p2, pa) copy, movewithdir, diverge, renamedelete = ret for of, fl in diverge.iteritems(): actions.append((of, "dr", (fl,), "divergent renames")) for of, fl in renamedelete.iteritems(): actions.append((of, "rd", (fl,), "rename and delete")) repo.ui.note(_("resolving manifests\n")) repo.ui.debug(" branchmerge: %s, force: %s, partial: %s\n" % (bool(branchmerge), bool(force), bool(partial))) repo.ui.debug(" ancestor: %s, local: %s, remote: %s\n" % (pa, wctx, p2)) m1, m2, ma = wctx.manifest(), p2.manifest(), pa.manifest() copied = set(copy.values()) copied.update(movewithdir.values()) if '.hgsubstate' in m1: # check whether sub state is modified for s in sorted(wctx.substate): if wctx.sub(s).dirty(): m1['.hgsubstate'] += "+" break aborts, prompts = [], [] # Compare manifests fdiff = dicthelpers.diff(m1, m2) flagsdiff = m1.flagsdiff(m2) diff12 = dicthelpers.join(fdiff, flagsdiff) for f, (n12, fl12) in diff12.iteritems(): if n12: n1, n2 = n12 else: # file contents didn't change, but flags did n1 = n2 = m1.get(f, None) if n1 is None: # Since n1 == n2, the file isn't present in m2 either. This # means that the file was removed or deleted locally and # removed remotely, but that residual entries remain in flags. # This can happen in manifests generated by workingctx. continue if fl12: fl1, fl2 = fl12 else: # flags didn't change, file contents did fl1 = fl2 = m1.flags(f) if partial and not partial(f): continue if n1 and n2: fla = ma.flags(f) nol = 'l' not in fl1 + fl2 + fla a = ma.get(f, nullid) if n2 == a and fl2 == fla: pass # remote unchanged - keep local elif n1 == a and fl1 == fla: # local unchanged - use remote if n1 == n2: # optimization: keep local content actions.append((f, "e", (fl2,), "update permissions")) else: actions.append((f, "g", (fl2,), "remote is newer")) elif nol and n2 == a: # remote only changed 'x' actions.append((f, "e", (fl2,), "update permissions")) elif nol and n1 == a: # local only changed 'x' actions.append((f, "g", (fl1,), "remote is newer")) else: # both changed something actions.append((f, "m", (f, f, False), "versions differ")) elif f in copied: # files we'll deal with on m2 side pass elif n1 and f in movewithdir: # directory rename f2 = movewithdir[f] actions.append((f, "d", (None, f2, fl1), "remote renamed directory to " + f2)) elif n1 and f in copy: f2 = copy[f] actions.append((f, "m", (f2, f, False), "local copied/moved to " + f2)) elif n1 and f in ma: # clean, a different, no remote if n1 != ma[f]: prompts.append((f, "cd")) # prompt changed/deleted elif n1[20:] == "a": # added, no remote actions.append((f, "f", None, "remote deleted")) else: actions.append((f, "r", None, "other deleted")) elif n2 and f in movewithdir: f2 = movewithdir[f] actions.append((None, "d", (f, f2, fl2), "local renamed directory to " + f2)) elif n2 and f in copy: f2 = copy[f] if f2 in m2: actions.append((f2, "m", (f, f, False), "remote copied to " + f)) else: actions.append((f2, "m", (f, f, True), "remote moved to " + f)) elif n2 and f not in ma: # local unknown, remote created: the logic is described by the # following table: # # force branchmerge different | action # n * n | get # n * y | abort # y n * | get # y y n | get # y y y | merge # # Checking whether the files are different is expensive, so we # don't do that when we can avoid it. if force and not branchmerge: actions.append((f, "g", (fl2,), "remote created")) else: different = _checkunknownfile(repo, wctx, p2, f) if force and branchmerge and different: actions.append((f, "m", (f, f, False), "remote differs from untracked local")) elif not force and different: aborts.append((f, "ud")) else: actions.append((f, "g", (fl2,), "remote created")) elif n2 and n2 != ma[f]: prompts.append((f, "dc")) # prompt deleted/changed for f, m in sorted(aborts): if m == "ud": repo.ui.warn(_("%s: untracked file differs\n") % f) else: assert False, m if aborts: raise util.Abort(_("untracked files in working directory differ " "from files in requested revision")) if not util.checkcase(repo.path): # check collision between files only in p2 for clean update if (not branchmerge and (force or not wctx.dirty(missing=True, branch=False))): _checkcollision(repo, m2, [], []) else: _checkcollision(repo, m1, actions, prompts) for f, m in sorted(prompts): if m == "cd": if acceptremote: actions.append((f, "r", None, "remote delete")) elif repo.ui.promptchoice( _("local changed %s which remote deleted\n" "use (c)hanged version or (d)elete?" "$$ &Changed $$ &Delete") % f, 0): actions.append((f, "r", None, "prompt delete")) else: actions.append((f, "a", None, "prompt keep")) elif m == "dc": if acceptremote: actions.append((f, "g", (m2.flags(f),), "remote recreating")) elif repo.ui.promptchoice( _("remote changed %s which local deleted\n" "use (c)hanged version or leave (d)eleted?" "$$ &Changed $$ &Deleted") % f, 0) == 0: actions.append((f, "g", (m2.flags(f),), "prompt recreating")) else: assert False, m return actions
def manifestmerge(repo, wctx, p2, pa, branchmerge, force, partial, acceptremote=False): """ Merge p1 and p2 with ancestor pa and generate merge action list branchmerge and force are as passed in to update partial = function to filter file lists acceptremote = accept the incoming changes without prompting """ overwrite = force and not branchmerge actions, copy, movewithdir = [], {}, {} followcopies = False if overwrite: pa = wctx elif pa == p2: # backwards pa = wctx.p1() elif not branchmerge and not wctx.dirty(missing=True): pass elif pa and repo.ui.configbool("merge", "followcopies", True): followcopies = True # manifests fetched in order are going to be faster, so prime the caches [ x.manifest() for x in sorted(wctx.parents() + [p2, pa], key=lambda x: x.rev()) ] if followcopies: ret = copies.mergecopies(repo, wctx, p2, pa) copy, movewithdir, diverge, renamedelete = ret for of, fl in diverge.iteritems(): actions.append((of, "dr", (fl, ), "divergent renames")) for of, fl in renamedelete.iteritems(): actions.append((of, "rd", (fl, ), "rename and delete")) repo.ui.note(_("resolving manifests\n")) repo.ui.debug(" branchmerge: %s, force: %s, partial: %s\n" % (bool(branchmerge), bool(force), bool(partial))) repo.ui.debug(" ancestor: %s, local: %s, remote: %s\n" % (pa, wctx, p2)) m1, m2, ma = wctx.manifest(), p2.manifest(), pa.manifest() copied = set(copy.values()) copied.update(movewithdir.values()) if '.hgsubstate' in m1: # check whether sub state is modified for s in sorted(wctx.substate): if wctx.sub(s).dirty(): m1['.hgsubstate'] += "+" break aborts, prompts = [], [] # Compare manifests fdiff = dicthelpers.diff(m1, m2) flagsdiff = m1.flagsdiff(m2) diff12 = dicthelpers.join(fdiff, flagsdiff) for f, (n12, fl12) in diff12.iteritems(): if n12: n1, n2 = n12 else: # file contents didn't change, but flags did n1 = n2 = m1.get(f, None) if n1 is None: # Since n1 == n2, the file isn't present in m2 either. This # means that the file was removed or deleted locally and # removed remotely, but that residual entries remain in flags. # This can happen in manifests generated by workingctx. continue if fl12: fl1, fl2 = fl12 else: # flags didn't change, file contents did fl1 = fl2 = m1.flags(f) if partial and not partial(f): continue if n1 and n2: fla = ma.flags(f) nol = 'l' not in fl1 + fl2 + fla a = ma.get(f, nullid) if n2 == a and fl2 == fla: pass # remote unchanged - keep local elif n1 == a and fl1 == fla: # local unchanged - use remote if n1 == n2: # optimization: keep local content actions.append((f, "e", (fl2, ), "update permissions")) else: actions.append((f, "g", (fl2, ), "remote is newer")) elif nol and n2 == a: # remote only changed 'x' actions.append((f, "e", (fl2, ), "update permissions")) elif nol and n1 == a: # local only changed 'x' actions.append((f, "g", (fl1, ), "remote is newer")) else: # both changed something actions.append((f, "m", (f, f, False), "versions differ")) elif f in copied: # files we'll deal with on m2 side pass elif n1 and f in movewithdir: # directory rename f2 = movewithdir[f] actions.append( (f, "d", (None, f2, fl1), "remote renamed directory to " + f2)) elif n1 and f in copy: f2 = copy[f] actions.append( (f, "m", (f2, f, False), "local copied/moved to " + f2)) elif n1 and f in ma: # clean, a different, no remote if n1 != ma[f]: prompts.append((f, "cd")) # prompt changed/deleted elif n1[20:] == "a": # added, no remote actions.append((f, "f", None, "remote deleted")) else: actions.append((f, "r", None, "other deleted")) elif n2 and f in movewithdir: f2 = movewithdir[f] actions.append( (None, "d", (f, f2, fl2), "local renamed directory to " + f2)) elif n2 and f in copy: f2 = copy[f] if f2 in m2: actions.append( (f2, "m", (f, f, False), "remote copied to " + f)) else: actions.append((f2, "m", (f, f, True), "remote moved to " + f)) elif n2 and f not in ma: # local unknown, remote created: the logic is described by the # following table: # # force branchmerge different | action # n * n | get # n * y | abort # y n * | get # y y n | get # y y y | merge # # Checking whether the files are different is expensive, so we # don't do that when we can avoid it. if force and not branchmerge: actions.append((f, "g", (fl2, ), "remote created")) else: different = _checkunknownfile(repo, wctx, p2, f) if force and branchmerge and different: actions.append((f, "m", (f, f, False), "remote differs from untracked local")) elif not force and different: aborts.append((f, "ud")) else: actions.append((f, "g", (fl2, ), "remote created")) elif n2 and n2 != ma[f]: prompts.append((f, "dc")) # prompt deleted/changed for f, m in sorted(aborts): if m == "ud": repo.ui.warn(_("%s: untracked file differs\n") % f) else: assert False, m if aborts: raise util.Abort( _("untracked files in working directory differ " "from files in requested revision")) if not util.checkcase(repo.path): # check collision between files only in p2 for clean update if (not branchmerge and (force or not wctx.dirty(missing=True, branch=False))): _checkcollision(repo, m2, [], []) else: _checkcollision(repo, m1, actions, prompts) for f, m in sorted(prompts): if m == "cd": if acceptremote: actions.append((f, "r", None, "remote delete")) elif repo.ui.promptchoice( _("local changed %s which remote deleted\n" "use (c)hanged version or (d)elete?" "$$ &Changed $$ &Delete") % f, 0): actions.append((f, "r", None, "prompt delete")) else: actions.append((f, "a", None, "prompt keep")) elif m == "dc": if acceptremote: actions.append((f, "g", (m2.flags(f), ), "remote recreating")) elif repo.ui.promptchoice( _("remote changed %s which local deleted\n" "use (c)hanged version or leave (d)eleted?" "$$ &Changed $$ &Deleted") % f, 0) == 0: actions.append((f, "g", (m2.flags(f), ), "prompt recreating")) else: assert False, m return actions