transplant.py
931 lines
| 30.6 KiB
| text/x-python
|
PythonLexer
/ hgext / transplant.py
|
r3714 | # Patch transplanting extension for Mercurial | ||
# | ||||
|
r4635 | # Copyright 2006, 2007 Brendan Cully <brendan@kublai.com> | ||
|
r3714 | # | ||
|
r8225 | # This software may be used and distributed according to the terms of the | ||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r3714 | |||
|
r8934 | '''command to transplant changesets from another branch | ||
|
r3714 | |||
|
r19028 | This extension allows you to transplant changes to another parent revision, | ||
possibly in another repository. The transplant is done using 'diff' patches. | ||||
|
r3714 | |||
|
r8000 | Transplanted patches are recorded in .hg/transplant/transplants, as a | ||
map from a changeset hash to its hash in the source repository. | ||||
|
r3714 | ''' | ||
|
r28481 | from __future__ import absolute_import | ||
|
r3714 | |||
|
r28481 | import os | ||
|
r38182 | |||
|
r7629 | from mercurial.i18n import _ | ||
|
r43355 | from mercurial.pycompat import open | ||
|
r28481 | from mercurial import ( | ||
bundlerepo, | ||||
cmdutil, | ||||
error, | ||||
exchange, | ||||
hg, | ||||
|
r35906 | logcmdutil, | ||
|
r28481 | match, | ||
merge, | ||||
node as nodemod, | ||||
patch, | ||||
|
r30925 | pycompat, | ||
|
r28481 | registrar, | ||
revlog, | ||||
revset, | ||||
scmutil, | ||||
|
r31023 | smartset, | ||
|
r42729 | state as statemod, | ||
|
r28481 | util, | ||
|
r31245 | vfs as vfsmod, | ||
|
r28481 | ) | ||
|
r37102 | from mercurial.utils import ( | ||
|
r37138 | procutil, | ||
|
r37102 | stringutil, | ||
) | ||||
|
r7629 | |||
|
r43346 | |||
|
r16507 | class TransplantError(error.Abort): | ||
pass | ||||
|
r43346 | |||
|
r14308 | cmdtable = {} | ||
|
r32337 | command = registrar.command(cmdtable) | ||
|
r29841 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||
|
r25186 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | ||
# be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | ||||
|
r43347 | testedwith = b'ships-with-hg-core' | ||
|
r14308 | |||
|
r34470 | configtable = {} | ||
configitem = registrar.configitem(configtable) | ||||
|
r43346 | configitem( | ||
|
r43347 | b'transplant', b'filter', default=None, | ||
|
r34470 | ) | ||
|
r43346 | configitem( | ||
|
r43347 | b'transplant', b'log', default=None, | ||
|
r34471 | ) | ||
|
r34470 | |||
|
r43346 | |||
|
r8778 | class transplantentry(object): | ||
|
r3714 | def __init__(self, lnode, rnode): | ||
self.lnode = lnode | ||||
self.rnode = rnode | ||||
|
r43346 | |||
|
r8778 | class transplants(object): | ||
|
r3714 | def __init__(self, path=None, transplantfile=None, opener=None): | ||
self.path = path | ||||
self.transplantfile = transplantfile | ||||
self.opener = opener | ||||
if not opener: | ||||
|
r31245 | self.opener = vfsmod.vfs(self.path) | ||
|
r12313 | self.transplants = {} | ||
|
r3714 | self.dirty = False | ||
self.read() | ||||
def read(self): | ||||
abspath = os.path.join(self.path, self.transplantfile) | ||||
if self.transplantfile and os.path.exists(abspath): | ||||
|
r14168 | for line in self.opener.read(self.transplantfile).splitlines(): | ||
|
r43347 | lnode, rnode = map(revlog.bin, line.split(b':')) | ||
|
r12313 | list = self.transplants.setdefault(rnode, []) | ||
list.append(transplantentry(lnode, rnode)) | ||||
|
r3714 | |||
def write(self): | ||||
if self.dirty and self.transplantfile: | ||||
if not os.path.isdir(self.path): | ||||
os.mkdir(self.path) | ||||
|
r43347 | fp = self.opener(self.transplantfile, b'w') | ||
|
r12349 | for list in self.transplants.itervalues(): | ||
for t in list: | ||||
|
r28480 | l, r = map(nodemod.hex, (t.lnode, t.rnode)) | ||
|
r43347 | fp.write(l + b':' + r + b'\n') | ||
|
r3714 | fp.close() | ||
self.dirty = False | ||||
def get(self, rnode): | ||||
|
r12313 | return self.transplants.get(rnode) or [] | ||
|
r3714 | |||
def set(self, lnode, rnode): | ||||
|
r12313 | list = self.transplants.setdefault(rnode, []) | ||
list.append(transplantentry(lnode, rnode)) | ||||
|
r3714 | self.dirty = True | ||
def remove(self, transplant): | ||||
|
r12313 | list = self.transplants.get(transplant.rnode) | ||
if list: | ||||
del list[list.index(transplant)] | ||||
self.dirty = True | ||||
|
r3714 | |||
|
r43346 | |||
|
r8778 | class transplanter(object): | ||
|
r21411 | def __init__(self, ui, repo, opts): | ||
|
r3714 | self.ui = ui | ||
|
r43347 | self.path = repo.vfs.join(b'transplant') | ||
|
r31245 | self.opener = vfsmod.vfs(self.path) | ||
|
r43346 | self.transplants = transplants( | ||
|
r43347 | self.path, b'transplants', opener=self.opener | ||
|
r43346 | ) | ||
|
r22252 | def getcommiteditor(): | ||
|
r43347 | editform = cmdutil.mergeeditform(repo[None], b'transplant') | ||
|
r43346 | return cmdutil.getcommiteditor( | ||
editform=editform, **pycompat.strkwargs(opts) | ||||
) | ||||
|
r22252 | self.getcommiteditor = getcommiteditor | ||
|
r3714 | |||
def applied(self, repo, node, parent): | ||||
'''returns True if a node is already an ancestor of parent | ||||
|
r17010 | or is parent or has already been transplanted''' | ||
if hasnode(repo, parent): | ||||
parentrev = repo.changelog.rev(parent) | ||||
|
r3714 | if hasnode(repo, node): | ||
|
r17010 | rev = repo.changelog.rev(node) | ||
|
r43346 | reachable = repo.changelog.ancestors( | ||
[parentrev], rev, inclusive=True | ||||
) | ||||
|
r17010 | if rev in reachable: | ||
|
r3714 | return True | ||
for t in self.transplants.get(node): | ||||
# it might have been stripped | ||||
if not hasnode(repo, t.lnode): | ||||
self.transplants.remove(t) | ||||
return False | ||||
|
r17010 | lnoderev = repo.changelog.rev(t.lnode) | ||
|
r43346 | if lnoderev in repo.changelog.ancestors( | ||
[parentrev], lnoderev, inclusive=True | ||||
): | ||||
|
r3714 | return True | ||
return False | ||||
|
r26346 | def apply(self, repo, source, revmap, merges, opts=None): | ||
|
r3714 | '''apply the revisions in revmap one by one in revision order''' | ||
|
r26346 | if opts is None: | ||
opts = {} | ||||
|
r8209 | revs = sorted(revmap) | ||
|
r41444 | p1 = repo.dirstate.p1() | ||
|
r3714 | pulls = [] | ||
|
r23452 | diffopts = patch.difffeatureopts(self.ui, opts) | ||
|
r3714 | diffopts.git = True | ||
|
r27289 | lock = tr = None | ||
|
r3714 | try: | ||
|
r4915 | lock = repo.lock() | ||
|
r43347 | tr = repo.transaction(b'transplant') | ||
|
r3714 | for rev in revs: | ||
node = revmap[rev] | ||||
|
r43347 | revstr = b'%d:%s' % (rev, nodemod.short(node)) | ||
|
r3714 | |||
if self.applied(repo, node, p1): | ||||
|
r43346 | self.ui.warn( | ||
|
r43347 | _(b'skipping already applied revision %s\n') % revstr | ||
|
r43346 | ) | ||
|
r3714 | continue | ||
parents = source.changelog.parents(node) | ||||
|
r43347 | if not (opts.get(b'filter') or opts.get(b'log')): | ||
|
r7744 | # If the changeset parent is the same as the | ||
# wdir's parent, just pull it. | ||||
|
r3714 | if parents[0] == p1: | ||
pulls.append(node) | ||||
p1 = node | ||||
continue | ||||
if pulls: | ||||
if source != repo: | ||||
|
r22699 | exchange.pull(repo, source.peer(), heads=pulls) | ||
|
r43346 | merge.update( | ||
repo, pulls[-1], branchmerge=False, force=False | ||||
) | ||||
|
r41444 | p1 = repo.dirstate.p1() | ||
|
r3714 | pulls = [] | ||
domerge = False | ||||
if node in merges: | ||||
|
r7744 | # pulling all the merge revs at once would mean we | ||
# couldn't transplant after the latest even if | ||||
# transplants before them fail. | ||||
|
r3714 | domerge = True | ||
if not hasnode(repo, node): | ||||
|
r22699 | exchange.pull(repo, source.peer(), heads=[node]) | ||
|
r3714 | |||
|
r16400 | skipmerge = False | ||
|
r3714 | if parents[1] != revlog.nullid: | ||
|
r43347 | if not opts.get(b'parent'): | ||
|
r43346 | self.ui.note( | ||
|
r43347 | _(b'skipping merge changeset %d:%s\n') | ||
|
r43346 | % (rev, nodemod.short(node)) | ||
) | ||||
|
r16400 | skipmerge = True | ||
else: | ||||
|
r43347 | parent = source.lookup(opts[b'parent']) | ||
|
r16400 | if parent not in parents: | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b'%s is not a parent of %s') | ||
|
r43346 | % (nodemod.short(parent), nodemod.short(node)) | ||
) | ||||
|
r16400 | else: | ||
parent = parents[0] | ||||
if skipmerge: | ||||
|
r3714 | patchfile = None | ||
else: | ||||
|
r43347 | fd, patchfile = pycompat.mkstemp(prefix=b'hg-transplant-') | ||
|
r36853 | fp = os.fdopen(fd, r'wb') | ||
|
r16400 | gen = patch.diff(source, parent, node, opts=diffopts) | ||
|
r7308 | for chunk in gen: | ||
fp.write(chunk) | ||||
|
r3714 | fp.close() | ||
del revmap[rev] | ||||
if patchfile or domerge: | ||||
try: | ||||
|
r16507 | try: | ||
|
r43346 | n = self.applyone( | ||
repo, | ||||
node, | ||||
source.changelog.read(node), | ||||
patchfile, | ||||
merge=domerge, | ||||
|
r43347 | log=opts.get(b'log'), | ||
filter=opts.get(b'filter'), | ||||
|
r43346 | ) | ||
|
r16507 | except TransplantError: | ||
# Do not rollback, it is up to the user to | ||||
# fix the merge or cancel everything | ||||
tr.close() | ||||
raise | ||||
|
r4251 | if n and domerge: | ||
|
r43346 | self.ui.status( | ||
|
r43347 | _(b'%s merged at %s\n') | ||
|
r43346 | % (revstr, nodemod.short(n)) | ||
) | ||||
|
r4251 | elif n: | ||
|
r43346 | self.ui.status( | ||
|
r43347 | _(b'%s transplanted to %s\n') | ||
|
r43346 | % (nodemod.short(node), nodemod.short(n)) | ||
) | ||||
|
r3714 | finally: | ||
if patchfile: | ||||
os.unlink(patchfile) | ||||
|
r15204 | tr.close() | ||
|
r3714 | if pulls: | ||
|
r22699 | exchange.pull(repo, source.peer(), heads=pulls) | ||
|
r40402 | merge.update(repo, pulls[-1], branchmerge=False, force=False) | ||
|
r3714 | finally: | ||
self.saveseries(revmap, merges) | ||||
self.transplants.write() | ||||
|
r15204 | if tr: | ||
tr.release() | ||||
|
r25879 | if lock: | ||
lock.release() | ||||
|
r3714 | |||
|
r13579 | def filter(self, filter, node, changelog, patchfile): | ||
|
r3714 | '''arbitrarily rewrite changeset before applying it''' | ||
|
r43347 | self.ui.status(_(b'filtering %s\n') % patchfile) | ||
|
r3759 | user, date, msg = (changelog[1], changelog[2], changelog[4]) | ||
|
r43347 | fd, headerfile = pycompat.mkstemp(prefix=b'hg-transplant-') | ||
|
r36853 | fp = os.fdopen(fd, r'wb') | ||
|
r43347 | fp.write(b"# HG changeset patch\n") | ||
fp.write(b"# User %s\n" % user) | ||||
fp.write(b"# Date %d %d\n" % date) | ||||
fp.write(msg + b'\n') | ||||
|
r3759 | fp.close() | ||
try: | ||||
|
r43346 | self.ui.system( | ||
|
r43347 | b'%s %s %s' | ||
|
r43346 | % ( | ||
filter, | ||||
procutil.shellquote(headerfile), | ||||
procutil.shellquote(patchfile), | ||||
), | ||||
environ={ | ||||
|
r43347 | b'HGUSER': changelog[1], | ||
b'HGREVISION': nodemod.hex(node), | ||||
|
r43346 | }, | ||
onerr=error.Abort, | ||||
|
r43347 | errprefix=_(b'filter failed'), | ||
blockedtag=b'transplant_filter', | ||||
|
r43346 | ) | ||
|
r43347 | user, date, msg = self.parselog(open(headerfile, b'rb'))[1:4] | ||
|
r3759 | finally: | ||
os.unlink(headerfile) | ||||
return (user, date, msg) | ||||
|
r3714 | |||
|
r43346 | def applyone( | ||
self, repo, node, cl, patchfile, merge=False, log=False, filter=None | ||||
): | ||||
|
r3714 | '''apply the patch in patchfile to the repository as a transplant''' | ||
(manifest, user, (time, timezone), files, message) = cl[:5] | ||||
|
r43347 | date = b"%d %d" % (time, timezone) | ||
extra = {b'transplant_source': node} | ||||
|
r3714 | if filter: | ||
|
r13579 | (user, date, message) = self.filter(filter, node, cl, patchfile) | ||
|
r3714 | |||
if log: | ||||
|
r9183 | # we don't translate messages inserted into commits | ||
|
r43347 | message += b'\n(transplanted from %s)' % nodemod.hex(node) | ||
|
r3714 | |||
|
r43347 | self.ui.status(_(b'applying %s\n') % nodemod.short(node)) | ||
self.ui.note(b'%s %s\n%s\n' % (user, date, message)) | ||||
|
r3714 | |||
if not patchfile and not merge: | ||||
|
r43347 | raise error.Abort(_(b'can only omit patchfile if merging')) | ||
|
r3714 | if patchfile: | ||
try: | ||||
|
r14564 | files = set() | ||
|
r14382 | patch.patch(self.ui, repo, patchfile, files=files, eolmode=None) | ||
|
r14260 | files = list(files) | ||
|
r25660 | except Exception as inst: | ||
|
r43347 | seriespath = os.path.join(self.path, b'series') | ||
|
r3757 | if os.path.exists(seriespath): | ||
os.unlink(seriespath) | ||||
|
r13878 | p1 = repo.dirstate.p1() | ||
|
r3714 | p2 = node | ||
|
r3725 | self.log(user, date, message, p1, p2, merge=merge) | ||
|
r43347 | self.ui.write(stringutil.forcebytestr(inst) + b'\n') | ||
|
r43346 | raise TransplantError( | ||
_( | ||||
|
r43347 | b'fix up the working directory and run ' | ||
b'hg transplant --continue' | ||||
|
r43346 | ) | ||
) | ||||
|
r3714 | else: | ||
files = None | ||||
if merge: | ||||
|
r41444 | p1 = repo.dirstate.p1() | ||
|
r16551 | repo.setparents(p1, node) | ||
|
r41825 | m = match.always() | ||
|
r8703 | else: | ||
|
r41825 | m = match.exact(files) | ||
|
r3714 | |||
|
r43346 | n = repo.commit( | ||
message, | ||||
user, | ||||
date, | ||||
extra=extra, | ||||
match=m, | ||||
editor=self.getcommiteditor(), | ||||
) | ||||
|
r11638 | if not n: | ||
|
r43346 | self.ui.warn( | ||
|
r43347 | _(b'skipping emptied changeset %s\n') % nodemod.short(node) | ||
|
r43346 | ) | ||
|
r17319 | return None | ||
|
r3714 | if not merge: | ||
self.transplants.set(n, node) | ||||
return n | ||||
|
r27677 | def canresume(self): | ||
|
r43347 | return os.path.exists(os.path.join(self.path, b'journal')) | ||
|
r27677 | |||
|
r18919 | def resume(self, repo, source, opts): | ||
|
r3714 | '''recover last transaction and apply remaining changesets''' | ||
|
r43347 | if os.path.exists(os.path.join(self.path, b'journal')): | ||
|
r18926 | n, node = self.recover(repo, source, opts) | ||
|
r23781 | if n: | ||
|
r43346 | self.ui.status( | ||
|
r43347 | _(b'%s transplanted as %s\n') | ||
|
r43346 | % (nodemod.short(node), nodemod.short(n)) | ||
) | ||||
|
r23781 | else: | ||
|
r43346 | self.ui.status( | ||
|
r43347 | _(b'%s skipped due to empty diff\n') | ||
% (nodemod.short(node),) | ||||
|
r43346 | ) | ||
|
r43347 | seriespath = os.path.join(self.path, b'series') | ||
|
r3714 | if not os.path.exists(seriespath): | ||
|
r3758 | self.transplants.write() | ||
|
r3714 | return | ||
nodes, merges = self.readseries() | ||||
revmap = {} | ||||
for n in nodes: | ||||
revmap[source.changelog.rev(n)] = n | ||||
os.unlink(seriespath) | ||||
self.apply(repo, source, revmap, merges, opts) | ||||
|
r18926 | def recover(self, repo, source, opts): | ||
|
r3714 | '''commit working directory using journal metadata''' | ||
node, user, date, message, parents = self.readlog() | ||||
|
r16400 | merge = False | ||
|
r3714 | |||
if not user or not date or not message or not parents[0]: | ||||
|
r43347 | raise error.Abort(_(b'transplant log file is corrupt')) | ||
|
r3714 | |||
|
r16400 | parent = parents[0] | ||
if len(parents) > 1: | ||||
|
r43347 | if opts.get(b'parent'): | ||
parent = source.lookup(opts[b'parent']) | ||||
|
r16400 | if parent not in parents: | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b'%s is not a parent of %s') | ||
|
r43346 | % (nodemod.short(parent), nodemod.short(node)) | ||
) | ||||
|
r16400 | else: | ||
merge = True | ||||
|
r43347 | extra = {b'transplant_source': node} | ||
|
r4915 | try: | ||
|
r41444 | p1 = repo.dirstate.p1() | ||
|
r16400 | if p1 != parent: | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b'working directory not at transplant ' b'parent %s') | ||
|
r43346 | % nodemod.hex(parent) | ||
) | ||||
|
r4915 | if merge: | ||
|
r16551 | repo.setparents(p1, parents[1]) | ||
|
r23781 | modified, added, removed, deleted = repo.status()[:4] | ||
if merge or modified or added or removed or deleted: | ||||
|
r43346 | n = repo.commit( | ||
message, | ||||
user, | ||||
date, | ||||
extra=extra, | ||||
editor=self.getcommiteditor(), | ||||
) | ||||
|
r23781 | if not n: | ||
|
r43347 | raise error.Abort(_(b'commit failed')) | ||
|
r23781 | if not merge: | ||
self.transplants.set(n, node) | ||||
else: | ||||
n = None | ||||
|
r4915 | self.unlog() | ||
|
r3714 | |||
|
r4915 | return n, node | ||
finally: | ||||
|
r27289 | # TODO: get rid of this meaningless try/finally enclosing. | ||
# this is kept only to reduce changes in a patch. | ||||
pass | ||||
|
r3714 | |||
|
r43030 | def stop(self, ui, repo): | ||
"""logic to stop an interrupted transplant""" | ||||
if self.canresume(): | ||||
|
r43347 | startctx = repo[b'.'] | ||
|
r43030 | hg.updaterepo(repo, startctx.node(), overwrite=True) | ||
|
r43347 | ui.status(_(b"stopped the interrupted transplant\n")) | ||
|
r43346 | ui.status( | ||
|
r43347 | _(b"working directory is now at %s\n") % startctx.hex()[:12] | ||
|
r43346 | ) | ||
|
r43030 | self.unlog() | ||
return 0 | ||||
|
r3714 | def readseries(self): | ||
nodes = [] | ||||
merges = [] | ||||
cur = nodes | ||||
|
r43347 | for line in self.opener.read(b'series').splitlines(): | ||
if line.startswith(b'# Merges'): | ||||
|
r3714 | cur = merges | ||
continue | ||||
cur.append(revlog.bin(line)) | ||||
return (nodes, merges) | ||||
def saveseries(self, revmap, merges): | ||||
if not revmap: | ||||
return | ||||
if not os.path.isdir(self.path): | ||||
os.mkdir(self.path) | ||||
|
r43347 | series = self.opener(b'series', b'w') | ||
|
r8209 | for rev in sorted(revmap): | ||
|
r43347 | series.write(nodemod.hex(revmap[rev]) + b'\n') | ||
|
r3714 | if merges: | ||
|
r43347 | series.write(b'# Merges\n') | ||
|
r3714 | for m in merges: | ||
|
r43347 | series.write(nodemod.hex(m) + b'\n') | ||
|
r3714 | series.close() | ||
|
r3759 | def parselog(self, fp): | ||
parents = [] | ||||
message = [] | ||||
node = revlog.nullid | ||||
inmsg = False | ||||
|
r13789 | user = None | ||
date = None | ||||
|
r3759 | for line in fp.read().splitlines(): | ||
if inmsg: | ||||
message.append(line) | ||||
|
r43347 | elif line.startswith(b'# User '): | ||
|
r3759 | user = line[7:] | ||
|
r43347 | elif line.startswith(b'# Date '): | ||
|
r3759 | date = line[7:] | ||
|
r43347 | elif line.startswith(b'# Node ID '): | ||
|
r3759 | node = revlog.bin(line[10:]) | ||
|
r43347 | elif line.startswith(b'# Parent '): | ||
|
r3759 | parents.append(revlog.bin(line[9:])) | ||
|
r43347 | elif not line.startswith(b'# '): | ||
|
r3759 | inmsg = True | ||
message.append(line) | ||||
|
r13789 | if None in (user, date): | ||
|
r43347 | raise error.Abort( | ||
_(b"filter corrupted changeset (no user or date)") | ||||
) | ||||
return (node, user, date, b'\n'.join(message), parents) | ||||
|
r4516 | |||
|
r3725 | def log(self, user, date, message, p1, p2, merge=False): | ||
|
r3714 | '''journal changelog metadata for later recover''' | ||
if not os.path.isdir(self.path): | ||||
os.mkdir(self.path) | ||||
|
r43347 | fp = self.opener(b'journal', b'w') | ||
fp.write(b'# User %s\n' % user) | ||||
fp.write(b'# Date %s\n' % date) | ||||
fp.write(b'# Node ID %s\n' % nodemod.hex(p2)) | ||||
fp.write(b'# Parent ' + nodemod.hex(p1) + b'\n') | ||||
|
r3714 | if merge: | ||
|
r43347 | fp.write(b'# Parent ' + nodemod.hex(p2) + b'\n') | ||
fp.write(message.rstrip() + b'\n') | ||||
|
r3714 | fp.close() | ||
def readlog(self): | ||||
|
r43347 | return self.parselog(self.opener(b'journal')) | ||
|
r3714 | |||
def unlog(self): | ||||
'''remove changelog journal''' | ||||
|
r43347 | absdst = os.path.join(self.path, b'journal') | ||
|
r3714 | if os.path.exists(absdst): | ||
os.unlink(absdst) | ||||
def transplantfilter(self, repo, source, root): | ||||
def matchfn(node): | ||||
if self.applied(repo, node, root): | ||||
return False | ||||
if source.changelog.parents(node)[1] != revlog.nullid: | ||||
return False | ||||
extra = source.changelog.read(node)[5] | ||||
|
r43347 | cnode = extra.get(b'transplant_source') | ||
|
r3714 | if cnode and self.applied(repo, cnode, root): | ||
return False | ||||
return True | ||||
return matchfn | ||||
|
r43346 | |||
|
r3714 | def hasnode(repo, node): | ||
try: | ||||
|
r13031 | return repo.changelog.rev(node) is not None | ||
|
r39813 | except error.StorageError: | ||
|
r3714 | return False | ||
|
r43346 | |||
|
r3714 | def browserevs(ui, repo, nodes, opts): | ||
'''interactively transplant changesets''' | ||||
|
r35906 | displayer = logcmdutil.changesetdisplayer(ui, repo, opts) | ||
|
r3714 | transplants = [] | ||
merges = [] | ||||
|
r43346 | prompt = _( | ||
|
r43347 | b'apply changeset? [ynmpcq?]:' | ||
b'$$ &yes, transplant this changeset' | ||||
b'$$ &no, skip this changeset' | ||||
b'$$ &merge at this changeset' | ||||
b'$$ show &patch' | ||||
b'$$ &commit selected changesets' | ||||
b'$$ &quit and cancel transplant' | ||||
b'$$ &? (show this help)' | ||||
|
r43346 | ) | ||
|
r3714 | for node in nodes: | ||
|
r7369 | displayer.show(repo[node]) | ||
|
r3714 | action = None | ||
while not action: | ||||
|
r38389 | choice = ui.promptchoice(prompt) | ||
|
r43347 | action = b'ynmpcq?'[choice : choice + 1] | ||
if action == b'?': | ||||
|
r20269 | for c, t in ui.extractchoices(prompt)[1]: | ||
|
r43347 | ui.write(b'%s: %s\n' % (c, t)) | ||
|
r3714 | action = None | ||
|
r43347 | elif action == b'p': | ||
|
r3714 | parent = repo.changelog.parents(node)[0] | ||
|
r7308 | for chunk in patch.diff(repo, parent, node): | ||
|
r8615 | ui.write(chunk) | ||
|
r3714 | action = None | ||
|
r43347 | if action == b'y': | ||
|
r3714 | transplants.append(node) | ||
|
r43347 | elif action == b'm': | ||
|
r3714 | merges.append(node) | ||
|
r43347 | elif action == b'c': | ||
|
r3714 | break | ||
|
r43347 | elif action == b'q': | ||
|
r3714 | transplants = () | ||
merges = () | ||||
break | ||||
|
r10152 | displayer.close() | ||
|
r3714 | return (transplants, merges) | ||
|
r43346 | |||
@command( | ||||
|
r43347 | b'transplant', | ||
|
r43346 | [ | ||
|
r43347 | ( | ||
b's', | ||||
b'source', | ||||
b'', | ||||
_(b'transplant changesets from REPO'), | ||||
_(b'REPO'), | ||||
), | ||||
|
r43346 | ( | ||
|
r43347 | b'b', | ||
b'branch', | ||||
[], | ||||
_(b'use this source changeset as head'), | ||||
_(b'REV'), | ||||
), | ||||
( | ||||
b'a', | ||||
b'all', | ||||
|
r43346 | None, | ||
|
r43347 | _(b'pull all changesets up to the --branch revisions'), | ||
|
r43346 | ), | ||
|
r43347 | (b'p', b'prune', [], _(b'skip over REV'), _(b'REV')), | ||
(b'm', b'merge', [], _(b'merge at REV'), _(b'REV')), | ||||
|
r43346 | ( | ||
|
r43347 | b'', | ||
b'parent', | ||||
b'', | ||||
_(b'parent to choose when transplanting merge'), | ||||
_(b'REV'), | ||||
|
r43346 | ), | ||
|
r43347 | (b'e', b'edit', False, _(b'invoke editor on commit messages')), | ||
(b'', b'log', None, _(b'append transplant info to log message')), | ||||
(b'', b'stop', False, _(b'stop interrupted transplant')), | ||||
( | ||||
b'c', | ||||
b'continue', | ||||
None, | ||||
_(b'continue last transplant session ' b'after fixing conflicts'), | ||||
), | ||||
|
r43346 | ( | ||
|
r43347 | b'', | ||
b'filter', | ||||
b'', | ||||
_(b'filter changesets through command'), | ||||
_(b'CMD'), | ||||
|
r43346 | ), | ||
], | ||||
|
r43347 | _( | ||
b'hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] ' | ||||
b'[-m REV] [REV]...' | ||||
), | ||||
|
r43346 | helpcategory=command.CATEGORY_CHANGE_MANAGEMENT, | ||
) | ||||
|
r3714 | def transplant(ui, repo, *revs, **opts): | ||
'''transplant changesets from another branch | ||||
Selected changesets will be applied on top of the current working | ||||
|
r13605 | directory with the log of the original changeset. The changesets | ||
|
r19028 | are copied and will thus appear twice in the history with different | ||
identities. | ||||
Consider using the graft command if everything is inside the same | ||||
repository - it will use merges and will usually give a better result. | ||||
Use the rebase extension if the changesets are unpublished and you want | ||||
to move them instead of copying them. | ||||
|
r13605 | |||
If --log is specified, log messages will have a comment appended | ||||
of the form:: | ||||
|
r3714 | |||
|
r9200 | (transplanted from CHANGESETHASH) | ||
|
r3714 | |||
You can rewrite the changelog message with the --filter option. | ||||
|
r8000 | Its argument will be invoked with the current changelog message as | ||
$1 and the patch as $2. | ||||
|
r3714 | |||
|
r19028 | --source/-s specifies another repository to use for selecting changesets, | ||
just as if it temporarily had been pulled. | ||||
|
r19027 | If --branch/-b is specified, these revisions will be used as | ||
|
r19951 | heads when deciding which changesets to transplant, just as if only | ||
|
r19027 | these revisions had been pulled. | ||
If --all/-a is specified, all the revisions up to the heads specified | ||||
with --branch will be transplanted. | ||||
|
r3714 | |||
|
r19027 | Example: | ||
- transplant all changes up to REV on top of your current revision:: | ||||
hg transplant --branch REV --all | ||||
|
r3714 | |||
|
r8000 | You can optionally mark selected transplanted changesets as merge | ||
changesets. You will not be prompted to transplant any ancestors | ||||
of a merged transplant, and you can merge descendants of them | ||||
normally instead of transplanting them. | ||||
|
r3714 | |||
|
r16400 | Merge changesets may be transplanted directly by specifying the | ||
|
r16457 | proper parent changeset by calling :hg:`transplant --parent`. | ||
|
r16400 | |||
|
r11193 | If no merges or revisions are provided, :hg:`transplant` will | ||
start an interactive changeset browser. | ||||
|
r3714 | |||
|
r8000 | If a changeset application fails, you can fix the merge by hand | ||
|
r11193 | and then resume where you left off by calling :hg:`transplant | ||
--continue/-c`. | ||||
|
r3714 | ''' | ||
|
r27840 | with repo.wlock(): | ||
|
r27289 | return _dotransplant(ui, repo, *revs, **opts) | ||
|
r43346 | |||
|
r27289 | def _dotransplant(ui, repo, *revs, **opts): | ||
|
r14161 | def incwalk(repo, csets, match=util.always): | ||
for node in csets: | ||||
|
r3714 | if match(node): | ||
yield node | ||||
|
r19027 | def transplantwalk(repo, dest, heads, match=util.always): | ||
'''Yield all nodes that are ancestors of a head but not ancestors | ||||
of dest. | ||||
If no heads are specified, the heads of repo will be used.''' | ||||
if not heads: | ||||
heads = repo.heads() | ||||
|
r3714 | ancestors = [] | ||
|
r20988 | ctx = repo[dest] | ||
|
r19027 | for head in heads: | ||
|
r20988 | ancestors.append(ctx.ancestor(repo[head]).node()) | ||
|
r19027 | for node in repo.changelog.nodesbetween(ancestors, heads)[0]: | ||
|
r3714 | if match(node): | ||
yield node | ||||
def checkopts(opts, revs): | ||||
|
r43347 | if opts.get(b'continue'): | ||
if opts.get(b'branch') or opts.get(b'all') or opts.get(b'merge'): | ||||
|
r43346 | raise error.Abort( | ||
_( | ||||
|
r43347 | b'--continue is incompatible with ' | ||
b'--branch, --all and --merge' | ||||
|
r43346 | ) | ||
) | ||||
|
r3714 | return | ||
|
r43347 | if opts.get(b'stop'): | ||
if opts.get(b'branch') or opts.get(b'all') or opts.get(b'merge'): | ||||
|
r43346 | raise error.Abort( | ||
_( | ||||
|
r43347 | b'--stop is incompatible with ' | ||
b'--branch, --all and --merge' | ||||
|
r43346 | ) | ||
) | ||||
|
r43030 | return | ||
|
r43346 | if not ( | ||
|
r43347 | opts.get(b'source') | ||
|
r43346 | or revs | ||
|
r43347 | or opts.get(b'merge') | ||
or opts.get(b'branch') | ||||
|
r43346 | ): | ||
raise error.Abort( | ||||
_( | ||||
|
r43347 | b'no source URL, branch revision, or revision ' | ||
b'list provided' | ||||
|
r43346 | ) | ||
) | ||||
|
r43347 | if opts.get(b'all'): | ||
if not opts.get(b'branch'): | ||||
raise error.Abort(_(b'--all requires a branch revision')) | ||||
|
r3714 | if revs: | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b'--all is incompatible with a ' b'revision list') | ||
|
r43346 | ) | ||
|
r3714 | |||
|
r36208 | opts = pycompat.byteskwargs(opts) | ||
|
r3714 | checkopts(opts, revs) | ||
|
r43347 | if not opts.get(b'log'): | ||
|
r25828 | # deprecated config: transplant.log | ||
|
r43347 | opts[b'log'] = ui.config(b'transplant', b'log') | ||
if not opts.get(b'filter'): | ||||
|
r25828 | # deprecated config: transplant.filter | ||
|
r43347 | opts[b'filter'] = ui.config(b'transplant', b'filter') | ||
|
r3714 | |||
|
r21411 | tp = transplanter(ui, repo, opts) | ||
|
r3714 | |||
|
r41444 | p1 = repo.dirstate.p1() | ||
|
r8176 | if len(repo) > 0 and p1 == revlog.nullid: | ||
|
r43347 | raise error.Abort(_(b'no revision checked out')) | ||
if opts.get(b'continue'): | ||||
|
r27677 | if not tp.canresume(): | ||
|
r43347 | raise error.Abort(_(b'no transplant to continue')) | ||
elif opts.get(b'stop'): | ||||
|
r43033 | if not tp.canresume(): | ||
|
r43347 | raise error.Abort(_(b'no interrupted transplant found')) | ||
|
r43033 | return tp.stop(ui, repo) | ||
|
r27677 | else: | ||
cmdutil.checkunfinished(repo) | ||||
|
r41416 | cmdutil.bailifchanged(repo) | ||
|
r3714 | |||
|
r43347 | sourcerepo = opts.get(b'source') | ||
|
r14161 | if sourcerepo: | ||
|
r17874 | peer = hg.peer(repo, opts, ui.expandpath(sourcerepo)) | ||
|
r43347 | heads = pycompat.maplist(peer.lookup, opts.get(b'branch', ())) | ||
|
r25679 | target = set(heads) | ||
for r in revs: | ||||
try: | ||||
target.add(peer.lookup(r)) | ||||
except error.RepoError: | ||||
pass | ||||
|
r43346 | source, csets, cleanupfn = bundlerepo.getremotechanges( | ||
ui, repo, peer, onlyheads=sorted(target), force=True | ||||
) | ||||
|
r3714 | else: | ||
source = repo | ||||
|
r43347 | heads = pycompat.maplist(source.lookup, opts.get(b'branch', ())) | ||
|
r14161 | cleanupfn = None | ||
|
r3714 | |||
try: | ||||
|
r43347 | if opts.get(b'continue'): | ||
|
r3724 | tp.resume(repo, source, opts) | ||
|
r3714 | return | ||
|
r10394 | tf = tp.transplantfilter(repo, source, p1) | ||
|
r43347 | if opts.get(b'prune'): | ||
|
r43346 | prune = set( | ||
source[r].node() | ||||
|
r43347 | for r in scmutil.revrange(source, opts.get(b'prune')) | ||
|
r43346 | ) | ||
|
r3714 | matchfn = lambda x: tf(x) and x not in prune | ||
else: | ||||
matchfn = tf | ||||
|
r43347 | merges = pycompat.maplist(source.lookup, opts.get(b'merge', ())) | ||
|
r3714 | revmap = {} | ||
if revs: | ||||
|
r14319 | for r in scmutil.revrange(source, revs): | ||
|
r37327 | revmap[int(r)] = source[r].node() | ||
|
r43347 | elif opts.get(b'all') or not merges: | ||
|
r3714 | if source != repo: | ||
|
r14161 | alltransplants = incwalk(source, csets, match=matchfn) | ||
|
r3714 | else: | ||
|
r43346 | alltransplants = transplantwalk( | ||
source, p1, heads, match=matchfn | ||||
) | ||||
|
r43347 | if opts.get(b'all'): | ||
|
r3714 | revs = alltransplants | ||
else: | ||||
revs, newmerges = browserevs(ui, source, alltransplants, opts) | ||||
merges.extend(newmerges) | ||||
for r in revs: | ||||
revmap[source.changelog.rev(r)] = r | ||||
for r in merges: | ||||
revmap[source.changelog.rev(r)] = r | ||||
tp.apply(repo, source, revmap, merges, opts) | ||||
finally: | ||||
|
r14161 | if cleanupfn: | ||
cleanupfn() | ||||
|
r3714 | |||
|
r43346 | |||
|
r42943 | def continuecmd(ui, repo): | ||
"""logic to resume an interrupted transplant using | ||||
'hg continue'""" | ||||
with repo.wlock(): | ||||
tp = transplanter(ui, repo, {}) | ||||
return tp.resume(repo, repo, {}) | ||||
|
r43346 | |||
|
r28394 | revsetpredicate = registrar.revsetpredicate() | ||
|
r27586 | |||
|
r43346 | |||
|
r43347 | @revsetpredicate(b'transplanted([set])') | ||
|
r12581 | def revsettransplanted(repo, subset, x): | ||
|
r27586 | """Transplanted changesets in set, or all transplanted changesets. | ||
|
r12822 | """ | ||
|
r12581 | if x: | ||
|
r17299 | s = revset.getset(repo, subset, x) | ||
|
r12581 | else: | ||
|
r17299 | s = subset | ||
|
r43346 | return smartset.baseset( | ||
|
r43347 | [r for r in s if repo[r].extra().get(b'transplant_source')] | ||
|
r43346 | ) | ||
|
r12581 | |||
|
r28540 | templatekeyword = registrar.templatekeyword() | ||
|
r43346 | |||
|
r43347 | @templatekeyword(b'transplanted', requires={b'ctx'}) | ||
|
r36531 | def kwtransplanted(context, mapping): | ||
|
r28540 | """String. The node identifier of the transplanted | ||
|
r13689 | changeset if any.""" | ||
|
r43347 | ctx = context.resource(mapping, b'ctx') | ||
n = ctx.extra().get(b'transplant_source') | ||||
return n and nodemod.hex(n) or b'' | ||||
|
r12581 | |||
|
r43346 | |||
|
r12822 | def extsetup(ui): | ||
|
r43346 | statemod.addunfinished( | ||
|
r43347 | b'transplant', | ||
fname=b'transplant/journal', | ||||
|
r43346 | clearable=True, | ||
|
r42943 | continuefunc=continuecmd, | ||
|
r43346 | statushint=_( | ||
|
r43347 | b'To continue: hg transplant --continue\n' | ||
b'To stop: hg transplant --stop' | ||||
|
r43346 | ), | ||
|
r43347 | cmdhint=_(b"use 'hg transplant --continue' or 'hg transplant --stop'"), | ||
|
r42730 | ) | ||
|
r12581 | |||
|
r43346 | |||
|
r12823 | # tell hggettext to extract docstrings from these functions: | ||
|
r13698 | i18nfunctions = [revsettransplanted, kwtransplanted] | ||