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