transplant.py
760 lines
| 27.9 KiB
| text/x-python
|
PythonLexer
/ hgext / transplant.py
Brendan Cully
|
r3714 | # Patch transplanting extension for Mercurial | ||
# | ||||
Thomas Arendsen Hein
|
r4635 | # Copyright 2006, 2007 Brendan Cully <brendan@kublai.com> | ||
Brendan Cully
|
r3714 | # | ||
Martin Geisler
|
r8225 | # This software may be used and distributed according to the terms of the | ||
Matt Mackall
|
r10263 | # GNU General Public License version 2 or any later version. | ||
Brendan Cully
|
r3714 | |||
Dirkjan Ochtman
|
r8934 | '''command to transplant changesets from another branch | ||
Brendan Cully
|
r3714 | |||
Mads Kiilerich
|
r19028 | This extension allows you to transplant changes to another parent revision, | ||
possibly in another repository. The transplant is done using 'diff' patches. | ||||
Brendan Cully
|
r3714 | |||
Martin Geisler
|
r8000 | Transplanted patches are recorded in .hg/transplant/transplants, as a | ||
map from a changeset hash to its hash in the source repository. | ||||
Brendan Cully
|
r3714 | ''' | ||
timeless
|
r28481 | from __future__ import absolute_import | ||
Brendan Cully
|
r3714 | |||
timeless
|
r28481 | import os | ||
import tempfile | ||||
Dirkjan Ochtman
|
r7629 | from mercurial.i18n import _ | ||
timeless
|
r28481 | from mercurial import ( | ||
bundlerepo, | ||||
cmdutil, | ||||
error, | ||||
exchange, | ||||
hg, | ||||
Yuya Nishihara
|
r35906 | logcmdutil, | ||
timeless
|
r28481 | match, | ||
merge, | ||||
node as nodemod, | ||||
patch, | ||||
Pulkit Goyal
|
r30925 | pycompat, | ||
timeless
|
r28481 | registrar, | ||
revlog, | ||||
revset, | ||||
scmutil, | ||||
Yuya Nishihara
|
r31023 | smartset, | ||
timeless
|
r28481 | util, | ||
Pierre-Yves David
|
r31245 | vfs as vfsmod, | ||
timeless
|
r28481 | ) | ||
Dirkjan Ochtman
|
r7629 | |||
Patrick Mezard
|
r16507 | class TransplantError(error.Abort): | ||
pass | ||||
Adrian Buehlmann
|
r14308 | cmdtable = {} | ||
Yuya Nishihara
|
r32337 | command = registrar.command(cmdtable) | ||
Augie Fackler
|
r29841 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||
Augie Fackler
|
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. | ||||
Augie Fackler
|
r29841 | testedwith = 'ships-with-hg-core' | ||
Adrian Buehlmann
|
r14308 | |||
Boris Feld
|
r34470 | configtable = {} | ||
configitem = registrar.configitem(configtable) | ||||
configitem('transplant', 'filter', | ||||
default=None, | ||||
) | ||||
Boris Feld
|
r34471 | configitem('transplant', 'log', | ||
default=None, | ||||
) | ||||
Boris Feld
|
r34470 | |||
Benoit Boissinot
|
r8778 | class transplantentry(object): | ||
Brendan Cully
|
r3714 | def __init__(self, lnode, rnode): | ||
self.lnode = lnode | ||||
self.rnode = rnode | ||||
Benoit Boissinot
|
r8778 | class transplants(object): | ||
Brendan Cully
|
r3714 | def __init__(self, path=None, transplantfile=None, opener=None): | ||
self.path = path | ||||
self.transplantfile = transplantfile | ||||
self.opener = opener | ||||
if not opener: | ||||
Pierre-Yves David
|
r31245 | self.opener = vfsmod.vfs(self.path) | ||
Peter Arrenbrecht
|
r12313 | self.transplants = {} | ||
Brendan Cully
|
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): | ||||
Dan Villiom Podlaski Christiansen
|
r14168 | for line in self.opener.read(self.transplantfile).splitlines(): | ||
Brendan Cully
|
r3714 | lnode, rnode = map(revlog.bin, line.split(':')) | ||
Peter Arrenbrecht
|
r12313 | list = self.transplants.setdefault(rnode, []) | ||
list.append(transplantentry(lnode, rnode)) | ||||
Brendan Cully
|
r3714 | |||
def write(self): | ||||
if self.dirty and self.transplantfile: | ||||
if not os.path.isdir(self.path): | ||||
os.mkdir(self.path) | ||||
fp = self.opener(self.transplantfile, 'w') | ||||
Peter Arrenbrecht
|
r12349 | for list in self.transplants.itervalues(): | ||
for t in list: | ||||
timeless
|
r28480 | l, r = map(nodemod.hex, (t.lnode, t.rnode)) | ||
Peter Arrenbrecht
|
r12313 | fp.write(l + ':' + r + '\n') | ||
Brendan Cully
|
r3714 | fp.close() | ||
self.dirty = False | ||||
def get(self, rnode): | ||||
Peter Arrenbrecht
|
r12313 | return self.transplants.get(rnode) or [] | ||
Brendan Cully
|
r3714 | |||
def set(self, lnode, rnode): | ||||
Peter Arrenbrecht
|
r12313 | list = self.transplants.setdefault(rnode, []) | ||
list.append(transplantentry(lnode, rnode)) | ||||
Brendan Cully
|
r3714 | self.dirty = True | ||
def remove(self, transplant): | ||||
Peter Arrenbrecht
|
r12313 | list = self.transplants.get(transplant.rnode) | ||
if list: | ||||
del list[list.index(transplant)] | ||||
self.dirty = True | ||||
Brendan Cully
|
r3714 | |||
Benoit Boissinot
|
r8778 | class transplanter(object): | ||
FUJIWARA Katsunori
|
r21411 | def __init__(self, ui, repo, opts): | ||
Brendan Cully
|
r3714 | self.ui = ui | ||
Pierre-Yves David
|
r31336 | self.path = repo.vfs.join('transplant') | ||
Pierre-Yves David
|
r31245 | self.opener = vfsmod.vfs(self.path) | ||
Martin Geisler
|
r7744 | self.transplants = transplants(self.path, 'transplants', | ||
opener=self.opener) | ||||
FUJIWARA Katsunori
|
r22252 | def getcommiteditor(): | ||
editform = cmdutil.mergeeditform(repo[None], 'transplant') | ||||
Pulkit Goyal
|
r36208 | return cmdutil.getcommiteditor(editform=editform, | ||
**pycompat.strkwargs(opts)) | ||||
FUJIWARA Katsunori
|
r22252 | self.getcommiteditor = getcommiteditor | ||
Brendan Cully
|
r3714 | |||
def applied(self, repo, node, parent): | ||||
'''returns True if a node is already an ancestor of parent | ||||
Joshua Redstone
|
r17010 | or is parent or has already been transplanted''' | ||
if hasnode(repo, parent): | ||||
parentrev = repo.changelog.rev(parent) | ||||
Brendan Cully
|
r3714 | if hasnode(repo, node): | ||
Joshua Redstone
|
r17010 | rev = repo.changelog.rev(node) | ||
Siddharth Agarwal
|
r18082 | reachable = repo.changelog.ancestors([parentrev], rev, | ||
inclusive=True) | ||||
Joshua Redstone
|
r17010 | if rev in reachable: | ||
Brendan Cully
|
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 | ||||
Joshua Redstone
|
r17010 | lnoderev = repo.changelog.rev(t.lnode) | ||
Siddharth Agarwal
|
r18082 | if lnoderev in repo.changelog.ancestors([parentrev], lnoderev, | ||
inclusive=True): | ||||
Brendan Cully
|
r3714 | return True | ||
return False | ||||
Pierre-Yves David
|
r26346 | def apply(self, repo, source, revmap, merges, opts=None): | ||
Brendan Cully
|
r3714 | '''apply the revisions in revmap one by one in revision order''' | ||
Pierre-Yves David
|
r26346 | if opts is None: | ||
opts = {} | ||||
Matt Mackall
|
r8209 | revs = sorted(revmap) | ||
Brendan Cully
|
r3714 | p1, p2 = repo.dirstate.parents() | ||
pulls = [] | ||||
Siddharth Agarwal
|
r23452 | diffopts = patch.difffeatureopts(self.ui, opts) | ||
Brendan Cully
|
r3714 | diffopts.git = True | ||
FUJIWARA Katsunori
|
r27289 | lock = tr = None | ||
Brendan Cully
|
r3714 | try: | ||
Matt Mackall
|
r4915 | lock = repo.lock() | ||
Greg Ward
|
r15204 | tr = repo.transaction('transplant') | ||
Brendan Cully
|
r3714 | for rev in revs: | ||
node = revmap[rev] | ||||
Pulkit Goyal
|
r36206 | revstr = '%d:%s' % (rev, nodemod.short(node)) | ||
Brendan Cully
|
r3714 | |||
if self.applied(repo, node, p1): | ||||
self.ui.warn(_('skipping already applied revision %s\n') % | ||||
revstr) | ||||
continue | ||||
parents = source.changelog.parents(node) | ||||
Levi Bard
|
r16627 | if not (opts.get('filter') or opts.get('log')): | ||
Martin Geisler
|
r7744 | # If the changeset parent is the same as the | ||
# wdir's parent, just pull it. | ||||
Brendan Cully
|
r3714 | if parents[0] == p1: | ||
pulls.append(node) | ||||
p1 = node | ||||
continue | ||||
if pulls: | ||||
if source != repo: | ||||
Pierre-Yves David
|
r22699 | exchange.pull(repo, source.peer(), heads=pulls) | ||
Augie Fackler
|
r27344 | merge.update(repo, pulls[-1], False, False) | ||
Brendan Cully
|
r3714 | p1, p2 = repo.dirstate.parents() | ||
pulls = [] | ||||
domerge = False | ||||
if node in merges: | ||||
Martin Geisler
|
r7744 | # pulling all the merge revs at once would mean we | ||
# couldn't transplant after the latest even if | ||||
# transplants before them fail. | ||||
Brendan Cully
|
r3714 | domerge = True | ||
if not hasnode(repo, node): | ||||
Pierre-Yves David
|
r22699 | exchange.pull(repo, source.peer(), heads=[node]) | ||
Brendan Cully
|
r3714 | |||
Steven Stallion
|
r16400 | skipmerge = False | ||
Brendan Cully
|
r3714 | if parents[1] != revlog.nullid: | ||
Steven Stallion
|
r16400 | if not opts.get('parent'): | ||
Pulkit Goyal
|
r36206 | self.ui.note(_('skipping merge changeset %d:%s\n') | ||
timeless
|
r28480 | % (rev, nodemod.short(node))) | ||
Steven Stallion
|
r16400 | skipmerge = True | ||
else: | ||||
parent = source.lookup(opts['parent']) | ||||
if parent not in parents: | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_('%s is not a parent of %s') % | ||
timeless
|
r28480 | (nodemod.short(parent), | ||
nodemod.short(node))) | ||||
Steven Stallion
|
r16400 | else: | ||
parent = parents[0] | ||||
if skipmerge: | ||||
Brendan Cully
|
r3714 | patchfile = None | ||
else: | ||||
fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-') | ||||
Pulkit Goyal
|
r36205 | fp = os.fdopen(fd, pycompat.sysstr('wb')) | ||
Steven Stallion
|
r16400 | gen = patch.diff(source, parent, node, opts=diffopts) | ||
Dirkjan Ochtman
|
r7308 | for chunk in gen: | ||
fp.write(chunk) | ||||
Brendan Cully
|
r3714 | fp.close() | ||
del revmap[rev] | ||||
if patchfile or domerge: | ||||
try: | ||||
Patrick Mezard
|
r16507 | try: | ||
n = self.applyone(repo, node, | ||||
source.changelog.read(node), | ||||
patchfile, merge=domerge, | ||||
log=opts.get('log'), | ||||
filter=opts.get('filter')) | ||||
except TransplantError: | ||||
# Do not rollback, it is up to the user to | ||||
# fix the merge or cancel everything | ||||
tr.close() | ||||
raise | ||||
Brendan Cully
|
r4251 | if n and domerge: | ||
Brendan Cully
|
r3714 | self.ui.status(_('%s merged at %s\n') % (revstr, | ||
timeless
|
r28480 | nodemod.short(n))) | ||
Brendan Cully
|
r4251 | elif n: | ||
Martin Geisler
|
r7744 | self.ui.status(_('%s transplanted to %s\n') | ||
timeless
|
r28480 | % (nodemod.short(node), | ||
nodemod.short(n))) | ||||
Brendan Cully
|
r3714 | finally: | ||
if patchfile: | ||||
os.unlink(patchfile) | ||||
Greg Ward
|
r15204 | tr.close() | ||
Brendan Cully
|
r3714 | if pulls: | ||
Pierre-Yves David
|
r22699 | exchange.pull(repo, source.peer(), heads=pulls) | ||
Augie Fackler
|
r27344 | merge.update(repo, pulls[-1], False, False) | ||
Brendan Cully
|
r3714 | finally: | ||
self.saveseries(revmap, merges) | ||||
self.transplants.write() | ||||
Greg Ward
|
r15204 | if tr: | ||
tr.release() | ||||
FUJIWARA Katsunori
|
r25879 | if lock: | ||
lock.release() | ||||
Brendan Cully
|
r3714 | |||
Luke Plant
|
r13579 | def filter(self, filter, node, changelog, patchfile): | ||
Brendan Cully
|
r3714 | '''arbitrarily rewrite changeset before applying it''' | ||
Martin Geisler
|
r6966 | self.ui.status(_('filtering %s\n') % patchfile) | ||
Brendan Cully
|
r3759 | user, date, msg = (changelog[1], changelog[2], changelog[4]) | ||
fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-') | ||||
Pulkit Goyal
|
r36205 | fp = os.fdopen(fd, pycompat.sysstr('wb')) | ||
Brendan Cully
|
r3759 | fp.write("# HG changeset patch\n") | ||
fp.write("# User %s\n" % user) | ||||
fp.write("# Date %d %d\n" % date) | ||||
Mads Kiilerich
|
r9433 | fp.write(msg + '\n') | ||
Brendan Cully
|
r3759 | fp.close() | ||
try: | ||||
Yuya Nishihara
|
r23270 | self.ui.system('%s %s %s' % (filter, util.shellquote(headerfile), | ||
util.shellquote(patchfile)), | ||||
environ={'HGUSER': changelog[1], | ||||
timeless
|
r28480 | 'HGREVISION': nodemod.hex(node), | ||
Yuya Nishihara
|
r23270 | }, | ||
Simon Farnsworth
|
r31202 | onerr=error.Abort, errprefix=_('filter failed'), | ||
blockedtag='transplant_filter') | ||||
Pulkit Goyal
|
r36207 | user, date, msg = self.parselog(open(headerfile, 'rb'))[1:4] | ||
Brendan Cully
|
r3759 | finally: | ||
os.unlink(headerfile) | ||||
return (user, date, msg) | ||||
Brendan Cully
|
r3714 | |||
def applyone(self, repo, node, cl, patchfile, merge=False, log=False, | ||||
Matt Mackall
|
r4917 | filter=None): | ||
Brendan Cully
|
r3714 | '''apply the patch in patchfile to the repository as a transplant''' | ||
(manifest, user, (time, timezone), files, message) = cl[:5] | ||||
date = "%d %d" % (time, timezone) | ||||
extra = {'transplant_source': node} | ||||
if filter: | ||||
Luke Plant
|
r13579 | (user, date, message) = self.filter(filter, node, cl, patchfile) | ||
Brendan Cully
|
r3714 | |||
if log: | ||||
Martin Geisler
|
r9183 | # we don't translate messages inserted into commits | ||
timeless
|
r28480 | message += '\n(transplanted from %s)' % nodemod.hex(node) | ||
Brendan Cully
|
r3714 | |||
timeless
|
r28480 | self.ui.status(_('applying %s\n') % nodemod.short(node)) | ||
Brendan Cully
|
r3714 | self.ui.note('%s %s\n%s\n' % (user, date, message)) | ||
if not patchfile and not merge: | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_('can only omit patchfile if merging')) | ||
Brendan Cully
|
r3714 | if patchfile: | ||
try: | ||||
Patrick Mezard
|
r14564 | files = set() | ||
Patrick Mezard
|
r14382 | patch.patch(self.ui, repo, patchfile, files=files, eolmode=None) | ||
Patrick Mezard
|
r14260 | files = list(files) | ||
Gregory Szorc
|
r25660 | except Exception as inst: | ||
Brendan Cully
|
r3757 | seriespath = os.path.join(self.path, 'series') | ||
if os.path.exists(seriespath): | ||||
os.unlink(seriespath) | ||||
Matt Mackall
|
r13878 | p1 = repo.dirstate.p1() | ||
Brendan Cully
|
r3714 | p2 = node | ||
Brendan Cully
|
r3725 | self.log(user, date, message, p1, p2, merge=merge) | ||
Pulkit Goyal
|
r36247 | self.ui.write(util.forcebytestr(inst) + '\n') | ||
timeless
|
r27676 | raise TransplantError(_('fix up the working directory and run ' | ||
Patrick Mezard
|
r16507 | 'hg transplant --continue')) | ||
Brendan Cully
|
r3714 | else: | ||
files = None | ||||
if merge: | ||||
p1, p2 = repo.dirstate.parents() | ||||
Patrick Mezard
|
r16551 | repo.setparents(p1, node) | ||
Matt Mackall
|
r8703 | m = match.always(repo.root, '') | ||
else: | ||||
m = match.exact(repo.root, '', files) | ||||
Brendan Cully
|
r3714 | |||
Matt Mackall
|
r15220 | n = repo.commit(message, user, date, extra=extra, match=m, | ||
FUJIWARA Katsunori
|
r22252 | editor=self.getcommiteditor()) | ||
Greg Ward
|
r11638 | if not n: | ||
timeless
|
r28480 | self.ui.warn(_('skipping emptied changeset %s\n') % | ||
nodemod.short(node)) | ||||
Patrick Mezard
|
r17319 | return None | ||
Brendan Cully
|
r3714 | if not merge: | ||
self.transplants.set(n, node) | ||||
return n | ||||
timeless
|
r27677 | def canresume(self): | ||
return os.path.exists(os.path.join(self.path, 'journal')) | ||||
Bryan O'Sullivan
|
r18919 | def resume(self, repo, source, opts): | ||
Brendan Cully
|
r3714 | '''recover last transaction and apply remaining changesets''' | ||
if os.path.exists(os.path.join(self.path, 'journal')): | ||||
Bryan O'Sullivan
|
r18926 | n, node = self.recover(repo, source, opts) | ||
Pierre-Yves David
|
r23781 | if n: | ||
timeless
|
r28480 | self.ui.status(_('%s transplanted as %s\n') % | ||
(nodemod.short(node), | ||||
nodemod.short(n))) | ||||
Pierre-Yves David
|
r23781 | else: | ||
self.ui.status(_('%s skipped due to empty diff\n') | ||||
timeless
|
r28480 | % (nodemod.short(node),)) | ||
Brendan Cully
|
r3714 | seriespath = os.path.join(self.path, 'series') | ||
if not os.path.exists(seriespath): | ||||
Brendan Cully
|
r3758 | self.transplants.write() | ||
Brendan Cully
|
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) | ||||
Bryan O'Sullivan
|
r18926 | def recover(self, repo, source, opts): | ||
Brendan Cully
|
r3714 | '''commit working directory using journal metadata''' | ||
node, user, date, message, parents = self.readlog() | ||||
Steven Stallion
|
r16400 | merge = False | ||
Brendan Cully
|
r3714 | |||
if not user or not date or not message or not parents[0]: | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_('transplant log file is corrupt')) | ||
Brendan Cully
|
r3714 | |||
Steven Stallion
|
r16400 | parent = parents[0] | ||
if len(parents) > 1: | ||||
if opts.get('parent'): | ||||
parent = source.lookup(opts['parent']) | ||||
if parent not in parents: | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_('%s is not a parent of %s') % | ||
timeless
|
r28480 | (nodemod.short(parent), | ||
nodemod.short(node))) | ||||
Steven Stallion
|
r16400 | else: | ||
merge = True | ||||
Brendan Cully
|
r3758 | extra = {'transplant_source': node} | ||
Matt Mackall
|
r4915 | try: | ||
p1, p2 = repo.dirstate.parents() | ||||
Steven Stallion
|
r16400 | if p1 != parent: | ||
Pierre-Yves David
|
r26587 | raise error.Abort(_('working directory not at transplant ' | ||
timeless
|
r28480 | 'parent %s') % nodemod.hex(parent)) | ||
Matt Mackall
|
r4915 | if merge: | ||
Patrick Mezard
|
r16551 | repo.setparents(p1, parents[1]) | ||
Pierre-Yves David
|
r23781 | modified, added, removed, deleted = repo.status()[:4] | ||
if merge or modified or added or removed or deleted: | ||||
n = repo.commit(message, user, date, extra=extra, | ||||
editor=self.getcommiteditor()) | ||||
if not n: | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_('commit failed')) | ||
Pierre-Yves David
|
r23781 | if not merge: | ||
self.transplants.set(n, node) | ||||
else: | ||||
n = None | ||||
Matt Mackall
|
r4915 | self.unlog() | ||
Brendan Cully
|
r3714 | |||
Matt Mackall
|
r4915 | return n, node | ||
finally: | ||||
FUJIWARA Katsunori
|
r27289 | # TODO: get rid of this meaningless try/finally enclosing. | ||
# this is kept only to reduce changes in a patch. | ||||
pass | ||||
Brendan Cully
|
r3714 | |||
def readseries(self): | ||||
nodes = [] | ||||
merges = [] | ||||
cur = nodes | ||||
Dan Villiom Podlaski Christiansen
|
r14168 | for line in self.opener.read('series').splitlines(): | ||
Brendan Cully
|
r3714 | if line.startswith('# Merges'): | ||
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) | ||||
series = self.opener('series', 'w') | ||||
Matt Mackall
|
r8209 | for rev in sorted(revmap): | ||
timeless
|
r28480 | series.write(nodemod.hex(revmap[rev]) + '\n') | ||
Brendan Cully
|
r3714 | if merges: | ||
series.write('# Merges\n') | ||||
for m in merges: | ||||
timeless
|
r28480 | series.write(nodemod.hex(m) + '\n') | ||
Brendan Cully
|
r3714 | series.close() | ||
Brendan Cully
|
r3759 | def parselog(self, fp): | ||
parents = [] | ||||
message = [] | ||||
node = revlog.nullid | ||||
inmsg = False | ||||
Luke Plant
|
r13789 | user = None | ||
date = None | ||||
Brendan Cully
|
r3759 | for line in fp.read().splitlines(): | ||
if inmsg: | ||||
message.append(line) | ||||
elif line.startswith('# User '): | ||||
user = line[7:] | ||||
elif line.startswith('# Date '): | ||||
date = line[7:] | ||||
elif line.startswith('# Node ID '): | ||||
node = revlog.bin(line[10:]) | ||||
elif line.startswith('# Parent '): | ||||
parents.append(revlog.bin(line[9:])) | ||||
Georg Brandl
|
r11411 | elif not line.startswith('# '): | ||
Brendan Cully
|
r3759 | inmsg = True | ||
message.append(line) | ||||
Luke Plant
|
r13789 | if None in (user, date): | ||
Pierre-Yves David
|
r26587 | raise error.Abort(_("filter corrupted changeset (no user or date)")) | ||
Brendan Cully
|
r3759 | return (node, user, date, '\n'.join(message), parents) | ||
Thomas Arendsen Hein
|
r4516 | |||
Brendan Cully
|
r3725 | def log(self, user, date, message, p1, p2, merge=False): | ||
Brendan Cully
|
r3714 | '''journal changelog metadata for later recover''' | ||
if not os.path.isdir(self.path): | ||||
os.mkdir(self.path) | ||||
fp = self.opener('journal', 'w') | ||||
Brendan Cully
|
r3725 | fp.write('# User %s\n' % user) | ||
fp.write('# Date %s\n' % date) | ||||
timeless
|
r28480 | fp.write('# Node ID %s\n' % nodemod.hex(p2)) | ||
fp.write('# Parent ' + nodemod.hex(p1) + '\n') | ||||
Brendan Cully
|
r3714 | if merge: | ||
timeless
|
r28480 | fp.write('# Parent ' + nodemod.hex(p2) + '\n') | ||
Brendan Cully
|
r3725 | fp.write(message.rstrip() + '\n') | ||
Brendan Cully
|
r3714 | fp.close() | ||
def readlog(self): | ||||
Brendan Cully
|
r3759 | return self.parselog(self.opener('journal')) | ||
Brendan Cully
|
r3714 | |||
def unlog(self): | ||||
'''remove changelog journal''' | ||||
absdst = os.path.join(self.path, 'journal') | ||||
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] | ||||
cnode = extra.get('transplant_source') | ||||
if cnode and self.applied(repo, cnode, root): | ||||
return False | ||||
return True | ||||
return matchfn | ||||
def hasnode(repo, node): | ||||
try: | ||||
Martin Geisler
|
r13031 | return repo.changelog.rev(node) is not None | ||
Matt Mackall
|
r7633 | except error.RevlogError: | ||
Brendan Cully
|
r3714 | return False | ||
def browserevs(ui, repo, nodes, opts): | ||||
'''interactively transplant changesets''' | ||||
Yuya Nishihara
|
r35906 | displayer = logcmdutil.changesetdisplayer(ui, repo, opts) | ||
Brendan Cully
|
r3714 | transplants = [] | ||
merges = [] | ||||
FUJIWARA Katsunori
|
r20268 | prompt = _('apply changeset? [ynmpcq?]:' | ||
'$$ &yes, transplant this changeset' | ||||
'$$ &no, skip this changeset' | ||||
'$$ &merge at this changeset' | ||||
'$$ show &patch' | ||||
'$$ &commit selected changesets' | ||||
'$$ &quit and cancel transplant' | ||||
'$$ &? (show this help)') | ||||
Brendan Cully
|
r3714 | for node in nodes: | ||
Dirkjan Ochtman
|
r7369 | displayer.show(repo[node]) | ||
Brendan Cully
|
r3714 | action = None | ||
while not action: | ||||
FUJIWARA Katsunori
|
r20268 | action = 'ynmpcq?'[ui.promptchoice(prompt)] | ||
Brendan Cully
|
r3714 | if action == '?': | ||
FUJIWARA Katsunori
|
r20269 | for c, t in ui.extractchoices(prompt)[1]: | ||
ui.write('%s: %s\n' % (c, t)) | ||||
Brendan Cully
|
r3714 | action = None | ||
elif action == 'p': | ||||
parent = repo.changelog.parents(node)[0] | ||||
Dirkjan Ochtman
|
r7308 | for chunk in patch.diff(repo, parent, node): | ||
Martin Geisler
|
r8615 | ui.write(chunk) | ||
Brendan Cully
|
r3714 | action = None | ||
if action == 'y': | ||||
transplants.append(node) | ||||
elif action == 'm': | ||||
merges.append(node) | ||||
elif action == 'c': | ||||
break | ||||
elif action == 'q': | ||||
transplants = () | ||||
merges = () | ||||
break | ||||
Robert Bachmann
|
r10152 | displayer.close() | ||
Brendan Cully
|
r3714 | return (transplants, merges) | ||
Adrian Buehlmann
|
r14308 | @command('transplant', | ||
Mads Kiilerich
|
r19028 | [('s', 'source', '', _('transplant changesets from REPO'), _('REPO')), | ||
Mads Kiilerich
|
r19027 | ('b', 'branch', [], _('use this source changeset as head'), _('REV')), | ||
('a', 'all', None, _('pull all changesets up to the --branch revisions')), | ||||
Adrian Buehlmann
|
r14308 | ('p', 'prune', [], _('skip over REV'), _('REV')), | ||
('m', 'merge', [], _('merge at REV'), _('REV')), | ||||
Steven Stallion
|
r16400 | ('', 'parent', '', | ||
_('parent to choose when transplanting merge'), _('REV')), | ||||
Matt Mackall
|
r15220 | ('e', 'edit', False, _('invoke editor on commit messages')), | ||
Adrian Buehlmann
|
r14308 | ('', 'log', None, _('append transplant info to log message')), | ||
('c', 'continue', None, _('continue last transplant session ' | ||||
Mads Kiilerich
|
r19028 | 'after fixing conflicts')), | ||
Adrian Buehlmann
|
r14308 | ('', 'filter', '', | ||
_('filter changesets through command'), _('CMD'))], | ||||
_('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] ' | ||||
'[-m REV] [REV]...')) | ||||
Brendan Cully
|
r3714 | def transplant(ui, repo, *revs, **opts): | ||
'''transplant changesets from another branch | ||||
Selected changesets will be applied on top of the current working | ||||
Martin Geisler
|
r13605 | directory with the log of the original changeset. The changesets | ||
Mads Kiilerich
|
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. | ||||
Martin Geisler
|
r13605 | |||
If --log is specified, log messages will have a comment appended | ||||
of the form:: | ||||
Brendan Cully
|
r3714 | |||
Martin Geisler
|
r9200 | (transplanted from CHANGESETHASH) | ||
Brendan Cully
|
r3714 | |||
You can rewrite the changelog message with the --filter option. | ||||
Martin Geisler
|
r8000 | Its argument will be invoked with the current changelog message as | ||
$1 and the patch as $2. | ||||
Brendan Cully
|
r3714 | |||
Mads Kiilerich
|
r19028 | --source/-s specifies another repository to use for selecting changesets, | ||
just as if it temporarily had been pulled. | ||||
Mads Kiilerich
|
r19027 | If --branch/-b is specified, these revisions will be used as | ||
Mads Kiilerich
|
r19951 | heads when deciding which changesets to transplant, just as if only | ||
Mads Kiilerich
|
r19027 | these revisions had been pulled. | ||
If --all/-a is specified, all the revisions up to the heads specified | ||||
with --branch will be transplanted. | ||||
Brendan Cully
|
r3714 | |||
Mads Kiilerich
|
r19027 | Example: | ||
- transplant all changes up to REV on top of your current revision:: | ||||
hg transplant --branch REV --all | ||||
Brendan Cully
|
r3714 | |||
Martin Geisler
|
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. | ||||
Brendan Cully
|
r3714 | |||
Steven Stallion
|
r16400 | Merge changesets may be transplanted directly by specifying the | ||
Steven Stallion
|
r16457 | proper parent changeset by calling :hg:`transplant --parent`. | ||
Steven Stallion
|
r16400 | |||
Martin Geisler
|
r11193 | If no merges or revisions are provided, :hg:`transplant` will | ||
start an interactive changeset browser. | ||||
Brendan Cully
|
r3714 | |||
Martin Geisler
|
r8000 | If a changeset application fails, you can fix the merge by hand | ||
Martin Geisler
|
r11193 | and then resume where you left off by calling :hg:`transplant | ||
--continue/-c`. | ||||
Brendan Cully
|
r3714 | ''' | ||
Bryan O'Sullivan
|
r27840 | with repo.wlock(): | ||
FUJIWARA Katsunori
|
r27289 | return _dotransplant(ui, repo, *revs, **opts) | ||
def _dotransplant(ui, repo, *revs, **opts): | ||||
Peter Arrenbrecht
|
r14161 | def incwalk(repo, csets, match=util.always): | ||
for node in csets: | ||||
Brendan Cully
|
r3714 | if match(node): | ||
yield node | ||||
Mads Kiilerich
|
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() | ||||
Brendan Cully
|
r3714 | ancestors = [] | ||
Mads Kiilerich
|
r20988 | ctx = repo[dest] | ||
Mads Kiilerich
|
r19027 | for head in heads: | ||
Mads Kiilerich
|
r20988 | ancestors.append(ctx.ancestor(repo[head]).node()) | ||
Mads Kiilerich
|
r19027 | for node in repo.changelog.nodesbetween(ancestors, heads)[0]: | ||
Brendan Cully
|
r3714 | if match(node): | ||
yield node | ||||
def checkopts(opts, revs): | ||||
if opts.get('continue'): | ||||
Matt Mackall
|
r10282 | if opts.get('branch') or opts.get('all') or opts.get('merge'): | ||
Pierre-Yves David
|
r26587 | raise error.Abort(_('--continue is incompatible with ' | ||
Mads Kiilerich
|
r19028 | '--branch, --all and --merge')) | ||
Brendan Cully
|
r3714 | return | ||
if not (opts.get('source') or revs or | ||||
opts.get('merge') or opts.get('branch')): | ||||
timeless
|
r27322 | raise error.Abort(_('no source URL, branch revision, or revision ' | ||
Martin Geisler
|
r7744 | 'list provided')) | ||
Brendan Cully
|
r3714 | if opts.get('all'): | ||
if not opts.get('branch'): | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_('--all requires a branch revision')) | ||
Brendan Cully
|
r3714 | if revs: | ||
Pierre-Yves David
|
r26587 | raise error.Abort(_('--all is incompatible with a ' | ||
Martin Geisler
|
r7744 | 'revision list')) | ||
Brendan Cully
|
r3714 | |||
Pulkit Goyal
|
r36208 | opts = pycompat.byteskwargs(opts) | ||
Brendan Cully
|
r3714 | checkopts(opts, revs) | ||
if not opts.get('log'): | ||||
Matt Mackall
|
r25828 | # deprecated config: transplant.log | ||
Brendan Cully
|
r3714 | opts['log'] = ui.config('transplant', 'log') | ||
if not opts.get('filter'): | ||||
Matt Mackall
|
r25828 | # deprecated config: transplant.filter | ||
Brendan Cully
|
r3714 | opts['filter'] = ui.config('transplant', 'filter') | ||
FUJIWARA Katsunori
|
r21411 | tp = transplanter(ui, repo, opts) | ||
Brendan Cully
|
r3714 | |||
p1, p2 = repo.dirstate.parents() | ||||
Brendan Cully
|
r8176 | if len(repo) > 0 and p1 == revlog.nullid: | ||
Pierre-Yves David
|
r26587 | raise error.Abort(_('no revision checked out')) | ||
timeless
|
r27677 | if opts.get('continue'): | ||
if not tp.canresume(): | ||||
raise error.Abort(_('no transplant to continue')) | ||||
else: | ||||
cmdutil.checkunfinished(repo) | ||||
Brendan Cully
|
r3714 | if p2 != revlog.nullid: | ||
Pierre-Yves David
|
r26587 | raise error.Abort(_('outstanding uncommitted merges')) | ||
Brendan Cully
|
r3714 | m, a, r, d = repo.status()[:4] | ||
if m or a or r or d: | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_('outstanding local changes')) | ||
Brendan Cully
|
r3714 | |||
Peter Arrenbrecht
|
r14161 | sourcerepo = opts.get('source') | ||
if sourcerepo: | ||||
Simon Heimberg
|
r17874 | peer = hg.peer(repo, opts, ui.expandpath(sourcerepo)) | ||
Mads Kiilerich
|
r19027 | heads = map(peer.lookup, opts.get('branch', ())) | ||
Pierre-Yves David
|
r25679 | target = set(heads) | ||
for r in revs: | ||||
try: | ||||
target.add(peer.lookup(r)) | ||||
except error.RepoError: | ||||
pass | ||||
Sune Foldager
|
r17191 | source, csets, cleanupfn = bundlerepo.getremotechanges(ui, repo, peer, | ||
Pierre-Yves David
|
r25679 | onlyheads=sorted(target), force=True) | ||
Brendan Cully
|
r3714 | else: | ||
source = repo | ||||
Mads Kiilerich
|
r19027 | heads = map(source.lookup, opts.get('branch', ())) | ||
Peter Arrenbrecht
|
r14161 | cleanupfn = None | ||
Brendan Cully
|
r3714 | |||
try: | ||||
if opts.get('continue'): | ||||
Brendan Cully
|
r3724 | tp.resume(repo, source, opts) | ||
Brendan Cully
|
r3714 | return | ||
Benoit Boissinot
|
r10394 | tf = tp.transplantfilter(repo, source, p1) | ||
Brendan Cully
|
r3714 | if opts.get('prune'): | ||
Mads Kiilerich
|
r19055 | prune = set(source.lookup(r) | ||
for r in scmutil.revrange(source, opts.get('prune'))) | ||||
Brendan Cully
|
r3714 | matchfn = lambda x: tf(x) and x not in prune | ||
else: | ||||
matchfn = tf | ||||
merges = map(source.lookup, opts.get('merge', ())) | ||||
revmap = {} | ||||
if revs: | ||||
Matt Mackall
|
r14319 | for r in scmutil.revrange(source, revs): | ||
Brendan Cully
|
r3714 | revmap[int(r)] = source.lookup(r) | ||
elif opts.get('all') or not merges: | ||||
if source != repo: | ||||
Peter Arrenbrecht
|
r14161 | alltransplants = incwalk(source, csets, match=matchfn) | ||
Brendan Cully
|
r3714 | else: | ||
Mads Kiilerich
|
r19027 | alltransplants = transplantwalk(source, p1, heads, | ||
Martin Geisler
|
r7744 | match=matchfn) | ||
Brendan Cully
|
r3714 | if opts.get('all'): | ||
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: | ||||
Peter Arrenbrecht
|
r14161 | if cleanupfn: | ||
cleanupfn() | ||||
Brendan Cully
|
r3714 | |||
FUJIWARA Katsunori
|
r28394 | revsetpredicate = registrar.revsetpredicate() | ||
FUJIWARA Katsunori
|
r27586 | |||
@revsetpredicate('transplanted([set])') | ||||
Juan Pablo Aroztegi
|
r12581 | def revsettransplanted(repo, subset, x): | ||
FUJIWARA Katsunori
|
r27586 | """Transplanted changesets in set, or all transplanted changesets. | ||
Patrick Mezard
|
r12822 | """ | ||
Juan Pablo Aroztegi
|
r12581 | if x: | ||
Mads Kiilerich
|
r17299 | s = revset.getset(repo, subset, x) | ||
Juan Pablo Aroztegi
|
r12581 | else: | ||
Mads Kiilerich
|
r17299 | s = subset | ||
Yuya Nishihara
|
r31023 | return smartset.baseset([r for r in s if | ||
Lucas Moscovicz
|
r20442 | repo[r].extra().get('transplant_source')]) | ||
Juan Pablo Aroztegi
|
r12581 | |||
FUJIWARA Katsunori
|
r28540 | templatekeyword = registrar.templatekeyword() | ||
@templatekeyword('transplanted') | ||||
Patrick Mezard
|
r13689 | def kwtransplanted(repo, ctx, **args): | ||
FUJIWARA Katsunori
|
r28540 | """String. The node identifier of the transplanted | ||
Patrick Mezard
|
r13689 | changeset if any.""" | ||
n = ctx.extra().get('transplant_source') | ||||
timeless
|
r28480 | return n and nodemod.hex(n) or '' | ||
Juan Pablo Aroztegi
|
r12581 | |||
Patrick Mezard
|
r12822 | def extsetup(ui): | ||
Matt Mackall
|
r19480 | cmdutil.unfinishedstates.append( | ||
timeless
|
r27678 | ['transplant/journal', True, False, _('transplant in progress'), | ||
Matt Mackall
|
r19480 | _("use 'hg transplant --continue' or 'hg update' to abort")]) | ||
Juan Pablo Aroztegi
|
r12581 | |||
Patrick Mezard
|
r12823 | # tell hggettext to extract docstrings from these functions: | ||
Patrick Mezard
|
r13698 | i18nfunctions = [revsettransplanted, kwtransplanted] | ||