##// END OF EJS Templates
transplant: permit merge changesets via --parent...
Steven Stallion -
r16400:f2ba409d default
parent child Browse files
Show More
@@ -1,643 +1,670
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 patches from another branch.
11 11
12 12 Transplanted patches are recorded in .hg/transplant/transplants, as a
13 13 map from a changeset hash to its hash in the source repository.
14 14 '''
15 15
16 16 from mercurial.i18n import _
17 17 import os, tempfile
18 18 from mercurial.node import short
19 19 from mercurial import bundlerepo, hg, merge, match
20 20 from mercurial import patch, revlog, scmutil, util, error, cmdutil
21 21 from mercurial import revset, templatekw
22 22
23 23 cmdtable = {}
24 24 command = cmdutil.command(cmdtable)
25 25
26 26 class transplantentry(object):
27 27 def __init__(self, lnode, rnode):
28 28 self.lnode = lnode
29 29 self.rnode = rnode
30 30
31 31 class transplants(object):
32 32 def __init__(self, path=None, transplantfile=None, opener=None):
33 33 self.path = path
34 34 self.transplantfile = transplantfile
35 35 self.opener = opener
36 36
37 37 if not opener:
38 38 self.opener = scmutil.opener(self.path)
39 39 self.transplants = {}
40 40 self.dirty = False
41 41 self.read()
42 42
43 43 def read(self):
44 44 abspath = os.path.join(self.path, self.transplantfile)
45 45 if self.transplantfile and os.path.exists(abspath):
46 46 for line in self.opener.read(self.transplantfile).splitlines():
47 47 lnode, rnode = map(revlog.bin, line.split(':'))
48 48 list = self.transplants.setdefault(rnode, [])
49 49 list.append(transplantentry(lnode, rnode))
50 50
51 51 def write(self):
52 52 if self.dirty and self.transplantfile:
53 53 if not os.path.isdir(self.path):
54 54 os.mkdir(self.path)
55 55 fp = self.opener(self.transplantfile, 'w')
56 56 for list in self.transplants.itervalues():
57 57 for t in list:
58 58 l, r = map(revlog.hex, (t.lnode, t.rnode))
59 59 fp.write(l + ':' + r + '\n')
60 60 fp.close()
61 61 self.dirty = False
62 62
63 63 def get(self, rnode):
64 64 return self.transplants.get(rnode) or []
65 65
66 66 def set(self, lnode, rnode):
67 67 list = self.transplants.setdefault(rnode, [])
68 68 list.append(transplantentry(lnode, rnode))
69 69 self.dirty = True
70 70
71 71 def remove(self, transplant):
72 72 list = self.transplants.get(transplant.rnode)
73 73 if list:
74 74 del list[list.index(transplant)]
75 75 self.dirty = True
76 76
77 77 class transplanter(object):
78 78 def __init__(self, ui, repo):
79 79 self.ui = ui
80 80 self.path = repo.join('transplant')
81 81 self.opener = scmutil.opener(self.path)
82 82 self.transplants = transplants(self.path, 'transplants',
83 83 opener=self.opener)
84 84 self.editor = None
85 85
86 86 def applied(self, repo, node, parent):
87 87 '''returns True if a node is already an ancestor of parent
88 88 or has already been transplanted'''
89 89 if hasnode(repo, node):
90 90 if node in repo.changelog.reachable(parent, stop=node):
91 91 return True
92 92 for t in self.transplants.get(node):
93 93 # it might have been stripped
94 94 if not hasnode(repo, t.lnode):
95 95 self.transplants.remove(t)
96 96 return False
97 97 if t.lnode in repo.changelog.reachable(parent, stop=t.lnode):
98 98 return True
99 99 return False
100 100
101 101 def apply(self, repo, source, revmap, merges, opts={}):
102 102 '''apply the revisions in revmap one by one in revision order'''
103 103 revs = sorted(revmap)
104 104 p1, p2 = repo.dirstate.parents()
105 105 pulls = []
106 106 diffopts = patch.diffopts(self.ui, opts)
107 107 diffopts.git = True
108 108
109 109 lock = wlock = tr = None
110 110 try:
111 111 wlock = repo.wlock()
112 112 lock = repo.lock()
113 113 tr = repo.transaction('transplant')
114 114 for rev in revs:
115 115 node = revmap[rev]
116 116 revstr = '%s:%s' % (rev, short(node))
117 117
118 118 if self.applied(repo, node, p1):
119 119 self.ui.warn(_('skipping already applied revision %s\n') %
120 120 revstr)
121 121 continue
122 122
123 123 parents = source.changelog.parents(node)
124 124 if not opts.get('filter'):
125 125 # If the changeset parent is the same as the
126 126 # wdir's parent, just pull it.
127 127 if parents[0] == p1:
128 128 pulls.append(node)
129 129 p1 = node
130 130 continue
131 131 if pulls:
132 132 if source != repo:
133 133 repo.pull(source, heads=pulls)
134 134 merge.update(repo, pulls[-1], False, False, None)
135 135 p1, p2 = repo.dirstate.parents()
136 136 pulls = []
137 137
138 138 domerge = False
139 139 if node in merges:
140 140 # pulling all the merge revs at once would mean we
141 141 # couldn't transplant after the latest even if
142 142 # transplants before them fail.
143 143 domerge = True
144 144 if not hasnode(repo, node):
145 145 repo.pull(source, heads=[node])
146 146
147 skipmerge = False
147 148 if parents[1] != revlog.nullid:
148 self.ui.note(_('skipping merge changeset %s:%s\n')
149 % (rev, short(node)))
149 if not opts.get('parent'):
150 self.ui.note(_('skipping merge changeset %s:%s\n')
151 % (rev, short(node)))
152 skipmerge = True
153 else:
154 parent = source.lookup(opts['parent'])
155 if parent not in parents:
156 raise util.Abort(_('%s is not a parent of %s') %
157 (short(parent), short(node)))
158 else:
159 parent = parents[0]
160
161 if skipmerge:
150 162 patchfile = None
151 163 else:
152 164 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
153 165 fp = os.fdopen(fd, 'w')
154 gen = patch.diff(source, parents[0], node, opts=diffopts)
166 gen = patch.diff(source, parent, node, opts=diffopts)
155 167 for chunk in gen:
156 168 fp.write(chunk)
157 169 fp.close()
158 170
159 171 del revmap[rev]
160 172 if patchfile or domerge:
161 173 try:
162 174 n = self.applyone(repo, node,
163 175 source.changelog.read(node),
164 176 patchfile, merge=domerge,
165 177 log=opts.get('log'),
166 178 filter=opts.get('filter'))
167 179 if n and domerge:
168 180 self.ui.status(_('%s merged at %s\n') % (revstr,
169 181 short(n)))
170 182 elif n:
171 183 self.ui.status(_('%s transplanted to %s\n')
172 184 % (short(node),
173 185 short(n)))
174 186 finally:
175 187 if patchfile:
176 188 os.unlink(patchfile)
177 189 tr.close()
178 190 if pulls:
179 191 repo.pull(source, heads=pulls)
180 192 merge.update(repo, pulls[-1], False, False, None)
181 193 finally:
182 194 self.saveseries(revmap, merges)
183 195 self.transplants.write()
184 196 if tr:
185 197 tr.release()
186 198 lock.release()
187 199 wlock.release()
188 200
189 201 def filter(self, filter, node, changelog, patchfile):
190 202 '''arbitrarily rewrite changeset before applying it'''
191 203
192 204 self.ui.status(_('filtering %s\n') % patchfile)
193 205 user, date, msg = (changelog[1], changelog[2], changelog[4])
194 206 fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-')
195 207 fp = os.fdopen(fd, 'w')
196 208 fp.write("# HG changeset patch\n")
197 209 fp.write("# User %s\n" % user)
198 210 fp.write("# Date %d %d\n" % date)
199 211 fp.write(msg + '\n')
200 212 fp.close()
201 213
202 214 try:
203 215 util.system('%s %s %s' % (filter, util.shellquote(headerfile),
204 216 util.shellquote(patchfile)),
205 217 environ={'HGUSER': changelog[1],
206 218 'HGREVISION': revlog.hex(node),
207 219 },
208 220 onerr=util.Abort, errprefix=_('filter failed'),
209 221 out=self.ui.fout)
210 222 user, date, msg = self.parselog(file(headerfile))[1:4]
211 223 finally:
212 224 os.unlink(headerfile)
213 225
214 226 return (user, date, msg)
215 227
216 228 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
217 229 filter=None):
218 230 '''apply the patch in patchfile to the repository as a transplant'''
219 231 (manifest, user, (time, timezone), files, message) = cl[:5]
220 232 date = "%d %d" % (time, timezone)
221 233 extra = {'transplant_source': node}
222 234 if filter:
223 235 (user, date, message) = self.filter(filter, node, cl, patchfile)
224 236
225 237 if log:
226 238 # we don't translate messages inserted into commits
227 239 message += '\n(transplanted from %s)' % revlog.hex(node)
228 240
229 241 self.ui.status(_('applying %s\n') % short(node))
230 242 self.ui.note('%s %s\n%s\n' % (user, date, message))
231 243
232 244 if not patchfile and not merge:
233 245 raise util.Abort(_('can only omit patchfile if merging'))
234 246 if patchfile:
235 247 try:
236 248 files = set()
237 249 patch.patch(self.ui, repo, patchfile, files=files, eolmode=None)
238 250 files = list(files)
239 251 if not files:
240 252 self.ui.warn(_('%s: empty changeset') % revlog.hex(node))
241 253 return None
242 254 except Exception, inst:
243 255 seriespath = os.path.join(self.path, 'series')
244 256 if os.path.exists(seriespath):
245 257 os.unlink(seriespath)
246 258 p1 = repo.dirstate.p1()
247 259 p2 = node
248 260 self.log(user, date, message, p1, p2, merge=merge)
249 261 self.ui.write(str(inst) + '\n')
250 262 raise util.Abort(_('fix up the merge and run '
251 263 'hg transplant --continue'))
252 264 else:
253 265 files = None
254 266 if merge:
255 267 p1, p2 = repo.dirstate.parents()
256 268 repo.dirstate.setparents(p1, node)
257 269 m = match.always(repo.root, '')
258 270 else:
259 271 m = match.exact(repo.root, '', files)
260 272
261 273 n = repo.commit(message, user, date, extra=extra, match=m,
262 274 editor=self.editor)
263 275 if not n:
264 276 # Crash here to prevent an unclear crash later, in
265 277 # transplants.write(). This can happen if patch.patch()
266 278 # does nothing but claims success or if repo.status() fails
267 279 # to report changes done by patch.patch(). These both
268 280 # appear to be bugs in other parts of Mercurial, but dying
269 281 # here, as soon as we can detect the problem, is preferable
270 282 # to silently dropping changesets on the floor.
271 283 raise RuntimeError('nothing committed after transplant')
272 284 if not merge:
273 285 self.transplants.set(n, node)
274 286
275 287 return n
276 288
277 289 def resume(self, repo, source, opts=None):
278 290 '''recover last transaction and apply remaining changesets'''
279 291 if os.path.exists(os.path.join(self.path, 'journal')):
280 292 n, node = self.recover(repo)
281 293 self.ui.status(_('%s transplanted as %s\n') % (short(node),
282 294 short(n)))
283 295 seriespath = os.path.join(self.path, 'series')
284 296 if not os.path.exists(seriespath):
285 297 self.transplants.write()
286 298 return
287 299 nodes, merges = self.readseries()
288 300 revmap = {}
289 301 for n in nodes:
290 302 revmap[source.changelog.rev(n)] = n
291 303 os.unlink(seriespath)
292 304
293 305 self.apply(repo, source, revmap, merges, opts)
294 306
295 307 def recover(self, repo):
296 308 '''commit working directory using journal metadata'''
297 309 node, user, date, message, parents = self.readlog()
298 merge = len(parents) == 2
310 merge = False
299 311
300 312 if not user or not date or not message or not parents[0]:
301 313 raise util.Abort(_('transplant log file is corrupt'))
302 314
315 parent = parents[0]
316 if len(parents) > 1:
317 if opts.get('parent'):
318 parent = source.lookup(opts['parent'])
319 if parent not in parents:
320 raise util.Abort(_('%s is not a parent of %s') %
321 (short(parent), short(node)))
322 else:
323 merge = True
324
303 325 extra = {'transplant_source': node}
304 326 wlock = repo.wlock()
305 327 try:
306 328 p1, p2 = repo.dirstate.parents()
307 if p1 != parents[0]:
329 if p1 != parent:
308 330 raise util.Abort(
309 331 _('working dir not at transplant parent %s') %
310 revlog.hex(parents[0]))
332 revlog.hex(parent))
311 333 if merge:
312 334 repo.dirstate.setparents(p1, parents[1])
313 335 n = repo.commit(message, user, date, extra=extra,
314 336 editor=self.editor)
315 337 if not n:
316 338 raise util.Abort(_('commit failed'))
317 339 if not merge:
318 340 self.transplants.set(n, node)
319 341 self.unlog()
320 342
321 343 return n, node
322 344 finally:
323 345 wlock.release()
324 346
325 347 def readseries(self):
326 348 nodes = []
327 349 merges = []
328 350 cur = nodes
329 351 for line in self.opener.read('series').splitlines():
330 352 if line.startswith('# Merges'):
331 353 cur = merges
332 354 continue
333 355 cur.append(revlog.bin(line))
334 356
335 357 return (nodes, merges)
336 358
337 359 def saveseries(self, revmap, merges):
338 360 if not revmap:
339 361 return
340 362
341 363 if not os.path.isdir(self.path):
342 364 os.mkdir(self.path)
343 365 series = self.opener('series', 'w')
344 366 for rev in sorted(revmap):
345 367 series.write(revlog.hex(revmap[rev]) + '\n')
346 368 if merges:
347 369 series.write('# Merges\n')
348 370 for m in merges:
349 371 series.write(revlog.hex(m) + '\n')
350 372 series.close()
351 373
352 374 def parselog(self, fp):
353 375 parents = []
354 376 message = []
355 377 node = revlog.nullid
356 378 inmsg = False
357 379 user = None
358 380 date = None
359 381 for line in fp.read().splitlines():
360 382 if inmsg:
361 383 message.append(line)
362 384 elif line.startswith('# User '):
363 385 user = line[7:]
364 386 elif line.startswith('# Date '):
365 387 date = line[7:]
366 388 elif line.startswith('# Node ID '):
367 389 node = revlog.bin(line[10:])
368 390 elif line.startswith('# Parent '):
369 391 parents.append(revlog.bin(line[9:]))
370 392 elif not line.startswith('# '):
371 393 inmsg = True
372 394 message.append(line)
373 395 if None in (user, date):
374 396 raise util.Abort(_("filter corrupted changeset (no user or date)"))
375 397 return (node, user, date, '\n'.join(message), parents)
376 398
377 399 def log(self, user, date, message, p1, p2, merge=False):
378 400 '''journal changelog metadata for later recover'''
379 401
380 402 if not os.path.isdir(self.path):
381 403 os.mkdir(self.path)
382 404 fp = self.opener('journal', 'w')
383 405 fp.write('# User %s\n' % user)
384 406 fp.write('# Date %s\n' % date)
385 407 fp.write('# Node ID %s\n' % revlog.hex(p2))
386 408 fp.write('# Parent ' + revlog.hex(p1) + '\n')
387 409 if merge:
388 410 fp.write('# Parent ' + revlog.hex(p2) + '\n')
389 411 fp.write(message.rstrip() + '\n')
390 412 fp.close()
391 413
392 414 def readlog(self):
393 415 return self.parselog(self.opener('journal'))
394 416
395 417 def unlog(self):
396 418 '''remove changelog journal'''
397 419 absdst = os.path.join(self.path, 'journal')
398 420 if os.path.exists(absdst):
399 421 os.unlink(absdst)
400 422
401 423 def transplantfilter(self, repo, source, root):
402 424 def matchfn(node):
403 425 if self.applied(repo, node, root):
404 426 return False
405 427 if source.changelog.parents(node)[1] != revlog.nullid:
406 428 return False
407 429 extra = source.changelog.read(node)[5]
408 430 cnode = extra.get('transplant_source')
409 431 if cnode and self.applied(repo, cnode, root):
410 432 return False
411 433 return True
412 434
413 435 return matchfn
414 436
415 437 def hasnode(repo, node):
416 438 try:
417 439 return repo.changelog.rev(node) is not None
418 440 except error.RevlogError:
419 441 return False
420 442
421 443 def browserevs(ui, repo, nodes, opts):
422 444 '''interactively transplant changesets'''
423 445 def browsehelp(ui):
424 446 ui.write(_('y: transplant this changeset\n'
425 447 'n: skip this changeset\n'
426 448 'm: merge at this changeset\n'
427 449 'p: show patch\n'
428 450 'c: commit selected changesets\n'
429 451 'q: cancel transplant\n'
430 452 '?: show this help\n'))
431 453
432 454 displayer = cmdutil.show_changeset(ui, repo, opts)
433 455 transplants = []
434 456 merges = []
435 457 for node in nodes:
436 458 displayer.show(repo[node])
437 459 action = None
438 460 while not action:
439 461 action = ui.prompt(_('apply changeset? [ynmpcq?]:'))
440 462 if action == '?':
441 463 browsehelp(ui)
442 464 action = None
443 465 elif action == 'p':
444 466 parent = repo.changelog.parents(node)[0]
445 467 for chunk in patch.diff(repo, parent, node):
446 468 ui.write(chunk)
447 469 action = None
448 470 elif action not in ('y', 'n', 'm', 'c', 'q'):
449 471 ui.write(_('no such option\n'))
450 472 action = None
451 473 if action == 'y':
452 474 transplants.append(node)
453 475 elif action == 'm':
454 476 merges.append(node)
455 477 elif action == 'c':
456 478 break
457 479 elif action == 'q':
458 480 transplants = ()
459 481 merges = ()
460 482 break
461 483 displayer.close()
462 484 return (transplants, merges)
463 485
464 486 @command('transplant',
465 487 [('s', 'source', '', _('pull patches from REPO'), _('REPO')),
466 488 ('b', 'branch', [],
467 489 _('pull patches from branch BRANCH'), _('BRANCH')),
468 490 ('a', 'all', None, _('pull all changesets up to BRANCH')),
469 491 ('p', 'prune', [], _('skip over REV'), _('REV')),
470 492 ('m', 'merge', [], _('merge at REV'), _('REV')),
493 ('', 'parent', '',
494 _('parent to choose when transplanting merge'), _('REV')),
471 495 ('e', 'edit', False, _('invoke editor on commit messages')),
472 496 ('', 'log', None, _('append transplant info to log message')),
473 497 ('c', 'continue', None, _('continue last transplant session '
474 498 'after repair')),
475 499 ('', 'filter', '',
476 500 _('filter changesets through command'), _('CMD'))],
477 501 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
478 502 '[-m REV] [REV]...'))
479 503 def transplant(ui, repo, *revs, **opts):
480 504 '''transplant changesets from another branch
481 505
482 506 Selected changesets will be applied on top of the current working
483 507 directory with the log of the original changeset. The changesets
484 508 are copied and will thus appear twice in the history. Use the
485 509 rebase extension instead if you want to move a whole branch of
486 510 unpublished changesets.
487 511
488 512 If --log is specified, log messages will have a comment appended
489 513 of the form::
490 514
491 515 (transplanted from CHANGESETHASH)
492 516
493 517 You can rewrite the changelog message with the --filter option.
494 518 Its argument will be invoked with the current changelog message as
495 519 $1 and the patch as $2.
496 520
497 521 If --source/-s is specified, selects changesets from the named
498 522 repository. If --branch/-b is specified, selects changesets from
499 523 the branch holding the named revision, up to that revision. If
500 524 --all/-a is specified, all changesets on the branch will be
501 525 transplanted, otherwise you will be prompted to select the
502 526 changesets you want.
503 527
504 528 :hg:`transplant --branch REVISION --all` will transplant the
505 529 selected branch (up to the named revision) onto your current
506 530 working directory.
507 531
508 532 You can optionally mark selected transplanted changesets as merge
509 533 changesets. You will not be prompted to transplant any ancestors
510 534 of a merged transplant, and you can merge descendants of them
511 535 normally instead of transplanting them.
512 536
537 Merge changesets may be transplanted directly by specifying the
538 proper parent changeset by calling :hg: `transplant --parent`.
539
513 540 If no merges or revisions are provided, :hg:`transplant` will
514 541 start an interactive changeset browser.
515 542
516 543 If a changeset application fails, you can fix the merge by hand
517 544 and then resume where you left off by calling :hg:`transplant
518 545 --continue/-c`.
519 546 '''
520 547 def incwalk(repo, csets, match=util.always):
521 548 for node in csets:
522 549 if match(node):
523 550 yield node
524 551
525 552 def transplantwalk(repo, root, branches, match=util.always):
526 553 if not branches:
527 554 branches = repo.heads()
528 555 ancestors = []
529 556 for branch in branches:
530 557 ancestors.append(repo.changelog.ancestor(root, branch))
531 558 for node in repo.changelog.nodesbetween(ancestors, branches)[0]:
532 559 if match(node):
533 560 yield node
534 561
535 562 def checkopts(opts, revs):
536 563 if opts.get('continue'):
537 564 if opts.get('branch') or opts.get('all') or opts.get('merge'):
538 565 raise util.Abort(_('--continue is incompatible with '
539 566 'branch, all or merge'))
540 567 return
541 568 if not (opts.get('source') or revs or
542 569 opts.get('merge') or opts.get('branch')):
543 570 raise util.Abort(_('no source URL, branch tag or revision '
544 571 'list provided'))
545 572 if opts.get('all'):
546 573 if not opts.get('branch'):
547 574 raise util.Abort(_('--all requires a branch revision'))
548 575 if revs:
549 576 raise util.Abort(_('--all is incompatible with a '
550 577 'revision list'))
551 578
552 579 checkopts(opts, revs)
553 580
554 581 if not opts.get('log'):
555 582 opts['log'] = ui.config('transplant', 'log')
556 583 if not opts.get('filter'):
557 584 opts['filter'] = ui.config('transplant', 'filter')
558 585
559 586 tp = transplanter(ui, repo)
560 587 if opts.get('edit'):
561 588 tp.editor = cmdutil.commitforceeditor
562 589
563 590 p1, p2 = repo.dirstate.parents()
564 591 if len(repo) > 0 and p1 == revlog.nullid:
565 592 raise util.Abort(_('no revision checked out'))
566 593 if not opts.get('continue'):
567 594 if p2 != revlog.nullid:
568 595 raise util.Abort(_('outstanding uncommitted merges'))
569 596 m, a, r, d = repo.status()[:4]
570 597 if m or a or r or d:
571 598 raise util.Abort(_('outstanding local changes'))
572 599
573 600 sourcerepo = opts.get('source')
574 601 if sourcerepo:
575 602 source = hg.peer(ui, opts, ui.expandpath(sourcerepo))
576 603 branches = map(source.lookup, opts.get('branch', ()))
577 604 source, csets, cleanupfn = bundlerepo.getremotechanges(ui, repo, source,
578 605 onlyheads=branches, force=True)
579 606 else:
580 607 source = repo
581 608 branches = map(source.lookup, opts.get('branch', ()))
582 609 cleanupfn = None
583 610
584 611 try:
585 612 if opts.get('continue'):
586 613 tp.resume(repo, source, opts)
587 614 return
588 615
589 616 tf = tp.transplantfilter(repo, source, p1)
590 617 if opts.get('prune'):
591 618 prune = [source.lookup(r)
592 619 for r in scmutil.revrange(source, opts.get('prune'))]
593 620 matchfn = lambda x: tf(x) and x not in prune
594 621 else:
595 622 matchfn = tf
596 623 merges = map(source.lookup, opts.get('merge', ()))
597 624 revmap = {}
598 625 if revs:
599 626 for r in scmutil.revrange(source, revs):
600 627 revmap[int(r)] = source.lookup(r)
601 628 elif opts.get('all') or not merges:
602 629 if source != repo:
603 630 alltransplants = incwalk(source, csets, match=matchfn)
604 631 else:
605 632 alltransplants = transplantwalk(source, p1, branches,
606 633 match=matchfn)
607 634 if opts.get('all'):
608 635 revs = alltransplants
609 636 else:
610 637 revs, newmerges = browserevs(ui, source, alltransplants, opts)
611 638 merges.extend(newmerges)
612 639 for r in revs:
613 640 revmap[source.changelog.rev(r)] = r
614 641 for r in merges:
615 642 revmap[source.changelog.rev(r)] = r
616 643
617 644 tp.apply(repo, source, revmap, merges, opts)
618 645 finally:
619 646 if cleanupfn:
620 647 cleanupfn()
621 648
622 649 def revsettransplanted(repo, subset, x):
623 650 """``transplanted([set])``
624 651 Transplanted changesets in set, or all transplanted changesets.
625 652 """
626 653 if x:
627 654 s = revset.getset(repo, subset, x)
628 655 else:
629 656 s = subset
630 657 return [r for r in s if repo[r].extra().get('transplant_source')]
631 658
632 659 def kwtransplanted(repo, ctx, **args):
633 660 """:transplanted: String. The node identifier of the transplanted
634 661 changeset if any."""
635 662 n = ctx.extra().get('transplant_source')
636 663 return n and revlog.hex(n) or ''
637 664
638 665 def extsetup(ui):
639 666 revset.symbols['transplanted'] = revsettransplanted
640 667 templatekw.keywords['transplanted'] = kwtransplanted
641 668
642 669 # tell hggettext to extract docstrings from these functions:
643 670 i18nfunctions = [revsettransplanted, kwtransplanted]
@@ -1,426 +1,480
1 1 $ "$TESTDIR/hghave" serve || exit 80
2 2
3 3 $ cat <<EOF >> $HGRCPATH
4 4 > [extensions]
5 5 > transplant=
6 6 > EOF
7 7
8 8 $ hg init t
9 9 $ cd t
10 10 $ echo r1 > r1
11 11 $ hg ci -Amr1 -d'0 0'
12 12 adding r1
13 13 $ echo r2 > r2
14 14 $ hg ci -Amr2 -d'1 0'
15 15 adding r2
16 16 $ hg up 0
17 17 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
18 18
19 19 $ echo b1 > b1
20 20 $ hg ci -Amb1 -d '0 0'
21 21 adding b1
22 22 created new head
23 23 $ echo b2 > b2
24 24 $ hg ci -Amb2 -d '1 0'
25 25 adding b2
26 26 $ echo b3 > b3
27 27 $ hg ci -Amb3 -d '2 0'
28 28 adding b3
29 29
30 30 $ hg log --template '{rev} {parents} {desc}\n'
31 31 4 b3
32 32 3 b2
33 33 2 0:17ab29e464c6 b1
34 34 1 r2
35 35 0 r1
36 36
37 37 $ hg clone . ../rebase
38 38 updating to branch default
39 39 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
40 40 $ cd ../rebase
41 41
42 42 $ hg up -C 1
43 43 1 files updated, 0 files merged, 3 files removed, 0 files unresolved
44 44
45 45 rebase b onto r1
46 46
47 47 $ hg transplant -a -b tip
48 48 applying 37a1297eb21b
49 49 37a1297eb21b transplanted to e234d668f844
50 50 applying 722f4667af76
51 51 722f4667af76 transplanted to 539f377d78df
52 52 applying a53251cdf717
53 53 a53251cdf717 transplanted to ffd6818a3975
54 54 $ hg log --template '{rev} {parents} {desc}\n'
55 55 7 b3
56 56 6 b2
57 57 5 1:d11e3596cc1a b1
58 58 4 b3
59 59 3 b2
60 60 2 0:17ab29e464c6 b1
61 61 1 r2
62 62 0 r1
63 63
64 64 test transplanted revset
65 65
66 66 $ hg log -r 'transplanted()' --template '{rev} {parents} {desc}\n'
67 67 5 1:d11e3596cc1a b1
68 68 6 b2
69 69 7 b3
70 70 $ hg help revsets | grep transplanted
71 71 "transplanted([set])"
72 72 Transplanted changesets in set, or all transplanted changesets.
73 73
74 74 test tranplanted keyword
75 75
76 76 $ hg log --template '{rev} {transplanted}\n'
77 77 7 a53251cdf717679d1907b289f991534be05c997a
78 78 6 722f4667af767100cb15b6a79324bf8abbfe1ef4
79 79 5 37a1297eb21b3ef5c5d2ffac22121a0988ed9f21
80 80 4
81 81 3
82 82 2
83 83 1
84 84 0
85 85
86 86 rollback the transplant
87 87 $ hg rollback
88 88 repository tip rolled back to revision 4 (undo transplant)
89 89 working directory now based on revision 1
90 90 $ hg tip -q
91 91 4:a53251cdf717
92 92 $ hg parents -q
93 93 1:d11e3596cc1a
94 94 $ hg status
95 95 ? b1
96 96 ? b2
97 97 ? b3
98 98
99 99 $ hg clone ../t ../prune
100 100 updating to branch default
101 101 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
102 102 $ cd ../prune
103 103
104 104 $ hg up -C 1
105 105 1 files updated, 0 files merged, 3 files removed, 0 files unresolved
106 106
107 107 rebase b onto r1, skipping b2
108 108
109 109 $ hg transplant -a -b tip -p 3
110 110 applying 37a1297eb21b
111 111 37a1297eb21b transplanted to e234d668f844
112 112 applying a53251cdf717
113 113 a53251cdf717 transplanted to 7275fda4d04f
114 114 $ hg log --template '{rev} {parents} {desc}\n'
115 115 6 b3
116 116 5 1:d11e3596cc1a b1
117 117 4 b3
118 118 3 b2
119 119 2 0:17ab29e464c6 b1
120 120 1 r2
121 121 0 r1
122 122
123 123
124 124 remote transplant
125 125
126 126 $ hg clone -r 1 ../t ../remote
127 127 adding changesets
128 128 adding manifests
129 129 adding file changes
130 130 added 2 changesets with 2 changes to 2 files
131 131 updating to branch default
132 132 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
133 133 $ cd ../remote
134 134 $ hg transplant --log -s ../t 2 4
135 135 searching for changes
136 136 applying 37a1297eb21b
137 137 37a1297eb21b transplanted to c19cf0ccb069
138 138 applying a53251cdf717
139 139 a53251cdf717 transplanted to f7fe5bf98525
140 140 $ hg log --template '{rev} {parents} {desc}\n'
141 141 3 b3
142 142 (transplanted from a53251cdf717679d1907b289f991534be05c997a)
143 143 2 b1
144 144 (transplanted from 37a1297eb21b3ef5c5d2ffac22121a0988ed9f21)
145 145 1 r2
146 146 0 r1
147 147
148 148 skip previous transplants
149 149
150 150 $ hg transplant -s ../t -a -b 4
151 151 searching for changes
152 152 applying 722f4667af76
153 153 722f4667af76 transplanted to 47156cd86c0b
154 154 $ hg log --template '{rev} {parents} {desc}\n'
155 155 4 b2
156 156 3 b3
157 157 (transplanted from a53251cdf717679d1907b289f991534be05c997a)
158 158 2 b1
159 159 (transplanted from 37a1297eb21b3ef5c5d2ffac22121a0988ed9f21)
160 160 1 r2
161 161 0 r1
162 162
163 163 skip local changes transplanted to the source
164 164
165 165 $ echo b4 > b4
166 166 $ hg ci -Amb4 -d '3 0'
167 167 adding b4
168 168 $ hg clone ../t ../pullback
169 169 updating to branch default
170 170 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
171 171 $ cd ../pullback
172 172 $ hg transplant -s ../remote -a -b tip
173 173 searching for changes
174 174 applying 4333daefcb15
175 175 4333daefcb15 transplanted to 5f42c04e07cc
176 176
177 177
178 178 remote transplant with pull
179 179
180 180 $ hg -R ../t serve -p $HGPORT -d --pid-file=../t.pid
181 181 $ cat ../t.pid >> $DAEMON_PIDS
182 182
183 183 $ hg clone -r 0 ../t ../rp
184 184 adding changesets
185 185 adding manifests
186 186 adding file changes
187 187 added 1 changesets with 1 changes to 1 files
188 188 updating to branch default
189 189 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
190 190 $ cd ../rp
191 191 $ hg transplant -s http://localhost:$HGPORT/ 2 4
192 192 searching for changes
193 193 searching for changes
194 194 adding changesets
195 195 adding manifests
196 196 adding file changes
197 197 added 1 changesets with 1 changes to 1 files
198 198 applying a53251cdf717
199 199 a53251cdf717 transplanted to 8d9279348abb
200 200 $ hg log --template '{rev} {parents} {desc}\n'
201 201 2 b3
202 202 1 b1
203 203 0 r1
204 204
205 205 transplant --continue
206 206
207 207 $ hg init ../tc
208 208 $ cd ../tc
209 209 $ cat <<EOF > foo
210 210 > foo
211 211 > bar
212 212 > baz
213 213 > EOF
214 214 $ echo toremove > toremove
215 215 $ hg ci -Amfoo
216 216 adding foo
217 217 adding toremove
218 218 $ cat <<EOF > foo
219 219 > foo2
220 220 > bar2
221 221 > baz2
222 222 > EOF
223 223 $ rm toremove
224 224 $ echo added > added
225 225 $ hg ci -Amfoo2
226 226 adding added
227 227 removing toremove
228 228 $ echo bar > bar
229 229 $ hg ci -Ambar
230 230 adding bar
231 231 $ echo bar2 >> bar
232 232 $ hg ci -mbar2
233 233 $ hg up 0
234 234 2 files updated, 0 files merged, 2 files removed, 0 files unresolved
235 235 $ echo foobar > foo
236 236 $ hg ci -mfoobar
237 237 created new head
238 238 $ hg transplant 1:3
239 239 applying a1e30dd1b8e7
240 240 patching file foo
241 241 Hunk #1 FAILED at 0
242 242 1 out of 1 hunks FAILED -- saving rejects to file foo.rej
243 243 patch failed to apply
244 244 abort: fix up the merge and run hg transplant --continue
245 245 [255]
246 246
247 247 transplant -c shouldn't use an old changeset
248 248
249 249 $ hg up -C
250 250 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
251 251 $ rm added
252 252 $ hg transplant 1
253 253 applying a1e30dd1b8e7
254 254 patching file foo
255 255 Hunk #1 FAILED at 0
256 256 1 out of 1 hunks FAILED -- saving rejects to file foo.rej
257 257 patch failed to apply
258 258 abort: fix up the merge and run hg transplant --continue
259 259 [255]
260 260 $ hg transplant --continue
261 261 a1e30dd1b8e7 transplanted as f1563cf27039
262 262 $ hg transplant 1:3
263 263 skipping already applied revision 1:a1e30dd1b8e7
264 264 applying 1739ac5f6139
265 265 1739ac5f6139 transplanted to d649c221319f
266 266 applying 0282d5fbbe02
267 267 0282d5fbbe02 transplanted to 77418277ccb3
268 268 $ hg locate
269 269 added
270 270 bar
271 271 foo
272 272 $ cd ..
273 273
274 274 Issue1111: Test transplant --merge
275 275
276 276 $ hg init t1111
277 277 $ cd t1111
278 278 $ echo a > a
279 279 $ hg ci -Am adda
280 280 adding a
281 281 $ echo b >> a
282 282 $ hg ci -m appendb
283 283 $ echo c >> a
284 284 $ hg ci -m appendc
285 285 $ hg up -C 0
286 286 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
287 287 $ echo d >> a
288 288 $ hg ci -m appendd
289 289 created new head
290 290
291 291 tranplant
292 292
293 293 $ hg transplant -m 1
294 294 applying 42dc4432fd35
295 295 1:42dc4432fd35 merged at a9f4acbac129
296 296 $ cd ..
297 297
298 298 test transplant into empty repository
299 299
300 300 $ hg init empty
301 301 $ cd empty
302 302 $ hg transplant -s ../t -b tip -a
303 303 adding changesets
304 304 adding manifests
305 305 adding file changes
306 306 added 4 changesets with 4 changes to 4 files
307 307 $ cd ..
308 308
309 309
310 310 test filter
311 311
312 312 $ hg init filter
313 313 $ cd filter
314 314 $ cat <<'EOF' >test-filter
315 315 > #!/bin/sh
316 316 > sed 's/r1/r2/' $1 > $1.new
317 317 > mv $1.new $1
318 318 > EOF
319 319 $ chmod +x test-filter
320 320 $ hg transplant -s ../t -b tip -a --filter ./test-filter
321 321 filtering * (glob)
322 322 applying 17ab29e464c6
323 323 17ab29e464c6 transplanted to e9ffc54ea104
324 324 filtering * (glob)
325 325 applying 37a1297eb21b
326 326 37a1297eb21b transplanted to 348b36d0b6a5
327 327 filtering * (glob)
328 328 applying 722f4667af76
329 329 722f4667af76 transplanted to 0aa6979afb95
330 330 filtering * (glob)
331 331 applying a53251cdf717
332 332 a53251cdf717 transplanted to 14f8512272b5
333 333 $ hg log --template '{rev} {parents} {desc}\n'
334 334 3 b3
335 335 2 b2
336 336 1 b1
337 337 0 r2
338 338 $ cd ..
339 339
340 340
341 341 test filter with failed patch
342 342
343 343 $ cd filter
344 344 $ hg up 0
345 345 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
346 346 $ echo foo > b1
347 347 $ hg ci -Am foo
348 348 adding b1
349 349 adding test-filter
350 350 created new head
351 351 $ hg transplant 1 --filter ./test-filter
352 352 filtering * (glob)
353 353 applying 348b36d0b6a5
354 354 file b1 already exists
355 355 1 out of 1 hunks FAILED -- saving rejects to file b1.rej
356 356 patch failed to apply
357 357 abort: fix up the merge and run hg transplant --continue
358 358 [255]
359 359 $ cd ..
360 360
361 361 test environment passed to filter
362 362
363 363 $ hg init filter-environment
364 364 $ cd filter-environment
365 365 $ cat <<'EOF' >test-filter-environment
366 366 > #!/bin/sh
367 367 > echo "Transplant by $HGUSER" >> $1
368 368 > echo "Transplant from rev $HGREVISION" >> $1
369 369 > EOF
370 370 $ chmod +x test-filter-environment
371 371 $ hg transplant -s ../t --filter ./test-filter-environment 0
372 372 filtering * (glob)
373 373 applying 17ab29e464c6
374 374 17ab29e464c6 transplanted to 5190e68026a0
375 375
376 376 $ hg log --template '{rev} {parents} {desc}\n'
377 377 0 r1
378 378 Transplant by test
379 379 Transplant from rev 17ab29e464c6ca53e329470efe2a9918ac617a6f
380 380 $ cd ..
381 381
382 382 test transplant with filter handles invalid changelog
383 383
384 384 $ hg init filter-invalid-log
385 385 $ cd filter-invalid-log
386 386 $ cat <<'EOF' >test-filter-invalid-log
387 387 > #!/bin/sh
388 388 > echo "" > $1
389 389 > EOF
390 390 $ chmod +x test-filter-invalid-log
391 391 $ hg transplant -s ../t --filter ./test-filter-invalid-log 0
392 392 filtering * (glob)
393 393 abort: filter corrupted changeset (no user or date)
394 394 [255]
395 395
396 396 test with a win32ext like setup (differing EOLs)
397 397
398 398 $ hg init twin1
399 399 $ cd twin1
400 400 $ echo a > a
401 401 $ echo b > b
402 402 $ echo b >> b
403 403 $ hg ci -Am t
404 404 adding a
405 405 adding b
406 406 $ echo a > b
407 407 $ echo b >> b
408 408 $ hg ci -m changeb
409 409 $ cd ..
410 410
411 411 $ hg init twin2
412 412 $ cd twin2
413 413 $ echo '[patch]' >> .hg/hgrc
414 414 $ echo 'eol = crlf' >> .hg/hgrc
415 415 $ python -c "file('b', 'wb').write('b\r\nb\r\n')"
416 416 $ hg ci -Am addb
417 417 adding b
418 418 $ hg transplant -s ../twin1 tip
419 419 searching for changes
420 420 warning: repository is unrelated
421 421 applying 2e849d776c17
422 422 2e849d776c17 transplanted to 8e65bebc063e
423 423 $ cat b
424 424 a\r (esc)
425 425 b\r (esc)
426 426 $ cd ..
427
428 test transplant with merge changeset is skipped
429
430 $ hg init merge1a
431 $ cd merge1a
432 $ echo a > a
433 $ hg ci -Am a
434 adding a
435 $ hg branch b
436 marked working directory as branch b
437 (branches are permanent and global, did you want a bookmark?)
438 $ hg ci -m branchb
439 $ echo b > b
440 $ hg ci -Am b
441 adding b
442 $ hg update default
443 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
444 $ hg merge b
445 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
446 (branch merge, don't forget to commit)
447 $ hg ci -m mergeb
448 $ cd ..
449
450 $ hg init merge1b
451 $ cd merge1b
452 $ hg transplant -s ../merge1a tip
453
454 test transplant with merge changeset accepts --parent
455
456 $ hg init merge2a
457 $ cd merge2a
458 $ echo a > a
459 $ hg ci -Am a
460 adding a
461 $ hg branch b
462 marked working directory as branch b
463 (branches are permanent and global, did you want a bookmark?)
464 $ hg ci -m branchb
465 $ echo b > b
466 $ hg ci -Am b
467 adding b
468 $ hg update default
469 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
470 $ hg merge b
471 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
472 (branch merge, don't forget to commit)
473 $ hg ci -m mergeb
474 $ cd ..
475
476 $ hg init merge2b
477 $ cd merge2b
478 $ hg transplant -s ../merge2a --parent 0 tip
479 applying be9f9b39483f
480 be9f9b39483f transplanted to 9959e51f94d1
General Comments 0
You need to be logged in to leave comments. Login now