import: add --bypass option

This feature is more a way to test patching without a working directory than
something people asked about. Adding a --rev option to specify the parent patch
revision would make it a little more useful.

What this change introduces is patch.repobackend class which let patches be
applied against repository revisions. The caller must supply a filestore object
to receive patched content, which can be turned into a memctx with
patch.makememctx() helper.
This commit is contained in:
Patrick Mezard 2011-06-14 23:26:35 +02:00
parent 7350aaca49
commit fd8786d770
4 changed files with 415 additions and 47 deletions

View File

@ -3002,6 +3002,8 @@ def identify(ui, repo, source=None, rev=None,
('f', 'force', None, _('skip check for outstanding uncommitted changes')),
('', 'no-commit', None,
_("don't commit, just update the working directory")),
('', 'bypass', None,
_("apply patch without touching the working directory")),
('', 'exact', None,
_('apply patch to the nodes from which it was generated')),
('', 'import-branch', None,
@ -3035,6 +3037,11 @@ def import_(ui, repo, patch1, *patches, **opts):
the patch. This may happen due to character set problems or other
deficiencies in the text patch format.
Use --bypass to apply and commit patches directly to the
repository, not touching the working directory. Without --exact,
patches will be applied on top of the working directory parent
revision.
With -s/--similarity, hg will attempt to discover renames and
copies in the patch in the same way as 'addremove'.
@ -3050,14 +3057,19 @@ def import_(ui, repo, patch1, *patches, **opts):
if date:
opts['date'] = util.parsedate(date)
update = not opts.get('bypass')
if not update and opts.get('no_commit'):
raise util.Abort(_('cannot use --no-commit with --bypass'))
try:
sim = float(opts.get('similarity') or 0)
except ValueError:
raise util.Abort(_('similarity must be a number'))
if sim < 0 or sim > 100:
raise util.Abort(_('similarity must be between 0 and 100'))
if sim and not update:
raise util.Abort(_('cannot use --similarity with --bypass'))
if opts.get('exact') or not opts.get('force'):
if (opts.get('exact') or not opts.get('force')) and update:
cmdutil.bailifchanged(repo)
d = opts["base"]
@ -3065,7 +3077,12 @@ def import_(ui, repo, patch1, *patches, **opts):
wlock = lock = None
msgs = []
def tryone(ui, hunk):
def checkexact(repo, n, nodeid):
if opts.get('exact') and hex(n) != nodeid:
repo.rollback()
raise util.Abort(_('patch is damaged or loses information'))
def tryone(ui, hunk, parents):
tmpname, message, user, date, branch, nodeid, p1, p2 = \
patch.extract(ui, hunk)
@ -3086,9 +3103,8 @@ def import_(ui, repo, patch1, *patches, **opts):
message = None
ui.debug('message:\n%s\n' % message)
wp = repo.parents()
if len(wp) == 1:
wp.append(repo[nullid])
if len(parents) == 1:
parents.append(repo[nullid])
if opts.get('exact'):
if not nodeid or not p1:
raise util.Abort(_('not a Mercurial patch'))
@ -3099,44 +3115,65 @@ def import_(ui, repo, patch1, *patches, **opts):
p1 = repo[p1]
p2 = repo[p2]
except error.RepoError:
p1, p2 = wp
p1, p2 = parents
else:
p1, p2 = wp
p1, p2 = parents
if opts.get('exact') and p1 != wp[0]:
hg.clean(repo, p1.node())
if p1 != wp[0] and p2 != wp[1]:
repo.dirstate.setparents(p1.node(), p2.node())
n = None
if update:
if opts.get('exact') and p1 != parents[0]:
hg.clean(repo, p1.node())
if p1 != parents[0] and p2 != parents[1]:
repo.dirstate.setparents(p1.node(), p2.node())
if opts.get('exact') or opts.get('import_branch'):
repo.dirstate.setbranch(branch or 'default')
if opts.get('exact') or opts.get('import_branch'):
repo.dirstate.setbranch(branch or 'default')
files = set()
patch.patch(ui, repo, tmpname, strip=strip, files=files,
eolmode=None, similarity=sim / 100.0)
files = list(files)
if opts.get('no_commit'):
if message:
msgs.append(message)
else:
if opts.get('exact'):
m = None
files = set()
patch.patch(ui, repo, tmpname, strip=strip, files=files,
eolmode=None, similarity=sim / 100.0)
files = list(files)
if opts.get('no_commit'):
if message:
msgs.append(message)
else:
m = scmutil.matchfiles(repo, files or [])
n = repo.commit(message, opts.get('user') or user,
opts.get('date') or date, match=m,
editor=cmdutil.commiteditor)
if opts.get('exact'):
if hex(n) != nodeid:
repo.rollback()
raise util.Abort(_('patch is damaged'
' or loses information'))
# Force a dirstate write so that the next transaction
# backups an up-do-date file.
repo.dirstate.write()
if n:
commitid = short(n)
if opts.get('exact'):
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=cmdutil.commiteditor)
checkexact(repo, n, nodeid)
# Force a dirstate write so that the next transaction
# backups an up-to-date file.
repo.dirstate.write()
else:
if opts.get('exact') or opts.get('import_branch'):
branch = branch or 'default'
else:
branch = p1.branch()
store = patch.filestore()
try:
files = set()
try:
patch.patchrepo(ui, repo, p1, store, tmpname, strip,
files, eolmode=None)
except patch.PatchError, e:
raise util.Abort(str(e))
memctx = patch.makememctx(repo, (p1.node(), p2.node()),
message,
opts.get('user') or user,
opts.get('date') or date,
branch, files, store,
editor=cmdutil.commiteditor)
repo.savecommitmessage(memctx.description())
n = memctx.commit()
checkexact(repo, n, nodeid)
finally:
store.close()
if n:
commitid = short(n)
return commitid
finally:
os.unlink(tmpname)
@ -3144,6 +3181,7 @@ def import_(ui, repo, patch1, *patches, **opts):
try:
wlock = repo.wlock()
lock = repo.lock()
parents = repo.parents()
lastcommit = None
for p in patches:
pf = os.path.join(d, p)
@ -3157,12 +3195,16 @@ def import_(ui, repo, patch1, *patches, **opts):
haspatch = False
for hunk in patch.split(pf):
commitid = tryone(ui, hunk)
commitid = tryone(ui, hunk, parents)
if commitid:
haspatch = True
if lastcommit:
ui.status(_('applied %s\n') % lastcommit)
lastcommit = commitid
if update or opts.get('exact'):
parents = repo.parents()
else:
parents = [repo[commitid]]
if not haspatch:
raise util.Abort(_('no diffs found'))

View File

@ -11,7 +11,8 @@ import tempfile, zlib, shutil
from i18n import _
from node import hex, nullid, short
import base85, mdiff, scmutil, util, diffhelpers, copies, encoding
import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
import context
gitre = re.compile('diff --git a/(.*) b/(.*)')
@ -511,6 +512,48 @@ class filestore(object):
if self.opener:
shutil.rmtree(self.opener.base)
class repobackend(abstractbackend):
def __init__(self, ui, repo, ctx, store):
super(repobackend, self).__init__(ui)
self.repo = repo
self.ctx = ctx
self.store = store
self.changed = set()
self.removed = set()
self.copied = {}
def _checkknown(self, fname):
if fname not in self.ctx:
raise PatchError(_('cannot patch %s: file is not tracked') % fname)
def getfile(self, fname):
try:
fctx = self.ctx[fname]
except error.LookupError:
raise IOError()
flags = fctx.flags()
return fctx.data(), ('l' in flags, 'x' in flags)
def setfile(self, fname, data, mode, copysource):
if copysource:
self._checkknown(copysource)
if data is None:
data = self.ctx[fname].data()
self.store.setfile(fname, data, mode, copysource)
self.changed.add(fname)
if copysource:
self.copied[fname] = copysource
def unlink(self, fname):
self._checkknown(fname)
self.removed.add(fname)
def exists(self, fname):
return fname in self.ctx
def close(self):
return self.changed | self.removed
# @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
@ -1332,11 +1375,7 @@ def _externalpatch(ui, repo, patcher, patchname, strip, files,
util.explainexit(code)[0])
return fuzz
def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
similarity=0):
"""use builtin patch to apply <patchobj> to the working directory.
returns whether patch was applied with fuzz factor."""
def patchbackend(ui, backend, patchobj, strip, files=None, eolmode='strict'):
if files is None:
files = set()
if eolmode is None:
@ -1346,7 +1385,6 @@ def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
eolmode = eolmode.lower()
store = filestore()
backend = workingbackend(ui, repo, similarity)
try:
fp = open(patchobj, 'rb')
except TypeError:
@ -1363,6 +1401,33 @@ def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
raise PatchError(_('patch failed to apply'))
return ret > 0
def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
similarity=0):
"""use builtin patch to apply <patchobj> to the working directory.
returns whether patch was applied with fuzz factor."""
backend = workingbackend(ui, repo, similarity)
return patchbackend(ui, backend, patchobj, strip, files, eolmode)
def patchrepo(ui, repo, ctx, store, patchobj, strip, files=None,
eolmode='strict'):
backend = repobackend(ui, repo, ctx, store)
return patchbackend(ui, backend, patchobj, strip, files, eolmode)
def makememctx(repo, parents, text, user, date, branch, files, store,
editor=None):
def getfilectx(repo, memctx, path):
data, (islink, isexec), copied = store.getfile(path)
return context.memfilectx(path, data, islink=islink, isexec=isexec,
copied=copied)
extra = {}
if branch:
extra['branch'] = encoding.fromlocal(branch)
ctx = context.memctx(repo, parents, text, files, getfilectx, user,
date, extra)
if editor:
ctx._text = editor(repo, ctx, [])
return ctx
def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
similarity=0):
"""Apply <patchname> to the working directory.

View File

@ -245,7 +245,7 @@ Show all commands + options
heads: rev, topo, active, closed, style, template
help: extension, command
identify: rev, num, id, branch, tags, bookmarks
import: strip, base, force, no-commit, exact, import-branch, message, logfile, date, user, similarity
import: strip, base, force, no-commit, bypass, exact, import-branch, message, logfile, date, user, similarity
incoming: force, newest-first, bundle, rev, bookmarks, branch, patch, git, limit, no-merges, stat, style, template, ssh, remotecmd, insecure, subrepos
locate: rev, print0, fullpath, include, exclude
manifest: rev, all

261
tests/test-import-bypass.t Normal file
View File

@ -0,0 +1,261 @@
$ echo "[extensions]" >> $HGRCPATH
$ echo "purge=" >> $HGRCPATH
$ echo "graphlog=" >> $HGRCPATH
$ shortlog() {
> hg glog --template '{rev}:{node|short} {author} {date|hgdate} - {branch} - {desc|firstline}\n'
> }
Test --bypass with other options
$ hg init repo-options
$ cd repo-options
$ echo a > a
$ hg ci -Am adda
adding a
$ echo a >> a
$ hg branch foo
marked working directory as branch foo
$ hg ci -Am changea
$ hg export . > ../test.diff
$ hg up null
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
Test importing an existing revision
$ hg import --bypass --exact ../test.diff
applying ../test.diff
$ shortlog
o 1:4e322f7ce8e3 test 0 0 - foo - changea
|
o 0:07f494440405 test 0 0 - default - adda
Test failure without --exact
$ hg import --bypass ../test.diff
applying ../test.diff
unable to find 'a' for patching
abort: patch failed to apply
[255]
$ hg st
$ shortlog
o 1:4e322f7ce8e3 test 0 0 - foo - changea
|
o 0:07f494440405 test 0 0 - default - adda
Test --user, --date and --message
$ hg up 0
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ hg import --bypass --u test2 -d '1 0' -m patch2 ../test.diff
applying ../test.diff
$ cat .hg/last-message.txt
patch2 (no-eol)
$ shortlog
o 2:2e127d1da504 test2 1 0 - default - patch2
|
| o 1:4e322f7ce8e3 test 0 0 - foo - changea
|/
@ 0:07f494440405 test 0 0 - default - adda
$ hg rollback
repository tip rolled back to revision 1 (undo commit)
working directory now based on revision 0
Test --import-branch
$ hg import --bypass --import-branch ../test.diff
applying ../test.diff
$ shortlog
o 1:4e322f7ce8e3 test 0 0 - foo - changea
|
@ 0:07f494440405 test 0 0 - default - adda
$ hg rollback
repository tip rolled back to revision 1 (undo commit)
working directory now based on revision 0
Test --strip
$ hg import --bypass --strip 0 - <<EOF
> # HG changeset patch
> # User test
> # Date 0 0
> # Branch foo
> # Node ID 4e322f7ce8e3e4203950eac9ece27bf7e45ffa6c
> # Parent 07f4944404050f47db2e5c5071e0e84e7a27bba9
> changea
>
> diff -r 07f494440405 -r 4e322f7ce8e3 a
> --- a Thu Jan 01 00:00:00 1970 +0000
> +++ a Thu Jan 01 00:00:00 1970 +0000
> @@ -1,1 +1,2 @@
> a
> +a
> EOF
applying patch from stdin
$ hg rollback
repository tip rolled back to revision 1 (undo commit)
working directory now based on revision 0
Test unsupported combinations
$ hg import --bypass --no-commit ../test.diff
abort: cannot use --no-commit with --bypass
[255]
$ hg import --bypass --similarity 50 ../test.diff
abort: cannot use --similarity with --bypass
[255]
Test commit editor
$ hg diff -c 1 > ../test.diff
$ HGEDITOR=cat hg import --bypass ../test.diff
applying ../test.diff
HG: Enter commit message. Lines beginning with 'HG:' are removed.
HG: Leave message empty to abort commit.
HG: --
HG: user: test
HG: branch 'default'
HG: changed a
abort: empty commit message
[255]
Test patch.eol is handled
$ python -c 'file("a", "wb").write("a\r\n")'
$ hg ci -m makeacrlf
$ hg import -m 'should fail because of eol' --bypass ../test.diff
applying ../test.diff
patching file a
Hunk #1 FAILED at 0
abort: patch failed to apply
[255]
$ hg --config patch.eol=auto import -d '0 0' -m 'test patch.eol' --bypass ../test.diff
applying ../test.diff
$ shortlog
o 3:d7805b4d2cb3 test 0 0 - default - test patch.eol
|
@ 2:872023de769d test 0 0 - default - makeacrlf
|
| o 1:4e322f7ce8e3 test 0 0 - foo - changea
|/
o 0:07f494440405 test 0 0 - default - adda
Test applying multiple patches
$ hg up -qC 0
$ echo e > e
$ hg ci -Am adde
adding e
created new head
$ hg export . > ../patch1.diff
$ hg up -qC 1
$ echo f > f
$ hg ci -Am addf
adding f
$ hg export . > ../patch2.diff
$ cd ..
$ hg clone -r1 repo-options repo-multi1
adding changesets
adding manifests
adding file changes
added 2 changesets with 2 changes to 1 files
updating to branch foo
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ cd repo-multi1
$ hg up 0
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ hg import --bypass ../patch1.diff ../patch2.diff
applying ../patch1.diff
applying ../patch2.diff
applied 16581080145e
$ shortlog
o 3:bc8ca3f8a7c4 test 0 0 - default - addf
|
o 2:16581080145e test 0 0 - default - adde
|
| o 1:4e322f7ce8e3 test 0 0 - foo - changea
|/
@ 0:07f494440405 test 0 0 - default - adda
Test applying multiple patches with --exact
$ cd ..
$ hg clone -r1 repo-options repo-multi2
adding changesets
adding manifests
adding file changes
added 2 changesets with 2 changes to 1 files
updating to branch foo
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ cd repo-multi2
$ hg import --bypass --exact ../patch1.diff ../patch2.diff
applying ../patch1.diff
applying ../patch2.diff
applied 16581080145e
$ shortlog
o 3:d60cb8989666 test 0 0 - foo - addf
|
| o 2:16581080145e test 0 0 - default - adde
| |
@ | 1:4e322f7ce8e3 test 0 0 - foo - changea
|/
o 0:07f494440405 test 0 0 - default - adda
$ cd ..
Test complicated patch with --exact
$ hg init repo-exact
$ cd repo-exact
$ echo a > a
$ echo c > c
$ echo d > d
$ echo e > e
$ echo f > f
$ chmod +x f
$ ln -s c linkc
$ hg ci -Am t
adding a
adding c
adding d
adding e
adding f
adding linkc
$ hg cp a aa1
$ echo b >> a
$ echo b > b
$ hg add b
$ hg cp a aa2
$ echo aa >> aa2
$ chmod +x e
$ chmod -x f
$ ln -s a linka
$ hg rm d
$ hg rm linkc
$ hg mv c cc
$ hg ci -m patch
$ hg export --git . > ../test.diff
$ hg up -C null
0 files updated, 0 files merged, 7 files removed, 0 files unresolved
$ hg purge
$ hg st
$ hg import --bypass --exact ../test.diff
applying ../test.diff
The patch should have matched the exported revision and generated no additional
data. If not, diff both heads to debug it.
$ shortlog
o 1:2978fd5c8aa4 test 0 0 - default - patch
|
o 0:a0e19e636a43 test 0 0 - default - t