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