[PATCH 3 of 3] graft: apply get-only changes in-memory

David Schleimer dschleimer at fb.com
Mon Nov 19 14:01:47 CST 2012


# HG changeset patch
# User David Schleimer <dschleimer at fb.com>
# Date 1353354272 28800
# Node ID d4f1efa15442572e7ea658b50a02e093492797d7
# Parent  a77f8405c800c27ba5cb97b7482410c6438c81c8
graft: apply get-only changes in-memory

This changes the graft code to apply the simplest-possible changes
in-memory.  Specifically, changes where all we need to do is set the
contents of files to the version in the commit being grafted.  This
means that no files were deleted and no files were modified on by both
the target branch and the change we're grafting.

Based on one of our real-world branches, with 497 grafts,
approximately 80% of cherry-picked commits will fall into the fast
case here.  This patch takes a graft with all 497 revisions on the
branch in question from 5250 seconds to 3251 seconds.

The revisions that fall in the fast case were more common near the
beginning of the branch than the end.  Longer lived branches are
likely to see less of an improvement from this patch, but they should
be unlikely to see a regression.

diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -2823,45 +2823,20 @@
 
     wlock = repo.wlock()
     try:
+        current = repo['.']
         for pos, ctx in enumerate(repo.set("%ld", revs)):
-            current = repo['.']
 
             ui.status(_('grafting revision %s\n') % ctx.rev())
             if opts.get('dry_run'):
                 continue
 
-            # we don't merge the first commit when continuing
-            if not cont:
-                # perform the graft merge with p1(rev) as 'ancestor'
-                try:
-                    # ui.forcemerge is an internal variable, do not document
-                    repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
-                    stats = mergemod.update(repo, ctx.node(), True, True, False,
-                                            ctx.p1().node())
-                finally:
-                    repo.ui.setconfig('ui', 'forcemerge', '')
-                # report any conflicts
-                if stats and stats[3] > 0:
-                    # write out state for --continue
-                    nodelines = [repo[rev].hex() + "\n" for rev in revs[pos:]]
-                    repo.opener.write('graftstate', ''.join(nodelines))
-                    raise util.Abort(
-                        _("unresolved conflicts, can't continue"),
-                        hint=_('use hg resolve and hg graft --continue'))
-            else:
-                cont = False
-
-            # drop the second merge parent
-            repo.setparents(current.node(), nullid)
-            repo.dirstate.write()
-            # fix up dirstate for copies and renames
-            cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
-
-            # commit
             source = ctx.extra().get('source')
             if not source:
                 source = ctx.hex()
-            extra = {'source': source}
+            extra = {
+                'source': source,
+                'branch': current.extra().get('branch', 'default'),
+                }
             user = ctx.user()
             if opts.get('user'):
                 user = opts['user']
@@ -2871,10 +2846,114 @@
             message = ctx.description()
             if opts.get('log'):
                 message += '\n(grafted from %s)' % ctx.hex()
