changeset 10384:832f35386067

import: import each patch in a file or stream as a separate change Supports hg export <revrange>, mail messages, and mailboxes. Does not support multiple patches in a single MIME attachment. Closes issue167.
author Brendan Cully <brendan@kublai.com>
date Sun, 07 Feb 2010 18:06:52 +0100
parents f83291e5643e
children 3dfc6b2cf916
files mercurial/commands.py mercurial/patch.py tests/test-import tests/test-import.out
diffstat 4 files changed, 221 insertions(+), 60 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/commands.py	Sun Feb 07 16:44:55 2010 +0100
+++ b/mercurial/commands.py	Sun Feb 07 18:06:52 2010 +0100
@@ -1833,9 +1833,82 @@
     d = opts["base"]
     strip = opts["strip"]
     wlock = lock = None
+
+    def tryone(ui, hunk):
+        tmpname, message, user, date, branch, nodeid, p1, p2 = patch.extract(ui, hunk)
+
+        if not tmpname:
+            return None
+        commitid = _('to working directory')
+
+        try:
+            cmdline_message = cmdutil.logmessage(opts)
+            if cmdline_message:
+                # pickup the cmdline msg
+                message = cmdline_message
+            elif message:
+                # pickup the patch msg
+                message = message.strip()
+            else:
+                # launch the editor
+                message = None
+            ui.debug('message:\n%s\n' % message)
+
+            wp = repo.parents()
+            if opts.get('exact'):
+                if not nodeid or not p1:
+                    raise util.Abort(_('not a Mercurial patch'))
+                p1 = repo.lookup(p1)
+                p2 = repo.lookup(p2 or hex(nullid))
+
+                if p1 != wp[0].node():
+                    hg.clean(repo, p1)
+                repo.dirstate.setparents(p1, p2)
+            elif p2:
+                try:
+                    p1 = repo.lookup(p1)
+                    p2 = repo.lookup(p2)
+                    if p1 == wp[0].node():
+                        repo.dirstate.setparents(p1, p2)
+                except error.RepoError:
+                    pass
+            if opts.get('exact') or opts.get('import_branch'):
+                repo.dirstate.setbranch(branch or 'default')
+
+            files = {}
+            try:
+                patch.patch(tmpname, ui, strip=strip, cwd=repo.root,
+                            files=files, eolmode=None)
+            finally:
+                files = patch.updatedir(ui, repo, files,
+                                        similarity=sim / 100.0)
+            if not opts.get('no_commit'):
+                if opts.get('exact'):
+                    m = None
+                else:
+                    m = cmdutil.matchfiles(repo, files or [])
+                n = repo.commit(message, opts.get('user') or user,
+                                opts.get('date') or date, match=m,
+                                editor=cmdutil.commiteditor)
+                if opts.get('exact'):
+                    if hex(n) != nodeid:
+                        repo.rollback()
+                        raise util.Abort(_('patch is damaged'
+                                           ' or loses information'))
+                # Force a dirstate write so that the next transaction
+                # backups an up-do-date file.
+                repo.dirstate.write()
+                if n:
+                    commitid = short(n)
+
+            return commitid
+        finally:
+            os.unlink(tmpname)
+        
     try:
         wlock = repo.wlock()
         lock = repo.lock()
+        lastcommit = None
         for p in patches:
             pf = os.path.join(d, p)
 
@@ -1845,68 +1918,19 @@
             else:
                 ui.status(_("applying %s\n") % p)
                 pf = url.open(ui, pf)
-            data = patch.extract(ui, pf)
-            tmpname, message, user, date, branch, nodeid, p1, p2 = data
-
-            if tmpname is None:
+
+            haspatch = False
+            for hunk in patch.split(pf):
+                commitid = tryone(ui, hunk)
+                if commitid:
+                    haspatch = True
+                    if lastcommit:
+                        ui.status(_('applied %s\n') % lastcommit)
+                    lastcommit = commitid
+
+            if not haspatch:
                 raise util.Abort(_('no diffs found'))
 
-            try:
-                cmdline_message = cmdutil.logmessage(opts)
-                if cmdline_message:
-                    # pickup the cmdline msg
-                    message = cmdline_message
-                elif message:
-                    # pickup the patch msg
-                    message = message.strip()
-                else:
-                    # launch the editor
-                    message = None
-                ui.debug('message:\n%s\n' % message)
-
-                wp = repo.parents()
-                if opts.get('exact'):
-                    if not nodeid or not p1:
-                        raise util.Abort(_('not a Mercurial patch'))
-                    p1 = repo.lookup(p1)
-                    p2 = repo.lookup(p2 or hex(nullid))
-
-                    if p1 != wp[0].node():
-                        hg.clean(repo, p1)
-                    repo.dirstate.setparents(p1, p2)
-                elif p2:
-                    try:
-                        p1 = repo.lookup(p1)
-                        p2 = repo.lookup(p2)
-                        if p1 == wp[0].node():
-                            repo.dirstate.setparents(p1, p2)
-                    except error.RepoError:
-                        pass
-                if opts.get('exact') or opts.get('import_branch'):
-                    repo.dirstate.setbranch(branch or 'default')
-
-                files = {}
-                try:
-                    patch.patch(tmpname, ui, strip=strip, cwd=repo.root,
-                                files=files, eolmode=None)
-                finally:
-                    files = patch.updatedir(ui, repo, files,
-                                            similarity=sim / 100.0)
-                if not opts.get('no_commit'):
-                    m = cmdutil.matchfiles(repo, files or [])
-                    n = repo.commit(message, opts.get('user') or user,
-                                    opts.get('date') or date, match=m,
-                                    editor=cmdutil.commiteditor)
-                    if opts.get('exact'):
-                        if hex(n) != nodeid:
-                            repo.rollback()
-                            raise util.Abort(_('patch is damaged'
-                                               ' or loses information'))
-                    # Force a dirstate write so that the next transaction
-                    # backups an up-do-date file.
-                    repo.dirstate.write()
-            finally:
-                os.unlink(tmpname)
     finally:
         release(lock, wlock)
 
