##// END OF EJS Templates
histedit: factorise node stripping logic...
Pierre-Yves David -
r17664:4eb13b61 default
parent child Browse files
Show More
@@ -1,749 +1,736
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 ui.debug('should strip created nodes %s\n' %
486 ', '.join([node.short(n) for n in created]))
487 ui.debug('should strip temp nodes %s\n' %
488 ', '.join([node.short(n) for n in tmpnodes]))
489 for nodes in (created, tmpnodes):
490 lock = None
491 try:
492 lock = repo.lock()
493 for n in reversed(nodes):
494 try:
495 repair.strip(ui, repo, n)
496 except error.LookupError:
497 pass
498 finally:
499 lockmod.release(lock)
485 cleanupnode(ui, repo, 'created', created)
486 cleanupnode(ui, repo, 'temp', tmpnodes)
500 487 os.unlink(os.path.join(repo.path, 'histedit-state'))
501 488 return
502 489 else:
503 490 cmdutil.bailifchanged(repo)
504 491 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
505 492 raise util.Abort(_('history edit already in progress, try '
506 493 '--continue or --abort'))
507 494
508 495 tip, empty = repo.dirstate.parents()
509 496
510 497
511 498 if len(parent) != 1:
512 499 raise util.Abort(_('histedit requires exactly one parent revision'))
513 500 parent = scmutil.revsingle(repo, parent[0]).node()
514 501
515 502 keep = opts.get('keep', False)
516 503 revs = between(repo, parent, tip, keep)
517 504
518 505 ctxs = [repo[r] for r in revs]
519 506 existing = [r.node() for r in ctxs]
520 507 rules = opts.get('commands', '')
521 508 if not rules:
522 509 rules = '\n'.join([makedesc(c) for c in ctxs])
523 510 rules += '\n\n'
524 511 rules += editcomment % (node.short(parent), node.short(tip))
525 512 rules = ui.edit(rules, ui.username())
526 513 # Save edit rules in .hg/histedit-last-edit.txt in case
527 514 # the user needs to ask for help after something
528 515 # surprising happens.
529 516 f = open(repo.join('histedit-last-edit.txt'), 'w')
530 517 f.write(rules)
531 518 f.close()
532 519 else:
533 520 f = open(rules)
534 521 rules = f.read()
535 522 f.close()
536 523 rules = [l for l in (r.strip() for r in rules.splitlines())
537 524 if l and not l[0] == '#']
538 525 rules = verifyrules(rules, repo, ctxs)
539 526
540 527 parentctx = repo[parent].parents()[0]
541 528 keep = opts.get('keep', False)
542 529 replaced = []
543 530 replacemap = {}
544 531 tmpnodes = []
545 532 created = []
546 533
547 534
548 535 while rules:
549 536 writestate(repo, parentctx.node(), created, replaced,
550 537 tmpnodes, existing, rules, keep, tip, replacemap)
551 538 action, ha = rules.pop(0)
552 539 ui.debug('histedit: processing %s %s\n' % (action, ha))
553 540 (parentctx, created_, replaced_, tmpnodes_) = actiontable[action](
554 541 ui, repo, parentctx, ha, opts)
555 542
556 543 if replaced_:
557 544 clen, rlen = len(created_), len(replaced_)
558 545 if clen == rlen == 1:
559 546 ui.debug('histedit: exact replacement of %s with %s\n' % (
560 547 node.short(replaced_[0]), node.short(created_[0])))
561 548
562 549 replacemap[replaced_[0]] = created_[0]
563 550 elif clen > rlen:
564 551 assert rlen == 1, ('unexpected replacement of '
565 552 '%d changes with %d changes' % (rlen, clen))
566 553 # made more changesets than we're replacing
567 554 # TODO synthesize patch names for created patches
568 555 replacemap[replaced_[0]] = created_[-1]
569 556 ui.debug('histedit: created many, assuming %s replaced by %s' %
570 557 (node.short(replaced_[0]), node.short(created_[-1])))
571 558 elif rlen > clen:
572 559 if not created_:
573 560 # This must be a drop. Try and put our metadata on
574 561 # the parent change.
575 562 assert rlen == 1
576 563 r = replaced_[0]
577 564 ui.debug('histedit: %s seems replaced with nothing, '
578 565 'finding a parent\n' % (node.short(r)))
579 566 pctx = repo[r].parents()[0]
580 567 if pctx.node() in replacemap:
581 568 ui.debug('histedit: parent is already replaced\n')
582 569 replacemap[r] = replacemap[pctx.node()]
583 570 else:
584 571 replacemap[r] = pctx.node()
585 572 ui.debug('histedit: %s best replaced by %s\n' % (
586 573 node.short(r), node.short(replacemap[r])))
587 574 else:
588 575 assert len(created_) == 1
589 576 for r in replaced_:
590 577 ui.debug('histedit: %s replaced by %s\n' % (
591 578 node.short(r), node.short(created_[0])))
592 579 replacemap[r] = created_[0]
593 580 else:
594 581 assert False, (
595 582 'Unhandled case in replacement mapping! '
596 583 'replacing %d changes with %d changes' % (rlen, clen))
597 584 created.extend(created_)
598 585 replaced.extend(replaced_)
599 586 tmpnodes.extend(tmpnodes_)
600 587
601 588 hg.update(repo, parentctx.node())
602 589
603 590 if not keep:
604 591 if replacemap:
605 592 movebookmarks(ui, repo, replacemap, tmpnodes, created)
606 593 # TODO update mq state
594 cleanupnode(ui, repo, 'replaced', replaced)
607 595
608 ui.debug('should strip replaced nodes %s\n' %
609 ', '.join([node.short(n) for n in replaced]))
610 lock = None
611 try:
612 lock = repo.lock()
613 for n in sorted(replaced, key=lambda x: repo[x].rev()):
614 try:
615 repair.strip(ui, repo, n)
616 except error.LookupError:
617 pass
618 finally:
619 lockmod.release(lock)
620
621 ui.debug('should strip temp nodes %s\n' %
622 ', '.join([node.short(n) for n in tmpnodes]))
623 lock = None
624 try:
625 lock = repo.lock()
626 for n in reversed(tmpnodes):
627 try:
628 repair.strip(ui, repo, n)
629 except error.LookupError:
630 pass
631 finally:
632 lockmod.release(lock)
596 cleanupnode(ui, repo, 'temp', tmpnodes)
633 597 os.unlink(os.path.join(repo.path, 'histedit-state'))
634 598 if os.path.exists(repo.sjoin('undo')):
635 599 os.unlink(repo.sjoin('undo'))
636 600
637 601
638 602 def between(repo, old, new, keep):
639 603 """select and validate the set of revision to edit
640 604
641 605 When keep is false, the specified set can't have children."""
642 606 revs = [old]
643 607 current = old
644 608 while current != new:
645 609 ctx = repo[current]
646 610 if not keep and len(ctx.children()) > 1:
647 611 raise util.Abort(_('cannot edit history that would orphan nodes'))
648 612 if len(ctx.parents()) != 1 and ctx.parents()[1] != node.nullid:
649 613 raise util.Abort(_("can't edit history with merges"))
650 614 if not ctx.children():
651 615 current = new
652 616 else:
653 617 current = ctx.children()[0].node()
654 618 revs.append(current)
655 619 if len(repo[current].children()) and not keep:
656 620 raise util.Abort(_('cannot edit history that would orphan nodes'))
657 621 return revs
658 622
659 623
660 624 def writestate(repo, parentctxnode, created, replaced,
661 625 tmpnodes, existing, rules, keep, oldtip, replacemap):
662 626 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
663 627 pickle.dump((parentctxnode, created, replaced,
664 628 tmpnodes, existing, rules, keep, oldtip, replacemap),
665 629 fp)
666 630 fp.close()
667 631
668 632 def readstate(repo):
669 633 """Returns a tuple of (parentnode, created, replaced, tmp, existing, rules,
670 634 keep, oldtip, replacemap ).
671 635 """
672 636 fp = open(os.path.join(repo.path, 'histedit-state'))
673 637 return pickle.load(fp)
674 638
675 639
676 640 def makedesc(c):
677 641 """build a initial action line for a ctx `c`
678 642
679 643 line are in the form:
680 644
681 645 pick <hash> <rev> <summary>
682 646 """
683 647 summary = ''
684 648 if c.description():
685 649 summary = c.description().splitlines()[0]
686 650 line = 'pick %s %d %s' % (c, c.rev(), summary)
687 651 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
688 652
689 653 def verifyrules(rules, repo, ctxs):
690 654 """Verify that there exists exactly one edit rule per given changeset.
691 655
692 656 Will abort if there are to many or too few rules, a malformed rule,
693 657 or a rule on a changeset outside of the user-given range.
694 658 """
695 659 parsed = []
696 660 if len(rules) != len(ctxs):
697 661 raise util.Abort(_('must specify a rule for each changeset once'))
698 662 for r in rules:
699 663 if ' ' not in r:
700 664 raise util.Abort(_('malformed line "%s"') % r)
701 665 action, rest = r.split(' ', 1)
702 666 if ' ' in rest.strip():
703 667 ha, rest = rest.split(' ', 1)
704 668 else:
705 669 ha = r.strip()
706 670 try:
707 671 if repo[ha] not in ctxs:
708 672 raise util.Abort(
709 673 _('may not use changesets other than the ones listed'))
710 674 except error.RepoError:
711 675 raise util.Abort(_('unknown changeset %s listed') % ha)
712 676 if action not in actiontable:
713 677 raise util.Abort(_('unknown action "%s"') % action)
714 678 parsed.append([action, ha])
715 679 return parsed
716 680
717 681 def movebookmarks(ui, repo, replacemap, tmpnodes, created):
718 682 """Move bookmark from old to newly created node"""
719 683 ui.note(_('histedit: Should update metadata for the following '
720 684 'changes:\n'))
721 685
722 686 def copybms(old, new):
723 687 if old in tmpnodes or old in created:
724 688 # can't have any metadata we'd want to update
725 689 return
726 690 while new in replacemap:
727 691 new = replacemap[new]
728 692 ui.note(_('histedit: %s to %s\n') % (node.short(old),
729 693 node.short(new)))
730 694 octx = repo[old]
731 695 marks = octx.bookmarks()
732 696 if marks:
733 697 ui.note(_('histedit: moving bookmarks %s\n') %
734 698 ', '.join(marks))
735 699 for mark in marks:
736 700 repo._bookmarks[mark] = new
737 701 bookmarks.write(repo)
738 702
739 703 # We assume that bookmarks on the tip should remain
740 704 # tipmost, but bookmarks on non-tip changesets should go
741 705 # to their most reasonable successor. As a result, find
742 706 # the old tip and new tip and copy those bookmarks first,
743 707 # then do the rest of the bookmark copies.
744 708 oldtip = sorted(replacemap.keys(), key=repo.changelog.rev)[-1]
745 709 newtip = sorted(replacemap.values(), key=repo.changelog.rev)[-1]
746 710 copybms(oldtip, newtip)
747 711
748 712 for old, new in sorted(replacemap.iteritems()):
749 713 copybms(old, new)
714
715 def cleanupnode(ui, repo, name, nodes):
716 """strip a group of nodes from the repository
717
718 The set of node to strip may contains unknown nodes."""
719 ui.debug('should strip %s nodes %s\n' %
720 (name, ', '.join([node.short(n) for n in nodes])))
721 lock = None
722 try:
723 lock = repo.lock()
724 # Find all node that need to be stripped
725 # (we hg %lr instead of %ln to silently ignore unknown item
726 nm = repo.changelog.nodemap
727 nodes = [n for n in nodes if n in nm]
728 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
729 for c in roots:
730 # We should process node in reverse order to strip tip most first.
731 # but this trigger a bug in changegroup hook.
732 # This would reduce bundle overhead
733 repair.strip(ui, repo, c)
734 finally:
735 lockmod.release(lock)
736
General Comments 0
You need to be logged in to leave comments. Login now