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