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