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