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