Patchwork import: add --partial flag to create a changeset despite failed hunk

login
register
mail settings
Submitter Pierre-Yves David
Date May 9, 2014, 1:59 a.m.
Message ID <1357e80831082710134d.1399600776@marginatus.alto.octopoid.net>
Download mbox | patch
Permalink /patch/4696/
State Changes Requested
Headers show

Comments

Pierre-Yves David - May 9, 2014, 1:59 a.m.
# HG changeset patch
# User Pierre-Yves David <pierre-yves.david@fb.com>
# Date 1399594097 25200
#      Thu May 08 17:08:17 2014 -0700
# Node ID 1357e80831082710134d6472229f6e60aeb3ea1e
# Parent  62a2749895e4151f766a4243fa870b1ddd7386d0
import: add --partial flag to create a changeset despite failed hunk

The `hg import` commands gains a `--partial` flag. When specified, a commit will
always be created from a patch import. Any hunk that fails to apply will
create .rej file, same as what `hg qimport` would do. This changes in mainly
aimed at preserving changeset metadata when applying a patch, something very
important for reviewers.

In case of failure with `--partial`, `hg import` returns 1 and the following
message is displayed:

    patch applied partially
    (fix the .rej files and run `hg commit --amend`)

When multiple patches are imported, we stop at the first one with failed hunks.

In the future, someone may feel brave enough to tackle a --continue flag to
import.
Augie Fackler - May 13, 2014, 1:03 a.m.
On Thu, May 08, 2014 at 06:59:36PM -0700, pierre-yves.david@ens-lyon.org wrote:
> # HG changeset patch
> # User Pierre-Yves David <pierre-yves.david@fb.com>
> # Date 1399594097 25200
> #      Thu May 08 17:08:17 2014 -0700
> # Node ID 1357e80831082710134d6472229f6e60aeb3ea1e
> # Parent  62a2749895e4151f766a4243fa870b1ddd7386d0
> import: add --partial flag to create a changeset despite failed hunk

I've not yet looked at the patch. This feature weirds me out a little
- I think I'd rather see us drop into some sort of "resolution
required" state, although I'm not sure exactly how to implement that
without some significant refactoring.

Could we get most of the win of this patch by instead doing an import
to the patch's declared parent ID if present and then rebasing?