-            node = repo.commit(text=message, user=user,
-                        date=date, extra=extra, editor=editor)
+
+            memoryapply = False
+            # we don't merge the first commit when continuing
+            if not cont:
+                # perform the graft merge with p1(rev) as 'ancestor'
+
+                try:
+                    repo.ui.setconfig('ui', 'forcemerge',
+                                      opts.get('tool', ''))
+
+                    action = mergemod.calculateupdates(repo, current, ctx,
+                                                       ctx.p1(),
+                                                       branchmerge=True,
+                                                       force=True,
+                                                       partial=False)
+                finally:
+                    repo.ui.setconfig('ui', 'forcemerge', '')
+
+                supportedactions = set(['g'])
+
+                actionmap = {}
+                for a in action:
+                    if a[1] in supportedactions:
+                            memoryapply = True
+                            actionmap[a[0]] = a
+                    else:
+                        memoryapply = False
+                        break
+
+                if memoryapply:
+
+                    copymap = copies.pathcopies(ctx.p1(), ctx)
+
+                    def filectxfn(repo, memctx, path):
+                        a = actionmap[path]
+                        flags = a[2]
+                        islink = 'l' in flags
+                        isexec = 'x' in flags
+                        mfilectx = ctx.filectx(path)
+                        return context.memfilectx(path, mfilectx.data(),
+                                                  islink=islink,
+                                                  isexec=isexec,
+                                                  copied=copymap.get(path))
+
+
+                    memctx = context.memctx(repo, (current.node(), None),
+                                            text=message,
+                                            files=actionmap.keys(),
+                                            filectxfn=filectxfn,
+                                            user=user,
+                                            date=date,
+                                            extra=extra)
+                else:
+                    try:
+                        repo.ui.debug("falling back to on-disk merge\n")
+                        # first update to the current rev if we've
+                        # commited anything since the last update
+                        if current.node() != repo['.'].node():
+                            mergemod.update(repo, current.node(),
+                                            branchmerge=False,
+                                            force=True,
+                                            partial=None)
+                        # ui.forcemerge is an internal variable, do not document
+                        repo.ui.setconfig('ui', 'forcemerge',
+                                          opts.get('tool', ''))
+                        stats = mergemod.applyupdates(repo, action,
+                                                      wctx=repo[None],
+                                                      mctx=ctx,
+                                                      actx=ctx.p1(),
+                                                      overwrite=False)
+                        repo.setparents(current.node(), ctx.node())
+                        mergemod.recordupdates(repo, action, branchmerge=True)
+                    finally:
+                        repo.ui.setconfig('ui', 'forcemerge', '')
+                    # report any conflicts
+                    if stats and stats[3] > 0:
+                        # write out state for --continue
+                        nodelines = [repo[rev].hex() + "\n"
+                                     for rev in revs[pos:]]
+                        repo.opener.write('graftstate', ''.join(nodelines))
+                        raise util.Abort(
+                            _("unresolved conflicts, can't continue"),
+                            hint=_('use hg resolve and hg graft --continue'))
+
+            if cont or not memoryapply:
+                cont = False
+                repo.setparents(current.node(), nullid)
+                repo.dirstate.write()
+                # fix up dirstate for copies and renames
+                cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
+
+            # commit
+                node = repo.commit(text=message, user=user,
+                                   date=date, extra=extra, editor=editor)
+            else:
+                node = repo.commitctx(memctx)
+
             if node is None:
                 ui.status(_('graft for revision %s is empty\n') % ctx.rev())
+            else:
+                current = repo[node]
+
+        # if we've committed at least one revision, update to the most recent
+        if current.node() != repo['.'].node():
+            mergemod.update(repo, current.node(),
+                            branchmerge=False,
+                            force=True,
+                            partial=False)
     finally:
         wlock.release()
 
diff --git a/tests/test-graft.t b/tests/test-graft.t
--- a/tests/test-graft.t
+++ b/tests/test-graft.t
@@ -135,8 +135,9 @@
     checking for directory renames
   resolving manifests
    overwrite: False, partial: False
-   ancestor: 68795b066622, local: ef0ef43d49e7+, remote: 5d205f8b35b6
+   ancestor: 68795b066622, local: ef0ef43d49e7, remote: 5d205f8b35b6
    b: local copied/moved to a -> m
+  falling back to on-disk merge
   preserving b for resolve of b
   updating: b 1/1 files (100.00%)
   picked tool 'internal:merge' for b (binary False symlink False)
@@ -148,18 +149,23 @@
     searching for copies back to rev 1
   resolving manifests
    overwrite: False, partial: False
-   ancestor: 4c60f11aa304, local: 6b9e5368ca4e+, remote: 97f8bfe72746
+   ancestor: 4c60f11aa304, local: 6b9e5368ca4e, remote: 97f8bfe72746
    e: remote is newer -> g
-  updating: e 1/1 files (100.00%)
-  getting e
   e
   grafting revision 4
     searching for copies back to rev 1
   resolving manifests
    overwrite: False, partial: False
-   ancestor: 4c60f11aa304, local: 1905859650ec+, remote: 9c233e8e184d
+   ancestor: 4c60f11aa304, local: 1905859650ec, remote: 9c233e8e184d
    e: versions differ -> m
    d: remote is newer -> g
+  falling back to on-disk merge
+  resolving manifests
+   overwrite: True, partial: False
+   ancestor: 6b9e5368ca4e+, local: 6b9e5368ca4e+, remote: 1905859650ec
+   e: remote is newer -> g
+  updating: e 1/1 files (100.00%)
+  getting e
   preserving e for resolve of e
   updating: d 1/2 files (50.00%)
   getting d


More information about the Mercurial-devel mailing list