diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -1833,9 +1833,82 @@ def import_(ui, repo, patch1, *patches, 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 @@ def import_(ui, repo, patch1, *patches, 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) diff --git a/mercurial/patch.py b/mercurial/patch.py --- a/mercurial/patch.py +++ b/mercurial/patch.py @@ -41,6 +41,130 @@ def copyfile(src, dst, basedir): # 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. diff --git a/tests/test-import b/tests/test-import --- a/tests/test-import +++ b/tests/test-import @@ -74,6 +74,13 @@ hg clone -r0 a b 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' - diff --git a/tests/test-import.out b/tests/test-import.out --- a/tests/test-import.out +++ b/tests/test-import.out @@ -100,6 +100,11 @@ added 1 changesets with 2 changes to 2 f 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 @@ next line parent: 0 applying ../patch1 applying ../patch2 +applied 1d4bd90af0e4 rolling back last transaction parent: 1 % hg import in a subdirectory