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