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