# HG changeset patch # User Mads Kiilerich # Date 1393550758 -3600 # Node ID 43eecb4e23f879f880dce8518a7afe3c868a9655 # Parent 6062593d8b06aaacd6e5e9eb9e84acff69ba038b merge: use separate lists for each action type This replaces the grand unified action list that had multiple action types as tuples in one big list. That list was iterated multiple times just to find actions of a specific type. This data model also made some code more convoluted than necessary. Instead we now store actions as a tuple of lists. Using multiple lists gives a bit of cut'n'pasted code but also enables other optimizations. This patch uses 'if True:' to preserve indentations and help reviewing. It also limits the number of conflicts with other pending patches. It can trivially be cleaned up later. diff -r 6062593d8b06 -r 43eecb4e23f8 hgext/largefiles/overrides.py --- a/hgext/largefiles/overrides.py Fri May 23 13:10:31 2014 -0700 +++ b/hgext/largefiles/overrides.py Fri Feb 28 02:25:58 2014 +0100 @@ -410,14 +410,13 @@ if overwrite: return actions - removes = set(a[0] for a in actions if a[1] == 'r') - processed = [] + removes = set(a[0] for a in actions['r']) - for action in actions: - f, m, args, msg = action - + newglist = [] + for action in actions['g']: + f, args, msg = action splitstandin = f and lfutil.splitstandin(f) - if (m == "g" and splitstandin is not None and + if (splitstandin is not None and splitstandin in p1 and splitstandin not in removes): # Case 1: normal file in the working copy, largefile in # the second parent @@ -427,12 +426,11 @@ 'use (l)argefile or keep (n)ormal file?' '$$ &Largefile $$ &Normal file') % lfile if repo.ui.promptchoice(msg, 0) == 0: - processed.append((lfile, "r", None, msg)) - processed.append((standin, "g", (p2.flags(standin),), msg)) + actions['r'].append((lfile, None, msg)) + newglist.append((standin, (p2.flags(standin),), msg)) else: - processed.append((standin, "r", None, msg)) - elif (m == "g" and - lfutil.standin(f) in p1 and lfutil.standin(f) not in removes): + actions['r'].append((standin, None, msg)) + elif lfutil.standin(f) in p1 and lfutil.standin(f) not in removes: # Case 2: largefile in the working copy, normal file in # the second parent standin = lfutil.standin(f) @@ -441,14 +439,17 @@ 'keep (l)argefile or use (n)ormal file?' '$$ &Largefile $$ &Normal file') % lfile if repo.ui.promptchoice(msg, 0) == 0: - processed.append((lfile, "r", None, msg)) + actions['r'].append((lfile, None, msg)) else: - processed.append((standin, "r", None, msg)) - processed.append((lfile, "g", (p2.flags(lfile),), msg)) + actions['r'].append((standin, None, msg)) + newglist.append((lfile, (p2.flags(lfile),), msg)) else: - processed.append(action) + newglist.append(action) - return processed + newglist.sort() + actions['g'] = newglist + + return actions # Override filemerge to prompt the user about how they wish to merge # largefiles. This will handle identical edits without prompting the user. diff -r 6062593d8b06 -r 43eecb4e23f8 mercurial/merge.py --- a/mercurial/merge.py Fri May 23 13:10:31 2014 -0700 +++ b/mercurial/merge.py Fri Feb 28 02:25:58 2014 +0100 @@ -331,62 +331,44 @@ as removed. """ - actions = [] - state = branchmerge and 'r' or 'f' + ractions = [] + factions = xactions = [] + if branchmerge: + xactions = ractions for f in wctx.deleted(): if f not in mctx: - actions.append((f, state, None, "forget deleted")) + xactions.append((f, None, "forget deleted")) if not branchmerge: for f in wctx.removed(): if f not in mctx: - actions.append((f, "f", None, "forget removed")) + factions.append((f, None, "forget removed")) - return actions + return ractions, factions def _checkcollision(repo, wmf, actions): # build provisional merged manifest up pmmf = set(wmf) - def addop(f, args): - pmmf.add(f) - def removeop(f, args): - pmmf.discard(f) - def nop(f, args): - pass - - def renamemoveop(f, args): - f2, flags = args - pmmf.discard(f2) - pmmf.add(f) - def renamegetop(f, args): - f2, flags = args - pmmf.add(f) - def mergeop(f, args): - f1, f2, fa, move, anc = args - if move: - pmmf.discard(f1) - pmmf.add(f) - - opmap = { - "a": addop, - "dm": renamemoveop, - "dg": renamegetop, - "dr": nop, - "e": nop, - "k": nop, - "f": addop, # untracked file should be kept in working directory - "g": addop, - "m": mergeop, - "r": removeop, - "rd": nop, - "cd": addop, - "dc": addop, - } - for f, m, args, msg in actions: - op = opmap.get(m) - assert op, m - op(f, args) + if actions: + # k, dr, e and rd are no-op + for m in 'a', 'f', 'g', 'cd', 'dc': + for f, args, msg in actions[m]: + pmmf.add(f) + for f, args, msg in actions['r']: + pmmf.discard(f) + for f, args, msg in actions['dm']: + f2, flags = args + pmmf.discard(f2) + pmmf.add(f) + for f, args, msg in actions['dg']: + f2, flags = args + pmmf.add(f) + for f, args, msg in actions['m']: + f1, f2, fa, move, anc = args + if move: + pmmf.discard(f1) + pmmf.add(f) # check case-folding collision in provisional merged manifest foldmap = {} @@ -407,7 +389,8 @@ acceptremote = accept the incoming changes without prompting """ - actions, copy, movewithdir = [], {}, {} + actions = dict((m, []) for m in 'a f g cd dc r dm dg m dr e rd k'.split()) + copy, movewithdir = {}, {} # manifests fetched in order are going to be faster, so prime the caches [x.manifest() for x in @@ -417,9 +400,9 @@ 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")) + actions['dr'].append((of, (fl,), "divergent renames")) for of, fl in renamedelete.iteritems(): - actions.append((of, "rd", (fl,), "rename and delete")) + actions['rd'].append((of, (fl,), "rename and delete")) repo.ui.note(_("resolving manifests\n")) repo.ui.debug(" branchmerge: %s, force: %s, partial: %s\n" @@ -471,50 +454,50 @@ fla = ma.flags(fa) nol = 'l' not in fl1 + fl2 + fla if n2 == a and fl2 == fla: - actions.append((f, "k", (), "keep")) # remote unchanged + actions['k'].append((f, (), "keep")) # remote unchanged elif n1 == a and fl1 == fla: # local unchanged - use remote if n1 == n2: # optimization: keep local content - actions.append((f, "e", (fl2,), "update permissions")) + actions['e'].append((f, (fl2,), "update permissions")) else: - actions.append((f, "g", (fl2,), "remote is newer")) + actions['g'].append((f, (fl2,), "remote is newer")) elif nol and n2 == a: # remote only changed 'x' - actions.append((f, "e", (fl2,), "update permissions")) + actions['e'].append((f, (fl2,), "update permissions")) elif nol and n1 == a: # local only changed 'x' - actions.append((f, "g", (fl1,), "remote is newer")) + actions['g'].append((f, (fl1,), "remote is newer")) else: # both changed something - actions.append((f, "m", (f, f, fa, False, pa.node()), + actions['m'].append((f, (f, f, fa, False, pa.node()), "versions differ")) elif f in copied: # files we'll deal with on m2 side pass elif n1 and f in movewithdir: # directory rename, move local f2 = movewithdir[f] - actions.append((f2, "dm", (f, fl1), + actions['dm'].append((f2, (f, fl1), "remote directory rename - move from " + f)) elif n1 and f in copy: f2 = copy[f] - actions.append((f, "m", (f, f2, f2, False, pa.node()), + actions['m'].append((f, (f, f2, f2, False, pa.node()), "local copied/moved from " + f2)) elif n1 and f in ma: # clean, a different, no remote if n1 != ma[f]: if acceptremote: - actions.append((f, "r", None, "remote delete")) + actions['r'].append((f, None, "remote delete")) else: - actions.append((f, "cd", None, "prompt changed/deleted")) + actions['cd'].append((f, None, "prompt changed/deleted")) elif n1[20:] == "a": # added, no remote - actions.append((f, "f", None, "remote deleted")) + actions['f'].append((f, None, "remote deleted")) else: - actions.append((f, "r", None, "other deleted")) + actions['r'].append((f, None, "other deleted")) elif n2 and f in movewithdir: f2 = movewithdir[f] - actions.append((f2, "dg", (f, fl2), + actions['dg'].append((f2, (f, fl2), "local directory rename - get from " + f)) elif n2 and f in copy: f2 = copy[f] if f2 in m2: - actions.append((f, "m", (f2, f, f2, False, pa.node()), + actions['m'].append((f, (f2, f, f2, False, pa.node()), "remote copied from " + f2)) else: - actions.append((f, "m", (f2, f, f2, True, pa.node()), + actions['m'].append((f, (f2, f, f2, True, pa.node()), "remote moved from " + f2)) elif n2 and f not in ma: # local unknown, remote created: the logic is described by the @@ -530,17 +513,17 @@ # 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")) + actions['g'].append((f, (fl2,), "remote created")) else: different = _checkunknownfile(repo, wctx, p2, f) if force and branchmerge and different: # FIXME: This is wrong - f is not in ma ... - actions.append((f, "m", (f, f, f, False, pa.node()), + actions['m'].append((f, (f, f, f, False, pa.node()), "remote differs from untracked local")) elif not force and different: aborts.append((f, "ud")) else: - actions.append((f, "g", (fl2,), "remote created")) + actions['g'].append((f, (fl2,), "remote created")) elif n2 and n2 != ma[f]: different = _checkunknownfile(repo, wctx, p2, f) if not force and different: @@ -548,10 +531,10 @@ else: # if different: old untracked f may be overwritten and lost if acceptremote: - actions.append((f, "g", (m2.flags(f),), + actions['g'].append((f, (m2.flags(f),), "remote recreating")) else: - actions.append((f, "dc", (m2.flags(f),), + actions['dc'].append((f, (m2.flags(f),), "prompt deleted/changed")) for f, m in sorted(aborts): @@ -566,18 +549,12 @@ # 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, []) + _checkcollision(repo, m2, None) else: _checkcollision(repo, m1, actions) return actions -actionpriority = dict((m, p) for p, m in enumerate( - ['r', 'f', 'g', 'a', 'k', 'm', 'dm', 'dg', 'dr', 'cd', 'dc', 'rd', 'e'])) - -def actionkey(a): - return actionpriority[a[1]], a - def batchremove(repo, actions): """apply removes to the working directory @@ -588,7 +565,7 @@ wjoin = repo.wjoin audit = repo.wopener.audit i = 0 - for f, m, args, msg in actions: + for f, args, msg in actions: repo.ui.debug(" %s: %s -> r\n" % (f, msg)) if True: if verbose: @@ -617,7 +594,7 @@ fctx = mctx.filectx wwrite = repo.wwrite i = 0 - for f, m, args, msg in actions: + for f, args, msg in actions: repo.ui.debug(" %s: %s -> g\n" % (f, msg)) if True: if verbose: @@ -644,12 +621,12 @@ ms = mergestate(repo) ms.reset(wctx.p1().node(), mctx.node()) moves = [] - actions.sort(key=actionkey) + for m, l in actions.items(): + l.sort() # prescan for merges - for a in actions: - f, m, args, msg = a - if m == "m": # merge + for f, args, msg in actions['m']: + if True: f1, f2, fa, move, anc = args if f == '.hgsubstate': # merged internally continue @@ -677,55 +654,50 @@ audit(f) util.unlinkpath(repo.wjoin(f)) - numupdates = len([a for a in actions if a[1] != 'k']) - workeractions = [a for a in actions if a[1] in 'gr'] - updateactions = [a for a in workeractions if a[1] == 'g'] - updated = len(updateactions) - removeactions = [a for a in workeractions if a[1] == 'r'] - removed = len(removeactions) - actions = [a for a in actions if a[1] not in 'gr'] + numupdates = sum(len(l) for m, l in actions.items() if m != 'k') - hgsub = [a[1] for a in workeractions if a[0] == '.hgsubstate'] - if hgsub and hgsub[0] == 'r': + if [a for a in actions['r'] if a[0] == '.hgsubstate']: subrepo.submerge(repo, wctx, mctx, wctx, overwrite) # remove in parallel (must come first) z = 0 - prog = worker.worker(repo.ui, 0.001, batchremove, (repo,), removeactions) + prog = worker.worker(repo.ui, 0.001, batchremove, (repo,), actions['r']) for i, item in prog: z += i progress(_updating, z, item=item, total=numupdates, unit=_files) + removed = len(actions['r']) # get in parallel - prog = worker.worker(repo.ui, 0.001, batchget, (repo, mctx), updateactions) + prog = worker.worker(repo.ui, 0.001, batchget, (repo, mctx), actions['g']) for i, item in prog: z += i progress(_updating, z, item=item, total=numupdates, unit=_files) + updated = len(actions['g']) - if hgsub and hgsub[0] == 'g': + if [a for a in actions['g'] if a[0] == '.hgsubstate']: subrepo.submerge(repo, wctx, mctx, wctx, overwrite) - for f, m, args, msg in actions: + if True: # forget (manifest only, just log it) (must come first) - if m == "f": + for f, args, msg in actions['f']: repo.ui.debug(" %s: %s -> f\n" % (f, msg)) z += 1 progress(_updating, z, item=f, total=numupdates, unit=_files) # re-add (manifest only, just log it) - elif m == "a": + for f, args, msg in actions['a']: repo.ui.debug(" %s: %s -> a\n" % (f, msg)) z += 1 progress(_updating, z, item=f, total=numupdates, unit=_files) # keep (noop, just log it) - elif m == "k": + for f, args, msg in actions['k']: repo.ui.debug(" %s: %s -> k\n" % (f, msg)) # no progress # merge - elif m == "m": + for f, args, msg in actions['m']: repo.ui.debug(" %s: %s -> m\n" % (f, msg)) z += 1 progress(_updating, z, item=f, total=numupdates, unit=_files) @@ -745,7 +717,7 @@ merged += 1 # directory rename, move local - elif m == "dm": + for f, args, msg in actions['dm']: repo.ui.debug(" %s: %s -> dm\n" % (f, msg)) z += 1 progress(_updating, z, item=f, total=numupdates, unit=_files) @@ -757,7 +729,7 @@ updated += 1 # local directory rename, get - elif m == "dg": + for f, args, msg in actions['dg']: repo.ui.debug(" %s: %s -> dg\n" % (f, msg)) z += 1 progress(_updating, z, item=f, total=numupdates, unit=_files) @@ -767,7 +739,7 @@ updated += 1 # divergent renames - elif m == "dr": + for f, args, msg in actions['dr']: repo.ui.debug(" %s: %s -> dr\n" % (f, msg)) z += 1 progress(_updating, z, item=f, total=numupdates, unit=_files) @@ -778,7 +750,7 @@ repo.ui.warn(" %s\n" % nf) # rename and delete - elif m == "rd": + for f, args, msg in actions['rd']: repo.ui.debug(" %s: %s -> rd\n" % (f, msg)) z += 1 progress(_updating, z, item=f, total=numupdates, unit=_files) @@ -789,7 +761,7 @@ repo.ui.warn(" %s\n" % nf) # exec - elif m == "e": + for f, args, msg in actions['e']: repo.ui.debug(" %s: %s -> e\n" % (f, msg)) z += 1 progress(_updating, z, item=f, total=numupdates, unit=_files) @@ -818,132 +790,127 @@ (wctx, mctx, _(' and ').join(str(anc) for anc in ancestors))) # Call for bids - fbids = {} # mapping filename to list af action bids + fbids = {} # mapping filename to bids (action method to list af actions) for ancestor in ancestors: repo.ui.note(_('\ncalculating bids for ancestor %s\n') % ancestor) actions = manifestmerge(repo, wctx, mctx, ancestor, branchmerge, force, partial, acceptremote, followcopies) - for a in sorted(actions, key=lambda a: (a[1], a)): - f, m, args, msg = a - repo.ui.debug(' %s: %s -> %s\n' % (f, msg, m)) - if f in fbids: - fbids[f].append(a) - else: - fbids[f] = [a] + for m, l in sorted(actions.items()): + for a in l: + f, args, msg = a + repo.ui.debug(' %s: %s -> %s\n' % (f, msg, m)) + if f in fbids: + d = fbids[f] + if m in d: + d[m].append(a) + else: + d[m] = [a] + else: + fbids[f] = {m: [a]} # Pick the best bid for each file repo.ui.note(_('\nauction for merging merge bids\n')) - actions = [] - for f, bidsl in sorted(fbids.items()): + actions = dict((m, []) for m in actions.keys()) + for f, bids in sorted(fbids.items()): + # bids is a mapping from action method to list af actions # Consensus? - a0 = bidsl[0] - if util.all(a == a0 for a in bidsl[1:]): # len(bidsl) is > 1 - repo.ui.note(" %s: consensus for %s\n" % (f, a0[1])) - actions.append(a0) - continue - # Group bids by kind of action - bids = {} - for a in bidsl: - m = a[1] - if m in bids: - bids[m].append(a) - else: - bids[m] = [a] + if len(bids) == 1: # all bids are the same kind of method + m, l = bids.items()[0] + if util.all(a == l[0] for a in l[1:]): # len(bids) is > 1 + repo.ui.note(" %s: consensus for %s\n" % (f, m)) + actions[m].append(l[0]) + continue # If keep is an option, just do it. if "k" in bids: repo.ui.note(" %s: picking 'keep' action\n" % f) - actions.append(bids["k"][0]) + actions['k'].append(bids["k"][0]) continue - # If all gets agree [how could they not?], just do it. + # If there are gets and they all agree [how could they not?], do it. if "g" in bids: ga0 = bids["g"][0] if util.all(a == ga0 for a in bids["g"][1:]): repo.ui.note(" %s: picking 'get' action\n" % f) - actions.append(ga0) + actions['g'].append(ga0) continue # TODO: Consider other simple actions such as mode changes # Handle inefficient democrazy. repo.ui.note(_(' %s: multiple bids for merge action:\n') % f) - for _f, m, args, msg in bidsl: - repo.ui.note(' %s -> %s\n' % (msg, m)) + for m, l in sorted(bids.items()): + for _f, args, msg in l: + repo.ui.note(' %s -> %s\n' % (msg, m)) # Pick random action. TODO: Instead, prompt user when resolving - a0 = bidsl[0] + m, l = bids.items()[0] repo.ui.warn(_(' %s: ambiguous merge - picked %s action\n') % - (f, a0[1])) - actions.append(a0) + (f, m)) + actions[m].append(l[0]) continue repo.ui.note(_('end of auction\n\n')) - # Filter out prompts. - newactions, prompts = [], [] - for a in actions: - if a[1] in ("cd", "dc"): - prompts.append(a) - else: - newactions.append(a) # Prompt and create actions. TODO: Move this towards resolve phase. - for f, m, args, msg in sorted(prompts): - if m == "cd": + if True: + for f, args, msg in actions['cd']: if repo.ui.promptchoice( _("local changed %s which remote deleted\n" "use (c)hanged version or (d)elete?" "$$ &Changed $$ &Delete") % f, 0): - newactions.append((f, "r", None, "prompt delete")) + actions['r'].append((f, None, "prompt delete")) else: - newactions.append((f, "a", None, "prompt keep")) - elif m == "dc": + actions['a'].append((f, None, "prompt keep")) + del actions['cd'][:] + + for f, args, msg in actions['dc']: flags, = args if repo.ui.promptchoice( _("remote changed %s which local deleted\n" "use (c)hanged version or leave (d)eleted?" "$$ &Changed $$ &Deleted") % f, 0) == 0: - newactions.append((f, "g", (flags,), "prompt recreating")) - else: assert False, m + actions['g'].append((f, (flags,), "prompt recreating")) + del actions['dc'][:] if wctx.rev() is None: - newactions += _forgetremoved(wctx, mctx, branchmerge) + ractions, factions = _forgetremoved(wctx, mctx, branchmerge) + actions['r'].extend(ractions) + actions['f'].extend(factions) - return newactions + return actions def recordupdates(repo, actions, branchmerge): "record merge actions to the dirstate" - - for f, m, args, msg in actions: - + if True: # remove (must come first) - if m == "r": # remove + for f, args, msg in actions['r']: if branchmerge: repo.dirstate.remove(f) else: repo.dirstate.drop(f) # forget (must come first) - elif m == "f": + for f, args, msg in actions['f']: repo.dirstate.drop(f) # re-add - elif m == "a": + for f, args, msg in actions['a']: if not branchmerge: repo.dirstate.add(f) # exec change - elif m == "e": + for f, args, msg in actions['e']: repo.dirstate.normallookup(f) # keep - elif m == "k": + for f, args, msg in actions['k']: pass # get - elif m == "g": + for f, args, msg in actions['g']: if branchmerge: repo.dirstate.otherparent(f) else: repo.dirstate.normal(f) # merge - elif m == "m": + for f, args, msg in actions['m']: f1, f2, fa, move, anc = args if branchmerge: # We've done a branch merge, mark this file as merged @@ -968,7 +935,7 @@ repo.dirstate.drop(f1) # directory rename, move local - elif m == "dm": + for f, args, msg in actions['dm']: f0, flag = args if f0 not in repo.dirstate: # untracked file moved @@ -982,7 +949,7 @@ repo.dirstate.drop(f0) # directory rename, get - elif m == "dg": + for f, args, msg in actions['dg']: f0, flag = args if branchmerge: repo.dirstate.add(f)