[PATCH] import: add --partial flag to create a changeset despite failed hunk
pierre-yves.david at ens-lyon.org
pierre-yves.david at ens-lyon.org
Thu May 8 20:59:36 CDT 2014
# HG changeset patch
# User Pierre-Yves David <pierre-yves.david at 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.
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
More information about the Mercurial-devel
mailing list