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