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