changeset 14611:adbf5e7df96d

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.
author Patrick Mezard <pmezard@gmail.com>
date Tue, 14 Jun 2011 23:26:35 +0200
parents 5d6244930559
children 4e1ccd4c2b6d
files mercurial/commands.py mercurial/patch.py tests/test-debugcomplete.t tests/test-import-bypass.t
diffstat 4 files changed, 418 insertions(+), 50 deletions(-) [+]
line wrap: on
line diff
--- 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
+