[PATCH] record: use in-memory patching for recording changes (fixes #3591)

michalsznajder at gmail.com michalsznajder at gmail.com
Mon Aug 27 14:29:13 CDT 2012


# HG changeset patch
# User Michal Sznajder <michalsznajder at gmail.com>
# Date 1345485746 -7200
# Node ID afe2f7b7a7fdd131827c889e472e01564f21f63d
# Parent  99a2a4ae35e2180b7f825ef2677c36d538eac4ba
record: use in-memory patching for recording changes (fixes #3591)

record extensions was using revert and patching of working directory to achieve
its work. This led to bugs in the past and is somewhat ridiculous as an
implementation method.

Solution is to use in-memory commit context and create a revison without
touching working directory.

=================
Notes on changes:
- test-record.t passes without one small tests (when someone breaks patch
  while editing it - i'll try to fix it later)
- patch.filestore(): do we want to get rid of it and do everything in memeory? how?
- repo.setparents(newrev): is this good way to update to given revison?

diff --git a/hgext/record.py b/hgext/record.py
--- a/hgext/record.py
+++ b/hgext/record.py
@@ -497,20 +497,14 @@
                          cmdsuggest)
 
     def recordfunc(ui, repo, message, match, opts):
         """This is generic record driver.
 
-        Its job is to interactively filter local changes, and
-        accordingly prepare working directory into a state in which the
-        job can be delegated to a non-interactive commit command such as
-        'commit' or 'qrefresh'.
+        Its job is to interactively filter local changes, and create 
+        new revision out of selected ones.
 
-        After the actual job is done by non-interactive command, the
-        working directory is restored to its original state.
-
-        In the end we'll record interesting changes, and everything else
-        will be left in place, so the user can continue working.
+        In the end working directory is updated to newly created revison.
         """
 
         merge = len(repo[None].parents()) > 1
         if merge:
             raise util.Abort(_('cannot partially commit a merge '
@@ -525,110 +519,54 @@
         chunks = patch.diff(repo, changes=changes, opts=diffopts)
         fp = cStringIO.StringIO()
         fp.write(''.join(chunks))
         fp.seek(0)
 
-        # 1. filter patch, so we have intending-to apply subset of it
+        # filter patch, so we have intending-to apply subset of it
         chunks = filterpatch(ui, parsepatch(fp))
         del fp
 
         contenders = set()
-        for h in chunks:
-            try:
-                contenders.update(set(h.files()))
-            except AttributeError:
-                pass
+        fp = cStringIO.StringIO()
+        for chunk in chunks:
+            chunk.write(fp)
+            contenders.add(chunk.filename())
+        fp.seek(0)
 
-        changed = changes[0] + changes[1] + changes[2]
-        newfiles = [f for f in changed if f in contenders]
-        if not newfiles:
+        if not contenders:
             ui.status(_('no changes to record\n'))
             return 0
+        contenders = set(contenders)
 
-        modified = set(changes[0])
+        newrev = None            
+        store = patch.filestore()
+        try:
+            # patch files in tmp directory
+            try:
+                patch.patchrepo(ui, repo, repo['.'], store, fp, 1, contenders)
+            except patch.PatchError, e:
+                raise util.Abort(str(e))
+           
+            # create new revision from memory
+            memctx = patch.makememctx(repo, (repo['.'].node(), None),
+                                      message,
+                                      opts.get('user'),
+                                      opts.get('date'),
+                                      repo[None].branch(), contenders, store,
+                                      editor=cmdutil.commiteditor)
+            newrev = memctx.commit()
+        finally:
+            store.close()
 
-        # 2. backup changed files, so we can restore them in the end
-        if backupall:
-            tobackup = changed
-        else:
-            tobackup = [f for f in newfiles if f in modified]
-
-        backups = {}
-        if tobackup:
-            backupdir = repo.join('record-backups')
+        # move working directory to new revision
+        if newrev:
+            wlock = repo.wlock()
             try:
-                os.mkdir(backupdir)
-            except OSError, err:
-                if err.errno != errno.EEXIST:
-                    raise
-        try:
-            # backup continues
-            for f in tobackup:
-                fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
-                                               dir=backupdir)
-                os.close(fd)
-                ui.debug('backup %r as %r\n' % (f, tmpname))
-                util.copyfile(repo.wjoin(f), tmpname)
-                shutil.copystat(repo.wjoin(f), tmpname)
-                backups[f] = tmpname
-
-            fp = cStringIO.StringIO()
-            for c in chunks:
-                if c.filename() in backups:
-                    c.write(fp)
-            dopatch = fp.tell()
-            fp.seek(0)
-
-            # 3a. apply filtered patch to clean repo  (clean)
-            if backups:
-                hg.revert(repo, repo.dirstate.p1(),
-                          lambda key: key in backups)
-
-            # 3b. (apply)
-            if dopatch:
-                try:
-                    ui.debug('applying patch\n')
-                    ui.debug(fp.getvalue())
-                    patch.internalpatch(ui, repo, fp, 1, eolmode=None)
-                except patch.PatchError, err:
-                    raise util.Abort(str(err))
-            del fp
-
-            # 4. We prepared working directory according to filtered
-            #    patch. Now is the time to delegate the job to
-            #    commit/qrefresh or the like!
-
-            # it is important to first chdir to repo root -- we'll call
-            # a highlevel command with list of pathnames relative to
-            # repo root
-            cwd = os.getcwd()
-            os.chdir(repo.root)
-            try:
-                commitfunc(ui, repo, *newfiles, **opts)
+                repo.setparents(newrev)
+                repo.dirstate.rebuild(repo[newrev].node(), repo[newrev].manifest())
             finally:
-                os.chdir(cwd)
-
-            return 0
-        finally:
-            # 5. finally restore backed-up files
-            try:
-                for realname, tmpname in backups.iteritems():
-                    ui.debug('restoring %r to %r\n' % (tmpname, realname))
-                    util.copyfile(tmpname, repo.wjoin(realname))
-                    # Our calls to copystat() here and above are a
-                    # hack to trick any editors that have f open that
-                    # we haven't modified them.
-                    #
-                    # Also note that this racy as an editor could
-                    # notice the file's mtime before we've finished
-                    # writing it.
-                    shutil.copystat(tmpname, repo.wjoin(realname))
-                    os.unlink(tmpname)
-                if tobackup:
-                    os.rmdir(backupdir)
-            except OSError:
-                pass
+                wlock.release()
 
     # wrap ui.write so diff output can be labeled/colorized
     def wrapwrite(orig, *args, **kw):
         label = kw.pop('label', '')
         for chunk, l in patch.difflabel(lambda: args):


More information about the Mercurial-devel mailing list