##// END OF EJS Templates
Corrected synopsis for transplant.
Thomas Arendsen Hein -
r3991:da3dc89f default
parent child Browse files
Show More
@@ -1,591 +1,591 b''
1 1 # Patch transplanting extension for Mercurial
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from mercurial.i18n import _
9 9 import os, tempfile
10 10 from mercurial import bundlerepo, cmdutil, commands, hg, merge, patch, revlog
11 11 from mercurial import util
12 12
13 13 '''patch transplanting tool
14 14
15 15 This extension allows you to transplant patches from another branch.
16 16
17 17 Transplanted patches are recorded in .hg/transplant/transplants, as a map
18 18 from a changeset hash to its hash in the source repository.
19 19 '''
20 20
21 21 class transplantentry:
22 22 def __init__(self, lnode, rnode):
23 23 self.lnode = lnode
24 24 self.rnode = rnode
25 25
26 26 class transplants:
27 27 def __init__(self, path=None, transplantfile=None, opener=None):
28 28 self.path = path
29 29 self.transplantfile = transplantfile
30 30 self.opener = opener
31 31
32 32 if not opener:
33 33 self.opener = util.opener(self.path)
34 34 self.transplants = []
35 35 self.dirty = False
36 36 self.read()
37 37
38 38 def read(self):
39 39 abspath = os.path.join(self.path, self.transplantfile)
40 40 if self.transplantfile and os.path.exists(abspath):
41 41 for line in self.opener(self.transplantfile).read().splitlines():
42 42 lnode, rnode = map(revlog.bin, line.split(':'))
43 43 self.transplants.append(transplantentry(lnode, rnode))
44 44
45 45 def write(self):
46 46 if self.dirty and self.transplantfile:
47 47 if not os.path.isdir(self.path):
48 48 os.mkdir(self.path)
49 49 fp = self.opener(self.transplantfile, 'w')
50 50 for c in self.transplants:
51 51 l, r = map(revlog.hex, (c.lnode, c.rnode))
52 52 fp.write(l + ':' + r + '\n')
53 53 fp.close()
54 54 self.dirty = False
55 55
56 56 def get(self, rnode):
57 57 return [t for t in self.transplants if t.rnode == rnode]
58 58
59 59 def set(self, lnode, rnode):
60 60 self.transplants.append(transplantentry(lnode, rnode))
61 61 self.dirty = True
62 62
63 63 def remove(self, transplant):
64 64 del self.transplants[self.transplants.index(transplant)]
65 65 self.dirty = True
66 66
67 67 class transplanter:
68 68 def __init__(self, ui, repo):
69 69 self.ui = ui
70 70 self.path = repo.join('transplant')
71 71 self.opener = util.opener(self.path)
72 72 self.transplants = transplants(self.path, 'transplants', opener=self.opener)
73 73
74 74 def applied(self, repo, node, parent):
75 75 '''returns True if a node is already an ancestor of parent
76 76 or has already been transplanted'''
77 77 if hasnode(repo, node):
78 78 if node in repo.changelog.reachable(parent, stop=node):
79 79 return True
80 80 for t in self.transplants.get(node):
81 81 # it might have been stripped
82 82 if not hasnode(repo, t.lnode):
83 83 self.transplants.remove(t)
84 84 return False
85 85 if t.lnode in repo.changelog.reachable(parent, stop=t.lnode):
86 86 return True
87 87 return False
88 88
89 89 def apply(self, repo, source, revmap, merges, opts={}):
90 90 '''apply the revisions in revmap one by one in revision order'''
91 91 revs = revmap.keys()
92 92 revs.sort()
93 93
94 94 p1, p2 = repo.dirstate.parents()
95 95 pulls = []
96 96 diffopts = patch.diffopts(self.ui, opts)
97 97 diffopts.git = True
98 98
99 99 lock = repo.lock()
100 100 wlock = repo.wlock()
101 101 try:
102 102 for rev in revs:
103 103 node = revmap[rev]
104 104 revstr = '%s:%s' % (rev, revlog.short(node))
105 105
106 106 if self.applied(repo, node, p1):
107 107 self.ui.warn(_('skipping already applied revision %s\n') %
108 108 revstr)
109 109 continue
110 110
111 111 parents = source.changelog.parents(node)
112 112 if not opts.get('filter'):
113 113 # If the changeset parent is the same as the wdir's parent,
114 114 # just pull it.
115 115 if parents[0] == p1:
116 116 pulls.append(node)
117 117 p1 = node
118 118 continue
119 119 if pulls:
120 120 if source != repo:
121 121 repo.pull(source, heads=pulls, lock=lock)
122 122 merge.update(repo, pulls[-1], wlock=wlock)
123 123 p1, p2 = repo.dirstate.parents()
124 124 pulls = []
125 125
126 126 domerge = False
127 127 if node in merges:
128 128 # pulling all the merge revs at once would mean we couldn't
129 129 # transplant after the latest even if transplants before them
130 130 # fail.
131 131 domerge = True
132 132 if not hasnode(repo, node):
133 133 repo.pull(source, heads=[node], lock=lock)
134 134
135 135 if parents[1] != revlog.nullid:
136 136 self.ui.note(_('skipping merge changeset %s:%s\n')
137 137 % (rev, revlog.short(node)))
138 138 patchfile = None
139 139 else:
140 140 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
141 141 fp = os.fdopen(fd, 'w')
142 142 patch.diff(source, parents[0], node, fp=fp, opts=diffopts)
143 143 fp.close()
144 144
145 145 del revmap[rev]
146 146 if patchfile or domerge:
147 147 try:
148 148 n = self.applyone(repo, node, source.changelog.read(node),
149 149 patchfile, merge=domerge,
150 150 log=opts.get('log'),
151 151 filter=opts.get('filter'),
152 152 lock=lock, wlock=wlock)
153 153 if domerge:
154 154 self.ui.status(_('%s merged at %s\n') % (revstr,
155 155 revlog.short(n)))
156 156 else:
157 157 self.ui.status(_('%s transplanted to %s\n') % (revlog.short(node),
158 158 revlog.short(n)))
159 159 finally:
160 160 if patchfile:
161 161 os.unlink(patchfile)
162 162 if pulls:
163 163 repo.pull(source, heads=pulls, lock=lock)
164 164 merge.update(repo, pulls[-1], wlock=wlock)
165 165 finally:
166 166 self.saveseries(revmap, merges)
167 167 self.transplants.write()
168 168
169 169 def filter(self, filter, changelog, patchfile):
170 170 '''arbitrarily rewrite changeset before applying it'''
171 171
172 172 self.ui.status('filtering %s\n' % patchfile)
173 173 user, date, msg = (changelog[1], changelog[2], changelog[4])
174 174
175 175 fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-')
176 176 fp = os.fdopen(fd, 'w')
177 177 fp.write("# HG changeset patch\n")
178 178 fp.write("# User %s\n" % user)
179 179 fp.write("# Date %d %d\n" % date)
180 180 fp.write(changelog[4])
181 181 fp.close()
182 182
183 183 try:
184 184 util.system('%s %s %s' % (filter, util.shellquote(headerfile),
185 185 util.shellquote(patchfile)),
186 186 environ={'HGUSER': changelog[1]},
187 187 onerr=util.Abort, errprefix=_('filter failed'))
188 188 user, date, msg = self.parselog(file(headerfile))[1:4]
189 189 finally:
190 190 os.unlink(headerfile)
191 191
192 192 return (user, date, msg)
193 193
194 194 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
195 195 filter=None, lock=None, wlock=None):
196 196 '''apply the patch in patchfile to the repository as a transplant'''
197 197 (manifest, user, (time, timezone), files, message) = cl[:5]
198 198 date = "%d %d" % (time, timezone)
199 199 extra = {'transplant_source': node}
200 200 if filter:
201 201 (user, date, message) = self.filter(filter, cl, patchfile)
202 202
203 203 if log:
204 204 message += '\n(transplanted from %s)' % revlog.hex(node)
205 205
206 206 self.ui.status(_('applying %s\n') % revlog.short(node))
207 207 self.ui.note('%s %s\n%s\n' % (user, date, message))
208 208
209 209 if not patchfile and not merge:
210 210 raise util.Abort(_('can only omit patchfile if merging'))
211 211 if patchfile:
212 212 try:
213 213 files = {}
214 214 try:
215 215 fuzz = patch.patch(patchfile, self.ui, cwd=repo.root,
216 216 files=files)
217 217 if not files:
218 218 self.ui.warn(_('%s: empty changeset') % revlog.hex(node))
219 219 return
220 220 finally:
221 221 files = patch.updatedir(self.ui, repo, files, wlock=wlock)
222 222 except Exception, inst:
223 223 if filter:
224 224 os.unlink(patchfile)
225 225 seriespath = os.path.join(self.path, 'series')
226 226 if os.path.exists(seriespath):
227 227 os.unlink(seriespath)
228 228 p1 = repo.dirstate.parents()[0]
229 229 p2 = node
230 230 self.log(user, date, message, p1, p2, merge=merge)
231 231 self.ui.write(str(inst) + '\n')
232 232 raise util.Abort(_('Fix up the merge and run hg transplant --continue'))
233 233 else:
234 234 files = None
235 235 if merge:
236 236 p1, p2 = repo.dirstate.parents()
237 237 repo.dirstate.setparents(p1, node)
238 238
239 239 n = repo.commit(files, message, user, date, lock=lock, wlock=wlock,
240 240 extra=extra)
241 241 if not merge:
242 242 self.transplants.set(n, node)
243 243
244 244 return n
245 245
246 246 def resume(self, repo, source, opts=None):
247 247 '''recover last transaction and apply remaining changesets'''
248 248 if os.path.exists(os.path.join(self.path, 'journal')):
249 249 n, node = self.recover(repo)
250 250 self.ui.status(_('%s transplanted as %s\n') % (revlog.short(node),
251 251 revlog.short(n)))
252 252 seriespath = os.path.join(self.path, 'series')
253 253 if not os.path.exists(seriespath):
254 254 self.transplants.write()
255 255 return
256 256 nodes, merges = self.readseries()
257 257 revmap = {}
258 258 for n in nodes:
259 259 revmap[source.changelog.rev(n)] = n
260 260 os.unlink(seriespath)
261 261
262 262 self.apply(repo, source, revmap, merges, opts)
263 263
264 264 def recover(self, repo):
265 265 '''commit working directory using journal metadata'''
266 266 node, user, date, message, parents = self.readlog()
267 267 merge = len(parents) == 2
268 268
269 269 if not user or not date or not message or not parents[0]:
270 270 raise util.Abort(_('transplant log file is corrupt'))
271 271
272 272 extra = {'transplant_source': node}
273 273 wlock = repo.wlock()
274 274 p1, p2 = repo.dirstate.parents()
275 275 if p1 != parents[0]:
276 276 raise util.Abort(_('working dir not at transplant parent %s') %
277 277 revlog.hex(parents[0]))
278 278 if merge:
279 279 repo.dirstate.setparents(p1, parents[1])
280 280 n = repo.commit(None, message, user, date, wlock=wlock, extra=extra)
281 281 if not n:
282 282 raise util.Abort(_('commit failed'))
283 283 if not merge:
284 284 self.transplants.set(n, node)
285 285 self.unlog()
286 286
287 287 return n, node
288 288
289 289 def readseries(self):
290 290 nodes = []
291 291 merges = []
292 292 cur = nodes
293 293 for line in self.opener('series').read().splitlines():
294 294 if line.startswith('# Merges'):
295 295 cur = merges
296 296 continue
297 297 cur.append(revlog.bin(line))
298 298
299 299 return (nodes, merges)
300 300
301 301 def saveseries(self, revmap, merges):
302 302 if not revmap:
303 303 return
304 304
305 305 if not os.path.isdir(self.path):
306 306 os.mkdir(self.path)
307 307 series = self.opener('series', 'w')
308 308 revs = revmap.keys()
309 309 revs.sort()
310 310 for rev in revs:
311 311 series.write(revlog.hex(revmap[rev]) + '\n')
312 312 if merges:
313 313 series.write('# Merges\n')
314 314 for m in merges:
315 315 series.write(revlog.hex(m) + '\n')
316 316 series.close()
317 317
318 318 def parselog(self, fp):
319 319 parents = []
320 320 message = []
321 321 node = revlog.nullid
322 322 inmsg = False
323 323 for line in fp.read().splitlines():
324 324 if inmsg:
325 325 message.append(line)
326 326 elif line.startswith('# User '):
327 327 user = line[7:]
328 328 elif line.startswith('# Date '):
329 329 date = line[7:]
330 330 elif line.startswith('# Node ID '):
331 331 node = revlog.bin(line[10:])
332 332 elif line.startswith('# Parent '):
333 333 parents.append(revlog.bin(line[9:]))
334 334 elif not line.startswith('#'):
335 335 inmsg = True
336 336 message.append(line)
337 337 return (node, user, date, '\n'.join(message), parents)
338 338
339 339 def log(self, user, date, message, p1, p2, merge=False):
340 340 '''journal changelog metadata for later recover'''
341 341
342 342 if not os.path.isdir(self.path):
343 343 os.mkdir(self.path)
344 344 fp = self.opener('journal', 'w')
345 345 fp.write('# User %s\n' % user)
346 346 fp.write('# Date %s\n' % date)
347 347 fp.write('# Node ID %s\n' % revlog.hex(p2))
348 348 fp.write('# Parent ' + revlog.hex(p1) + '\n')
349 349 if merge:
350 350 fp.write('# Parent ' + revlog.hex(p2) + '\n')
351 351 fp.write(message.rstrip() + '\n')
352 352 fp.close()
353 353
354 354 def readlog(self):
355 355 return self.parselog(self.opener('journal'))
356 356
357 357 def unlog(self):
358 358 '''remove changelog journal'''
359 359 absdst = os.path.join(self.path, 'journal')
360 360 if os.path.exists(absdst):
361 361 os.unlink(absdst)
362 362
363 363 def transplantfilter(self, repo, source, root):
364 364 def matchfn(node):
365 365 if self.applied(repo, node, root):
366 366 return False
367 367 if source.changelog.parents(node)[1] != revlog.nullid:
368 368 return False
369 369 extra = source.changelog.read(node)[5]
370 370 cnode = extra.get('transplant_source')
371 371 if cnode and self.applied(repo, cnode, root):
372 372 return False
373 373 return True
374 374
375 375 return matchfn
376 376
377 377 def hasnode(repo, node):
378 378 try:
379 379 return repo.changelog.rev(node) != None
380 380 except revlog.RevlogError:
381 381 return False
382 382
383 383 def browserevs(ui, repo, nodes, opts):
384 384 '''interactively transplant changesets'''
385 385 def browsehelp(ui):
386 386 ui.write('y: transplant this changeset\n'
387 387 'n: skip this changeset\n'
388 388 'm: merge at this changeset\n'
389 389 'p: show patch\n'
390 390 'c: commit selected changesets\n'
391 391 'q: cancel transplant\n'
392 392 '?: show this help\n')
393 393
394 394 displayer = cmdutil.show_changeset(ui, repo, opts)
395 395 transplants = []
396 396 merges = []
397 397 for node in nodes:
398 398 displayer.show(changenode=node)
399 399 action = None
400 400 while not action:
401 401 action = ui.prompt(_('apply changeset? [ynmpcq?]:'))
402 402 if action == '?':
403 403 browsehelp(ui)
404 404 action = None
405 405 elif action == 'p':
406 406 parent = repo.changelog.parents(node)[0]
407 407 patch.diff(repo, parent, node)
408 408 action = None
409 409 elif action not in ('y', 'n', 'm', 'c', 'q'):
410 410 ui.write('no such option\n')
411 411 action = None
412 412 if action == 'y':
413 413 transplants.append(node)
414 414 elif action == 'm':
415 415 merges.append(node)
416 416 elif action == 'c':
417 417 break
418 418 elif action == 'q':
419 419 transplants = ()
420 420 merges = ()
421 421 break
422 422 return (transplants, merges)
423 423
424 424 def transplant(ui, repo, *revs, **opts):
425 425 '''transplant changesets from another branch
426 426
427 427 Selected changesets will be applied on top of the current working
428 428 directory with the log of the original changeset. If --log is
429 429 specified, log messages will have a comment appended of the form:
430 430
431 431 (transplanted from CHANGESETHASH)
432 432
433 433 You can rewrite the changelog message with the --filter option.
434 434 Its argument will be invoked with the current changelog message
435 435 as $1 and the patch as $2.
436 436
437 437 If --source is specified, selects changesets from the named
438 438 repository. If --branch is specified, selects changesets from the
439 439 branch holding the named revision, up to that revision. If --all
440 440 is specified, all changesets on the branch will be transplanted,
441 441 otherwise you will be prompted to select the changesets you want.
442 442
443 443 hg transplant --branch REVISION --all will rebase the selected branch
444 444 (up to the named revision) onto your current working directory.
445 445
446 446 You can optionally mark selected transplanted changesets as
447 447 merge changesets. You will not be prompted to transplant any
448 448 ancestors of a merged transplant, and you can merge descendants
449 449 of them normally instead of transplanting them.
450 450
451 451 If no merges or revisions are provided, hg transplant will start
452 452 an interactive changeset browser.
453 453
454 454 If a changeset application fails, you can fix the merge by hand and
455 455 then resume where you left off by calling hg transplant --continue.
456 456 '''
457 457 def getoneitem(opts, item, errmsg):
458 458 val = opts.get(item)
459 459 if val:
460 460 if len(val) > 1:
461 461 raise util.Abort(errmsg)
462 462 else:
463 463 return val[0]
464 464
465 465 def getremotechanges(repo, url):
466 466 sourcerepo = ui.expandpath(url)
467 467 source = hg.repository(ui, sourcerepo)
468 468 incoming = repo.findincoming(source, force=True)
469 469 if not incoming:
470 470 return (source, None, None)
471 471
472 472 bundle = None
473 473 if not source.local():
474 474 cg = source.changegroup(incoming, 'incoming')
475 475 bundle = commands.write_bundle(cg, compress=False)
476 476 source = bundlerepo.bundlerepository(ui, repo.root, bundle)
477 477
478 478 return (source, incoming, bundle)
479 479
480 480 def incwalk(repo, incoming, branches, match=util.always):
481 481 if not branches:
482 482 branches=None
483 483 for node in repo.changelog.nodesbetween(incoming, branches)[0]:
484 484 if match(node):
485 485 yield node
486 486
487 487 def transplantwalk(repo, root, branches, match=util.always):
488 488 if not branches:
489 489 branches = repo.heads()
490 490 ancestors = []
491 491 for branch in branches:
492 492 ancestors.append(repo.changelog.ancestor(root, branch))
493 493 for node in repo.changelog.nodesbetween(ancestors, branches)[0]:
494 494 if match(node):
495 495 yield node
496 496
497 497 def checkopts(opts, revs):
498 498 if opts.get('continue'):
499 499 if filter(lambda opt: opts.get(opt), ('branch', 'all', 'merge')):
500 500 raise util.Abort(_('--continue is incompatible with branch, all or merge'))
501 501 return
502 502 if not (opts.get('source') or revs or
503 503 opts.get('merge') or opts.get('branch')):
504 504 raise util.Abort(_('no source URL, branch tag or revision list provided'))
505 505 if opts.get('all'):
506 506 if not opts.get('branch'):
507 507 raise util.Abort(_('--all requires a branch revision'))
508 508 if revs:
509 509 raise util.Abort(_('--all is incompatible with a revision list'))
510 510
511 511 checkopts(opts, revs)
512 512
513 513 if not opts.get('log'):
514 514 opts['log'] = ui.config('transplant', 'log')
515 515 if not opts.get('filter'):
516 516 opts['filter'] = ui.config('transplant', 'filter')
517 517
518 518 tp = transplanter(ui, repo)
519 519
520 520 p1, p2 = repo.dirstate.parents()
521 521 if p1 == revlog.nullid:
522 522 raise util.Abort(_('no revision checked out'))
523 523 if not opts.get('continue'):
524 524 if p2 != revlog.nullid:
525 525 raise util.Abort(_('outstanding uncommitted merges'))
526 526 m, a, r, d = repo.status()[:4]
527 527 if m or a or r or d:
528 528 raise util.Abort(_('outstanding local changes'))
529 529
530 530 bundle = None
531 531 source = opts.get('source')
532 532 if source:
533 533 (source, incoming, bundle) = getremotechanges(repo, source)
534 534 else:
535 535 source = repo
536 536
537 537 try:
538 538 if opts.get('continue'):
539 539 tp.resume(repo, source, opts)
540 540 return
541 541
542 542 tf=tp.transplantfilter(repo, source, p1)
543 543 if opts.get('prune'):
544 544 prune = [source.lookup(r)
545 545 for r in cmdutil.revrange(source, opts.get('prune'))]
546 546 matchfn = lambda x: tf(x) and x not in prune
547 547 else:
548 548 matchfn = tf
549 549 branches = map(source.lookup, opts.get('branch', ()))
550 550 merges = map(source.lookup, opts.get('merge', ()))
551 551 revmap = {}
552 552 if revs:
553 553 for r in cmdutil.revrange(source, revs):
554 554 revmap[int(r)] = source.lookup(r)
555 555 elif opts.get('all') or not merges:
556 556 if source != repo:
557 557 alltransplants = incwalk(source, incoming, branches, match=matchfn)
558 558 else:
559 559 alltransplants = transplantwalk(source, p1, branches, match=matchfn)
560 560 if opts.get('all'):
561 561 revs = alltransplants
562 562 else:
563 563 revs, newmerges = browserevs(ui, source, alltransplants, opts)
564 564 merges.extend(newmerges)
565 565 for r in revs:
566 566 revmap[source.changelog.rev(r)] = r
567 567 for r in merges:
568 568 revmap[source.changelog.rev(r)] = r
569 569
570 570 revs = revmap.keys()
571 571 revs.sort()
572 572 pulls = []
573 573
574 574 tp.apply(repo, source, revmap, merges, opts)
575 575 finally:
576 576 if bundle:
577 577 os.unlink(bundle)
578 578
579 579 cmdtable = {
580 580 "transplant":
581 581 (transplant,
582 582 [('s', 'source', '', _('pull patches from REPOSITORY')),
583 583 ('b', 'branch', [], _('pull patches from branch BRANCH')),
584 584 ('a', 'all', None, _('pull all changesets up to BRANCH')),
585 585 ('p', 'prune', [], _('skip over REV')),
586 586 ('m', 'merge', [], _('merge at REV')),
587 587 ('', 'log', None, _('append transplant info to log message')),
588 588 ('c', 'continue', None, _('continue last transplant session after repair')),
589 589 ('', 'filter', '', _('filter changesets through FILTER'))],
590 _('hg transplant [-s REPOSITORY] [-b BRANCH] [-p REV] [-m REV] [-n] REV...'))
590 _('hg transplant [-s REPOSITORY] [-b BRANCH [-a]] [-p REV] [-m REV] [REV]...'))
591 591 }
General Comments 0
You need to be logged in to leave comments. Login now