##// END OF EJS Templates
i18n: mark strings for translation in transplant extension
Martin Geisler -
r6966:057ced2b default
parent child Browse files
Show More
@@ -1,589 +1,589 b''
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 from mercurial.i18n import _
9 9 import os, tempfile
10 10 from mercurial import bundlerepo, changegroup, cmdutil, hg, merge
11 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 = util.sort(revmap)
92 92 p1, p2 = repo.dirstate.parents()
93 93 pulls = []
94 94 diffopts = patch.diffopts(self.ui, opts)
95 95 diffopts.git = True
96 96
97 97 lock = wlock = None
98 98 try:
99 99 wlock = repo.wlock()
100 100 lock = repo.lock()
101 101 for rev in revs:
102 102 node = revmap[rev]
103 103 revstr = '%s:%s' % (rev, revlog.short(node))
104 104
105 105 if self.applied(repo, node, p1):
106 106 self.ui.warn(_('skipping already applied revision %s\n') %
107 107 revstr)
108 108 continue
109 109
110 110 parents = source.changelog.parents(node)
111 111 if not opts.get('filter'):
112 112 # If the changeset parent is the same as the wdir's parent,
113 113 # just pull it.
114 114 if parents[0] == p1:
115 115 pulls.append(node)
116 116 p1 = node
117 117 continue
118 118 if pulls:
119 119 if source != repo:
120 120 repo.pull(source, heads=pulls)
121 121 merge.update(repo, pulls[-1], False, False, None)
122 122 p1, p2 = repo.dirstate.parents()
123 123 pulls = []
124 124
125 125 domerge = False
126 126 if node in merges:
127 127 # pulling all the merge revs at once would mean we couldn't
128 128 # transplant after the latest even if transplants before them
129 129 # fail.
130 130 domerge = True
131 131 if not hasnode(repo, node):
132 132 repo.pull(source, heads=[node])
133 133
134 134 if parents[1] != revlog.nullid:
135 135 self.ui.note(_('skipping merge changeset %s:%s\n')
136 136 % (rev, revlog.short(node)))
137 137 patchfile = None
138 138 else:
139 139 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
140 140 fp = os.fdopen(fd, 'w')
141 141 patch.diff(source, parents[0], node, fp=fp, opts=diffopts)
142 142 fp.close()
143 143
144 144 del revmap[rev]
145 145 if patchfile or domerge:
146 146 try:
147 147 n = self.applyone(repo, node,
148 148 source.changelog.read(node),
149 149 patchfile, merge=domerge,
150 150 log=opts.get('log'),
151 151 filter=opts.get('filter'))
152 152 if n and domerge:
153 153 self.ui.status(_('%s merged at %s\n') % (revstr,
154 154 revlog.short(n)))
155 155 elif n:
156 156 self.ui.status(_('%s transplanted to %s\n') % (revlog.short(node),
157 157 revlog.short(n)))
158 158 finally:
159 159 if patchfile:
160 160 os.unlink(patchfile)
161 161 if pulls:
162 162 repo.pull(source, heads=pulls)
163 163 merge.update(repo, pulls[-1], False, False, None)
164 164 finally:
165 165 self.saveseries(revmap, merges)
166 166 self.transplants.write()
167 167 del lock, wlock
168 168
169 169 def filter(self, filter, changelog, patchfile):
170 170 '''arbitrarily rewrite changeset before applying it'''
171 171
172 self.ui.status('filtering %s\n' % patchfile)
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):
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 None
220 220 finally:
221 221 files = patch.updatedir(self.ui, repo, files)
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, extra=extra)
240 240 if not merge:
241 241 self.transplants.set(n, node)
242 242
243 243 return n
244 244
245 245 def resume(self, repo, source, opts=None):
246 246 '''recover last transaction and apply remaining changesets'''
247 247 if os.path.exists(os.path.join(self.path, 'journal')):
248 248 n, node = self.recover(repo)
249 249 self.ui.status(_('%s transplanted as %s\n') % (revlog.short(node),
250 250 revlog.short(n)))
251 251 seriespath = os.path.join(self.path, 'series')
252 252 if not os.path.exists(seriespath):
253 253 self.transplants.write()
254 254 return
255 255 nodes, merges = self.readseries()
256 256 revmap = {}
257 257 for n in nodes:
258 258 revmap[source.changelog.rev(n)] = n
259 259 os.unlink(seriespath)
260 260
261 261 self.apply(repo, source, revmap, merges, opts)
262 262
263 263 def recover(self, repo):
264 264 '''commit working directory using journal metadata'''
265 265 node, user, date, message, parents = self.readlog()
266 266 merge = len(parents) == 2
267 267
268 268 if not user or not date or not message or not parents[0]:
269 269 raise util.Abort(_('transplant log file is corrupt'))
270 270
271 271 extra = {'transplant_source': node}
272 272 wlock = repo.wlock()
273 273 try:
274 274 p1, p2 = repo.dirstate.parents()
275 275 if p1 != parents[0]:
276 276 raise util.Abort(
277 277 _('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, 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 finally:
290 290 del wlock
291 291
292 292 def readseries(self):
293 293 nodes = []
294 294 merges = []
295 295 cur = nodes
296 296 for line in self.opener('series').read().splitlines():
297 297 if line.startswith('# Merges'):
298 298 cur = merges
299 299 continue
300 300 cur.append(revlog.bin(line))
301 301
302 302 return (nodes, merges)
303 303
304 304 def saveseries(self, revmap, merges):
305 305 if not revmap:
306 306 return
307 307
308 308 if not os.path.isdir(self.path):
309 309 os.mkdir(self.path)
310 310 series = self.opener('series', 'w')
311 311 for rev in util.sort(revmap):
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 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 tp.apply(repo, source, revmap, merges, opts)
572 572 finally:
573 573 if bundle:
574 574 source.close()
575 575 os.unlink(bundle)
576 576
577 577 cmdtable = {
578 578 "transplant":
579 579 (transplant,
580 580 [('s', 'source', '', _('pull patches from REPOSITORY')),
581 581 ('b', 'branch', [], _('pull patches from branch BRANCH')),
582 582 ('a', 'all', None, _('pull all changesets up to BRANCH')),
583 583 ('p', 'prune', [], _('skip over REV')),
584 584 ('m', 'merge', [], _('merge at REV')),
585 585 ('', 'log', None, _('append transplant info to log message')),
586 586 ('c', 'continue', None, _('continue last transplant session after repair')),
587 587 ('', 'filter', '', _('filter changesets through FILTER'))],
588 588 _('hg transplant [-s REPOSITORY] [-b BRANCH [-a]] [-p REV] [-m REV] [REV]...'))
589 589 }
General Comments 0
You need to be logged in to leave comments. Login now