diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py --- a/mercurial/cmdutil.py +++ b/mercurial/cmdutil.py @@ -591,9 +591,11 @@ def tryimportone(ui, repo, hunk, parents strip = opts["strip"] sim = float(opts.get('similarity') or 0) if not tmpname: - return (None, None) + return (None, None, False) msg = _('applied to working directory') + rejects = False + try: cmdline_message = logmessage(ui, opts) if cmdline_message: @@ -639,9 +641,17 @@ def tryimportone(ui, repo, hunk, parents if opts.get('exact') or opts.get('import_branch'): repo.dirstate.setbranch(branch or 'default') + partial = opts.get('partial', False) files = set() - patch.patch(ui, repo, tmpname, strip=strip, files=files, - eolmode=None, similarity=sim / 100.0) + try: + patch.patch(ui, repo, tmpname, strip=strip, files=files, + eolmode=None, similarity=sim / 100.0) + except patch.PatchError, e: + if not partial: + raise util.Abort(str(e)) + if partial: + rejects = True + files = list(files) if opts.get('no_commit'): if message: @@ -656,7 +666,7 @@ def tryimportone(ui, repo, hunk, parents m = scmutil.matchfiles(repo, files or []) n = repo.commit(message, opts.get('user') or user, opts.get('date') or date, match=m, - editor=editor) + editor=editor, force=partial) else: if opts.get('exact') or opts.get('import_branch'): branch = branch or 'default' @@ -684,7 +694,7 @@ def tryimportone(ui, repo, hunk, parents if n: # i18n: refers to a short changeset id msg = _('created %s') % short(n) - return (msg, n) + return (msg, n, rejects) finally: os.unlink(tmpname) diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -3685,6 +3685,8 @@ def identify(ui, repo, source=None, rev= _("don't commit, just update the working directory")), ('', 'bypass', None, _("apply patch without touching the working directory")), + ('', 'partial', None, + _('commit even if some hunks fail')), ('', 'exact', None, _('apply patch to the nodes from which it was generated')), ('', 'import-branch', None, @@ -3726,6 +3728,16 @@ def import_(ui, repo, patch1=None, *patc With -s/--similarity, hg will attempt to discover renames and copies in the patch in the same way as :hg:`addremove`. + Use --partial to ensure a changeset will be created from the patch + even if some hunks fail to apply. Hunks that fail to apply will be + written to a .rej file. Conflicts can then be resolved + by hand before :hg:`commit --amend` is run to update the created + changeset. This flag exists to let people import patches that + partially apply without losing the associated metadata (author, + date, description, ...), Note that when none of the hunk applies + cleanly, :hg:`import --partial` will create an empty changeset, + importing only the patch metadata. + To read a patch from standard input, use "-" as the patch name. If a URL is specified, the patch will be downloaded from it. See :hg:`help dates` for a list of formats valid for -d/--date. @@ -3751,7 +3763,7 @@ def import_(ui, repo, patch1=None, *patc hg import --exact proposed-fix.patch - Returns 0 on success. + Returns 0 on success, 1 on partial success (see --partial). """ if not patch1: @@ -3783,6 +3795,7 @@ def import_(ui, repo, patch1=None, *patc base = opts["base"] wlock = lock = tr = None msgs = [] + ret = 0 try: @@ -3804,8 +3817,9 @@ def import_(ui, repo, patch1=None, *patc haspatch = False for hunk in patch.split(patchfile): - (msg, node) = cmdutil.tryimportone(ui, repo, hunk, parents, - opts, msgs, hg.clean) + (msg, node, rej) = cmdutil.tryimportone(ui, repo, hunk, + parents, opts, + msgs, hg.clean) if msg: haspatch = True ui.note(msg + '\n') @@ -3813,6 +3827,12 @@ def import_(ui, repo, patch1=None, *patc parents = repo.parents() else: parents = [repo[node]] + if rej: + ui.write_err(_("patch applied partially\n")) + ui.write_err(("(fix the .rej files and run " + "`hg commit --amend`)\n")) + ret = 1 + break if not haspatch: raise util.Abort(_('%s: no diffs found') % patchurl) @@ -3821,6 +3841,7 @@ def import_(ui, repo, patch1=None, *patc tr.close() if msgs: repo.savecommitmessage('\n* * *\n'.join(msgs)) + return ret except: # re-raises # wlock.release() indirectly calls dirstate.write(): since # we're crashing, we do not want to change the working dir diff --git a/mercurial/patch.py b/mercurial/patch.py --- a/mercurial/patch.py +++ b/mercurial/patch.py @@ -1521,14 +1521,11 @@ def patch(ui, repo, patchname, strip=1, patcher = ui.config('ui', 'patch') if files is None: files = set() - try: - if patcher: - return _externalpatch(ui, repo, patcher, patchname, strip, - files, similarity) - return internalpatch(ui, repo, patchname, strip, files, eolmode, - similarity) - except PatchError, err: - raise util.Abort(str(err)) + if patcher: + return _externalpatch(ui, repo, patcher, patchname, strip, + files, similarity) + return internalpatch(ui, repo, patchname, strip, files, eolmode, + similarity) def changedfiles(ui, repo, patchpath, strip=1): backend = fsbackend(ui, repo.root) diff --git a/tests/test-completion.t b/tests/test-completion.t --- a/tests/test-completion.t +++ b/tests/test-completion.t @@ -262,7 +262,7 @@ Show all commands + options heads: rev, topo, active, closed, style, template help: extension, command, keyword identify: rev, num, id, branch, tags, bookmarks, ssh, remotecmd, insecure - import: strip, base, edit, force, no-commit, bypass, exact, import-branch, message, logfile, date, user, similarity + import: strip, base, edit, force, no-commit, bypass, partial, exact, import-branch, message, logfile, date, user, similarity incoming: force, newest-first, bundle, rev, bookmarks, branch, patch, git, limit, no-merges, stat, graph, style, template, ssh, remotecmd, insecure, subrepos locate: rev, print0, fullpath, include, exclude manifest: rev, all diff --git a/tests/test-import.t b/tests/test-import.t --- a/tests/test-import.t +++ b/tests/test-import.t @@ -1181,5 +1181,272 @@ Test corner case involving fuzz and skew 3 4 line + $ cd .. - $ cd .. +Test partial application +------------------------ + +prepare a stack of patches depending on each other + + $ hg init partial + $ cd partial + $ cat << EOF > a + > one + > two + > three + > four + > five + > six + > seven + > EOF + $ hg add a + $ echo 'b' > b + $ hg add b + $ hg commit -m 'initial' -u Babar + $ cat << EOF > a + > one + > two + > 3 + > four + > five + > six + > seven + > EOF + $ hg commit -m 'three' -u Celeste + $ cat << EOF > a + > one + > two + > 3 + > 4 + > five + > six + > seven + > EOF + $ hg commit -m 'four' -u Rataxes + $ cat << EOF > a + > one + > two + > 3 + > 4 + > 5 + > six + > seven + > EOF + $ echo bb >> b + $ hg commit -m 'five' -u Arthur + $ echo 'Babar' > jungle + $ hg add jungle + $ hg ci -m 'jungle' -u Zephir + $ echo 'Celeste' >> jungle + $ hg ci -m 'extended jungle' -u Cornelius + $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n' + @ extended jungle [Cornelius] 1: +1/-0 + | + o jungle [Zephir] 1: +1/-0 + | + o five [Arthur] 2: +2/-1 + | + o four [Rataxes] 1: +1/-1 + | + o three [Celeste] 1: +1/-1 + | + o initial [Babar] 2: +8/-0 + + +Importing with some success and some errors: + + $ hg update --rev 'desc(initial)' + 2 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg export --rev 'desc(five)' | hg import --partial - + applying patch from stdin + patching file a + Hunk #1 FAILED at 1 + 1 out of 1 hunks FAILED -- saving rejects to file a.rej + patch applied partially + (fix the .rej files and run `hg commit --amend`) + [1] + + $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n' + @ five [Arthur] 1: +1/-0 + | + | o extended jungle [Cornelius] 1: +1/-0 + | | + | o jungle [Zephir] 1: +1/-0 + | | + | o five [Arthur] 2: +2/-1 + | | + | o four [Rataxes] 1: +1/-1 + | | + | o three [Celeste] 1: +1/-1 + |/ + o initial [Babar] 2: +8/-0 + + $ hg export + # HG changeset patch + # User Arthur + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID 26e6446bb2526e2be1037935f5fca2b2706f1509 + # Parent 8e4f0351909eae6b9cf68c2c076cb54c42b54b2e + five + + diff -r 8e4f0351909e -r 26e6446bb252 b + --- a/b Thu Jan 01 00:00:00 1970 +0000 + +++ b/b Thu Jan 01 00:00:00 1970 +0000 + @@ -1,1 +1,2 @@ + b + +bb + $ hg status -c . + C a + C b + $ ls + a + a.rej + b + +Importing with zero success: + + $ hg update --rev 'desc(initial)' + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg export --rev 'desc(four)' | hg import --partial - + applying patch from stdin + patching file a + Hunk #1 FAILED at 0 + 1 out of 1 hunks FAILED -- saving rejects to file a.rej + patch applied partially + (fix the .rej files and run `hg commit --amend`) + [1] + + $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n' + @ four [Rataxes] 0: +0/-0 + | + | o five [Arthur] 1: +1/-0 + |/ + | o extended jungle [Cornelius] 1: +1/-0 + | | + | o jungle [Zephir] 1: +1/-0 + | | + | o five [Arthur] 2: +2/-1 + | | + | o four [Rataxes] 1: +1/-1 + | | + | o three [Celeste] 1: +1/-1 + |/ + o initial [Babar] 2: +8/-0 + + $ hg export + # HG changeset patch + # User Rataxes + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID cb9b1847a74d9ad52e93becaf14b98dbcc274e1e + # Parent 8e4f0351909eae6b9cf68c2c076cb54c42b54b2e + four + + $ hg status -c . + C a + C b + $ ls + a + a.rej + b + +Importing with unknown file: + + $ hg update --rev 'desc(initial)' + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg export --rev 'desc("extended jungle")' | hg import --partial - + applying patch from stdin + unable to find 'jungle' for patching + 1 out of 1 hunks FAILED -- saving rejects to file jungle.rej + patch applied partially + (fix the .rej files and run `hg commit --amend`) + [1] + + $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n' + @ extended jungle [Cornelius] 0: +0/-0 + | + | o four [Rataxes] 0: +0/-0 + |/ + | o five [Arthur] 1: +1/-0 + |/ + | o extended jungle [Cornelius] 1: +1/-0 + | | + | o jungle [Zephir] 1: +1/-0 + | | + | o five [Arthur] 2: +2/-1 + | | + | o four [Rataxes] 1: +1/-1 + | | + | o three [Celeste] 1: +1/-1 + |/ + o initial [Babar] 2: +8/-0 + + $ hg export + # HG changeset patch + # User Cornelius + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID 1fb1f86bef43c5a75918178f8d23c29fb0a7398d + # Parent 8e4f0351909eae6b9cf68c2c076cb54c42b54b2e + extended jungle + + $ hg status -c . + C a + C b + $ ls + a + a.rej + b + jungle.rej + +Importing multiple failing patches: + + $ hg update --rev 'desc(initial)' + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo 'B' > b # just to make another commit + $ hg commit -m "a new base" + created new head + $ hg export --rev 'desc("extended jungle") + desc("four")' | hg import --partial - + applying patch from stdin + patching file a + Hunk #1 FAILED at 0 + 1 out of 1 hunks FAILED -- saving rejects to file a.rej + patch applied partially + (fix the .rej files and run `hg commit --amend`) + [1] + $ hg log -G --template '{desc|firstline} [{author}] {diffstat}\n' + @ four [Rataxes] 0: +0/-0 + | + o a new base [test] 1: +1/-1 + | + | o extended jungle [Cornelius] 0: +0/-0 + |/ + | o four [Rataxes] 0: +0/-0 + |/ + | o five [Arthur] 1: +1/-0 + |/ + | o extended jungle [Cornelius] 1: +1/-0 + | | + | o jungle [Zephir] 1: +1/-0 + | | + | o five [Arthur] 2: +2/-1 + | | + | o four [Rataxes] 1: +1/-1 + | | + | o three [Celeste] 1: +1/-1 + |/ + o initial [Babar] 2: +8/-0 + + $ hg export + # HG changeset patch + # User Rataxes + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID a9d7b6d0ffbb4eb12b7d5939250fcd42e8930a1d + # Parent f59f8d2e95a8ca5b1b4ca64320140da85f3b44fd + four + + $ hg status -c . + C a + C b