# HG changeset patch # User Gregory Szorc # Date 1520302236 28800 # Node ID 43ffd9070da153d811a37224600d9db80f230d5c # Parent aa5199c7aa42d6536567775b52d27d7ab1841a75 merge: use constants for actions We finish up establishing named constants in this file with actions. I remember scratching my head trying to figure out what this code was doing as part of addressing a recent security issue with subrepos. Having the named constants in place definitely makes things easier to read. I'm not convinced the new constants have the best names (I'm not an expert in this code). But they can be changed easily enough. Also, since these constants are internal only, we might want to change their values to something more human readable to facilitate debugging. Or maybe we could employ an enum type some day... Differential Revision: https://phab.mercurial-scm.org/D2701 diff -r aa5199c7aa42 -r 43ffd9070da1 mercurial/merge.py --- a/mercurial/merge.py Mon Mar 05 14:21:57 2018 -0500 +++ b/mercurial/merge.py Mon Mar 05 18:10:36 2018 -0800 @@ -71,6 +71,23 @@ MERGE_RECORD_RESOLVED_PATH = b'pr' MERGE_RECORD_DRIVER_RESOLVED = b'd' +ACTION_FORGET = b'f' +ACTION_REMOVE = b'r' +ACTION_ADD = b'a' +ACTION_GET = b'g' +ACTION_PATH_CONFLICT = b'p' +ACTION_PATH_CONFLICT_RESOLVE = b'pr' +ACTION_ADD_MODIFIED = b'am' +ACTION_CREATED = b'c' +ACTION_DELETED_CHANGED = b'dc' +ACTION_CHANGED_DELETED = b'cd' +ACTION_MERGE = b'm' +ACTION_LOCAL_DIR_RENAME_GET = b'dg' +ACTION_DIR_RENAME_MOVE_LOCAL = b'dm' +ACTION_KEEP = b'k' +ACTION_EXEC = b'e' +ACTION_CREATED_MERGE = b'cm' + class mergestate(object): '''track 3-way merge state of individual files @@ -588,18 +605,18 @@ if fcd.isabsent(): # dc: local picked. Need to drop if present, which may # happen on re-resolves. - action = 'f' + action = ACTION_FORGET else: # cd: remote picked (or otherwise deleted) - action = 'r' + action = ACTION_REMOVE else: if fcd.isabsent(): # dc: remote picked - action = 'g' + action = ACTION_GET elif fco.isabsent(): # cd: local picked if dfile in self.localctx: - action = 'am' + action = ACTION_ADD_MODIFIED else: - action = 'a' + action = ACTION_ADD # else: regular merges (no action necessary) self._results[dfile] = r, action @@ -631,7 +648,7 @@ if r is None: updated += 1 elif r == 0: - if action == 'r': + if action == ACTION_REMOVE: removed += 1 else: merged += 1 @@ -643,7 +660,13 @@ def actions(self): """return lists of actions to perform on the dirstate""" - actions = {'r': [], 'f': [], 'a': [], 'am': [], 'g': []} + actions = { + ACTION_REMOVE: [], + ACTION_FORGET: [], + ACTION_ADD: [], + ACTION_ADD_MODIFIED: [], + ACTION_GET: [], + } for f, (r, action) in self._results.iteritems(): if action is not None: actions[action].append((f, None, "merge result")) @@ -658,19 +681,19 @@ """queues a file to be removed from the dirstate Meant for use by custom merge drivers.""" - self._results[f] = 0, 'r' + self._results[f] = 0, ACTION_REMOVE def queueadd(self, f): """queues a file to be added to the dirstate Meant for use by custom merge drivers.""" - self._results[f] = 0, 'a' + self._results[f] = 0, ACTION_ADD def queueget(self, f): """queues a file to be marked modified in the dirstate Meant for use by custom merge drivers.""" - self._results[f] = 0, 'g' + self._results[f] = 0, ACTION_GET def _getcheckunknownconfig(repo, section, name): config = repo.ui.config(section, name) @@ -772,14 +795,14 @@ checkunknowndirs = _unknowndirschecker() for f, (m, args, msg) in actions.iteritems(): - if m in ('c', 'dc'): + if m in (ACTION_CREATED, ACTION_DELETED_CHANGED): if _checkunknownfile(repo, wctx, mctx, f): fileconflicts.add(f) elif pathconfig and f not in wctx: path = checkunknowndirs(repo, wctx, f) if path is not None: pathconflicts.add(path) - elif m == 'dg': + elif m == ACTION_LOCAL_DIR_RENAME_GET: if _checkunknownfile(repo, wctx, mctx, f, args[0]): fileconflicts.add(f) @@ -791,7 +814,7 @@ collectconflicts(unknownconflicts, unknownconfig) else: for f, (m, args, msg) in actions.iteritems(): - if m == 'cm': + if m == ACTION_CREATED_MERGE: fl2, anc = args different = _checkunknownfile(repo, wctx, mctx, f) if repo.dirstate._ignore(f): @@ -812,16 +835,16 @@ # don't like an abort happening in the middle of # merge.update. if not different: - actions[f] = ('g', (fl2, False), "remote created") + actions[f] = (ACTION_GET, (fl2, False), 'remote created') elif mergeforce or config == 'abort': - actions[f] = ('m', (f, f, None, False, anc), - "remote differs from untracked local") + actions[f] = (ACTION_MERGE, (f, f, None, False, anc), + 'remote differs from untracked local') elif config == 'abort': abortconflicts.add(f) else: if config == 'warn': warnconflicts.add(f) - actions[f] = ('g', (fl2, True), "remote created") + actions[f] = (ACTION_GET, (fl2, True), 'remote created') for f in sorted(abortconflicts): warn = repo.ui.warn @@ -843,11 +866,11 @@ repo.ui.warn(_("%s: replacing untracked files in directory\n") % f) for f, (m, args, msg) in actions.iteritems(): - if m == 'c': + if m == ACTION_CREATED: backup = (f in fileconflicts or f in pathconflicts or any(p in pathconflicts for p in util.finddirs(f))) flags, = args - actions[f] = ('g', (flags, backup), msg) + actions[f] = (ACTION_GET, (flags, backup), msg) def _forgetremoved(wctx, mctx, branchmerge): """ @@ -865,9 +888,9 @@ """ actions = {} - m = 'f' + m = ACTION_FORGET if branchmerge: - m = 'r' + m = ACTION_REMOVE for f in wctx.deleted(): if f not in mctx: actions[f] = m, None, "forget deleted" @@ -875,7 +898,7 @@ if not branchmerge: for f in wctx.removed(): if f not in mctx: - actions[f] = 'f', None, "forget removed" + actions[f] = ACTION_FORGET, None, "forget removed" return actions @@ -884,19 +907,20 @@ pmmf = set(wmf) if actions: - # k, dr, e and rd are no-op - for m in 'a', 'am', 'f', 'g', 'cd', 'dc': + # KEEP and EXEC are no-op + for m in (ACTION_ADD, ACTION_ADD_MODIFIED, ACTION_FORGET, ACTION_GET, + ACTION_CHANGED_DELETED, ACTION_DELETED_CHANGED): for f, args, msg in actions[m]: pmmf.add(f) - for f, args, msg in actions['r']: + for f, args, msg in actions[ACTION_REMOVE]: pmmf.discard(f) - for f, args, msg in actions['dm']: + for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]: f2, flags = args pmmf.discard(f2) pmmf.add(f) - for f, args, msg in actions['dg']: + for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]: pmmf.add(f) - for f, args, msg in actions['m']: + for f, args, msg in actions[ACTION_MERGE]: f1, f2, fa, move, anc = args if move: pmmf.discard(f1) @@ -972,7 +996,8 @@ deletedfiles = set() for f, (m, args, msg) in actions.items(): - if m in ('c', 'dc', 'm', 'cm'): + if m in (ACTION_CREATED, ACTION_DELETED_CHANGED, ACTION_MERGE, + ACTION_CREATED_MERGE): # This action may create a new local file. createdfiledirs.update(util.finddirs(f)) if mf.hasdir(f): @@ -981,13 +1006,13 @@ # will be checked once we know what all the deleted files are. remoteconflicts.add(f) # Track the names of all deleted files. - if m == 'r': + if m == ACTION_REMOVE: deletedfiles.add(f) - if m == 'm': + if m == ACTION_MERGE: f1, f2, fa, move, anc = args if move: deletedfiles.add(f1) - if m == 'dm': + if m == ACTION_DIR_RENAME_MOVE_LOCAL: f2, flags = args deletedfiles.add(f2) @@ -1003,7 +1028,10 @@ # A file is in a directory which aliases a local file. # We will need to rename the local file. localconflicts.add(p) - if p in actions and actions[p][0] in ('c', 'dc', 'm', 'cm'): + if p in actions and actions[p][0] in (ACTION_CREATED, + ACTION_DELETED_CHANGED, + ACTION_MERGE, + ACTION_CREATED_MERGE): # The file is in a directory which aliases a remote file. # This is an internal inconsistency within the remote # manifest. @@ -1014,8 +1042,10 @@ if p not in deletedfiles: ctxname = bytes(wctx).rstrip('+') pnew = util.safename(p, ctxname, wctx, set(actions.keys())) - actions[pnew] = ('pr', (p,), "local path conflict") - actions[p] = ('p', (pnew, 'l'), "path conflict") + actions[pnew] = (ACTION_PATH_CONFLICT_RESOLVE, (p,), + 'local path conflict') + actions[p] = (ACTION_PATH_CONFLICT, (pnew, 'l'), + 'path conflict') if remoteconflicts: # Check if all files in the conflicting directories have been removed. @@ -1024,14 +1054,16 @@ if f not in deletedfiles: m, args, msg = actions[p] pnew = util.safename(p, ctxname, wctx, set(actions.keys())) - if m in ('dc', 'm'): + if m in (ACTION_DELETED_CHANGED, ACTION_MERGE): # Action was merge, just update target. actions[pnew] = (m, args, msg) else: # Action was create, change to renamed get action. fl = args[0] - actions[pnew] = ('dg', (p, fl), "remote path conflict") - actions[p] = ('p', (pnew, 'r'), "path conflict") + actions[pnew] = (ACTION_LOCAL_DIR_RENAME_GET, (p, fl), + 'remote path conflict') + actions[p] = (ACTION_PATH_CONFLICT, (pnew, ACTION_REMOVE), + 'path conflict') remoteconflicts.remove(p) break @@ -1109,77 +1141,80 @@ if f not in ma: fa = copy.get(f, None) if fa is not None: - actions[f] = ('m', (f, f, fa, False, pa.node()), - "both renamed from " + fa) + actions[f] = (ACTION_MERGE, (f, f, fa, False, pa.node()), + 'both renamed from %s' % fa) else: - actions[f] = ('m', (f, f, None, False, pa.node()), - "both created") + actions[f] = (ACTION_MERGE, (f, f, None, False, pa.node()), + 'both created') else: a = ma[f] fla = ma.flags(f) nol = 'l' not in fl1 + fl2 + fla if n2 == a and fl2 == fla: - actions[f] = ('k', (), "remote unchanged") + actions[f] = (ACTION_KEEP, (), 'remote unchanged') elif n1 == a and fl1 == fla: # local unchanged - use remote if n1 == n2: # optimization: keep local content - actions[f] = ('e', (fl2,), "update permissions") + actions[f] = (ACTION_EXEC, (fl2,), 'update permissions') else: - actions[f] = ('g', (fl2, False), "remote is newer") + actions[f] = (ACTION_GET, (fl2, False), + 'remote is newer') elif nol and n2 == a: # remote only changed 'x' - actions[f] = ('e', (fl2,), "update permissions") + actions[f] = (ACTION_EXEC, (fl2,), 'update permissions') elif nol and n1 == a: # local only changed 'x' - actions[f] = ('g', (fl1, False), "remote is newer") + actions[f] = (ACTION_GET, (fl1, False), 'remote is newer') else: # both changed something - actions[f] = ('m', (f, f, f, False, pa.node()), - "versions differ") + actions[f] = (ACTION_MERGE, (f, f, f, False, pa.node()), + 'versions differ') elif n1: # file exists only on local side if f in copied: pass # we'll deal with it on m2 side elif f in movewithdir: # directory rename, move local f2 = movewithdir[f] if f2 in m2: - actions[f2] = ('m', (f, f2, None, True, pa.node()), - "remote directory rename, both created") + actions[f2] = (ACTION_MERGE, (f, f2, None, True, pa.node()), + 'remote directory rename, both created') else: - actions[f2] = ('dm', (f, fl1), - "remote directory rename - move from " + f) + actions[f2] = (ACTION_DIR_RENAME_MOVE_LOCAL, (f, fl1), + 'remote directory rename - move from %s' % f) elif f in copy: f2 = copy[f] - actions[f] = ('m', (f, f2, f2, False, pa.node()), - "local copied/moved from " + f2) + actions[f] = (ACTION_MERGE, (f, f2, f2, False, pa.node()), + 'local copied/moved from %s' % f2) elif f in ma: # clean, a different, no remote if n1 != ma[f]: if acceptremote: - actions[f] = ('r', None, "remote delete") + actions[f] = (ACTION_REMOVE, None, 'remote delete') else: - actions[f] = ('cd', (f, None, f, False, pa.node()), - "prompt changed/deleted") + actions[f] = (ACTION_CHANGED_DELETED, + (f, None, f, False, pa.node()), + 'prompt changed/deleted') elif n1 == addednodeid: # This extra 'a' is added by working copy manifest to mark # the file as locally added. We should forget it instead of # deleting it. - actions[f] = ('f', None, "remote deleted") + actions[f] = (ACTION_FORGET, None, 'remote deleted') else: - actions[f] = ('r', None, "other deleted") + actions[f] = (ACTION_REMOVE, None, 'other deleted') elif n2: # file exists only on remote side if f in copied: pass # we'll deal with it on m1 side elif f in movewithdir: f2 = movewithdir[f] if f2 in m1: - actions[f2] = ('m', (f2, f, None, False, pa.node()), - "local directory rename, both created") + actions[f2] = (ACTION_MERGE, + (f2, f, None, False, pa.node()), + 'local directory rename, both created') else: - actions[f2] = ('dg', (f, fl2), - "local directory rename - get from " + f) + actions[f2] = (ACTION_LOCAL_DIR_RENAME_GET, (f, fl2), + 'local directory rename - get from %s' % f) elif f in copy: f2 = copy[f] if f2 in m2: - actions[f] = ('m', (f2, f, f2, False, pa.node()), - "remote copied from " + f2) + actions[f] = (ACTION_MERGE, (f2, f, f2, False, pa.node()), + 'remote copied from %s' % f2) else: - actions[f] = ('m', (f2, f, f2, True, pa.node()), - "remote moved from " + f2) + actions[f] = (ACTION_MERGE, (f2, f, f2, True, pa.node()), + 'remote moved from %s' % f2) elif f not in ma: # local unknown, remote created: the logic is described by the # following table: @@ -1193,12 +1228,12 @@ # Checking whether the files are different is expensive, so we # don't do that when we can avoid it. if not force: - actions[f] = ('c', (fl2,), "remote created") + actions[f] = (ACTION_CREATED, (fl2,), 'remote created') elif not branchmerge: - actions[f] = ('c', (fl2,), "remote created") + actions[f] = (ACTION_CREATED, (fl2,), 'remote created') else: - actions[f] = ('cm', (fl2, pa.node()), - "remote created, get or merge") + actions[f] = (ACTION_CREATED_MERGE, (fl2, pa.node()), + 'remote created, get or merge') elif n2 != ma[f]: df = None for d in dirmove: @@ -1207,13 +1242,15 @@ df = dirmove[d] + f[len(d):] break if df is not None and df in m1: - actions[df] = ('m', (df, f, f, False, pa.node()), - "local directory rename - respect move from " + f) + actions[df] = (ACTION_MERGE, (df, f, f, False, pa.node()), + 'local directory rename - respect move ' + 'from %s' % f) elif acceptremote: - actions[f] = ('c', (fl2,), "remote recreating") + actions[f] = (ACTION_CREATED, (fl2,), 'remote recreating') else: - actions[f] = ('dc', (None, f, f, False, pa.node()), - "prompt deleted/changed") + actions[f] = (ACTION_DELETED_CHANGED, + (None, f, f, False, pa.node()), + 'prompt deleted/changed') if repo.ui.configbool('experimental', 'merge.checkpathconflicts'): # If we are merging, look for path conflicts. @@ -1227,10 +1264,12 @@ # We force a copy of actions.items() because we're going to mutate # actions as we resolve trivial conflicts. for f, (m, args, msg) in list(actions.items()): - if m == 'cd' and f in ancestor and not wctx[f].cmp(ancestor[f]): + if (m == ACTION_CHANGED_DELETED and f in ancestor + and not wctx[f].cmp(ancestor[f])): # local did change but ended up with same content - actions[f] = 'r', None, "prompt same" - elif m == 'dc' and f in ancestor and not mctx[f].cmp(ancestor[f]): + actions[f] = ACTION_REMOVE, None, 'prompt same' + elif (m == ACTION_DELETED_CHANGED and f in ancestor + and not mctx[f].cmp(ancestor[f])): # remote did change but ended up with same content del actions[f] # don't get = keep local deleted @@ -1294,18 +1333,18 @@ if all(a == l[0] for a in l[1:]): # len(bids) is > 1 repo.ui.note(_(" %s: consensus for %s\n") % (f, m)) actions[f] = l[0] - if m == 'dm': + if m == ACTION_DIR_RENAME_MOVE_LOCAL: dms.append(f) continue # If keep is an option, just do it. - if 'k' in bids: + if ACTION_KEEP in bids: repo.ui.note(_(" %s: picking 'keep' action\n") % f) - actions[f] = bids['k'][0] + actions[f] = bids[ACTION_KEEP][0] continue # If there are gets and they all agree [how could they not?], do it. - if 'g' in bids: - ga0 = bids['g'][0] - if all(a == ga0 for a in bids['g'][1:]): + if ACTION_GET in bids: + ga0 = bids[ACTION_GET][0] + if all(a == ga0 for a in bids[ACTION_GET][1:]): repo.ui.note(_(" %s: picking 'get' action\n") % f) actions[f] = ga0 continue @@ -1320,14 +1359,14 @@ repo.ui.warn(_(' %s: ambiguous merge - picked %s action\n') % (f, m)) actions[f] = l[0] - if m == 'dm': + if m == ACTION_DIR_RENAME_MOVE_LOCAL: dms.append(f) continue # Work around 'dm' that can cause multiple actions for the same file for f in dms: dm, (f0, flags), msg = actions[f] - assert dm == 'dm', dm - if f0 in actions and actions[f0][0] == 'r': + assert dm == ACTION_DIR_RENAME_MOVE_LOCAL, dm + if f0 in actions and actions[f0][0] == ACTION_REMOVE: # We have one bid for removing a file and another for moving it. # These two could be merged as first move and then delete ... # but instead drop moving and just delete. @@ -1432,7 +1471,8 @@ # Skipping 'a', 'am', 'f', 'r', 'dm', 'e', 'k', 'p' and 'pr', because they # don't touch the context to be merged in. 'cd' is skipped, because # changed/deleted never resolves to something from the remote side. - oplist = [actions[a] for a in 'g dc dg m'.split()] + oplist = [actions[a] for a in (ACTION_GET, ACTION_DELETED_CHANGED, + ACTION_LOCAL_DIR_RENAME_GET, ACTION_MERGE)] prefetch = scmutil.fileprefetchhooks prefetch(repo, ctx, [f for sublist in oplist for f, args, msg in sublist]) @@ -1479,9 +1519,9 @@ l.sort() # 'cd' and 'dc' actions are treated like other merge conflicts - mergeactions = sorted(actions['cd']) - mergeactions.extend(sorted(actions['dc'])) - mergeactions.extend(actions['m']) + mergeactions = sorted(actions[ACTION_CHANGED_DELETED]) + mergeactions.extend(sorted(actions[ACTION_DELETED_CHANGED])) + mergeactions.extend(actions[ACTION_MERGE]) for f, args, msg in mergeactions: f1, f2, fa, move, anc = args if f == '.hgsubstate': # merged internally @@ -1516,14 +1556,15 @@ wctx[f].audit() wctx[f].remove() - numupdates = sum(len(l) for m, l in actions.items() if m != 'k') + numupdates = sum(len(l) for m, l in actions.items() + if m != ACTION_KEEP) z = 0 - if [a for a in actions['r'] if a[0] == '.hgsubstate']: + if [a for a in actions[ACTION_REMOVE] if a[0] == '.hgsubstate']: subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels) # record path conflicts - for f, args, msg in actions['p']: + for f, args, msg in actions[ACTION_PATH_CONFLICT]: f1, fo = args s = repo.ui.status s(_("%s: path conflict - a file or link has the same name as a " @@ -1543,14 +1584,14 @@ # remove in parallel (must come before resolving path conflicts and getting) prog = worker.worker(repo.ui, cost, batchremove, (repo, wctx), - actions['r']) + actions[ACTION_REMOVE]) for i, item in prog: z += i progress(_updating, z, item=item, total=numupdates, unit=_files) - removed = len(actions['r']) + removed = len(actions[ACTION_REMOVE]) # resolve path conflicts (must come before getting) - for f, args, msg in actions['pr']: + for f, args, msg in actions[ACTION_PATH_CONFLICT_RESOLVE]: repo.ui.debug(" %s: %s -> pr\n" % (f, msg)) f0, = args if wctx[f0].lexists(): @@ -1563,40 +1604,40 @@ # get in parallel prog = worker.worker(repo.ui, cost, batchget, (repo, mctx, wctx), - actions['g']) + actions[ACTION_GET]) for i, item in prog: z += i progress(_updating, z, item=item, total=numupdates, unit=_files) - updated = len(actions['g']) + updated = len(actions[ACTION_GET]) - if [a for a in actions['g'] if a[0] == '.hgsubstate']: + if [a for a in actions[ACTION_GET] if a[0] == '.hgsubstate']: subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels) # forget (manifest only, just log it) (must come first) - for f, args, msg in actions['f']: + for f, args, msg in actions[ACTION_FORGET]: 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) - for f, args, msg in actions['a']: + for f, args, msg in actions[ACTION_ADD]: repo.ui.debug(" %s: %s -> a\n" % (f, msg)) z += 1 progress(_updating, z, item=f, total=numupdates, unit=_files) # re-add/mark as modified (manifest only, just log it) - for f, args, msg in actions['am']: + for f, args, msg in actions[ACTION_ADD_MODIFIED]: repo.ui.debug(" %s: %s -> am\n" % (f, msg)) z += 1 progress(_updating, z, item=f, total=numupdates, unit=_files) # keep (noop, just log it) - for f, args, msg in actions['k']: + for f, args, msg in actions[ACTION_KEEP]: repo.ui.debug(" %s: %s -> k\n" % (f, msg)) # no progress # directory rename, move local - for f, args, msg in actions['dm']: + for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]: repo.ui.debug(" %s: %s -> dm\n" % (f, msg)) z += 1 progress(_updating, z, item=f, total=numupdates, unit=_files) @@ -1608,7 +1649,7 @@ updated += 1 # local directory rename, get - for f, args, msg in actions['dg']: + for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]: repo.ui.debug(" %s: %s -> dg\n" % (f, msg)) z += 1 progress(_updating, z, item=f, total=numupdates, unit=_files) @@ -1618,7 +1659,7 @@ updated += 1 # exec - for f, args, msg in actions['e']: + for f, args, msg in actions[ACTION_EXEC]: repo.ui.debug(" %s: %s -> e\n" % (f, msg)) z += 1 progress(_updating, z, item=f, total=numupdates, unit=_files) @@ -1696,17 +1737,17 @@ extraactions = ms.actions() if extraactions: - mfiles = set(a[0] for a in actions['m']) + mfiles = set(a[0] for a in actions[ACTION_MERGE]) for k, acts in extraactions.iteritems(): actions[k].extend(acts) - # Remove these files from actions['m'] as well. This is important - # because in recordupdates, files in actions['m'] are processed - # after files in other actions, and the merge driver might add - # files to those actions via extraactions above. This can lead to a - # file being recorded twice, with poor results. This is especially - # problematic for actions['r'] (currently only possible with the - # merge driver in the initial merge process; interrupted merges - # don't go through this flow). + # Remove these files from actions[ACTION_MERGE] as well. This is + # important because in recordupdates, files in actions[ACTION_MERGE] + # are processed after files in other actions, and the merge driver + # might add files to those actions via extraactions above. This can + # lead to a file being recorded twice, with poor results. This is + # especially problematic for actions[ACTION_REMOVE] (currently only + # possible with the merge driver in the initial merge process; + # interrupted merges don't go through this flow). # # The real fix here is to have indexes by both file and action so # that when the action for a file is changed it is automatically @@ -1717,7 +1758,8 @@ # those lists aren't consulted again. mfiles.difference_update(a[0] for a in acts) - actions['m'] = [a for a in actions['m'] if a[0] in mfiles] + actions[ACTION_MERGE] = [a for a in actions[ACTION_MERGE] + if a[0] in mfiles] progress(_updating, None, total=numupdates, unit=_files) return updateresult(updated, merged, removed, unresolved) @@ -1725,18 +1767,18 @@ def recordupdates(repo, actions, branchmerge): "record merge actions to the dirstate" # remove (must come first) - for f, args, msg in actions.get('r', []): + for f, args, msg in actions.get(ACTION_REMOVE, []): if branchmerge: repo.dirstate.remove(f) else: repo.dirstate.drop(f) # forget (must come first) - for f, args, msg in actions.get('f', []): + for f, args, msg in actions.get(ACTION_FORGET, []): repo.dirstate.drop(f) # resolve path conflicts - for f, args, msg in actions.get('pr', []): + for f, args, msg in actions.get(ACTION_PATH_CONFLICT_RESOLVE, []): f0, = args origf0 = repo.dirstate.copied(f0) or f0 repo.dirstate.add(f) @@ -1747,33 +1789,33 @@ repo.dirstate.drop(f0) # re-add - for f, args, msg in actions.get('a', []): + for f, args, msg in actions.get(ACTION_ADD, []): repo.dirstate.add(f) # re-add/mark as modified - for f, args, msg in actions.get('am', []): + for f, args, msg in actions.get(ACTION_ADD_MODIFIED, []): if branchmerge: repo.dirstate.normallookup(f) else: repo.dirstate.add(f) # exec change - for f, args, msg in actions.get('e', []): + for f, args, msg in actions.get(ACTION_EXEC, []): repo.dirstate.normallookup(f) # keep - for f, args, msg in actions.get('k', []): + for f, args, msg in actions.get(ACTION_KEEP, []): pass # get - for f, args, msg in actions.get('g', []): + for f, args, msg in actions.get(ACTION_GET, []): if branchmerge: repo.dirstate.otherparent(f) else: repo.dirstate.normal(f) # merge - for f, args, msg in actions.get('m', []): + for f, args, msg in actions.get(ACTION_MERGE, []): f1, f2, fa, move, anc = args if branchmerge: # We've done a branch merge, mark this file as merged @@ -1798,7 +1840,7 @@ repo.dirstate.drop(f1) # directory rename, move local - for f, args, msg in actions.get('dm', []): + for f, args, msg in actions.get(ACTION_DIR_RENAME_MOVE_LOCAL, []): f0, flag = args if branchmerge: repo.dirstate.add(f) @@ -1809,7 +1851,7 @@ repo.dirstate.drop(f0) # directory rename, get - for f, args, msg in actions.get('dg', []): + for f, args, msg in actions.get(ACTION_LOCAL_DIR_RENAME_GET, []): f0, flag = args if branchmerge: repo.dirstate.add(f) @@ -1982,7 +2024,8 @@ if updatecheck == 'noconflict': for f, (m, args, msg) in actionbyfile.iteritems(): - if m not in ('g', 'k', 'e', 'r', 'pr'): + if m not in (ACTION_GET, ACTION_KEEP, ACTION_EXEC, + ACTION_REMOVE, ACTION_PATH_CONFLICT_RESOLVE): msg = _("conflicting changes") hint = _("commit or update --clean to discard changes") raise error.Abort(msg, hint=hint) @@ -1995,30 +2038,45 @@ m, args, msg = actionbyfile[f] prompts = filemerge.partextras(labels) prompts['f'] = f - if m == 'cd': + if m == ACTION_CHANGED_DELETED: if repo.ui.promptchoice( _("local%(l)s changed %(f)s which other%(o)s deleted\n" "use (c)hanged version or (d)elete?" "$$ &Changed $$ &Delete") % prompts, 0): - actionbyfile[f] = ('r', None, "prompt delete") + actionbyfile[f] = (ACTION_REMOVE, None, 'prompt delete') elif f in p1: - actionbyfile[f] = ('am', None, "prompt keep") + actionbyfile[f] = (ACTION_ADD_MODIFIED, None, 'prompt keep') else: - actionbyfile[f] = ('a', None, "prompt keep") - elif m == 'dc': + actionbyfile[f] = (ACTION_ADD, None, 'prompt keep') + elif m == ACTION_DELETED_CHANGED: f1, f2, fa, move, anc = args flags = p2[f2].flags() if repo.ui.promptchoice( _("other%(o)s changed %(f)s which local%(l)s deleted\n" "use (c)hanged version or leave (d)eleted?" "$$ &Changed $$ &Deleted") % prompts, 0) == 0: - actionbyfile[f] = ('g', (flags, False), "prompt recreating") + actionbyfile[f] = (ACTION_GET, (flags, False), + 'prompt recreating') else: del actionbyfile[f] # Convert to dictionary-of-lists format actions = dict((m, []) - for m in 'a am f g cd dc r dm dg m e k p pr'.split()) + for m in ( + ACTION_ADD, + ACTION_ADD_MODIFIED, + ACTION_FORGET, + ACTION_GET, + ACTION_CHANGED_DELETED, + ACTION_DELETED_CHANGED, + ACTION_REMOVE, + ACTION_DIR_RENAME_MOVE_LOCAL, + ACTION_LOCAL_DIR_RENAME_GET, + ACTION_MERGE, + ACTION_EXEC, + ACTION_KEEP, + ACTION_PATH_CONFLICT, + ACTION_PATH_CONFLICT_RESOLVE)) for f, (m, args, msg) in actionbyfile.iteritems(): if m not in actions: actions[m] = [] @@ -2081,7 +2139,7 @@ if (fsmonitorwarning and not fsmonitorenabled and p1.node() == nullid - and len(actions['g']) >= fsmonitorthreshold + and len(actions[ACTION_GET]) >= fsmonitorthreshold and pycompat.sysplatform.startswith(('linux', 'darwin'))): repo.ui.warn( _('(warning: large working directory being used without '