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