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