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