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