>
> The `hg import` commands gains a `--partial` flag. When specified, a commit will
> always be created from a patch import. Any hunk that fails to apply will
> create .rej file, same as what `hg qimport` would do. This changes in mainly
> aimed at preserving changeset metadata when applying a patch, something very
> important for reviewers.
>
> In case of failure with `--partial`, `hg import` returns 1 and the following
> message is displayed:
>
>     patch applied partially
>     (fix the .rej files and run `hg commit --amend`)
>
> When multiple patches are imported, we stop at the first one with failed hunks.
>
> In the future, someone may feel brave enough to tackle a --continue flag to
> import.
>
> diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
> --- a/mercurial/cmdutil.py
> +++ b/mercurial/cmdutil.py
> @@ -567,13 +567,15 @@ def tryimportone(ui, repo, hunk, parents
>          editor = commitforceeditor
>      update = not opts.get('bypass')
>      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:
>              # pickup the cmdline msg
>              message = cmdline_message
> @@ -615,13 +617,21 @@ def tryimportone(ui, repo, hunk, parents
>                  repo.setparents(p1.node(), p2.node())
>
>              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:
>                      msgs.append(message)
>              else:
> @@ -632,11 +642,11 @@ def tryimportone(ui, repo, hunk, parents
>                      m = None
>                  else:
>                      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'
>              else:
>                  branch = p1.branch()
> @@ -660,11 +670,11 @@ def tryimportone(ui, repo, hunk, parents
>          if opts.get('exact') and hex(n) != nodeid:
>              raise util.Abort(_('patch is damaged or loses information'))
>          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)
>
>  def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
>             opts=None):
> diff --git a/mercurial/commands.py b/mercurial/commands.py
> --- a/mercurial/commands.py
> +++ b/mercurial/commands.py
> @@ -3689,10 +3689,12 @@ def identify(ui, repo, source=None, rev=
>       _('skip check for outstanding uncommitted changes (DEPRECATED)')),
>      ('', 'no-commit', None,
>       _("don't commit, just update the working directory")),
>      ('', 'bypass', None,
>       _("apply patch without touching the working directory")),
> +    ('', 'partial', None,
> +     _('commit even if some hunks fails')),
>      ('', 'exact', None,
>       _('apply patch to the nodes from which it was generated')),
>      ('', 'import-branch', None,
>       _('use any branch information in patch (implied by --exact)'))] +
>      commitopts + commitopts2 + similarityopts,
> @@ -3730,10 +3732,20 @@ def import_(ui, repo, patch1=None, *patc
>      revision.
>
>      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 hunk fail to apply. Hunks that fail to apply will be
> +    written in a <target-file>.rej file. Conflicts can then be resolved
> +    by hands before :hg:`commit --amend` is run to update the created
> +    changeset.  This flag exists to let people import patches that
> +    partially apply without loosing 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.
>
>      .. container:: verbose
> @@ -3755,11 +3767,11 @@ def import_(ui, repo, patch1=None, *patc
>        - attempt to exactly restore an exported changeset (not always
>          possible)::
>
>            hg import --exact proposed-fix.patch
>
> -    Returns 0 on success.
> +    Returns 0 on success, 1 on partial success (see --partial).
>      """
>
>      if not patch1:
>          raise util.Abort(_('need at least one patch to import'))
>
> @@ -3787,10 +3799,11 @@ def import_(ui, repo, patch1=None, *patc
>          cmdutil.bailifchanged(repo)
>
>      base = opts["base"]
>      wlock = lock = tr = None
>      msgs = []
> +    ret = 0
>
>
>      try:
>          try:
>              wlock = repo.wlock()
> @@ -3808,27 +3821,35 @@ def import_(ui, repo, patch1=None, *patc
>                      ui.status(_('applying %s\n') % patchurl)
>                      patchfile = hg.openpath(ui, patchurl)
>
>                  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')
>                      if update or opts.get('exact'):
>                          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)
>
>              if tr:
>                  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
>              # parent after all, so make sure it writes nothing
>              repo.dirstate.invalidate()
> diff --git a/mercurial/patch.py b/mercurial/patch.py
> --- a/mercurial/patch.py
> +++ b/mercurial/patch.py
> @@ -1519,18 +1519,15 @@ def patch(ui, repo, patchname, strip=1,
>      Returns whether patch was applied with fuzz factor.
>      """
>      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)
>      fp = open(patchpath, 'rb')
>      try:
> diff --git a/tests/test-completion.t b/tests/test-completion.t
> --- a/tests/test-completion.t
> +++ b/tests/test-completion.t
> @@ -260,11 +260,11 @@ Show all commands + options
>    graft: rev, continue, edit, log, currentdate, currentuser, date, user, tool, dry-run
>    grep: print0, all, text, follow, ignore-case, files-with-matches, line-number, rev, user, date, include, exclude
>    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
>    outgoing: force, rev, newest-first, bookmarks, branch, patch, git, limit, no-merges, stat, graph, style, template, ssh, remotecmd, insecure, subrepos
>    parents: rev, style, template
> diff --git a/tests/test-import.t b/tests/test-import.t
> --- a/tests/test-import.t
> +++ b/tests/test-import.t
> @@ -1152,7 +1152,274 @@ Test corner case involving fuzz and skew
>    1
>    add some skew
>    3
>    4
>    line
> +  $ cd ..
>
> -  $ cd ..
> +Test partial application
> +------------------------
> +
> +prepare a stack of patch 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
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@selenic.com
> http://selenic.com/mailman/listinfo/mercurial-devel
Pierre-Yves David - May 13, 2014, 7:27 a.m.
On 05/12/2014 06:03 PM, Augie Fackler wrote:
> On Thu, May 08, 2014 at 06:59:36PM -0700, pierre-yves.david@ens-lyon.org wrote:
>> # HG changeset patch
>> # User Pierre-Yves David <pierre-yves.david@fb.com>
>> # Date 1399594097 25200
>> #      Thu May 08 17:08:17 2014 -0700
>> # Node ID 1357e80831082710134d6472229f6e60aeb3ea1e
>> # Parent  62a2749895e4151f766a4243fa870b1ddd7386d0
>> import: add --partial flag to create a changeset despite failed hunk
>
> I've not yet looked at the patch. This feature weirds me out a little
> - I think I'd rather see us drop into some sort of "resolution
> required" state, although I'm not sure exactly how to implement that
> without some significant refactoring.

We could do tons of stuff in the magical mystery computer world.

The new merge state format would allow any arbitrary data in there. If 
greg push his multi-step operation work forward it would be easy to but 
a --continue things there.

However none of those things are going to happen quickly, and this 
--partial flag is requested by Matt for a year. Matt queuing script is 
currently briefly using MQ to apply patches to make sure he is not 
loosing meta data. (yes MQ…). So I really would like to see core as 
capable as MQ on this aspect. It would help everyone doing queuing out 
there.

Here is the plan I would sugguest

1. (now) get `hg import --partial` into core to help people pushing 
patches and getting "feature parity" with MQ.
2. (later) adds some cool stuff that help the recovery of a failed 
--partial (merge state, --continue etc)
3. (a bit later) make --partial the default behavior and obsolete the flag.

> Could we get most of the win of this patch by instead doing an import
> to the patch's declared parent ID if present and then rebasing?

Having and easy way to do this would be nice. But it is fairly more 
complicated and does not solve all issues. So the good old simple way is 
fixed first:

1. The first lead was to use --exact. but --exact is broken whenever you 
have something in extra.
2. You not necessary have the parent information (Contributors from 
outer space)
3. stack of patches make it difficult to use the parent information.

(If you did not noticed it yet. This is a "I agree with you, but please 
queue this anyway." reply)
Matt Mackall - May 26, 2014, 9:55 p.m.
On Thu, 2014-05-08 at 18:59 -0700, pierre-yves.david@ens-lyon.org wrote:
> # HG changeset patch
> # User Pierre-Yves David <pierre-yves.david@fb.com>
> # Date 1399594097 25200
> #      Thu May 08 17:08:17 2014 -0700
> # Node ID 1357e80831082710134d6472229f6e60aeb3ea1e
> # Parent  62a2749895e4151f766a4243fa870b1ddd7386d0
> import: add --partial flag to create a changeset despite failed hunk

Queued for default, thanks.

Patch

diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -567,13 +567,15 @@  def tryimportone(ui, repo, hunk, parents
         editor = commitforceeditor
     update = not opts.get('bypass')
     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:
             # pickup the cmdline msg
             message = cmdline_message
@@ -615,13 +617,21 @@  def tryimportone(ui, repo, hunk, parents
                 repo.setparents(p1.node(), p2.node())
 
             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:
                     msgs.append(message)
             else:
@@ -632,11 +642,11 @@  def tryimportone(ui, repo, hunk, parents
                     m = None
                 else:
                     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'
             else:
                 branch = p1.branch()
@@ -660,11 +670,11 @@  def tryimportone(ui, repo, hunk, parents
         if opts.get('exact') and hex(n) != nodeid:
             raise util.Abort(_('patch is damaged or loses information'))
         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)
 
 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
            opts=None):
diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -3689,10 +3689,12 @@  def identify(ui, repo, source=None, rev=
      _('skip check for outstanding uncommitted changes (DEPRECATED)')),
     ('', 'no-commit', None,
      _("don't commit, just update the working directory")),
     ('', 'bypass', None,
      _("apply patch without touching the working directory")),
+    ('', 'partial', None,
+     _('commit even if some hunks fails')),
     ('', 'exact', None,
      _('apply patch to the nodes from which it was generated')),
     ('', 'import-branch', None,
      _('use any branch information in patch (implied by --exact)'))] +
     commitopts + commitopts2 + similarityopts,
@@ -3730,10 +3732,20 @@  def import_(ui, repo, patch1=None, *patc
     revision.
 
     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 hunk fail to apply. Hunks that fail to apply will be
+    written in a <target-file>.rej file. Conflicts can then be resolved
+    by hands before :hg:`commit --amend` is run to update the created
+    changeset.  This flag exists to let people import patches that
+    partially apply without loosing 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.
 
     .. container:: verbose
@@ -3755,11 +3767,11 @@  def import_(ui, repo, patch1=None, *patc
       - attempt to exactly restore an exported changeset (not always
         possible)::
 
           hg import --exact proposed-fix.patch
 
-    Returns 0 on success.
+    Returns 0 on success, 1 on partial success (see --partial).
     """
 
     if not patch1:
         raise util.Abort(_('need at least one patch to import'))
 
@@ -3787,10 +3799,11 @@  def import_(ui, repo, patch1=None, *patc
         cmdutil.bailifchanged(repo)
 
     base = opts["base"]
     wlock = lock = tr = None
     msgs = []
+    ret = 0
 
 
     try:
         try:
             wlock = repo.wlock()
@@ -3808,27 +3821,35 @@  def import_(ui, repo, patch1=None, *patc
                     ui.status(_('applying %s\n') % patchurl)
                     patchfile = hg.openpath(ui, patchurl)
 
                 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')
                     if update or opts.get('exact'):
                         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)
 
             if tr:
                 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
             # parent after all, so make sure it writes nothing
             repo.dirstate.invalidate()
diff --git a/mercurial/patch.py b/mercurial/patch.py
--- a/mercurial/patch.py
+++ b/mercurial/patch.py
@@ -1519,18 +1519,15 @@  def patch(ui, repo, patchname, strip=1, 
     Returns whether patch was applied with fuzz factor.
     """
     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)
     fp = open(patchpath, 'rb')
     try:
diff --git a/tests/test-completion.t b/tests/test-completion.t
--- a/tests/test-completion.t
+++ b/tests/test-completion.t
@@ -260,11 +260,11 @@  Show all commands + options
   graft: rev, continue, edit, log, currentdate, currentuser, date, user, tool, dry-run
   grep: print0, all, text, follow, ignore-case, files-with-matches, line-number, rev, user, date, include, exclude
   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
   outgoing: force, rev, newest-first, bookmarks, branch, patch, git, limit, no-merges, stat, graph, style, template, ssh, remotecmd, insecure, subrepos
   parents: rev, style, template
diff --git a/tests/test-import.t b/tests/test-import.t
--- a/tests/test-import.t
+++ b/tests/test-import.t
@@ -1152,7 +1152,274 @@  Test corner case involving fuzz and skew
   1
   add some skew
   3
   4
   line
+  $ cd ..
 
-  $ cd ..
+Test partial application
+------------------------
+
+prepare a stack of patch 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