##// END OF EJS Templates
histedit: extract bookmark logic in a dedicated function...
Pierre-Yves David -
r17663:c6de8c69 default
parent child Browse files
Show More
@@ -1,745 +1,749
1 1 # histedit.py - interactive history editing for mercurial
2 2 #
3 3 # Copyright 2009 Augie Fackler <raf@durin42.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 """interactive history editing
8 8
9 9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 10 is as follows, assuming the following history::
11 11
12 12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 13 | Add delta
14 14 |
15 15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 16 | Add gamma
17 17 |
18 18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 19 | Add beta
20 20 |
21 21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 22 Add alpha
23 23
24 24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 25 file open in your editor::
26 26
27 27 pick c561b4e977df Add beta
28 28 pick 030b686bedc4 Add gamma
29 29 pick 7c2fd3b9020c Add delta
30 30
31 31 # Edit history between 633536316234 and 7c2fd3b9020c
32 32 #
33 33 # Commands:
34 34 # p, pick = use commit
35 35 # e, edit = use commit, but stop for amending
36 36 # f, fold = use commit, but fold into previous commit (combines N and N-1)
37 37 # d, drop = remove commit from history
38 38 # m, mess = edit message without changing commit content
39 39 #
40 40
41 41 In this file, lines beginning with ``#`` are ignored. You must specify a rule
42 42 for each revision in your history. For example, if you had meant to add gamma
43 43 before beta, and then wanted to add delta in the same revision as beta, you
44 44 would reorganize the file to look like this::
45 45
46 46 pick 030b686bedc4 Add gamma
47 47 pick c561b4e977df Add beta
48 48 fold 7c2fd3b9020c Add delta
49 49
50 50 # Edit history between 633536316234 and 7c2fd3b9020c
51 51 #
52 52 # Commands:
53 53 # p, pick = use commit
54 54 # e, edit = use commit, but stop for amending
55 55 # f, fold = use commit, but fold into previous commit (combines N and N-1)
56 56 # d, drop = remove commit from history
57 57 # m, mess = edit message without changing commit content
58 58 #
59 59
60 60 At which point you close the editor and ``histedit`` starts working. When you
61 61 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
62 62 those revisions together, offering you a chance to clean up the commit message::
63 63
64 64 Add beta
65 65 ***
66 66 Add delta
67 67
68 68 Edit the commit message to your liking, then close the editor. For
69 69 this example, let's assume that the commit message was changed to
70 70 ``Add beta and delta.`` After histedit has run and had a chance to
71 71 remove any old or temporary revisions it needed, the history looks
72 72 like this::
73 73
74 74 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
75 75 | Add beta and delta.
76 76 |
77 77 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
78 78 | Add gamma
79 79 |
80 80 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
81 81 Add alpha
82 82
83 83 Note that ``histedit`` does *not* remove any revisions (even its own temporary
84 84 ones) until after it has completed all the editing operations, so it will
85 85 probably perform several strip operations when it's done. For the above example,
86 86 it had to run strip twice. Strip can be slow depending on a variety of factors,
87 87 so you might need to be a little patient. You can choose to keep the original
88 88 revisions by passing the ``--keep`` flag.
89 89
90 90 The ``edit`` operation will drop you back to a command prompt,
91 91 allowing you to edit files freely, or even use ``hg record`` to commit
92 92 some changes as a separate commit. When you're done, any remaining
93 93 uncommitted changes will be committed as well. When done, run ``hg
94 94 histedit --continue`` to finish this step. You'll be prompted for a
95 95 new commit message, but the default commit message will be the
96 96 original message for the ``edit`` ed revision.
97 97
98 98 The ``message`` operation will give you a chance to revise a commit
99 99 message without changing the contents. It's a shortcut for doing
100 100 ``edit`` immediately followed by `hg histedit --continue``.
101 101
102 102 If ``histedit`` encounters a conflict when moving a revision (while
103 103 handling ``pick`` or ``fold``), it'll stop in a similar manner to
104 104 ``edit`` with the difference that it won't prompt you for a commit
105 105 message when done. If you decide at this point that you don't like how
106 106 much work it will be to rearrange history, or that you made a mistake,
107 107 you can use ``hg histedit --abort`` to abandon the new changes you
108 108 have made and return to the state before you attempted to edit your
109 109 history.
110 110
111 111 If we clone the example repository above and add three more changes, such that
112 112 we have the following history::
113 113
114 114 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
115 115 | Add theta
116 116 |
117 117 o 5 140988835471 2009-04-27 18:04 -0500 stefan
118 118 | Add eta
119 119 |
120 120 o 4 122930637314 2009-04-27 18:04 -0500 stefan
121 121 | Add zeta
122 122 |
123 123 o 3 836302820282 2009-04-27 18:04 -0500 stefan
124 124 | Add epsilon
125 125 |
126 126 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
127 127 | Add beta and delta.
128 128 |
129 129 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
130 130 | Add gamma
131 131 |
132 132 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
133 133 Add alpha
134 134
135 135 If you run ``hg histedit --outgoing`` on the clone then it is the same
136 136 as running ``hg histedit 836302820282``. If you need plan to push to a
137 137 repository that Mercurial does not detect to be related to the source
138 138 repo, you can add a ``--force`` option.
139 139 """
140 140
141 141 try:
142 142 import cPickle as pickle
143 143 except ImportError:
144 144 import pickle
145 145 import os
146 146
147 147 from mercurial import bookmarks
148 148 from mercurial import cmdutil
149 149 from mercurial import discovery
150 150 from mercurial import error
151 151 from mercurial import copies
152 152 from mercurial import context
153 153 from mercurial import hg
154 154 from mercurial import lock as lockmod
155 155 from mercurial import node
156 156 from mercurial import repair
157 157 from mercurial import scmutil
158 158 from mercurial import util
159 159 from mercurial import merge as mergemod
160 160 from mercurial.i18n import _
161 161
162 162 cmdtable = {}
163 163 command = cmdutil.command(cmdtable)
164 164
165 165 testedwith = 'internal'
166 166
167 167 # i18n: command names and abbreviations must remain untranslated
168 168 editcomment = _("""# Edit history between %s and %s
169 169 #
170 170 # Commands:
171 171 # p, pick = use commit
172 172 # e, edit = use commit, but stop for amending
173 173 # f, fold = use commit, but fold into previous commit (combines N and N-1)
174 174 # d, drop = remove commit from history
175 175 # m, mess = edit message without changing commit content
176 176 #
177 177 """)
178 178
179 179 def applychanges(ui, repo, ctx, opts):
180 180 """Merge changeset from ctx (only) in the current working directory"""
181 181 wcpar = repo.dirstate.parents()[0]
182 182 if ctx.p1().node() == wcpar:
183 183 # edition ar "in place" we do not need to make any merge,
184 184 # just applies changes on parent for edition
185 185 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
186 186 stats = None
187 187 else:
188 188 try:
189 189 # ui.forcemerge is an internal variable, do not document
190 190 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
191 191 stats = mergemod.update(repo, ctx.node(), True, True, False,
192 192 ctx.p1().node())
193 193 finally:
194 194 repo.ui.setconfig('ui', 'forcemerge', '')
195 195 repo.setparents(wcpar, node.nullid)
196 196 repo.dirstate.write()
197 197 # fix up dirstate for copies and renames
198 198 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
199 199 return stats
200 200
201 201 def collapse(repo, first, last, commitopts):
202 202 """collapse the set of revisions from first to last as new one.
203 203
204 204 Expected commit options are:
205 205 - message
206 206 - date
207 207 - username
208 208 Edition of commit message is trigered in all case.
209 209
210 210 This function works in memory."""
211 211 ctxs = list(repo.set('%d::%d', first, last))
212 212 if not ctxs:
213 213 return None
214 214 base = first.parents()[0]
215 215
216 216 # commit a new version of the old changeset, including the update
217 217 # collect all files which might be affected
218 218 files = set()
219 219 for ctx in ctxs:
220 220 files.update(ctx.files())
221 221
222 222 # Recompute copies (avoid recording a -> b -> a)
223 223 copied = copies.pathcopies(first, last)
224 224
225 225 # prune files which were reverted by the updates
226 226 def samefile(f):
227 227 if f in last.manifest():
228 228 a = last.filectx(f)
229 229 if f in base.manifest():
230 230 b = base.filectx(f)
231 231 return (a.data() == b.data()
232 232 and a.flags() == b.flags())
233 233 else:
234 234 return False
235 235 else:
236 236 return f not in base.manifest()
237 237 files = [f for f in files if not samefile(f)]
238 238 # commit version of these files as defined by head
239 239 headmf = last.manifest()
240 240 def filectxfn(repo, ctx, path):
241 241 if path in headmf:
242 242 fctx = last[path]
243 243 flags = fctx.flags()
244 244 mctx = context.memfilectx(fctx.path(), fctx.data(),
245 245 islink='l' in flags,
246 246 isexec='x' in flags,
247 247 copied=copied.get(path))
248 248 return mctx
249 249 raise IOError()
250 250
251 251 if commitopts.get('message'):
252 252 message = commitopts['message']
253 253 else:
254 254 message = first.description()
255 255 user = commitopts.get('user')
256 256 date = commitopts.get('date')
257 257 extra = first.extra()
258 258
259 259 parents = (first.p1().node(), first.p2().node())
260 260 new = context.memctx(repo,
261 261 parents=parents,
262 262 text=message,
263 263 files=files,
264 264 filectxfn=filectxfn,
265 265 user=user,
266 266 date=date,
267 267 extra=extra)
268 268 new._text = cmdutil.commitforceeditor(repo, new, [])
269 269 return repo.commitctx(new)
270 270
271 271 def pick(ui, repo, ctx, ha, opts):
272 272 oldctx = repo[ha]
273 273 if oldctx.parents()[0] == ctx:
274 274 ui.debug('node %s unchanged\n' % ha)
275 275 return oldctx, [], [], []
276 276 hg.update(repo, ctx.node())
277 277 stats = applychanges(ui, repo, oldctx, opts)
278 278 if stats and stats[3] > 0:
279 279 raise util.Abort(_('Fix up the change and run '
280 280 'hg histedit --continue'))
281 281 # drop the second merge parent
282 282 n = repo.commit(text=oldctx.description(), user=oldctx.user(),
283 283 date=oldctx.date(), extra=oldctx.extra())
284 284 if n is None:
285 285 ui.warn(_('%s: empty changeset\n')
286 286 % node.hex(ha))
287 287 return ctx, [], [], []
288 288 return repo[n], [n], [oldctx.node()], []
289 289
290 290
291 291 def edit(ui, repo, ctx, ha, opts):
292 292 oldctx = repo[ha]
293 293 hg.update(repo, ctx.node())
294 294 applychanges(ui, repo, oldctx, opts)
295 295 raise util.Abort(_('Make changes as needed, you may commit or record as '
296 296 'needed now.\nWhen you are finished, run hg'
297 297 ' histedit --continue to resume.'))
298 298
299 299 def fold(ui, repo, ctx, ha, opts):
300 300 oldctx = repo[ha]
301 301 hg.update(repo, ctx.node())
302 302 stats = applychanges(ui, repo, oldctx, opts)
303 303 if stats and stats[3] > 0:
304 304 raise util.Abort(_('Fix up the change and run '
305 305 'hg histedit --continue'))
306 306 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
307 307 date=oldctx.date(), extra=oldctx.extra())
308 308 if n is None:
309 309 ui.warn(_('%s: empty changeset')
310 310 % node.hex(ha))
311 311 return ctx, [], [], []
312 312 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
313 313
314 314 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
315 315 parent = ctx.parents()[0].node()
316 316 hg.update(repo, parent)
317 317 ### prepare new commit data
318 318 commitopts = opts.copy()
319 319 # username
320 320 if ctx.user() == oldctx.user():
321 321 username = ctx.user()
322 322 else:
323 323 username = ui.username()
324 324 commitopts['user'] = username
325 325 # commit message
326 326 newmessage = '\n***\n'.join(
327 327 [ctx.description()] +
328 328 [repo[r].description() for r in internalchanges] +
329 329 [oldctx.description()]) + '\n'
330 330 commitopts['message'] = newmessage
331 331 # date
332 332 commitopts['date'] = max(ctx.date(), oldctx.date())
333 333 n = collapse(repo, ctx, repo[newnode], commitopts)
334 334 if n is None:
335 335 return ctx, [], [], []
336 336 hg.update(repo, n)
337 337 return repo[n], [n], [oldctx.node(), ctx.node()], [newnode]
338 338
339 339 def drop(ui, repo, ctx, ha, opts):
340 340 return ctx, [], [repo[ha].node()], []
341 341
342 342
343 343 def message(ui, repo, ctx, ha, opts):
344 344 oldctx = repo[ha]
345 345 hg.update(repo, ctx.node())
346 346 stats = applychanges(ui, repo, oldctx, opts)
347 347 if stats and stats[3] > 0:
348 348 raise util.Abort(_('Fix up the change and run '
349 349 'hg histedit --continue'))
350 350 message = oldctx.description() + '\n'
351 351 message = ui.edit(message, ui.username())
352 352 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
353 353 extra=oldctx.extra())
354 354 newctx = repo[new]
355 355 if oldctx.node() != newctx.node():
356 356 return newctx, [new], [oldctx.node()], []
357 357 # We didn't make an edit, so just indicate no replaced nodes
358 358 return newctx, [new], [], []
359 359
360 360 actiontable = {'p': pick,
361 361 'pick': pick,
362 362 'e': edit,
363 363 'edit': edit,
364 364 'f': fold,
365 365 'fold': fold,
366 366 'd': drop,
367 367 'drop': drop,
368 368 'm': message,
369 369 'mess': message,
370 370 }
371 371
372 372 @command('histedit',
373 373 [('', 'commands', '',
374 374 _('Read history edits from the specified file.')),
375 375 ('c', 'continue', False, _('continue an edit already in progress')),
376 376 ('k', 'keep', False,
377 377 _("don't strip old nodes after edit is complete")),
378 378 ('', 'abort', False, _('abort an edit in progress')),
379 379 ('o', 'outgoing', False, _('changesets not found in destination')),
380 380 ('f', 'force', False,
381 381 _('force outgoing even for unrelated repositories')),
382 382 ('r', 'rev', [], _('first revision to be edited'))],
383 383 _("[PARENT]"))
384 384 def histedit(ui, repo, *parent, **opts):
385 385 """interactively edit changeset history
386 386 """
387 387 # TODO only abort if we try and histedit mq patches, not just
388 388 # blanket if mq patches are applied somewhere
389 389 mq = getattr(repo, 'mq', None)
390 390 if mq and mq.applied:
391 391 raise util.Abort(_('source has mq patches applied'))
392 392
393 393 parent = list(parent) + opts.get('rev', [])
394 394 if opts.get('outgoing'):
395 395 if len(parent) > 1:
396 396 raise util.Abort(
397 397 _('only one repo argument allowed with --outgoing'))
398 398 elif parent:
399 399 parent = parent[0]
400 400
401 401 dest = ui.expandpath(parent or 'default-push', parent or 'default')
402 402 dest, revs = hg.parseurl(dest, None)[:2]
403 403 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
404 404
405 405 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
406 406 other = hg.peer(repo, opts, dest)
407 407
408 408 if revs:
409 409 revs = [repo.lookup(rev) for rev in revs]
410 410
411 411 parent = discovery.findcommonoutgoing(
412 412 repo, other, [], force=opts.get('force')).missing[0:1]
413 413 else:
414 414 if opts.get('force'):
415 415 raise util.Abort(_('--force only allowed with --outgoing'))
416 416
417 417 if opts.get('continue', False):
418 418 if len(parent) != 0:
419 419 raise util.Abort(_('no arguments allowed with --continue'))
420 420 (parentctxnode, created, replaced,
421 421 tmpnodes, existing, rules, keep, tip, replacemap) = readstate(repo)
422 422 currentparent, wantnull = repo.dirstate.parents()
423 423 parentctx = repo[parentctxnode]
424 424 # existing is the list of revisions initially considered by
425 425 # histedit. Here we use it to list new changesets, descendants
426 426 # of parentctx without an 'existing' changeset in-between. We
427 427 # also have to exclude 'existing' changesets which were
428 428 # previously dropped.
429 429 descendants = set(c.node() for c in
430 430 repo.set('(%n::) - %n', parentctxnode, parentctxnode))
431 431 existing = set(existing)
432 432 notdropped = set(n for n in existing if n in descendants and
433 433 (n not in replacemap or replacemap[n] in descendants))
434 434 # Discover any nodes the user has added in the interim. We can
435 435 # miss changesets which were dropped and recreated the same.
436 436 newchildren = list(c.node() for c in repo.set(
437 437 'sort(%ln - (%ln or %ln::))', descendants, existing, notdropped))
438 438 action, currentnode = rules.pop(0)
439 439 if action in ('f', 'fold'):
440 440 tmpnodes.extend(newchildren)
441 441 else:
442 442 created.extend(newchildren)
443 443
444 444 m, a, r, d = repo.status()[:4]
445 445 oldctx = repo[currentnode]
446 446 message = oldctx.description() + '\n'
447 447 if action in ('e', 'edit', 'm', 'mess'):
448 448 message = ui.edit(message, ui.username())
449 449 elif action in ('f', 'fold'):
450 450 message = 'fold-temp-revision %s' % currentnode
451 451 new = None
452 452 if m or a or r or d:
453 453 new = repo.commit(text=message, user=oldctx.user(),
454 454 date=oldctx.date(), extra=oldctx.extra())
455 455
456 456 # If we're resuming a fold and we have new changes, mark the
457 457 # replacements and finish the fold. If not, it's more like a
458 458 # drop of the changesets that disappeared, and we can skip
459 459 # this step.
460 460 if action in ('f', 'fold') and (new or newchildren):
461 461 if new:
462 462 tmpnodes.append(new)
463 463 else:
464 464 new = newchildren[-1]
465 465 (parentctx, created_, replaced_, tmpnodes_) = finishfold(
466 466 ui, repo, parentctx, oldctx, new, opts, newchildren)
467 467 replaced.extend(replaced_)
468 468 created.extend(created_)
469 469 tmpnodes.extend(tmpnodes_)
470 470 elif action not in ('d', 'drop'):
471 471 if new != oldctx.node():
472 472 replaced.append(oldctx.node())
473 473 if new:
474 474 if new != oldctx.node():
475 475 created.append(new)
476 476 parentctx = repo[new]
477 477
478 478 elif opts.get('abort', False):
479 479 if len(parent) != 0:
480 480 raise util.Abort(_('no arguments allowed with --abort'))
481 481 (parentctxnode, created, replaced, tmpnodes,
482 482 existing, rules, keep, tip, replacemap) = readstate(repo)
483 483 ui.debug('restore wc to old tip %s\n' % node.hex(tip))
484 484 hg.clean(repo, tip)
485 485 ui.debug('should strip created nodes %s\n' %
486 486 ', '.join([node.short(n) for n in created]))
487 487 ui.debug('should strip temp nodes %s\n' %
488 488 ', '.join([node.short(n) for n in tmpnodes]))
489 489 for nodes in (created, tmpnodes):
490 490 lock = None
491 491 try:
492 492 lock = repo.lock()
493 493 for n in reversed(nodes):
494 494 try:
495 495 repair.strip(ui, repo, n)
496 496 except error.LookupError:
497 497 pass
498 498 finally:
499 499 lockmod.release(lock)
500 500 os.unlink(os.path.join(repo.path, 'histedit-state'))
501 501 return
502 502 else:
503 503 cmdutil.bailifchanged(repo)
504 504 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
505 505 raise util.Abort(_('history edit already in progress, try '
506 506 '--continue or --abort'))
507 507
508 508 tip, empty = repo.dirstate.parents()
509 509
510 510
511 511 if len(parent) != 1:
512 512 raise util.Abort(_('histedit requires exactly one parent revision'))
513 513 parent = scmutil.revsingle(repo, parent[0]).node()
514 514
515 515 keep = opts.get('keep', False)
516 516 revs = between(repo, parent, tip, keep)
517 517
518 518 ctxs = [repo[r] for r in revs]
519 519 existing = [r.node() for r in ctxs]
520 520 rules = opts.get('commands', '')
521 521 if not rules:
522 522 rules = '\n'.join([makedesc(c) for c in ctxs])
523 523 rules += '\n\n'
524 524 rules += editcomment % (node.short(parent), node.short(tip))
525 525 rules = ui.edit(rules, ui.username())
526 526 # Save edit rules in .hg/histedit-last-edit.txt in case
527 527 # the user needs to ask for help after something
528 528 # surprising happens.
529 529 f = open(repo.join('histedit-last-edit.txt'), 'w')
530 530 f.write(rules)
531 531 f.close()
532 532 else:
533 533 f = open(rules)
534 534 rules = f.read()
535 535 f.close()
536 536 rules = [l for l in (r.strip() for r in rules.splitlines())
537 537 if l and not l[0] == '#']
538 538 rules = verifyrules(rules, repo, ctxs)
539 539
540 540 parentctx = repo[parent].parents()[0]
541 541 keep = opts.get('keep', False)
542 542 replaced = []
543 543 replacemap = {}
544 544 tmpnodes = []
545 545 created = []
546 546
547 547
548 548 while rules:
549 549 writestate(repo, parentctx.node(), created, replaced,
550 550 tmpnodes, existing, rules, keep, tip, replacemap)
551 551 action, ha = rules.pop(0)
552 552 ui.debug('histedit: processing %s %s\n' % (action, ha))
553 553 (parentctx, created_, replaced_, tmpnodes_) = actiontable[action](
554 554 ui, repo, parentctx, ha, opts)
555 555
556 556 if replaced_:
557 557 clen, rlen = len(created_), len(replaced_)
558 558 if clen == rlen == 1:
559 559 ui.debug('histedit: exact replacement of %s with %s\n' % (
560 560 node.short(replaced_[0]), node.short(created_[0])))
561 561
562 562 replacemap[replaced_[0]] = created_[0]
563 563 elif clen > rlen:
564 564 assert rlen == 1, ('unexpected replacement of '
565 565 '%d changes with %d changes' % (rlen, clen))
566 566 # made more changesets than we're replacing
567 567 # TODO synthesize patch names for created patches
568 568 replacemap[replaced_[0]] = created_[-1]
569 569 ui.debug('histedit: created many, assuming %s replaced by %s' %
570 570 (node.short(replaced_[0]), node.short(created_[-1])))
571 571 elif rlen > clen:
572 572 if not created_:
573 573 # This must be a drop. Try and put our metadata on
574 574 # the parent change.
575 575 assert rlen == 1
576 576 r = replaced_[0]
577 577 ui.debug('histedit: %s seems replaced with nothing, '
578 578 'finding a parent\n' % (node.short(r)))
579 579 pctx = repo[r].parents()[0]
580 580 if pctx.node() in replacemap:
581 581 ui.debug('histedit: parent is already replaced\n')
582 582 replacemap[r] = replacemap[pctx.node()]
583 583 else:
584 584 replacemap[r] = pctx.node()
585 585 ui.debug('histedit: %s best replaced by %s\n' % (
586 586 node.short(r), node.short(replacemap[r])))
587 587 else:
588 588 assert len(created_) == 1
589 589 for r in replaced_:
590 590 ui.debug('histedit: %s replaced by %s\n' % (
591 591 node.short(r), node.short(created_[0])))
592 592 replacemap[r] = created_[0]
593 593 else:
594 594 assert False, (
595 595 'Unhandled case in replacement mapping! '
596 596 'replacing %d changes with %d changes' % (rlen, clen))
597 597 created.extend(created_)
598 598 replaced.extend(replaced_)
599 599 tmpnodes.extend(tmpnodes_)
600 600
601 601 hg.update(repo, parentctx.node())
602 602
603 603 if not keep:
604 604 if replacemap:
605 ui.note(_('histedit: Should update metadata for the following '
606 'changes:\n'))
607
608 def copybms(old, new):
609 if old in tmpnodes or old in created:
610 # can't have any metadata we'd want to update
611 return
612 while new in replacemap:
613 new = replacemap[new]
614 ui.note(_('histedit: %s to %s\n') % (node.short(old),
615 node.short(new)))
616 octx = repo[old]
617 marks = octx.bookmarks()
618 if marks:
619 ui.note(_('histedit: moving bookmarks %s\n') %
620 ', '.join(marks))
621 for mark in marks:
622 repo._bookmarks[mark] = new
623 bookmarks.write(repo)
624
625 # We assume that bookmarks on the tip should remain
626 # tipmost, but bookmarks on non-tip changesets should go
627 # to their most reasonable successor. As a result, find
628 # the old tip and new tip and copy those bookmarks first,
629 # then do the rest of the bookmark copies.
630 oldtip = sorted(replacemap.keys(), key=repo.changelog.rev)[-1]
631 newtip = sorted(replacemap.values(), key=repo.changelog.rev)[-1]
632 copybms(oldtip, newtip)
633
634 for old, new in sorted(replacemap.iteritems()):
635 copybms(old, new)
636 # TODO update mq state
605 movebookmarks(ui, repo, replacemap, tmpnodes, created)
606 # TODO update mq state
637 607
638 608 ui.debug('should strip replaced nodes %s\n' %
639 609 ', '.join([node.short(n) for n in replaced]))
640 610 lock = None
641 611 try:
642 612 lock = repo.lock()
643 613 for n in sorted(replaced, key=lambda x: repo[x].rev()):
644 614 try:
645 615 repair.strip(ui, repo, n)
646 616 except error.LookupError:
647 617 pass
648 618 finally:
649 619 lockmod.release(lock)
650 620
651 621 ui.debug('should strip temp nodes %s\n' %
652 622 ', '.join([node.short(n) for n in tmpnodes]))
653 623 lock = None
654 624 try:
655 625 lock = repo.lock()
656 626 for n in reversed(tmpnodes):
657 627 try:
658 628 repair.strip(ui, repo, n)
659 629 except error.LookupError:
660 630 pass
661 631 finally:
662 632 lockmod.release(lock)
663 633 os.unlink(os.path.join(repo.path, 'histedit-state'))
664 634 if os.path.exists(repo.sjoin('undo')):
665 635 os.unlink(repo.sjoin('undo'))
666 636
667 637
668 638 def between(repo, old, new, keep):
669 639 """select and validate the set of revision to edit
670 640
671 641 When keep is false, the specified set can't have children."""
672 642 revs = [old]
673 643 current = old
674 644 while current != new:
675 645 ctx = repo[current]
676 646 if not keep and len(ctx.children()) > 1:
677 647 raise util.Abort(_('cannot edit history that would orphan nodes'))
678 648 if len(ctx.parents()) != 1 and ctx.parents()[1] != node.nullid:
679 649 raise util.Abort(_("can't edit history with merges"))
680 650 if not ctx.children():
681 651 current = new
682 652 else:
683 653 current = ctx.children()[0].node()
684 654 revs.append(current)
685 655 if len(repo[current].children()) and not keep:
686 656 raise util.Abort(_('cannot edit history that would orphan nodes'))
687 657 return revs
688 658
689 659
690 660 def writestate(repo, parentctxnode, created, replaced,
691 661 tmpnodes, existing, rules, keep, oldtip, replacemap):
692 662 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
693 663 pickle.dump((parentctxnode, created, replaced,
694 664 tmpnodes, existing, rules, keep, oldtip, replacemap),
695 665 fp)
696 666 fp.close()
697 667
698 668 def readstate(repo):
699 669 """Returns a tuple of (parentnode, created, replaced, tmp, existing, rules,
700 670 keep, oldtip, replacemap ).
701 671 """
702 672 fp = open(os.path.join(repo.path, 'histedit-state'))
703 673 return pickle.load(fp)
704 674
705 675
706 676 def makedesc(c):
707 677 """build a initial action line for a ctx `c`
708 678
709 679 line are in the form:
710 680
711 681 pick <hash> <rev> <summary>
712 682 """
713 683 summary = ''
714 684 if c.description():
715 685 summary = c.description().splitlines()[0]
716 686 line = 'pick %s %d %s' % (c, c.rev(), summary)
717 687 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
718 688
719 689 def verifyrules(rules, repo, ctxs):
720 690 """Verify that there exists exactly one edit rule per given changeset.
721 691
722 692 Will abort if there are to many or too few rules, a malformed rule,
723 693 or a rule on a changeset outside of the user-given range.
724 694 """
725 695 parsed = []
726 696 if len(rules) != len(ctxs):
727 697 raise util.Abort(_('must specify a rule for each changeset once'))
728 698 for r in rules:
729 699 if ' ' not in r:
730 700 raise util.Abort(_('malformed line "%s"') % r)
731 701 action, rest = r.split(' ', 1)
732 702 if ' ' in rest.strip():
733 703 ha, rest = rest.split(' ', 1)
734 704 else:
735 705 ha = r.strip()
736 706 try:
737 707 if repo[ha] not in ctxs:
738 708 raise util.Abort(
739 709 _('may not use changesets other than the ones listed'))
740 710 except error.RepoError:
741 711 raise util.Abort(_('unknown changeset %s listed') % ha)
742 712 if action not in actiontable:
743 713 raise util.Abort(_('unknown action "%s"') % action)
744 714 parsed.append([action, ha])
745 715 return parsed
716
717 def movebookmarks(ui, repo, replacemap, tmpnodes, created):
718 """Move bookmark from old to newly created node"""
719 ui.note(_('histedit: Should update metadata for the following '
720 'changes:\n'))
721
722 def copybms(old, new):
723 if old in tmpnodes or old in created:
724 # can't have any metadata we'd want to update
725 return
726 while new in replacemap:
727 new = replacemap[new]
728 ui.note(_('histedit: %s to %s\n') % (node.short(old),
729 node.short(new)))
730 octx = repo[old]
731 marks = octx.bookmarks()
732 if marks:
733 ui.note(_('histedit: moving bookmarks %s\n') %
734 ', '.join(marks))
735 for mark in marks:
736 repo._bookmarks[mark] = new
737 bookmarks.write(repo)
738
739 # We assume that bookmarks on the tip should remain
740 # tipmost, but bookmarks on non-tip changesets should go
741 # to their most reasonable successor. As a result, find
742 # the old tip and new tip and copy those bookmarks first,
743 # then do the rest of the bookmark copies.
744 oldtip = sorted(replacemap.keys(), key=repo.changelog.rev)[-1]
745 newtip = sorted(replacemap.values(), key=repo.changelog.rev)[-1]
746 copybms(oldtip, newtip)
747
748 for old, new in sorted(replacemap.iteritems()):
749 copybms(old, new)
General Comments 0
You need to be logged in to leave comments. Login now