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