##// END OF EJS Templates
transplant: unnest --stop case...
Yuya Nishihara -
r43033:0770e221 default
parent child Browse files
Show More
@@ -1,798 +1,798 b''
1 # Patch transplanting extension for Mercurial
1 # Patch transplanting extension for Mercurial
2 #
2 #
3 # Copyright 2006, 2007 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006, 2007 Brendan Cully <brendan@kublai.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''command to transplant changesets from another branch
8 '''command to transplant changesets from another branch
9
9
10 This extension allows you to transplant changes to another parent revision,
10 This extension allows you to transplant changes to another parent revision,
11 possibly in another repository. The transplant is done using 'diff' patches.
11 possibly in another repository. The transplant is done using 'diff' patches.
12
12
13 Transplanted patches are recorded in .hg/transplant/transplants, as a
13 Transplanted patches are recorded in .hg/transplant/transplants, as a
14 map from a changeset hash to its hash in the source repository.
14 map from a changeset hash to its hash in the source repository.
15 '''
15 '''
16 from __future__ import absolute_import
16 from __future__ import absolute_import
17
17
18 import os
18 import os
19
19
20 from mercurial.i18n import _
20 from mercurial.i18n import _
21 from mercurial import (
21 from mercurial import (
22 bundlerepo,
22 bundlerepo,
23 cmdutil,
23 cmdutil,
24 error,
24 error,
25 exchange,
25 exchange,
26 hg,
26 hg,
27 logcmdutil,
27 logcmdutil,
28 match,
28 match,
29 merge,
29 merge,
30 node as nodemod,
30 node as nodemod,
31 patch,
31 patch,
32 pycompat,
32 pycompat,
33 registrar,
33 registrar,
34 revlog,
34 revlog,
35 revset,
35 revset,
36 scmutil,
36 scmutil,
37 smartset,
37 smartset,
38 state as statemod,
38 state as statemod,
39 util,
39 util,
40 vfs as vfsmod,
40 vfs as vfsmod,
41 )
41 )
42 from mercurial.utils import (
42 from mercurial.utils import (
43 procutil,
43 procutil,
44 stringutil,
44 stringutil,
45 )
45 )
46
46
47 class TransplantError(error.Abort):
47 class TransplantError(error.Abort):
48 pass
48 pass
49
49
50 cmdtable = {}
50 cmdtable = {}
51 command = registrar.command(cmdtable)
51 command = registrar.command(cmdtable)
52 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
52 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
53 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
53 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
54 # be specifying the version(s) of Mercurial they are tested with, or
54 # be specifying the version(s) of Mercurial they are tested with, or
55 # leave the attribute unspecified.
55 # leave the attribute unspecified.
56 testedwith = 'ships-with-hg-core'
56 testedwith = 'ships-with-hg-core'
57
57
58 configtable = {}
58 configtable = {}
59 configitem = registrar.configitem(configtable)
59 configitem = registrar.configitem(configtable)
60
60
61 configitem('transplant', 'filter',
61 configitem('transplant', 'filter',
62 default=None,
62 default=None,
63 )
63 )
64 configitem('transplant', 'log',
64 configitem('transplant', 'log',
65 default=None,
65 default=None,
66 )
66 )
67
67
68 class transplantentry(object):
68 class transplantentry(object):
69 def __init__(self, lnode, rnode):
69 def __init__(self, lnode, rnode):
70 self.lnode = lnode
70 self.lnode = lnode
71 self.rnode = rnode
71 self.rnode = rnode
72
72
73 class transplants(object):
73 class transplants(object):
74 def __init__(self, path=None, transplantfile=None, opener=None):
74 def __init__(self, path=None, transplantfile=None, opener=None):
75 self.path = path
75 self.path = path
76 self.transplantfile = transplantfile
76 self.transplantfile = transplantfile
77 self.opener = opener
77 self.opener = opener
78
78
79 if not opener:
79 if not opener:
80 self.opener = vfsmod.vfs(self.path)
80 self.opener = vfsmod.vfs(self.path)
81 self.transplants = {}
81 self.transplants = {}
82 self.dirty = False
82 self.dirty = False
83 self.read()
83 self.read()
84
84
85 def read(self):
85 def read(self):
86 abspath = os.path.join(self.path, self.transplantfile)
86 abspath = os.path.join(self.path, self.transplantfile)
87 if self.transplantfile and os.path.exists(abspath):
87 if self.transplantfile and os.path.exists(abspath):
88 for line in self.opener.read(self.transplantfile).splitlines():
88 for line in self.opener.read(self.transplantfile).splitlines():
89 lnode, rnode = map(revlog.bin, line.split(':'))
89 lnode, rnode = map(revlog.bin, line.split(':'))
90 list = self.transplants.setdefault(rnode, [])
90 list = self.transplants.setdefault(rnode, [])
91 list.append(transplantentry(lnode, rnode))
91 list.append(transplantentry(lnode, rnode))
92
92
93 def write(self):
93 def write(self):
94 if self.dirty and self.transplantfile:
94 if self.dirty and self.transplantfile:
95 if not os.path.isdir(self.path):
95 if not os.path.isdir(self.path):
96 os.mkdir(self.path)
96 os.mkdir(self.path)
97 fp = self.opener(self.transplantfile, 'w')
97 fp = self.opener(self.transplantfile, 'w')
98 for list in self.transplants.itervalues():
98 for list in self.transplants.itervalues():
99 for t in list:
99 for t in list:
100 l, r = map(nodemod.hex, (t.lnode, t.rnode))
100 l, r = map(nodemod.hex, (t.lnode, t.rnode))
101 fp.write(l + ':' + r + '\n')
101 fp.write(l + ':' + r + '\n')
102 fp.close()
102 fp.close()
103 self.dirty = False
103 self.dirty = False
104
104
105 def get(self, rnode):
105 def get(self, rnode):
106 return self.transplants.get(rnode) or []
106 return self.transplants.get(rnode) or []
107
107
108 def set(self, lnode, rnode):
108 def set(self, lnode, rnode):
109 list = self.transplants.setdefault(rnode, [])
109 list = self.transplants.setdefault(rnode, [])
110 list.append(transplantentry(lnode, rnode))
110 list.append(transplantentry(lnode, rnode))
111 self.dirty = True
111 self.dirty = True
112
112
113 def remove(self, transplant):
113 def remove(self, transplant):
114 list = self.transplants.get(transplant.rnode)
114 list = self.transplants.get(transplant.rnode)
115 if list:
115 if list:
116 del list[list.index(transplant)]
116 del list[list.index(transplant)]
117 self.dirty = True
117 self.dirty = True
118
118
119 class transplanter(object):
119 class transplanter(object):
120 def __init__(self, ui, repo, opts):
120 def __init__(self, ui, repo, opts):
121 self.ui = ui
121 self.ui = ui
122 self.path = repo.vfs.join('transplant')
122 self.path = repo.vfs.join('transplant')
123 self.opener = vfsmod.vfs(self.path)
123 self.opener = vfsmod.vfs(self.path)
124 self.transplants = transplants(self.path, 'transplants',
124 self.transplants = transplants(self.path, 'transplants',
125 opener=self.opener)
125 opener=self.opener)
126 def getcommiteditor():
126 def getcommiteditor():
127 editform = cmdutil.mergeeditform(repo[None], 'transplant')
127 editform = cmdutil.mergeeditform(repo[None], 'transplant')
128 return cmdutil.getcommiteditor(editform=editform,
128 return cmdutil.getcommiteditor(editform=editform,
129 **pycompat.strkwargs(opts))
129 **pycompat.strkwargs(opts))
130 self.getcommiteditor = getcommiteditor
130 self.getcommiteditor = getcommiteditor
131
131
132 def applied(self, repo, node, parent):
132 def applied(self, repo, node, parent):
133 '''returns True if a node is already an ancestor of parent
133 '''returns True if a node is already an ancestor of parent
134 or is parent or has already been transplanted'''
134 or is parent or has already been transplanted'''
135 if hasnode(repo, parent):
135 if hasnode(repo, parent):
136 parentrev = repo.changelog.rev(parent)
136 parentrev = repo.changelog.rev(parent)
137 if hasnode(repo, node):
137 if hasnode(repo, node):
138 rev = repo.changelog.rev(node)
138 rev = repo.changelog.rev(node)
139 reachable = repo.changelog.ancestors([parentrev], rev,
139 reachable = repo.changelog.ancestors([parentrev], rev,
140 inclusive=True)
140 inclusive=True)
141 if rev in reachable:
141 if rev in reachable:
142 return True
142 return True
143 for t in self.transplants.get(node):
143 for t in self.transplants.get(node):
144 # it might have been stripped
144 # it might have been stripped
145 if not hasnode(repo, t.lnode):
145 if not hasnode(repo, t.lnode):
146 self.transplants.remove(t)
146 self.transplants.remove(t)
147 return False
147 return False
148 lnoderev = repo.changelog.rev(t.lnode)
148 lnoderev = repo.changelog.rev(t.lnode)
149 if lnoderev in repo.changelog.ancestors([parentrev], lnoderev,
149 if lnoderev in repo.changelog.ancestors([parentrev], lnoderev,
150 inclusive=True):
150 inclusive=True):
151 return True
151 return True
152 return False
152 return False
153
153
154 def apply(self, repo, source, revmap, merges, opts=None):
154 def apply(self, repo, source, revmap, merges, opts=None):
155 '''apply the revisions in revmap one by one in revision order'''
155 '''apply the revisions in revmap one by one in revision order'''
156 if opts is None:
156 if opts is None:
157 opts = {}
157 opts = {}
158 revs = sorted(revmap)
158 revs = sorted(revmap)
159 p1 = repo.dirstate.p1()
159 p1 = repo.dirstate.p1()
160 pulls = []
160 pulls = []
161 diffopts = patch.difffeatureopts(self.ui, opts)
161 diffopts = patch.difffeatureopts(self.ui, opts)
162 diffopts.git = True
162 diffopts.git = True
163
163
164 lock = tr = None
164 lock = tr = None
165 try:
165 try:
166 lock = repo.lock()
166 lock = repo.lock()
167 tr = repo.transaction('transplant')
167 tr = repo.transaction('transplant')
168 for rev in revs:
168 for rev in revs:
169 node = revmap[rev]
169 node = revmap[rev]
170 revstr = '%d:%s' % (rev, nodemod.short(node))
170 revstr = '%d:%s' % (rev, nodemod.short(node))
171
171
172 if self.applied(repo, node, p1):
172 if self.applied(repo, node, p1):
173 self.ui.warn(_('skipping already applied revision %s\n') %
173 self.ui.warn(_('skipping already applied revision %s\n') %
174 revstr)
174 revstr)
175 continue
175 continue
176
176
177 parents = source.changelog.parents(node)
177 parents = source.changelog.parents(node)
178 if not (opts.get('filter') or opts.get('log')):
178 if not (opts.get('filter') or opts.get('log')):
179 # If the changeset parent is the same as the
179 # If the changeset parent is the same as the
180 # wdir's parent, just pull it.
180 # wdir's parent, just pull it.
181 if parents[0] == p1:
181 if parents[0] == p1:
182 pulls.append(node)
182 pulls.append(node)
183 p1 = node
183 p1 = node
184 continue
184 continue
185 if pulls:
185 if pulls:
186 if source != repo:
186 if source != repo:
187 exchange.pull(repo, source.peer(), heads=pulls)
187 exchange.pull(repo, source.peer(), heads=pulls)
188 merge.update(repo, pulls[-1], branchmerge=False,
188 merge.update(repo, pulls[-1], branchmerge=False,
189 force=False)
189 force=False)
190 p1 = repo.dirstate.p1()
190 p1 = repo.dirstate.p1()
191 pulls = []
191 pulls = []
192
192
193 domerge = False
193 domerge = False
194 if node in merges:
194 if node in merges:
195 # pulling all the merge revs at once would mean we
195 # pulling all the merge revs at once would mean we
196 # couldn't transplant after the latest even if
196 # couldn't transplant after the latest even if
197 # transplants before them fail.
197 # transplants before them fail.
198 domerge = True
198 domerge = True
199 if not hasnode(repo, node):
199 if not hasnode(repo, node):
200 exchange.pull(repo, source.peer(), heads=[node])
200 exchange.pull(repo, source.peer(), heads=[node])
201
201
202 skipmerge = False
202 skipmerge = False
203 if parents[1] != revlog.nullid:
203 if parents[1] != revlog.nullid:
204 if not opts.get('parent'):
204 if not opts.get('parent'):
205 self.ui.note(_('skipping merge changeset %d:%s\n')
205 self.ui.note(_('skipping merge changeset %d:%s\n')
206 % (rev, nodemod.short(node)))
206 % (rev, nodemod.short(node)))
207 skipmerge = True
207 skipmerge = True
208 else:
208 else:
209 parent = source.lookup(opts['parent'])
209 parent = source.lookup(opts['parent'])
210 if parent not in parents:
210 if parent not in parents:
211 raise error.Abort(_('%s is not a parent of %s') %
211 raise error.Abort(_('%s is not a parent of %s') %
212 (nodemod.short(parent),
212 (nodemod.short(parent),
213 nodemod.short(node)))
213 nodemod.short(node)))
214 else:
214 else:
215 parent = parents[0]
215 parent = parents[0]
216
216
217 if skipmerge:
217 if skipmerge:
218 patchfile = None
218 patchfile = None
219 else:
219 else:
220 fd, patchfile = pycompat.mkstemp(prefix='hg-transplant-')
220 fd, patchfile = pycompat.mkstemp(prefix='hg-transplant-')
221 fp = os.fdopen(fd, r'wb')
221 fp = os.fdopen(fd, r'wb')
222 gen = patch.diff(source, parent, node, opts=diffopts)
222 gen = patch.diff(source, parent, node, opts=diffopts)
223 for chunk in gen:
223 for chunk in gen:
224 fp.write(chunk)
224 fp.write(chunk)
225 fp.close()
225 fp.close()
226
226
227 del revmap[rev]
227 del revmap[rev]
228 if patchfile or domerge:
228 if patchfile or domerge:
229 try:
229 try:
230 try:
230 try:
231 n = self.applyone(repo, node,
231 n = self.applyone(repo, node,
232 source.changelog.read(node),
232 source.changelog.read(node),
233 patchfile, merge=domerge,
233 patchfile, merge=domerge,
234 log=opts.get('log'),
234 log=opts.get('log'),
235 filter=opts.get('filter'))
235 filter=opts.get('filter'))
236 except TransplantError:
236 except TransplantError:
237 # Do not rollback, it is up to the user to
237 # Do not rollback, it is up to the user to
238 # fix the merge or cancel everything
238 # fix the merge or cancel everything
239 tr.close()
239 tr.close()
240 raise
240 raise
241 if n and domerge:
241 if n and domerge:
242 self.ui.status(_('%s merged at %s\n') % (revstr,
242 self.ui.status(_('%s merged at %s\n') % (revstr,
243 nodemod.short(n)))
243 nodemod.short(n)))
244 elif n:
244 elif n:
245 self.ui.status(_('%s transplanted to %s\n')
245 self.ui.status(_('%s transplanted to %s\n')
246 % (nodemod.short(node),
246 % (nodemod.short(node),
247 nodemod.short(n)))
247 nodemod.short(n)))
248 finally:
248 finally:
249 if patchfile:
249 if patchfile:
250 os.unlink(patchfile)
250 os.unlink(patchfile)
251 tr.close()
251 tr.close()
252 if pulls:
252 if pulls:
253 exchange.pull(repo, source.peer(), heads=pulls)
253 exchange.pull(repo, source.peer(), heads=pulls)
254 merge.update(repo, pulls[-1], branchmerge=False, force=False)
254 merge.update(repo, pulls[-1], branchmerge=False, force=False)
255 finally:
255 finally:
256 self.saveseries(revmap, merges)
256 self.saveseries(revmap, merges)
257 self.transplants.write()
257 self.transplants.write()
258 if tr:
258 if tr:
259 tr.release()
259 tr.release()
260 if lock:
260 if lock:
261 lock.release()
261 lock.release()
262
262
263 def filter(self, filter, node, changelog, patchfile):
263 def filter(self, filter, node, changelog, patchfile):
264 '''arbitrarily rewrite changeset before applying it'''
264 '''arbitrarily rewrite changeset before applying it'''
265
265
266 self.ui.status(_('filtering %s\n') % patchfile)
266 self.ui.status(_('filtering %s\n') % patchfile)
267 user, date, msg = (changelog[1], changelog[2], changelog[4])
267 user, date, msg = (changelog[1], changelog[2], changelog[4])
268 fd, headerfile = pycompat.mkstemp(prefix='hg-transplant-')
268 fd, headerfile = pycompat.mkstemp(prefix='hg-transplant-')
269 fp = os.fdopen(fd, r'wb')
269 fp = os.fdopen(fd, r'wb')
270 fp.write("# HG changeset patch\n")
270 fp.write("# HG changeset patch\n")
271 fp.write("# User %s\n" % user)
271 fp.write("# User %s\n" % user)
272 fp.write("# Date %d %d\n" % date)
272 fp.write("# Date %d %d\n" % date)
273 fp.write(msg + '\n')
273 fp.write(msg + '\n')
274 fp.close()
274 fp.close()
275
275
276 try:
276 try:
277 self.ui.system('%s %s %s' % (filter,
277 self.ui.system('%s %s %s' % (filter,
278 procutil.shellquote(headerfile),
278 procutil.shellquote(headerfile),
279 procutil.shellquote(patchfile)),
279 procutil.shellquote(patchfile)),
280 environ={'HGUSER': changelog[1],
280 environ={'HGUSER': changelog[1],
281 'HGREVISION': nodemod.hex(node),
281 'HGREVISION': nodemod.hex(node),
282 },
282 },
283 onerr=error.Abort, errprefix=_('filter failed'),
283 onerr=error.Abort, errprefix=_('filter failed'),
284 blockedtag='transplant_filter')
284 blockedtag='transplant_filter')
285 user, date, msg = self.parselog(open(headerfile, 'rb'))[1:4]
285 user, date, msg = self.parselog(open(headerfile, 'rb'))[1:4]
286 finally:
286 finally:
287 os.unlink(headerfile)
287 os.unlink(headerfile)
288
288
289 return (user, date, msg)
289 return (user, date, msg)
290
290
291 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
291 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
292 filter=None):
292 filter=None):
293 '''apply the patch in patchfile to the repository as a transplant'''
293 '''apply the patch in patchfile to the repository as a transplant'''
294 (manifest, user, (time, timezone), files, message) = cl[:5]
294 (manifest, user, (time, timezone), files, message) = cl[:5]
295 date = "%d %d" % (time, timezone)
295 date = "%d %d" % (time, timezone)
296 extra = {'transplant_source': node}
296 extra = {'transplant_source': node}
297 if filter:
297 if filter:
298 (user, date, message) = self.filter(filter, node, cl, patchfile)
298 (user, date, message) = self.filter(filter, node, cl, patchfile)
299
299
300 if log:
300 if log:
301 # we don't translate messages inserted into commits
301 # we don't translate messages inserted into commits
302 message += '\n(transplanted from %s)' % nodemod.hex(node)
302 message += '\n(transplanted from %s)' % nodemod.hex(node)
303
303
304 self.ui.status(_('applying %s\n') % nodemod.short(node))
304 self.ui.status(_('applying %s\n') % nodemod.short(node))
305 self.ui.note('%s %s\n%s\n' % (user, date, message))
305 self.ui.note('%s %s\n%s\n' % (user, date, message))
306
306
307 if not patchfile and not merge:
307 if not patchfile and not merge:
308 raise error.Abort(_('can only omit patchfile if merging'))
308 raise error.Abort(_('can only omit patchfile if merging'))
309 if patchfile:
309 if patchfile:
310 try:
310 try:
311 files = set()
311 files = set()
312 patch.patch(self.ui, repo, patchfile, files=files, eolmode=None)
312 patch.patch(self.ui, repo, patchfile, files=files, eolmode=None)
313 files = list(files)
313 files = list(files)
314 except Exception as inst:
314 except Exception as inst:
315 seriespath = os.path.join(self.path, 'series')
315 seriespath = os.path.join(self.path, 'series')
316 if os.path.exists(seriespath):
316 if os.path.exists(seriespath):
317 os.unlink(seriespath)
317 os.unlink(seriespath)
318 p1 = repo.dirstate.p1()
318 p1 = repo.dirstate.p1()
319 p2 = node
319 p2 = node
320 self.log(user, date, message, p1, p2, merge=merge)
320 self.log(user, date, message, p1, p2, merge=merge)
321 self.ui.write(stringutil.forcebytestr(inst) + '\n')
321 self.ui.write(stringutil.forcebytestr(inst) + '\n')
322 raise TransplantError(_('fix up the working directory and run '
322 raise TransplantError(_('fix up the working directory and run '
323 'hg transplant --continue'))
323 'hg transplant --continue'))
324 else:
324 else:
325 files = None
325 files = None
326 if merge:
326 if merge:
327 p1 = repo.dirstate.p1()
327 p1 = repo.dirstate.p1()
328 repo.setparents(p1, node)
328 repo.setparents(p1, node)
329 m = match.always()
329 m = match.always()
330 else:
330 else:
331 m = match.exact(files)
331 m = match.exact(files)
332
332
333 n = repo.commit(message, user, date, extra=extra, match=m,
333 n = repo.commit(message, user, date, extra=extra, match=m,
334 editor=self.getcommiteditor())
334 editor=self.getcommiteditor())
335 if not n:
335 if not n:
336 self.ui.warn(_('skipping emptied changeset %s\n') %
336 self.ui.warn(_('skipping emptied changeset %s\n') %
337 nodemod.short(node))
337 nodemod.short(node))
338 return None
338 return None
339 if not merge:
339 if not merge:
340 self.transplants.set(n, node)
340 self.transplants.set(n, node)
341
341
342 return n
342 return n
343
343
344 def canresume(self):
344 def canresume(self):
345 return os.path.exists(os.path.join(self.path, 'journal'))
345 return os.path.exists(os.path.join(self.path, 'journal'))
346
346
347 def resume(self, repo, source, opts):
347 def resume(self, repo, source, opts):
348 '''recover last transaction and apply remaining changesets'''
348 '''recover last transaction and apply remaining changesets'''
349 if os.path.exists(os.path.join(self.path, 'journal')):
349 if os.path.exists(os.path.join(self.path, 'journal')):
350 n, node = self.recover(repo, source, opts)
350 n, node = self.recover(repo, source, opts)
351 if n:
351 if n:
352 self.ui.status(_('%s transplanted as %s\n') %
352 self.ui.status(_('%s transplanted as %s\n') %
353 (nodemod.short(node),
353 (nodemod.short(node),
354 nodemod.short(n)))
354 nodemod.short(n)))
355 else:
355 else:
356 self.ui.status(_('%s skipped due to empty diff\n')
356 self.ui.status(_('%s skipped due to empty diff\n')
357 % (nodemod.short(node),))
357 % (nodemod.short(node),))
358 seriespath = os.path.join(self.path, 'series')
358 seriespath = os.path.join(self.path, 'series')
359 if not os.path.exists(seriespath):
359 if not os.path.exists(seriespath):
360 self.transplants.write()
360 self.transplants.write()
361 return
361 return
362 nodes, merges = self.readseries()
362 nodes, merges = self.readseries()
363 revmap = {}
363 revmap = {}
364 for n in nodes:
364 for n in nodes:
365 revmap[source.changelog.rev(n)] = n
365 revmap[source.changelog.rev(n)] = n
366 os.unlink(seriespath)
366 os.unlink(seriespath)
367
367
368 self.apply(repo, source, revmap, merges, opts)
368 self.apply(repo, source, revmap, merges, opts)
369
369
370 def recover(self, repo, source, opts):
370 def recover(self, repo, source, opts):
371 '''commit working directory using journal metadata'''
371 '''commit working directory using journal metadata'''
372 node, user, date, message, parents = self.readlog()
372 node, user, date, message, parents = self.readlog()
373 merge = False
373 merge = False
374
374
375 if not user or not date or not message or not parents[0]:
375 if not user or not date or not message or not parents[0]:
376 raise error.Abort(_('transplant log file is corrupt'))
376 raise error.Abort(_('transplant log file is corrupt'))
377
377
378 parent = parents[0]
378 parent = parents[0]
379 if len(parents) > 1:
379 if len(parents) > 1:
380 if opts.get('parent'):
380 if opts.get('parent'):
381 parent = source.lookup(opts['parent'])
381 parent = source.lookup(opts['parent'])
382 if parent not in parents:
382 if parent not in parents:
383 raise error.Abort(_('%s is not a parent of %s') %
383 raise error.Abort(_('%s is not a parent of %s') %
384 (nodemod.short(parent),
384 (nodemod.short(parent),
385 nodemod.short(node)))
385 nodemod.short(node)))
386 else:
386 else:
387 merge = True
387 merge = True
388
388
389 extra = {'transplant_source': node}
389 extra = {'transplant_source': node}
390 try:
390 try:
391 p1 = repo.dirstate.p1()
391 p1 = repo.dirstate.p1()
392 if p1 != parent:
392 if p1 != parent:
393 raise error.Abort(_('working directory not at transplant '
393 raise error.Abort(_('working directory not at transplant '
394 'parent %s') % nodemod.hex(parent))
394 'parent %s') % nodemod.hex(parent))
395 if merge:
395 if merge:
396 repo.setparents(p1, parents[1])
396 repo.setparents(p1, parents[1])
397 modified, added, removed, deleted = repo.status()[:4]
397 modified, added, removed, deleted = repo.status()[:4]
398 if merge or modified or added or removed or deleted:
398 if merge or modified or added or removed or deleted:
399 n = repo.commit(message, user, date, extra=extra,
399 n = repo.commit(message, user, date, extra=extra,
400 editor=self.getcommiteditor())
400 editor=self.getcommiteditor())
401 if not n:
401 if not n:
402 raise error.Abort(_('commit failed'))
402 raise error.Abort(_('commit failed'))
403 if not merge:
403 if not merge:
404 self.transplants.set(n, node)
404 self.transplants.set(n, node)
405 else:
405 else:
406 n = None
406 n = None
407 self.unlog()
407 self.unlog()
408
408
409 return n, node
409 return n, node
410 finally:
410 finally:
411 # TODO: get rid of this meaningless try/finally enclosing.
411 # TODO: get rid of this meaningless try/finally enclosing.
412 # this is kept only to reduce changes in a patch.
412 # this is kept only to reduce changes in a patch.
413 pass
413 pass
414
414
415 def stop(self, ui, repo):
415 def stop(self, ui, repo):
416 """logic to stop an interrupted transplant"""
416 """logic to stop an interrupted transplant"""
417 if self.canresume():
417 if self.canresume():
418 startctx = repo['.']
418 startctx = repo['.']
419 hg.updaterepo(repo, startctx.node(), overwrite=True)
419 hg.updaterepo(repo, startctx.node(), overwrite=True)
420 ui.status(_("stopped the interrupted transplant\n"))
420 ui.status(_("stopped the interrupted transplant\n"))
421 ui.status(_("working directory is now at %s\n") %
421 ui.status(_("working directory is now at %s\n") %
422 startctx.hex()[:12])
422 startctx.hex()[:12])
423 self.unlog()
423 self.unlog()
424 return 0
424 return 0
425
425
426 def readseries(self):
426 def readseries(self):
427 nodes = []
427 nodes = []
428 merges = []
428 merges = []
429 cur = nodes
429 cur = nodes
430 for line in self.opener.read('series').splitlines():
430 for line in self.opener.read('series').splitlines():
431 if line.startswith('# Merges'):
431 if line.startswith('# Merges'):
432 cur = merges
432 cur = merges
433 continue
433 continue
434 cur.append(revlog.bin(line))
434 cur.append(revlog.bin(line))
435
435
436 return (nodes, merges)
436 return (nodes, merges)
437
437
438 def saveseries(self, revmap, merges):
438 def saveseries(self, revmap, merges):
439 if not revmap:
439 if not revmap:
440 return
440 return
441
441
442 if not os.path.isdir(self.path):
442 if not os.path.isdir(self.path):
443 os.mkdir(self.path)
443 os.mkdir(self.path)
444 series = self.opener('series', 'w')
444 series = self.opener('series', 'w')
445 for rev in sorted(revmap):
445 for rev in sorted(revmap):
446 series.write(nodemod.hex(revmap[rev]) + '\n')
446 series.write(nodemod.hex(revmap[rev]) + '\n')
447 if merges:
447 if merges:
448 series.write('# Merges\n')
448 series.write('# Merges\n')
449 for m in merges:
449 for m in merges:
450 series.write(nodemod.hex(m) + '\n')
450 series.write(nodemod.hex(m) + '\n')
451 series.close()
451 series.close()
452
452
453 def parselog(self, fp):
453 def parselog(self, fp):
454 parents = []
454 parents = []
455 message = []
455 message = []
456 node = revlog.nullid
456 node = revlog.nullid
457 inmsg = False
457 inmsg = False
458 user = None
458 user = None
459 date = None
459 date = None
460 for line in fp.read().splitlines():
460 for line in fp.read().splitlines():
461 if inmsg:
461 if inmsg:
462 message.append(line)
462 message.append(line)
463 elif line.startswith('# User '):
463 elif line.startswith('# User '):
464 user = line[7:]
464 user = line[7:]
465 elif line.startswith('# Date '):
465 elif line.startswith('# Date '):
466 date = line[7:]
466 date = line[7:]
467 elif line.startswith('# Node ID '):
467 elif line.startswith('# Node ID '):
468 node = revlog.bin(line[10:])
468 node = revlog.bin(line[10:])
469 elif line.startswith('# Parent '):
469 elif line.startswith('# Parent '):
470 parents.append(revlog.bin(line[9:]))
470 parents.append(revlog.bin(line[9:]))
471 elif not line.startswith('# '):
471 elif not line.startswith('# '):
472 inmsg = True
472 inmsg = True
473 message.append(line)
473 message.append(line)
474 if None in (user, date):
474 if None in (user, date):
475 raise error.Abort(_("filter corrupted changeset (no user or date)"))
475 raise error.Abort(_("filter corrupted changeset (no user or date)"))
476 return (node, user, date, '\n'.join(message), parents)
476 return (node, user, date, '\n'.join(message), parents)
477
477
478 def log(self, user, date, message, p1, p2, merge=False):
478 def log(self, user, date, message, p1, p2, merge=False):
479 '''journal changelog metadata for later recover'''
479 '''journal changelog metadata for later recover'''
480
480
481 if not os.path.isdir(self.path):
481 if not os.path.isdir(self.path):
482 os.mkdir(self.path)
482 os.mkdir(self.path)
483 fp = self.opener('journal', 'w')
483 fp = self.opener('journal', 'w')
484 fp.write('# User %s\n' % user)
484 fp.write('# User %s\n' % user)
485 fp.write('# Date %s\n' % date)
485 fp.write('# Date %s\n' % date)
486 fp.write('# Node ID %s\n' % nodemod.hex(p2))
486 fp.write('# Node ID %s\n' % nodemod.hex(p2))
487 fp.write('# Parent ' + nodemod.hex(p1) + '\n')
487 fp.write('# Parent ' + nodemod.hex(p1) + '\n')
488 if merge:
488 if merge:
489 fp.write('# Parent ' + nodemod.hex(p2) + '\n')
489 fp.write('# Parent ' + nodemod.hex(p2) + '\n')
490 fp.write(message.rstrip() + '\n')
490 fp.write(message.rstrip() + '\n')
491 fp.close()
491 fp.close()
492
492
493 def readlog(self):
493 def readlog(self):
494 return self.parselog(self.opener('journal'))
494 return self.parselog(self.opener('journal'))
495
495
496 def unlog(self):
496 def unlog(self):
497 '''remove changelog journal'''
497 '''remove changelog journal'''
498 absdst = os.path.join(self.path, 'journal')
498 absdst = os.path.join(self.path, 'journal')
499 if os.path.exists(absdst):
499 if os.path.exists(absdst):
500 os.unlink(absdst)
500 os.unlink(absdst)
501
501
502 def transplantfilter(self, repo, source, root):
502 def transplantfilter(self, repo, source, root):
503 def matchfn(node):
503 def matchfn(node):
504 if self.applied(repo, node, root):
504 if self.applied(repo, node, root):
505 return False
505 return False
506 if source.changelog.parents(node)[1] != revlog.nullid:
506 if source.changelog.parents(node)[1] != revlog.nullid:
507 return False
507 return False
508 extra = source.changelog.read(node)[5]
508 extra = source.changelog.read(node)[5]
509 cnode = extra.get('transplant_source')
509 cnode = extra.get('transplant_source')
510 if cnode and self.applied(repo, cnode, root):
510 if cnode and self.applied(repo, cnode, root):
511 return False
511 return False
512 return True
512 return True
513
513
514 return matchfn
514 return matchfn
515
515
516 def hasnode(repo, node):
516 def hasnode(repo, node):
517 try:
517 try:
518 return repo.changelog.rev(node) is not None
518 return repo.changelog.rev(node) is not None
519 except error.StorageError:
519 except error.StorageError:
520 return False
520 return False
521
521
522 def browserevs(ui, repo, nodes, opts):
522 def browserevs(ui, repo, nodes, opts):
523 '''interactively transplant changesets'''
523 '''interactively transplant changesets'''
524 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
524 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
525 transplants = []
525 transplants = []
526 merges = []
526 merges = []
527 prompt = _('apply changeset? [ynmpcq?]:'
527 prompt = _('apply changeset? [ynmpcq?]:'
528 '$$ &yes, transplant this changeset'
528 '$$ &yes, transplant this changeset'
529 '$$ &no, skip this changeset'
529 '$$ &no, skip this changeset'
530 '$$ &merge at this changeset'
530 '$$ &merge at this changeset'
531 '$$ show &patch'
531 '$$ show &patch'
532 '$$ &commit selected changesets'
532 '$$ &commit selected changesets'
533 '$$ &quit and cancel transplant'
533 '$$ &quit and cancel transplant'
534 '$$ &? (show this help)')
534 '$$ &? (show this help)')
535 for node in nodes:
535 for node in nodes:
536 displayer.show(repo[node])
536 displayer.show(repo[node])
537 action = None
537 action = None
538 while not action:
538 while not action:
539 choice = ui.promptchoice(prompt)
539 choice = ui.promptchoice(prompt)
540 action = 'ynmpcq?'[choice:choice + 1]
540 action = 'ynmpcq?'[choice:choice + 1]
541 if action == '?':
541 if action == '?':
542 for c, t in ui.extractchoices(prompt)[1]:
542 for c, t in ui.extractchoices(prompt)[1]:
543 ui.write('%s: %s\n' % (c, t))
543 ui.write('%s: %s\n' % (c, t))
544 action = None
544 action = None
545 elif action == 'p':
545 elif action == 'p':
546 parent = repo.changelog.parents(node)[0]
546 parent = repo.changelog.parents(node)[0]
547 for chunk in patch.diff(repo, parent, node):
547 for chunk in patch.diff(repo, parent, node):
548 ui.write(chunk)
548 ui.write(chunk)
549 action = None
549 action = None
550 if action == 'y':
550 if action == 'y':
551 transplants.append(node)
551 transplants.append(node)
552 elif action == 'm':
552 elif action == 'm':
553 merges.append(node)
553 merges.append(node)
554 elif action == 'c':
554 elif action == 'c':
555 break
555 break
556 elif action == 'q':
556 elif action == 'q':
557 transplants = ()
557 transplants = ()
558 merges = ()
558 merges = ()
559 break
559 break
560 displayer.close()
560 displayer.close()
561 return (transplants, merges)
561 return (transplants, merges)
562
562
563 @command('transplant',
563 @command('transplant',
564 [('s', 'source', '', _('transplant changesets from REPO'), _('REPO')),
564 [('s', 'source', '', _('transplant changesets from REPO'), _('REPO')),
565 ('b', 'branch', [], _('use this source changeset as head'), _('REV')),
565 ('b', 'branch', [], _('use this source changeset as head'), _('REV')),
566 ('a', 'all', None, _('pull all changesets up to the --branch revisions')),
566 ('a', 'all', None, _('pull all changesets up to the --branch revisions')),
567 ('p', 'prune', [], _('skip over REV'), _('REV')),
567 ('p', 'prune', [], _('skip over REV'), _('REV')),
568 ('m', 'merge', [], _('merge at REV'), _('REV')),
568 ('m', 'merge', [], _('merge at REV'), _('REV')),
569 ('', 'parent', '',
569 ('', 'parent', '',
570 _('parent to choose when transplanting merge'), _('REV')),
570 _('parent to choose when transplanting merge'), _('REV')),
571 ('e', 'edit', False, _('invoke editor on commit messages')),
571 ('e', 'edit', False, _('invoke editor on commit messages')),
572 ('', 'log', None, _('append transplant info to log message')),
572 ('', 'log', None, _('append transplant info to log message')),
573 ('', 'stop', False, _('stop interrupted transplant')),
573 ('', 'stop', False, _('stop interrupted transplant')),
574 ('c', 'continue', None, _('continue last transplant session '
574 ('c', 'continue', None, _('continue last transplant session '
575 'after fixing conflicts')),
575 'after fixing conflicts')),
576 ('', 'filter', '',
576 ('', 'filter', '',
577 _('filter changesets through command'), _('CMD'))],
577 _('filter changesets through command'), _('CMD'))],
578 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
578 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
579 '[-m REV] [REV]...'),
579 '[-m REV] [REV]...'),
580 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
580 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
581 def transplant(ui, repo, *revs, **opts):
581 def transplant(ui, repo, *revs, **opts):
582 '''transplant changesets from another branch
582 '''transplant changesets from another branch
583
583
584 Selected changesets will be applied on top of the current working
584 Selected changesets will be applied on top of the current working
585 directory with the log of the original changeset. The changesets
585 directory with the log of the original changeset. The changesets
586 are copied and will thus appear twice in the history with different
586 are copied and will thus appear twice in the history with different
587 identities.
587 identities.
588
588
589 Consider using the graft command if everything is inside the same
589 Consider using the graft command if everything is inside the same
590 repository - it will use merges and will usually give a better result.
590 repository - it will use merges and will usually give a better result.
591 Use the rebase extension if the changesets are unpublished and you want
591 Use the rebase extension if the changesets are unpublished and you want
592 to move them instead of copying them.
592 to move them instead of copying them.
593
593
594 If --log is specified, log messages will have a comment appended
594 If --log is specified, log messages will have a comment appended
595 of the form::
595 of the form::
596
596
597 (transplanted from CHANGESETHASH)
597 (transplanted from CHANGESETHASH)
598
598
599 You can rewrite the changelog message with the --filter option.
599 You can rewrite the changelog message with the --filter option.
600 Its argument will be invoked with the current changelog message as
600 Its argument will be invoked with the current changelog message as
601 $1 and the patch as $2.
601 $1 and the patch as $2.
602
602
603 --source/-s specifies another repository to use for selecting changesets,
603 --source/-s specifies another repository to use for selecting changesets,
604 just as if it temporarily had been pulled.
604 just as if it temporarily had been pulled.
605 If --branch/-b is specified, these revisions will be used as
605 If --branch/-b is specified, these revisions will be used as
606 heads when deciding which changesets to transplant, just as if only
606 heads when deciding which changesets to transplant, just as if only
607 these revisions had been pulled.
607 these revisions had been pulled.
608 If --all/-a is specified, all the revisions up to the heads specified
608 If --all/-a is specified, all the revisions up to the heads specified
609 with --branch will be transplanted.
609 with --branch will be transplanted.
610
610
611 Example:
611 Example:
612
612
613 - transplant all changes up to REV on top of your current revision::
613 - transplant all changes up to REV on top of your current revision::
614
614
615 hg transplant --branch REV --all
615 hg transplant --branch REV --all
616
616
617 You can optionally mark selected transplanted changesets as merge
617 You can optionally mark selected transplanted changesets as merge
618 changesets. You will not be prompted to transplant any ancestors
618 changesets. You will not be prompted to transplant any ancestors
619 of a merged transplant, and you can merge descendants of them
619 of a merged transplant, and you can merge descendants of them
620 normally instead of transplanting them.
620 normally instead of transplanting them.
621
621
622 Merge changesets may be transplanted directly by specifying the
622 Merge changesets may be transplanted directly by specifying the
623 proper parent changeset by calling :hg:`transplant --parent`.
623 proper parent changeset by calling :hg:`transplant --parent`.
624
624
625 If no merges or revisions are provided, :hg:`transplant` will
625 If no merges or revisions are provided, :hg:`transplant` will
626 start an interactive changeset browser.
626 start an interactive changeset browser.
627
627
628 If a changeset application fails, you can fix the merge by hand
628 If a changeset application fails, you can fix the merge by hand
629 and then resume where you left off by calling :hg:`transplant
629 and then resume where you left off by calling :hg:`transplant
630 --continue/-c`.
630 --continue/-c`.
631 '''
631 '''
632 with repo.wlock():
632 with repo.wlock():
633 return _dotransplant(ui, repo, *revs, **opts)
633 return _dotransplant(ui, repo, *revs, **opts)
634
634
635 def _dotransplant(ui, repo, *revs, **opts):
635 def _dotransplant(ui, repo, *revs, **opts):
636 def incwalk(repo, csets, match=util.always):
636 def incwalk(repo, csets, match=util.always):
637 for node in csets:
637 for node in csets:
638 if match(node):
638 if match(node):
639 yield node
639 yield node
640
640
641 def transplantwalk(repo, dest, heads, match=util.always):
641 def transplantwalk(repo, dest, heads, match=util.always):
642 '''Yield all nodes that are ancestors of a head but not ancestors
642 '''Yield all nodes that are ancestors of a head but not ancestors
643 of dest.
643 of dest.
644 If no heads are specified, the heads of repo will be used.'''
644 If no heads are specified, the heads of repo will be used.'''
645 if not heads:
645 if not heads:
646 heads = repo.heads()
646 heads = repo.heads()
647 ancestors = []
647 ancestors = []
648 ctx = repo[dest]
648 ctx = repo[dest]
649 for head in heads:
649 for head in heads:
650 ancestors.append(ctx.ancestor(repo[head]).node())
650 ancestors.append(ctx.ancestor(repo[head]).node())
651 for node in repo.changelog.nodesbetween(ancestors, heads)[0]:
651 for node in repo.changelog.nodesbetween(ancestors, heads)[0]:
652 if match(node):
652 if match(node):
653 yield node
653 yield node
654
654
655 def checkopts(opts, revs):
655 def checkopts(opts, revs):
656 if opts.get('continue'):
656 if opts.get('continue'):
657 if opts.get('branch') or opts.get('all') or opts.get('merge'):
657 if opts.get('branch') or opts.get('all') or opts.get('merge'):
658 raise error.Abort(_('--continue is incompatible with '
658 raise error.Abort(_('--continue is incompatible with '
659 '--branch, --all and --merge'))
659 '--branch, --all and --merge'))
660 return
660 return
661 if opts.get('stop'):
661 if opts.get('stop'):
662 if opts.get('branch') or opts.get('all') or opts.get('merge'):
662 if opts.get('branch') or opts.get('all') or opts.get('merge'):
663 raise error.Abort(_('--stop is incompatible with '
663 raise error.Abort(_('--stop is incompatible with '
664 '--branch, --all and --merge'))
664 '--branch, --all and --merge'))
665 return
665 return
666 if not (opts.get('source') or revs or
666 if not (opts.get('source') or revs or
667 opts.get('merge') or opts.get('branch')):
667 opts.get('merge') or opts.get('branch')):
668 raise error.Abort(_('no source URL, branch revision, or revision '
668 raise error.Abort(_('no source URL, branch revision, or revision '
669 'list provided'))
669 'list provided'))
670 if opts.get('all'):
670 if opts.get('all'):
671 if not opts.get('branch'):
671 if not opts.get('branch'):
672 raise error.Abort(_('--all requires a branch revision'))
672 raise error.Abort(_('--all requires a branch revision'))
673 if revs:
673 if revs:
674 raise error.Abort(_('--all is incompatible with a '
674 raise error.Abort(_('--all is incompatible with a '
675 'revision list'))
675 'revision list'))
676
676
677 opts = pycompat.byteskwargs(opts)
677 opts = pycompat.byteskwargs(opts)
678 checkopts(opts, revs)
678 checkopts(opts, revs)
679
679
680 if not opts.get('log'):
680 if not opts.get('log'):
681 # deprecated config: transplant.log
681 # deprecated config: transplant.log
682 opts['log'] = ui.config('transplant', 'log')
682 opts['log'] = ui.config('transplant', 'log')
683 if not opts.get('filter'):
683 if not opts.get('filter'):
684 # deprecated config: transplant.filter
684 # deprecated config: transplant.filter
685 opts['filter'] = ui.config('transplant', 'filter')
685 opts['filter'] = ui.config('transplant', 'filter')
686
686
687 tp = transplanter(ui, repo, opts)
687 tp = transplanter(ui, repo, opts)
688
688
689 p1 = repo.dirstate.p1()
689 p1 = repo.dirstate.p1()
690 if len(repo) > 0 and p1 == revlog.nullid:
690 if len(repo) > 0 and p1 == revlog.nullid:
691 raise error.Abort(_('no revision checked out'))
691 raise error.Abort(_('no revision checked out'))
692 if opts.get('continue'):
692 if opts.get('continue'):
693 if not tp.canresume():
693 if not tp.canresume():
694 raise error.Abort(_('no transplant to continue'))
694 raise error.Abort(_('no transplant to continue'))
695 else:
695 elif opts.get('stop'):
696 if opts.get('stop'):
697 if not tp.canresume():
696 if not tp.canresume():
698 raise error.Abort(_('no interrupted transplant found'))
697 raise error.Abort(_('no interrupted transplant found'))
699 return tp.stop(ui, repo)
698 return tp.stop(ui, repo)
699 else:
700 cmdutil.checkunfinished(repo)
700 cmdutil.checkunfinished(repo)
701 cmdutil.bailifchanged(repo)
701 cmdutil.bailifchanged(repo)
702
702
703 sourcerepo = opts.get('source')
703 sourcerepo = opts.get('source')
704 if sourcerepo:
704 if sourcerepo:
705 peer = hg.peer(repo, opts, ui.expandpath(sourcerepo))
705 peer = hg.peer(repo, opts, ui.expandpath(sourcerepo))
706 heads = pycompat.maplist(peer.lookup, opts.get('branch', ()))
706 heads = pycompat.maplist(peer.lookup, opts.get('branch', ()))
707 target = set(heads)
707 target = set(heads)
708 for r in revs:
708 for r in revs:
709 try:
709 try:
710 target.add(peer.lookup(r))
710 target.add(peer.lookup(r))
711 except error.RepoError:
711 except error.RepoError:
712 pass
712 pass
713 source, csets, cleanupfn = bundlerepo.getremotechanges(ui, repo, peer,
713 source, csets, cleanupfn = bundlerepo.getremotechanges(ui, repo, peer,
714 onlyheads=sorted(target), force=True)
714 onlyheads=sorted(target), force=True)
715 else:
715 else:
716 source = repo
716 source = repo
717 heads = pycompat.maplist(source.lookup, opts.get('branch', ()))
717 heads = pycompat.maplist(source.lookup, opts.get('branch', ()))
718 cleanupfn = None
718 cleanupfn = None
719
719
720 try:
720 try:
721 if opts.get('continue'):
721 if opts.get('continue'):
722 tp.resume(repo, source, opts)
722 tp.resume(repo, source, opts)
723 return
723 return
724
724
725 tf = tp.transplantfilter(repo, source, p1)
725 tf = tp.transplantfilter(repo, source, p1)
726 if opts.get('prune'):
726 if opts.get('prune'):
727 prune = set(source[r].node()
727 prune = set(source[r].node()
728 for r in scmutil.revrange(source, opts.get('prune')))
728 for r in scmutil.revrange(source, opts.get('prune')))
729 matchfn = lambda x: tf(x) and x not in prune
729 matchfn = lambda x: tf(x) and x not in prune
730 else:
730 else:
731 matchfn = tf
731 matchfn = tf
732 merges = pycompat.maplist(source.lookup, opts.get('merge', ()))
732 merges = pycompat.maplist(source.lookup, opts.get('merge', ()))
733 revmap = {}
733 revmap = {}
734 if revs:
734 if revs:
735 for r in scmutil.revrange(source, revs):
735 for r in scmutil.revrange(source, revs):
736 revmap[int(r)] = source[r].node()
736 revmap[int(r)] = source[r].node()
737 elif opts.get('all') or not merges:
737 elif opts.get('all') or not merges:
738 if source != repo:
738 if source != repo:
739 alltransplants = incwalk(source, csets, match=matchfn)
739 alltransplants = incwalk(source, csets, match=matchfn)
740 else:
740 else:
741 alltransplants = transplantwalk(source, p1, heads,
741 alltransplants = transplantwalk(source, p1, heads,
742 match=matchfn)
742 match=matchfn)
743 if opts.get('all'):
743 if opts.get('all'):
744 revs = alltransplants
744 revs = alltransplants
745 else:
745 else:
746 revs, newmerges = browserevs(ui, source, alltransplants, opts)
746 revs, newmerges = browserevs(ui, source, alltransplants, opts)
747 merges.extend(newmerges)
747 merges.extend(newmerges)
748 for r in revs:
748 for r in revs:
749 revmap[source.changelog.rev(r)] = r
749 revmap[source.changelog.rev(r)] = r
750 for r in merges:
750 for r in merges:
751 revmap[source.changelog.rev(r)] = r
751 revmap[source.changelog.rev(r)] = r
752
752
753 tp.apply(repo, source, revmap, merges, opts)
753 tp.apply(repo, source, revmap, merges, opts)
754 finally:
754 finally:
755 if cleanupfn:
755 if cleanupfn:
756 cleanupfn()
756 cleanupfn()
757
757
758 def continuecmd(ui, repo):
758 def continuecmd(ui, repo):
759 """logic to resume an interrupted transplant using
759 """logic to resume an interrupted transplant using
760 'hg continue'"""
760 'hg continue'"""
761 with repo.wlock():
761 with repo.wlock():
762 tp = transplanter(ui, repo, {})
762 tp = transplanter(ui, repo, {})
763 return tp.resume(repo, repo, {})
763 return tp.resume(repo, repo, {})
764
764
765 revsetpredicate = registrar.revsetpredicate()
765 revsetpredicate = registrar.revsetpredicate()
766
766
767 @revsetpredicate('transplanted([set])')
767 @revsetpredicate('transplanted([set])')
768 def revsettransplanted(repo, subset, x):
768 def revsettransplanted(repo, subset, x):
769 """Transplanted changesets in set, or all transplanted changesets.
769 """Transplanted changesets in set, or all transplanted changesets.
770 """
770 """
771 if x:
771 if x:
772 s = revset.getset(repo, subset, x)
772 s = revset.getset(repo, subset, x)
773 else:
773 else:
774 s = subset
774 s = subset
775 return smartset.baseset([r for r in s if
775 return smartset.baseset([r for r in s if
776 repo[r].extra().get('transplant_source')])
776 repo[r].extra().get('transplant_source')])
777
777
778 templatekeyword = registrar.templatekeyword()
778 templatekeyword = registrar.templatekeyword()
779
779
780 @templatekeyword('transplanted', requires={'ctx'})
780 @templatekeyword('transplanted', requires={'ctx'})
781 def kwtransplanted(context, mapping):
781 def kwtransplanted(context, mapping):
782 """String. The node identifier of the transplanted
782 """String. The node identifier of the transplanted
783 changeset if any."""
783 changeset if any."""
784 ctx = context.resource(mapping, 'ctx')
784 ctx = context.resource(mapping, 'ctx')
785 n = ctx.extra().get('transplant_source')
785 n = ctx.extra().get('transplant_source')
786 return n and nodemod.hex(n) or ''
786 return n and nodemod.hex(n) or ''
787
787
788 def extsetup(ui):
788 def extsetup(ui):
789 statemod.addunfinished (
789 statemod.addunfinished (
790 'transplant', fname='transplant/journal', clearable=True,
790 'transplant', fname='transplant/journal', clearable=True,
791 continuefunc=continuecmd,
791 continuefunc=continuecmd,
792 statushint=_('To continue: hg transplant --continue\n'
792 statushint=_('To continue: hg transplant --continue\n'
793 'To stop: hg transplant --stop'),
793 'To stop: hg transplant --stop'),
794 cmdhint=_("use 'hg transplant --continue' or 'hg transplant --stop'")
794 cmdhint=_("use 'hg transplant --continue' or 'hg transplant --stop'")
795 )
795 )
796
796
797 # tell hggettext to extract docstrings from these functions:
797 # tell hggettext to extract docstrings from these functions:
798 i18nfunctions = [revsettransplanted, kwtransplanted]
798 i18nfunctions = [revsettransplanted, kwtransplanted]
General Comments 0
You need to be logged in to leave comments. Login now