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.
--- a/mercurial/commands.py Tue Jun 14 23:24:40 2011 +0200
+++ b/mercurial/commands.py Tue Jun 14 23:26:35 2011 +0200
@@ -3002,6 +3002,8 @@
('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 @@
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 @@
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 opts.get('exact') or not opts.get('force'):
+ if sim and not update:
+ raise util.Abort(_('cannot use --similarity with --bypass'))
+
+ if (opts.get('exact') or not opts.get('force')) and update:
cmdutil.bailifchanged(repo)
d = opts["base"]
@@ -3065,7 +3077,12 @@
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 @@
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 @@
p1 = repo[p1]
p2 = repo[p2]
except error.RepoError:
- p1, p2 = wp
+ p1, p2 = parents
else:
- p1, p2 = wp
-
- 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())
-
- 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)
+ p1, p2 = parents
+
+ 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')
+
+ 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
+ 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'):
- m = None
+ if opts.get('exact') or opts.get('import_branch'):
+ branch = branch or 'default'
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)
-
+ 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 @@
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 @@
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'))
--- a/mercurial/patch.py Tue Jun 14 23:24:40 2011 +0200
+++ b/mercurial/patch.py Tue Jun 14 23:26:35 2011 +0200
@@ -11,7 +11,8 @@
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 @@
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 @@
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 @@
eolmode = eolmode.lower()
store = filestore()
- backend = workingbackend(ui, repo, similarity)
try:
fp = open(patchobj, 'rb')
except TypeError:
@@ -1363,6 +1401,33 @@
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.
--- a/tests/test-debugcomplete.t Tue Jun 14 23:24:40 2011 +0200
+++ b/tests/test-debugcomplete.t Tue Jun 14 23:26:35 2011 +0200
@@ -245,7 +245,7 @@
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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-import-bypass.t Tue Jun 14 23:26:35 2011 +0200
@@ -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
+