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