changeset 37115:43ffd9070da1

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
author Gregory Szorc <gregory.szorc@gmail.com>
date Mon, 05 Mar 2018 18:10:36 -0800
parents aa5199c7aa42
children 704932ef8913
files mercurial/merge.py
diffstat 1 files changed, 197 insertions(+), 139 deletions(-) [+]
line wrap: on
line diff
--- 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 '