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