import: add --partial flag to create a changeset despite failed hunks
authorPierre-Yves David <pierre-yves.david@fb.com>
Thu, 08 May 2014 17:08:17 -0700
changeset 21553 bee0e1cffdd3
parent 21552 61151f429a5f
child 21554 7bcf4adadd2d
import: add --partial flag to create a changeset despite failed hunks The `hg import` command 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 change is 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.
mercurial/cmdutil.py
mercurial/commands.py
mercurial/patch.py
tests/test-completion.t
tests/test-import.t
--- a/mercurial/cmdutil.py	Mon May 19 01:53:34 2014 +0200
+++ b/mercurial/cmdutil.py	Thu May 08 17:08:17 2014 -0700
@@ -591,9 +591,11 @@
     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 @@
             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 @@
                     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 @@
         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)
 
--- a/mercurial/commands.py	Mon May 19 01:53:34 2014 +0200
+++ b/mercurial/commands.py	Thu May 08 17:08:17 2014 -0700
@@ -3685,6 +3685,8 @@
      _("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 @@
     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 <target-file>.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 @@
 
           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 @@
     base = opts["base"]
     wlock = lock = tr = None
     msgs = []
+    ret = 0
 
 
     try:
@@ -3804,8 +3817,9 @@
 
                 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 @@
                         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 @@
                 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
--- a/mercurial/patch.py	Mon May 19 01:53:34 2014 +0200
+++ b/mercurial/patch.py	Thu May 08 17:08:17 2014 -0700
@@ -1521,14 +1521,11 @@
     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)
--- a/tests/test-completion.t	Mon May 19 01:53:34 2014 +0200
+++ b/tests/test-completion.t	Thu May 08 17:08:17 2014 -0700
@@ -262,7 +262,7 @@
   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
--- a/tests/test-import.t	Mon May 19 01:53:34 2014 +0200
+++ b/tests/test-import.t	Thu May 08 17:08:17 2014 -0700
@@ -1181,5 +1181,272 @@
   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