##// END OF EJS Templates
histedit: check mutability of contexts correctly...
Augie Fackler -
r22416:810d3748 default
parent child Browse files
Show More
@@ -1,952 +1,952 b''
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 c561b4e977df and 7c2fd3b9020c
32 32 #
33 33 # Commits are listed from least to most recent
34 34 #
35 35 # Commands:
36 36 # p, pick = use commit
37 37 # e, edit = use commit, but stop for amending
38 38 # f, fold = use commit, but combine it with the one above
39 39 # r, roll = like fold, but discard this commit's description
40 40 # d, drop = remove commit from history
41 41 # m, mess = edit message without changing commit content
42 42 #
43 43
44 44 In this file, lines beginning with ``#`` are ignored. You must specify a rule
45 45 for each revision in your history. For example, if you had meant to add gamma
46 46 before beta, and then wanted to add delta in the same revision as beta, you
47 47 would reorganize the file to look like this::
48 48
49 49 pick 030b686bedc4 Add gamma
50 50 pick c561b4e977df Add beta
51 51 fold 7c2fd3b9020c Add delta
52 52
53 53 # Edit history between c561b4e977df and 7c2fd3b9020c
54 54 #
55 55 # Commits are listed from least to most recent
56 56 #
57 57 # Commands:
58 58 # p, pick = use commit
59 59 # e, edit = use commit, but stop for amending
60 60 # f, fold = use commit, but combine it with the one above
61 61 # r, roll = like fold, but discard this commit's description
62 62 # d, drop = remove commit from history
63 63 # m, mess = edit message without changing commit content
64 64 #
65 65
66 66 At which point you close the editor and ``histedit`` starts working. When you
67 67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
68 68 those revisions together, offering you a chance to clean up the commit message::
69 69
70 70 Add beta
71 71 ***
72 72 Add delta
73 73
74 74 Edit the commit message to your liking, then close the editor. For
75 75 this example, let's assume that the commit message was changed to
76 76 ``Add beta and delta.`` After histedit has run and had a chance to
77 77 remove any old or temporary revisions it needed, the history looks
78 78 like this::
79 79
80 80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
81 81 | Add beta and delta.
82 82 |
83 83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
84 84 | Add gamma
85 85 |
86 86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
87 87 Add alpha
88 88
89 89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
90 90 ones) until after it has completed all the editing operations, so it will
91 91 probably perform several strip operations when it's done. For the above example,
92 92 it had to run strip twice. Strip can be slow depending on a variety of factors,
93 93 so you might need to be a little patient. You can choose to keep the original
94 94 revisions by passing the ``--keep`` flag.
95 95
96 96 The ``edit`` operation will drop you back to a command prompt,
97 97 allowing you to edit files freely, or even use ``hg record`` to commit
98 98 some changes as a separate commit. When you're done, any remaining
99 99 uncommitted changes will be committed as well. When done, run ``hg
100 100 histedit --continue`` to finish this step. You'll be prompted for a
101 101 new commit message, but the default commit message will be the
102 102 original message for the ``edit`` ed revision.
103 103
104 104 The ``message`` operation will give you a chance to revise a commit
105 105 message without changing the contents. It's a shortcut for doing
106 106 ``edit`` immediately followed by `hg histedit --continue``.
107 107
108 108 If ``histedit`` encounters a conflict when moving a revision (while
109 109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
110 110 ``edit`` with the difference that it won't prompt you for a commit
111 111 message when done. If you decide at this point that you don't like how
112 112 much work it will be to rearrange history, or that you made a mistake,
113 113 you can use ``hg histedit --abort`` to abandon the new changes you
114 114 have made and return to the state before you attempted to edit your
115 115 history.
116 116
117 117 If we clone the histedit-ed example repository above and add four more
118 118 changes, such that we have the following history::
119 119
120 120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
121 121 | Add theta
122 122 |
123 123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
124 124 | Add eta
125 125 |
126 126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
127 127 | Add zeta
128 128 |
129 129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
130 130 | Add epsilon
131 131 |
132 132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
133 133 | Add beta and delta.
134 134 |
135 135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
136 136 | Add gamma
137 137 |
138 138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
139 139 Add alpha
140 140
141 141 If you run ``hg histedit --outgoing`` on the clone then it is the same
142 142 as running ``hg histedit 836302820282``. If you need plan to push to a
143 143 repository that Mercurial does not detect to be related to the source
144 144 repo, you can add a ``--force`` option.
145 145 """
146 146
147 147 try:
148 148 import cPickle as pickle
149 149 pickle.dump # import now
150 150 except ImportError:
151 151 import pickle
152 152 import errno
153 153 import os
154 154 import sys
155 155
156 156 from mercurial import cmdutil
157 157 from mercurial import discovery
158 158 from mercurial import error
159 159 from mercurial import copies
160 160 from mercurial import context
161 161 from mercurial import hg
162 162 from mercurial import node
163 163 from mercurial import repair
164 164 from mercurial import scmutil
165 165 from mercurial import util
166 166 from mercurial import obsolete
167 167 from mercurial import merge as mergemod
168 168 from mercurial.lock import release
169 169 from mercurial.i18n import _
170 170
171 171 cmdtable = {}
172 172 command = cmdutil.command(cmdtable)
173 173
174 174 testedwith = 'internal'
175 175
176 176 # i18n: command names and abbreviations must remain untranslated
177 177 editcomment = _("""# Edit history between %s and %s
178 178 #
179 179 # Commits are listed from least to most recent
180 180 #
181 181 # Commands:
182 182 # p, pick = use commit
183 183 # e, edit = use commit, but stop for amending
184 184 # f, fold = use commit, but combine it with the one above
185 185 # r, roll = like fold, but discard this commit's description
186 186 # d, drop = remove commit from history
187 187 # m, mess = edit message without changing commit content
188 188 #
189 189 """)
190 190
191 191 def commitfuncfor(repo, src):
192 192 """Build a commit function for the replacement of <src>
193 193
194 194 This function ensure we apply the same treatment to all changesets.
195 195
196 196 - Add a 'histedit_source' entry in extra.
197 197
198 198 Note that fold have its own separated logic because its handling is a bit
199 199 different and not easily factored out of the fold method.
200 200 """
201 201 phasemin = src.phase()
202 202 def commitfunc(**kwargs):
203 203 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
204 204 try:
205 205 repo.ui.setconfig('phases', 'new-commit', phasemin,
206 206 'histedit')
207 207 extra = kwargs.get('extra', {}).copy()
208 208 extra['histedit_source'] = src.hex()
209 209 kwargs['extra'] = extra
210 210 return repo.commit(**kwargs)
211 211 finally:
212 212 repo.ui.restoreconfig(phasebackup)
213 213 return commitfunc
214 214
215 215 def applychanges(ui, repo, ctx, opts):
216 216 """Merge changeset from ctx (only) in the current working directory"""
217 217 wcpar = repo.dirstate.parents()[0]
218 218 if ctx.p1().node() == wcpar:
219 219 # edition ar "in place" we do not need to make any merge,
220 220 # just applies changes on parent for edition
221 221 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
222 222 stats = None
223 223 else:
224 224 repo.dirstate.beginparentchange()
225 225 try:
226 226 # ui.forcemerge is an internal variable, do not document
227 227 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
228 228 'histedit')
229 229 stats = mergemod.update(repo, ctx.node(), True, True, False,
230 230 ctx.p1().node())
231 231 finally:
232 232 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
233 233 repo.setparents(wcpar, node.nullid)
234 234 repo.dirstate.endparentchange()
235 235 repo.dirstate.write()
236 236 # fix up dirstate for copies and renames
237 237 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
238 238 return stats
239 239
240 240 def collapse(repo, first, last, commitopts):
241 241 """collapse the set of revisions from first to last as new one.
242 242
243 243 Expected commit options are:
244 244 - message
245 245 - date
246 246 - username
247 247 Commit message is edited in all cases.
248 248
249 249 This function works in memory."""
250 250 ctxs = list(repo.set('%d::%d', first, last))
251 251 if not ctxs:
252 252 return None
253 253 base = first.parents()[0]
254 254
255 255 # commit a new version of the old changeset, including the update
256 256 # collect all files which might be affected
257 257 files = set()
258 258 for ctx in ctxs:
259 259 files.update(ctx.files())
260 260
261 261 # Recompute copies (avoid recording a -> b -> a)
262 262 copied = copies.pathcopies(base, last)
263 263
264 264 # prune files which were reverted by the updates
265 265 def samefile(f):
266 266 if f in last.manifest():
267 267 a = last.filectx(f)
268 268 if f in base.manifest():
269 269 b = base.filectx(f)
270 270 return (a.data() == b.data()
271 271 and a.flags() == b.flags())
272 272 else:
273 273 return False
274 274 else:
275 275 return f not in base.manifest()
276 276 files = [f for f in files if not samefile(f)]
277 277 # commit version of these files as defined by head
278 278 headmf = last.manifest()
279 279 def filectxfn(repo, ctx, path):
280 280 if path in headmf:
281 281 fctx = last[path]
282 282 flags = fctx.flags()
283 283 mctx = context.memfilectx(repo,
284 284 fctx.path(), fctx.data(),
285 285 islink='l' in flags,
286 286 isexec='x' in flags,
287 287 copied=copied.get(path))
288 288 return mctx
289 289 return None
290 290
291 291 if commitopts.get('message'):
292 292 message = commitopts['message']
293 293 else:
294 294 message = first.description()
295 295 user = commitopts.get('user')
296 296 date = commitopts.get('date')
297 297 extra = commitopts.get('extra')
298 298
299 299 parents = (first.p1().node(), first.p2().node())
300 300 editor = None
301 301 if not commitopts.get('rollup'):
302 302 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
303 303 new = context.memctx(repo,
304 304 parents=parents,
305 305 text=message,
306 306 files=files,
307 307 filectxfn=filectxfn,
308 308 user=user,
309 309 date=date,
310 310 extra=extra,
311 311 editor=editor)
312 312 return repo.commitctx(new)
313 313
314 314 def pick(ui, repo, ctx, ha, opts):
315 315 oldctx = repo[ha]
316 316 if oldctx.parents()[0] == ctx:
317 317 ui.debug('node %s unchanged\n' % ha)
318 318 return oldctx, []
319 319 hg.update(repo, ctx.node())
320 320 stats = applychanges(ui, repo, oldctx, opts)
321 321 if stats and stats[3] > 0:
322 322 raise error.InterventionRequired(_('Fix up the change and run '
323 323 'hg histedit --continue'))
324 324 # drop the second merge parent
325 325 commit = commitfuncfor(repo, oldctx)
326 326 n = commit(text=oldctx.description(), user=oldctx.user(),
327 327 date=oldctx.date(), extra=oldctx.extra())
328 328 if n is None:
329 329 ui.warn(_('%s: empty changeset\n')
330 330 % node.hex(ha))
331 331 return ctx, []
332 332 new = repo[n]
333 333 return new, [(oldctx.node(), (n,))]
334 334
335 335
336 336 def edit(ui, repo, ctx, ha, opts):
337 337 oldctx = repo[ha]
338 338 hg.update(repo, ctx.node())
339 339 applychanges(ui, repo, oldctx, opts)
340 340 raise error.InterventionRequired(
341 341 _('Make changes as needed, you may commit or record as needed now.\n'
342 342 'When you are finished, run hg histedit --continue to resume.'))
343 343
344 344 def rollup(ui, repo, ctx, ha, opts):
345 345 rollupopts = opts.copy()
346 346 rollupopts['rollup'] = True
347 347 return fold(ui, repo, ctx, ha, rollupopts)
348 348
349 349 def fold(ui, repo, ctx, ha, opts):
350 350 oldctx = repo[ha]
351 351 hg.update(repo, ctx.node())
352 352 stats = applychanges(ui, repo, oldctx, opts)
353 353 if stats and stats[3] > 0:
354 354 raise error.InterventionRequired(
355 355 _('Fix up the change and run hg histedit --continue'))
356 356 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
357 357 date=oldctx.date(), extra=oldctx.extra())
358 358 if n is None:
359 359 ui.warn(_('%s: empty changeset')
360 360 % node.hex(ha))
361 361 return ctx, []
362 362 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
363 363
364 364 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
365 365 parent = ctx.parents()[0].node()
366 366 hg.update(repo, parent)
367 367 ### prepare new commit data
368 368 commitopts = opts.copy()
369 369 commitopts['user'] = ctx.user()
370 370 # commit message
371 371 if opts.get('rollup'):
372 372 newmessage = ctx.description()
373 373 else:
374 374 newmessage = '\n***\n'.join(
375 375 [ctx.description()] +
376 376 [repo[r].description() for r in internalchanges] +
377 377 [oldctx.description()]) + '\n'
378 378 commitopts['message'] = newmessage
379 379 # date
380 380 commitopts['date'] = max(ctx.date(), oldctx.date())
381 381 extra = ctx.extra().copy()
382 382 # histedit_source
383 383 # note: ctx is likely a temporary commit but that the best we can do here
384 384 # This is sufficient to solve issue3681 anyway
385 385 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
386 386 commitopts['extra'] = extra
387 387 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
388 388 try:
389 389 phasemin = max(ctx.phase(), oldctx.phase())
390 390 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
391 391 n = collapse(repo, ctx, repo[newnode], commitopts)
392 392 finally:
393 393 repo.ui.restoreconfig(phasebackup)
394 394 if n is None:
395 395 return ctx, []
396 396 hg.update(repo, n)
397 397 replacements = [(oldctx.node(), (newnode,)),
398 398 (ctx.node(), (n,)),
399 399 (newnode, (n,)),
400 400 ]
401 401 for ich in internalchanges:
402 402 replacements.append((ich, (n,)))
403 403 return repo[n], replacements
404 404
405 405 def drop(ui, repo, ctx, ha, opts):
406 406 return ctx, [(repo[ha].node(), ())]
407 407
408 408
409 409 def message(ui, repo, ctx, ha, opts):
410 410 oldctx = repo[ha]
411 411 hg.update(repo, ctx.node())
412 412 stats = applychanges(ui, repo, oldctx, opts)
413 413 if stats and stats[3] > 0:
414 414 raise error.InterventionRequired(
415 415 _('Fix up the change and run hg histedit --continue'))
416 416 message = oldctx.description()
417 417 commit = commitfuncfor(repo, oldctx)
418 418 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
419 419 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
420 420 extra=oldctx.extra(),
421 421 editor=editor)
422 422 newctx = repo[new]
423 423 if oldctx.node() != newctx.node():
424 424 return newctx, [(oldctx.node(), (new,))]
425 425 # We didn't make an edit, so just indicate no replaced nodes
426 426 return newctx, []
427 427
428 428 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
429 429 """utility function to find the first outgoing changeset
430 430
431 431 Used by initialisation code"""
432 432 dest = ui.expandpath(remote or 'default-push', remote or 'default')
433 433 dest, revs = hg.parseurl(dest, None)[:2]
434 434 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
435 435
436 436 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
437 437 other = hg.peer(repo, opts, dest)
438 438
439 439 if revs:
440 440 revs = [repo.lookup(rev) for rev in revs]
441 441
442 442 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
443 443 if not outgoing.missing:
444 444 raise util.Abort(_('no outgoing ancestors'))
445 445 roots = list(repo.revs("roots(%ln)", outgoing.missing))
446 446 if 1 < len(roots):
447 447 msg = _('there are ambiguous outgoing revisions')
448 448 hint = _('see "hg help histedit" for more detail')
449 449 raise util.Abort(msg, hint=hint)
450 450 return repo.lookup(roots[0])
451 451
452 452 actiontable = {'p': pick,
453 453 'pick': pick,
454 454 'e': edit,
455 455 'edit': edit,
456 456 'f': fold,
457 457 'fold': fold,
458 458 'r': rollup,
459 459 'roll': rollup,
460 460 'd': drop,
461 461 'drop': drop,
462 462 'm': message,
463 463 'mess': message,
464 464 }
465 465
466 466 @command('histedit',
467 467 [('', 'commands', '',
468 468 _('Read history edits from the specified file.')),
469 469 ('c', 'continue', False, _('continue an edit already in progress')),
470 470 ('k', 'keep', False,
471 471 _("don't strip old nodes after edit is complete")),
472 472 ('', 'abort', False, _('abort an edit in progress')),
473 473 ('o', 'outgoing', False, _('changesets not found in destination')),
474 474 ('f', 'force', False,
475 475 _('force outgoing even for unrelated repositories')),
476 476 ('r', 'rev', [], _('first revision to be edited'))],
477 477 _("ANCESTOR | --outgoing [URL]"))
478 478 def histedit(ui, repo, *freeargs, **opts):
479 479 """interactively edit changeset history
480 480
481 481 This command edits changesets between ANCESTOR and the parent of
482 482 the working directory.
483 483
484 484 With --outgoing, this edits changesets not found in the
485 485 destination repository. If URL of the destination is omitted, the
486 486 'default-push' (or 'default') path will be used.
487 487
488 488 For safety, this command is aborted, also if there are ambiguous
489 489 outgoing revisions which may confuse users: for example, there are
490 490 multiple branches containing outgoing revisions.
491 491
492 492 Use "min(outgoing() and ::.)" or similar revset specification
493 493 instead of --outgoing to specify edit target revision exactly in
494 494 such ambiguous situation. See :hg:`help revsets` for detail about
495 495 selecting revisions.
496 496
497 497 Returns 0 on success, 1 if user intervention is required (not only
498 498 for intentional "edit" command, but also for resolving unexpected
499 499 conflicts).
500 500 """
501 501 lock = wlock = None
502 502 try:
503 503 wlock = repo.wlock()
504 504 lock = repo.lock()
505 505 _histedit(ui, repo, *freeargs, **opts)
506 506 finally:
507 507 release(lock, wlock)
508 508
509 509 def _histedit(ui, repo, *freeargs, **opts):
510 510 # TODO only abort if we try and histedit mq patches, not just
511 511 # blanket if mq patches are applied somewhere
512 512 mq = getattr(repo, 'mq', None)
513 513 if mq and mq.applied:
514 514 raise util.Abort(_('source has mq patches applied'))
515 515
516 516 # basic argument incompatibility processing
517 517 outg = opts.get('outgoing')
518 518 cont = opts.get('continue')
519 519 abort = opts.get('abort')
520 520 force = opts.get('force')
521 521 rules = opts.get('commands', '')
522 522 revs = opts.get('rev', [])
523 523 goal = 'new' # This invocation goal, in new, continue, abort
524 524 if force and not outg:
525 525 raise util.Abort(_('--force only allowed with --outgoing'))
526 526 if cont:
527 527 if util.any((outg, abort, revs, freeargs, rules)):
528 528 raise util.Abort(_('no arguments allowed with --continue'))
529 529 goal = 'continue'
530 530 elif abort:
531 531 if util.any((outg, revs, freeargs, rules)):
532 532 raise util.Abort(_('no arguments allowed with --abort'))
533 533 goal = 'abort'
534 534 else:
535 535 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
536 536 raise util.Abort(_('history edit already in progress, try '
537 537 '--continue or --abort'))
538 538 if outg:
539 539 if revs:
540 540 raise util.Abort(_('no revisions allowed with --outgoing'))
541 541 if len(freeargs) > 1:
542 542 raise util.Abort(
543 543 _('only one repo argument allowed with --outgoing'))
544 544 else:
545 545 revs.extend(freeargs)
546 546 if len(revs) != 1:
547 547 raise util.Abort(
548 548 _('histedit requires exactly one ancestor revision'))
549 549
550 550
551 551 if goal == 'continue':
552 552 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
553 553 parentctx = repo[parentctxnode]
554 554 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
555 555 replacements.extend(repl)
556 556 elif goal == 'abort':
557 557 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
558 558 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
559 559 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
560 560 # check whether we should update away
561 561 parentnodes = [c.node() for c in repo[None].parents()]
562 562 for n in leafs | set([parentctxnode]):
563 563 if n in parentnodes:
564 564 hg.clean(repo, topmost)
565 565 break
566 566 else:
567 567 pass
568 568 cleanupnode(ui, repo, 'created', tmpnodes)
569 569 cleanupnode(ui, repo, 'temp', leafs)
570 570 os.unlink(os.path.join(repo.path, 'histedit-state'))
571 571 return
572 572 else:
573 573 cmdutil.checkunfinished(repo)
574 574 cmdutil.bailifchanged(repo)
575 575
576 576 topmost, empty = repo.dirstate.parents()
577 577 if outg:
578 578 if freeargs:
579 579 remote = freeargs[0]
580 580 else:
581 581 remote = None
582 582 root = findoutgoing(ui, repo, remote, force, opts)
583 583 else:
584 584 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
585 585 if len(rr) != 1:
586 586 raise util.Abort(_('The specified revisions must have '
587 587 'exactly one common root'))
588 588 root = rr[0].node()
589 589
590 590 keep = opts.get('keep', False)
591 591 revs = between(repo, root, topmost, keep)
592 592 if not revs:
593 593 raise util.Abort(_('%s is not an ancestor of working directory') %
594 594 node.short(root))
595 595
596 596 ctxs = [repo[r] for r in revs]
597 597 if not rules:
598 598 rules = '\n'.join([makedesc(c) for c in ctxs])
599 599 rules += '\n\n'
600 600 rules += editcomment % (node.short(root), node.short(topmost))
601 601 rules = ui.edit(rules, ui.username())
602 602 # Save edit rules in .hg/histedit-last-edit.txt in case
603 603 # the user needs to ask for help after something
604 604 # surprising happens.
605 605 f = open(repo.join('histedit-last-edit.txt'), 'w')
606 606 f.write(rules)
607 607 f.close()
608 608 else:
609 609 if rules == '-':
610 610 f = sys.stdin
611 611 else:
612 612 f = open(rules)
613 613 rules = f.read()
614 614 f.close()
615 615 rules = [l for l in (r.strip() for r in rules.splitlines())
616 616 if l and not l.startswith('#')]
617 617 rules = verifyrules(rules, repo, ctxs)
618 618
619 619 parentctx = repo[root].parents()[0]
620 620 replacements = []
621 621
622 622
623 623 while rules:
624 624 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
625 625 action, ha = rules.pop(0)
626 626 ui.debug('histedit: processing %s %s\n' % (action, ha))
627 627 actfunc = actiontable[action]
628 628 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
629 629 replacements.extend(replacement_)
630 630
631 631 hg.update(repo, parentctx.node())
632 632
633 633 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
634 634 if mapping:
635 635 for prec, succs in mapping.iteritems():
636 636 if not succs:
637 637 ui.debug('histedit: %s is dropped\n' % node.short(prec))
638 638 else:
639 639 ui.debug('histedit: %s is replaced by %s\n' % (
640 640 node.short(prec), node.short(succs[0])))
641 641 if len(succs) > 1:
642 642 m = 'histedit: %s'
643 643 for n in succs[1:]:
644 644 ui.debug(m % node.short(n))
645 645
646 646 if not keep:
647 647 if mapping:
648 648 movebookmarks(ui, repo, mapping, topmost, ntm)
649 649 # TODO update mq state
650 650 if obsolete._enabled:
651 651 markers = []
652 652 # sort by revision number because it sound "right"
653 653 for prec in sorted(mapping, key=repo.changelog.rev):
654 654 succs = mapping[prec]
655 655 markers.append((repo[prec],
656 656 tuple(repo[s] for s in succs)))
657 657 if markers:
658 658 obsolete.createmarkers(repo, markers)
659 659 else:
660 660 cleanupnode(ui, repo, 'replaced', mapping)
661 661
662 662 cleanupnode(ui, repo, 'temp', tmpnodes)
663 663 os.unlink(os.path.join(repo.path, 'histedit-state'))
664 664 if os.path.exists(repo.sjoin('undo')):
665 665 os.unlink(repo.sjoin('undo'))
666 666
667 667 def gatherchildren(repo, ctx):
668 668 # is there any new commit between the expected parent and "."
669 669 #
670 670 # note: does not take non linear new change in account (but previous
671 671 # implementation didn't used them anyway (issue3655)
672 672 newchildren = [c.node() for c in repo.set('(%d::.)', ctx)]
673 673 if ctx.node() != node.nullid:
674 674 if not newchildren:
675 675 # `ctx` should match but no result. This means that
676 676 # currentnode is not a descendant from ctx.
677 677 msg = _('%s is not an ancestor of working directory')
678 678 hint = _('use "histedit --abort" to clear broken state')
679 679 raise util.Abort(msg % ctx, hint=hint)
680 680 newchildren.pop(0) # remove ctx
681 681 return newchildren
682 682
683 683 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
684 684 action, currentnode = rules.pop(0)
685 685 ctx = repo[currentnode]
686 686
687 687 newchildren = gatherchildren(repo, parentctx)
688 688
689 689 # Commit dirty working directory if necessary
690 690 new = None
691 691 m, a, r, d = repo.status()[:4]
692 692 if m or a or r or d:
693 693 # prepare the message for the commit to comes
694 694 if action in ('f', 'fold', 'r', 'roll'):
695 695 message = 'fold-temp-revision %s' % currentnode
696 696 else:
697 697 message = ctx.description()
698 698 editopt = action in ('e', 'edit', 'm', 'mess')
699 699 canonaction = {'e': 'edit', 'm': 'mess', 'p': 'pick'}
700 700 editform = 'histedit.%s' % canonaction.get(action, action)
701 701 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
702 702 commit = commitfuncfor(repo, ctx)
703 703 new = commit(text=message, user=ctx.user(),
704 704 date=ctx.date(), extra=ctx.extra(),
705 705 editor=editor)
706 706 if new is not None:
707 707 newchildren.append(new)
708 708
709 709 replacements = []
710 710 # track replacements
711 711 if ctx.node() not in newchildren:
712 712 # note: new children may be empty when the changeset is dropped.
713 713 # this happen e.g during conflicting pick where we revert content
714 714 # to parent.
715 715 replacements.append((ctx.node(), tuple(newchildren)))
716 716
717 717 if action in ('f', 'fold', 'r', 'roll'):
718 718 if newchildren:
719 719 # finalize fold operation if applicable
720 720 if new is None:
721 721 new = newchildren[-1]
722 722 else:
723 723 newchildren.pop() # remove new from internal changes
724 724 foldopts = opts
725 725 if action in ('r', 'roll'):
726 726 foldopts = foldopts.copy()
727 727 foldopts['rollup'] = True
728 728 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new,
729 729 foldopts, newchildren)
730 730 replacements.extend(repl)
731 731 else:
732 732 # newchildren is empty if the fold did not result in any commit
733 733 # this happen when all folded change are discarded during the
734 734 # merge.
735 735 replacements.append((ctx.node(), (parentctx.node(),)))
736 736 elif newchildren:
737 737 # otherwise update "parentctx" before proceeding to further operation
738 738 parentctx = repo[newchildren[-1]]
739 739 return parentctx, replacements
740 740
741 741
742 742 def between(repo, old, new, keep):
743 743 """select and validate the set of revision to edit
744 744
745 745 When keep is false, the specified set can't have children."""
746 746 ctxs = list(repo.set('%n::%n', old, new))
747 747 if ctxs and not keep:
748 748 if (not obsolete._enabled and
749 749 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
750 750 raise util.Abort(_('cannot edit history that would orphan nodes'))
751 751 if repo.revs('(%ld) and merge()', ctxs):
752 752 raise util.Abort(_('cannot edit history that contains merges'))
753 753 root = ctxs[0] # list is already sorted by repo.set
754 if not root.phase():
754 if not root.mutable():
755 755 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
756 756 return [c.node() for c in ctxs]
757 757
758 758
759 759 def writestate(repo, parentnode, rules, keep, topmost, replacements):
760 760 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
761 761 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
762 762 fp.close()
763 763
764 764 def readstate(repo):
765 765 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
766 766 """
767 767 try:
768 768 fp = open(os.path.join(repo.path, 'histedit-state'))
769 769 except IOError, err:
770 770 if err.errno != errno.ENOENT:
771 771 raise
772 772 raise util.Abort(_('no histedit in progress'))
773 773 return pickle.load(fp)
774 774
775 775
776 776 def makedesc(c):
777 777 """build a initial action line for a ctx `c`
778 778
779 779 line are in the form:
780 780
781 781 pick <hash> <rev> <summary>
782 782 """
783 783 summary = ''
784 784 if c.description():
785 785 summary = c.description().splitlines()[0]
786 786 line = 'pick %s %d %s' % (c, c.rev(), summary)
787 787 # trim to 80 columns so it's not stupidly wide in my editor
788 788 return util.ellipsis(line, 80)
789 789
790 790 def verifyrules(rules, repo, ctxs):
791 791 """Verify that there exists exactly one edit rule per given changeset.
792 792
793 793 Will abort if there are to many or too few rules, a malformed rule,
794 794 or a rule on a changeset outside of the user-given range.
795 795 """
796 796 parsed = []
797 797 expected = set(str(c) for c in ctxs)
798 798 seen = set()
799 799 for r in rules:
800 800 if ' ' not in r:
801 801 raise util.Abort(_('malformed line "%s"') % r)
802 802 action, rest = r.split(' ', 1)
803 803 ha = rest.strip().split(' ', 1)[0]
804 804 try:
805 805 ha = str(repo[ha]) # ensure its a short hash
806 806 except error.RepoError:
807 807 raise util.Abort(_('unknown changeset %s listed') % ha)
808 808 if ha not in expected:
809 809 raise util.Abort(
810 810 _('may not use changesets other than the ones listed'))
811 811 if ha in seen:
812 812 raise util.Abort(_('duplicated command for changeset %s') % ha)
813 813 seen.add(ha)
814 814 if action not in actiontable:
815 815 raise util.Abort(_('unknown action "%s"') % action)
816 816 parsed.append([action, ha])
817 817 missing = sorted(expected - seen) # sort to stabilize output
818 818 if missing:
819 819 raise util.Abort(_('missing rules for changeset %s') % missing[0],
820 820 hint=_('do you want to use the drop action?'))
821 821 return parsed
822 822
823 823 def processreplacement(repo, replacements):
824 824 """process the list of replacements to return
825 825
826 826 1) the final mapping between original and created nodes
827 827 2) the list of temporary node created by histedit
828 828 3) the list of new commit created by histedit"""
829 829 allsuccs = set()
830 830 replaced = set()
831 831 fullmapping = {}
832 832 # initialise basic set
833 833 # fullmapping record all operation recorded in replacement
834 834 for rep in replacements:
835 835 allsuccs.update(rep[1])
836 836 replaced.add(rep[0])
837 837 fullmapping.setdefault(rep[0], set()).update(rep[1])
838 838 new = allsuccs - replaced
839 839 tmpnodes = allsuccs & replaced
840 840 # Reduce content fullmapping into direct relation between original nodes
841 841 # and final node created during history edition
842 842 # Dropped changeset are replaced by an empty list
843 843 toproceed = set(fullmapping)
844 844 final = {}
845 845 while toproceed:
846 846 for x in list(toproceed):
847 847 succs = fullmapping[x]
848 848 for s in list(succs):
849 849 if s in toproceed:
850 850 # non final node with unknown closure
851 851 # We can't process this now
852 852 break
853 853 elif s in final:
854 854 # non final node, replace with closure
855 855 succs.remove(s)
856 856 succs.update(final[s])
857 857 else:
858 858 final[x] = succs
859 859 toproceed.remove(x)
860 860 # remove tmpnodes from final mapping
861 861 for n in tmpnodes:
862 862 del final[n]
863 863 # we expect all changes involved in final to exist in the repo
864 864 # turn `final` into list (topologically sorted)
865 865 nm = repo.changelog.nodemap
866 866 for prec, succs in final.items():
867 867 final[prec] = sorted(succs, key=nm.get)
868 868
869 869 # computed topmost element (necessary for bookmark)
870 870 if new:
871 871 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
872 872 elif not final:
873 873 # Nothing rewritten at all. we won't need `newtopmost`
874 874 # It is the same as `oldtopmost` and `processreplacement` know it
875 875 newtopmost = None
876 876 else:
877 877 # every body died. The newtopmost is the parent of the root.
878 878 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
879 879
880 880 return final, tmpnodes, new, newtopmost
881 881
882 882 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
883 883 """Move bookmark from old to newly created node"""
884 884 if not mapping:
885 885 # if nothing got rewritten there is not purpose for this function
886 886 return
887 887 moves = []
888 888 for bk, old in sorted(repo._bookmarks.iteritems()):
889 889 if old == oldtopmost:
890 890 # special case ensure bookmark stay on tip.
891 891 #
892 892 # This is arguably a feature and we may only want that for the
893 893 # active bookmark. But the behavior is kept compatible with the old
894 894 # version for now.
895 895 moves.append((bk, newtopmost))
896 896 continue
897 897 base = old
898 898 new = mapping.get(base, None)
899 899 if new is None:
900 900 continue
901 901 while not new:
902 902 # base is killed, trying with parent
903 903 base = repo[base].p1().node()
904 904 new = mapping.get(base, (base,))
905 905 # nothing to move
906 906 moves.append((bk, new[-1]))
907 907 if moves:
908 908 marks = repo._bookmarks
909 909 for mark, new in moves:
910 910 old = marks[mark]
911 911 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
912 912 % (mark, node.short(old), node.short(new)))
913 913 marks[mark] = new
914 914 marks.write()
915 915
916 916 def cleanupnode(ui, repo, name, nodes):
917 917 """strip a group of nodes from the repository
918 918
919 919 The set of node to strip may contains unknown nodes."""
920 920 ui.debug('should strip %s nodes %s\n' %
921 921 (name, ', '.join([node.short(n) for n in nodes])))
922 922 lock = None
923 923 try:
924 924 lock = repo.lock()
925 925 # Find all node that need to be stripped
926 926 # (we hg %lr instead of %ln to silently ignore unknown item
927 927 nm = repo.changelog.nodemap
928 928 nodes = [n for n in nodes if n in nm]
929 929 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
930 930 for c in roots:
931 931 # We should process node in reverse order to strip tip most first.
932 932 # but this trigger a bug in changegroup hook.
933 933 # This would reduce bundle overhead
934 934 repair.strip(ui, repo, c)
935 935 finally:
936 936 release(lock)
937 937
938 938 def summaryhook(ui, repo):
939 939 if not os.path.exists(repo.join('histedit-state')):
940 940 return
941 941 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
942 942 if rules:
943 943 # i18n: column positioning for "hg summary"
944 944 ui.write(_('hist: %s (histedit --continue)\n') %
945 945 (ui.label(_('%d remaining'), 'histedit.remaining') %
946 946 len(rules)))
947 947
948 948 def extsetup(ui):
949 949 cmdutil.summaryhooks.add('histedit', summaryhook)
950 950 cmdutil.unfinishedstates.append(
951 951 ['histedit-state', False, True, _('histedit in progress'),
952 952 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
General Comments 0
You need to be logged in to leave comments. Login now