[PATCH 5 of 6] patch: add mempatching support

Augie Fackler durin42 at gmail.com
Mon Apr 19 18:16:38 CDT 2010


# HG changeset patch
# User Augie Fackler <durin42 at gmail.com>
# Date 1271716503 18000
# Node ID 9b8a41043ab3634a237292cf6de53dd967f2ca2f
# Parent  03eea121b5531319bd3cb703371e390cd25490e3
patch: add mempatching support

Thanks to Patrick Mezard for his help figuring out all the fiddly bits
of what I was doing wrong.
* * *
fold missing errno import

diff --git a/mercurial/patch.py b/mercurial/patch.py
--- a/mercurial/patch.py
+++ b/mercurial/patch.py
@@ -6,12 +6,12 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-import cStringIO, email.Parser, os, re
+import cStringIO, email.Parser, os, re, errno
 import tempfile, zlib
 
 from i18n import _
 from node import hex, nullid, short
-import base85, cmdutil, mdiff, util, diffhelpers, copies
+import base85, cmdutil, mdiff, util, diffhelpers, copies, context
 
 gitre = re.compile('diff --git a/(.*) b/(.*)')
 
@@ -630,6 +630,108 @@
         self.rej.append(horig)
         return -1
 
+
+def _renamed(fctx):
+    copy = fctx.renamed()
+    if copy:
+        return copy[0]
+
+class mempatchfile(patchfile):
+    def __init__(self, ui, fname, opener, missing=False, eolmode='strict',
+                 parentctx=None, files=None):
+        """Create a mempatch object.
+
+        This patches files from a context in memory, and saves a dict
+        of the resulting files.
+        """
+        assert parentctx is not None
+        assert files is not None
+        self.files = files
+        self.parentctx = parentctx
+        patchfile.__init__(self, ui, fname, None, False, eolmode)
+
+    def readlines(self, fname):
+        if fname not in self.parentctx:
+            raise IOError(errno.ENOENT, 'Cannot find %r to patch' % fname)
+        # TODO(durin42): refactor so we can skip StringIO here
+        lr = linereader(cStringIO.StringIO(self.parentctx[fname].data()),
+                        self.eolmode != 'strict')
+        self.eol = lr.eol
+        return list(lr)
+
+    def writelines(self, fname, lines):
+        copy = islink = isexec = False
+        pendingctx = self.files.get(fname, None)
+        if pendingctx is not None:
+            islink = 'l' in pendingctx.flags()
+            isexec = 'x' in pendingctx.flags()
+            copy = _renamed(pendingctx)
+        elif fname in self.parentctx:
+            srcfctx = self.parentctx.filectx(fname)
+            islink = 'l' in srcfctx.flags()
+            isexec = 'x' in srcfctx.flags()
+        nfctx = context.memfilectx(fname, ''.join(lines),
+                                   islink, isexec, copy)
+        self.files[fname] = nfctx
+
+    def write_rej(self):
+        if self.rej:
+            raise PatchError('in-memory patching failed!')
+
+    def unlink(self, fname):
+        self.files[fname] = None
+
+
+class mempatcher(object):
+    def __init__(self, ctx):
+        self.ctx = ctx
+        self.files = {}
+
+    def _filectx(self, f):
+        if self.files.get(f, None) is not None:
+            return self.files[f]
+        elif f in self.ctx:
+            return self.ctx.filectx(f)
+
+    def patchfile(self, *args, **kwargs):
+        return mempatchfile(*args,  parentctx=self.ctx, files=self.files,
+                            **kwargs)
+
+    def copyfile(self, src, dest, basedir):
+        if self.files.get(dest, None) is not None or dest in self.ctx:
+            raise PatchError('%s already exists' % dest)
+        srcfctx = self._filectx(src)
+        if srcfctx is None:
+            raise util.Abort('missing copy source %s for %s' %
+                             (src, dest))
+        self.files[dest] = context.memfilectx(
+            dest, srcfctx.data(), 'l' in srcfctx.flags(),
+            'x' in srcfctx.flags(), src)
+
+    def setmode(self, f, islink, isexec):
+        basectx = self._filectx(f)
+        if basectx is not None:
+            if (islink == ('l' in basectx.flags())
+                and isexec == ('x' in basectx.flags())):
+                return
+            data = basectx.data()
+            copied = _renamed(basectx)
+        else:
+            data = ''
+            copied = False
+        self.files[f] = context.memfilectx(f, data, islink, isexec, copied)
+
+    def filectxfn(self, repo, ctx, path):
+        """Function to pass to memctx as filectxfn to commit this patch.
+        """
+        if self.files[path] is None:
+            raise IOError(errno.ENOENT, '%s is deleted' % path)
+        return self.files[path]
+
+    def unlink(self, fn):
+        self.files[fn] = None
+
+
 class hunk(object):
     def __init__(self, desc, num, lr, context, create=False, remove=False):
         self.number = num
@@ -1146,6 +1248,30 @@
         changed, strip=strip, sourcefile=sourcefile, eolmode=eolmode)
 
 
+def memapplydiff(ui, fp, ctx, strip=1, eolmode='strict'):
+    """Apply a diff in memory to ctx.
+
+    If the diff doesn't apply cleanly, this will raise a PatchError,
+    since reject files can't be sanely written.
+    """
+    patcher = mempatcher(ctx)
+    changed = {}
+    err = _applydiff(ui, fp, patcher.patchfile, patcher.copyfile,
+                     changed, strip=strip, sourcefile=None, eolmode=eolmode)
+    if err != 0:
+        raise PatchError('in-memory diff application failed')
+    # perform updatedir-like updates of things patching doesn't catch
+    for f, gp in changed.iteritems():
+        if gp and gp.op in ('RENAME', 'DELETE'):
+            if gp.op == 'RENAME':
+                patcher.unlink(gp.oldpath)
+            else:
+                patcher.unlink(gp.path)
+        if gp and gp.mode:
+            islink, isexec = gp.mode
+            patcher.setmode(f, islink, isexec)
+    return patcher
+
 def _applydiff(ui, fp, patcher, copyfn, changed, strip=1,
                sourcefile=None, eolmode='strict'):
     rejects = 0


More information about the Mercurial-devel mailing list