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