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