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