--- a/mercurial/patch.py	Sun Feb 07 16:44:55 2010 +0100
+++ b/mercurial/patch.py	Sun Feb 07 18:06:52 2010 +0100
@@ -41,6 +41,130 @@
 
 # public functions
 
+def split(stream):
+    '''return an iterator of individual patches from a stream'''
+    def isheader(line, inheader):
+        if inheader and line[0] in (' ', '\t'):
+            # continuation
+            return True
+        l = line.split(': ', 1)
+        return len(l) == 2 and ' ' not in l[0]
+
+    def chunk(lines):
+        return cStringIO.StringIO(''.join(lines))
+
+    def hgsplit(stream, cur):
+        inheader = True
+
+        for line in stream:
+            if not line.strip():
+                inheader = False
+            if not inheader and line.startswith('# HG changeset patch'):
+                yield chunk(cur)
+                cur = []
+                inheader = True
+
+            cur.append(line)
+
+        if cur:
+            yield chunk(cur)
+
+    def mboxsplit(stream, cur):
+        for line in stream:
+            if line.startswith('From '):
+                for c in split(chunk(cur[1:])):
+                    yield c
+                cur = []
+
+            cur.append(line)
+
+        if cur:
+            for c in split(chunk(cur[1:])):
+                yield c
+
+    def mimesplit(stream, cur):
+        def msgfp(m):
+            fp = cStringIO.StringIO()
+            g = email.Generator.Generator(fp, mangle_from_=False)
+            g.flatten(m)
+            fp.seek(0)
+            return fp
+
+        for line in stream:
+            cur.append(line)
+        c = chunk(cur)
+
+        m = email.Parser.Parser().parse(c)
+        if not m.is_multipart():
+            yield msgfp(m)
+        else:
+            ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
+            for part in m.walk():
+                ct = part.get_content_type()
+                if ct not in ok_types:
+                    continue
+                yield msgfp(part)
+
+    def headersplit(stream, cur):
+        inheader = False
+
+        for line in stream:
+            if not inheader and isheader(line, inheader):
+                yield chunk(cur)
+                cur = []
+                inheader = True
+            if inheader and not isheader(line, inheader):
+                inheader = False
+
+            cur.append(line)
+
+        if cur:
+            yield chunk(cur)
+
+    def remainder(cur):
+        yield chunk(cur)
+
+    class fiter(object):
+        def __init__(self, fp):
+            self.fp = fp
+
+        def __iter__(self):
+            return self
+
+        def next(self):
+            l = self.fp.readline()
+            if not l:
+                raise StopIteration
+            return l
+
+    inheader = False
+    cur = []
+
+    mimeheaders = ['content-type']
+
+    if not hasattr(stream, 'next'):
+        # http responses, for example, have readline but not next
+        stream = fiter(stream)
+
+    for line in stream:
+        cur.append(line)
+        if line.startswith('# HG changeset patch'):
+            return hgsplit(stream, cur)
+        elif line.startswith('From '):
+            return mboxsplit(stream, cur)
+        elif isheader(line, inheader):
+            inheader = True
+            if line.split(':', 1)[0].lower() in mimeheaders:
+                # let email parser handle this
+                return mimesplit(stream, cur)
+        elif inheader:
+            # No evil headers seen, split by hand
+            return headersplit(stream, cur)
+        # Not enough info, keep reading
+
+    # if we are here, we have a very plain patch
+    return remainder(cur)
+
 def extract(ui, fileobj):
     '''extract patch from data read from fileobj.
 
--- a/tests/test-import	Sun Feb 07 16:44:55 2010 +0100
+++ b/tests/test-import	Sun Feb 07 18:06:52 2010 +0100
@@ -74,6 +74,13 @@
 hg --cwd a export tip | hg --cwd b import -
 rm -r b
 
+echo % import two patches in one stream
+hg init b
+hg --cwd a export 0:tip | hg --cwd b import -
+hg --cwd a id
+hg --cwd b id
+rm -r b
+
 echo % override commit message
 hg clone -r0 a b
 hg --cwd a export tip | hg --cwd b import -m 'override' -
--- a/tests/test-import.out	Sun Feb 07 16:44:55 2010 +0100
+++ b/tests/test-import.out	Sun Feb 07 18:06:52 2010 +0100
@@ -100,6 +100,11 @@
 updating to branch default
 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
 applying patch from stdin
+% import two patches in one stream
+applying patch from stdin
+applied 80971e65b431
+1d4bd90af0e4 tip
+1d4bd90af0e4 tip
 % override commit message
 requesting all changes
 adding changesets
@@ -176,6 +181,7 @@
 parent: 0
 applying ../patch1
 applying ../patch2
+applied 1d4bd90af0e4
 rolling back last transaction
 parent: 1
 % hg import in a subdirectory