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