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.
--- 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