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