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