diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -3002,6 +3002,8 @@ def identify(ui, repo, source=None, rev= ('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, 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, 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 @@ def import_(ui, repo, patch1, *patches, 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, 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, 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 @@ def import_(ui, repo, patch1, *patches, 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, 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')) diff --git a/mercurial/patch.py b/mercurial/patch.py --- a/mercurial/patch.py +++ b/mercurial/patch.py @@ -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, pa util.explainexit(code)[0]) return fuzz -def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict', - similarity=0): - """use builtin patch to apply 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, st 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, st 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 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 to the working directory. diff --git a/tests/test-debugcomplete.t b/tests/test-debugcomplete.t --- a/tests/test-debugcomplete.t +++ b/tests/test-debugcomplete.t @@ -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 diff --git a/tests/test-import-bypass.t b/tests/test-import-bypass.t new file mode 100644 --- /dev/null +++ b/tests/test-import-bypass.t @@ -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 - < # 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 +