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