##// END OF EJS Templates
histedit: let the state expose a context but serialize correctly to nodes...
David Soria Parra -
r22979:bb22cd70 default
parent child Browse files
Show More
@@ -1,975 +1,971 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 def __init__(self, repo, parentctxnode=None, rules=None, keep=None,
192 def __init__(self, repo, parentctx=None, rules=None, keep=None,
193 193 topmost=None, replacements=None):
194 194 self.repo = repo
195 self.parentctxnode = parentctxnode
196 195 self.rules = rules
197 196 self.keep = keep
198 197 self.topmost = topmost
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 pickle.dump((self.parentctxnode, self.rules, self.keep,
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 parentctx = repo[state.parentctxnode]
573 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, state.rules,
574 opts)
572 state.parentctx, repl = bootstrapcontinue(ui, repo, state.parentctx,
573 state.rules, opts)
575 574 state.replacements.extend(repl)
576 state.parentctxnode = parentctx.node()
577 575 elif goal == 'abort':
578 576 state = readstate(repo)
579 577 mapping, tmpnodes, leafs, _ntm = processreplacement(repo,
580 578 state.replacements)
581 579 ui.debug('restore wc to old parent %s\n' % node.short(state.topmost))
582 580 # check whether we should update away
583 581 parentnodes = [c.node() for c in repo[None].parents()]
584 for n in leafs | set([state.parentctxnode]):
582 for n in leafs | set([state.parentctx.node()]):
585 583 if n in parentnodes:
586 584 hg.clean(repo, state.topmost)
587 585 break
588 586 else:
589 587 pass
590 588 cleanupnode(ui, repo, 'created', tmpnodes)
591 589 cleanupnode(ui, repo, 'temp', leafs)
592 590 state.clear()
593 591 return
594 592 else:
595 593 cmdutil.checkunfinished(repo)
596 594 cmdutil.bailifchanged(repo)
597 595
598 596 topmost, empty = repo.dirstate.parents()
599 597 if outg:
600 598 if freeargs:
601 599 remote = freeargs[0]
602 600 else:
603 601 remote = None
604 602 root = findoutgoing(ui, repo, remote, force, opts)
605 603 else:
606 604 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
607 605 if len(rr) != 1:
608 606 raise util.Abort(_('The specified revisions must have '
609 607 'exactly one common root'))
610 608 root = rr[0].node()
611 609
612 610 revs = between(repo, root, topmost, keep)
613 611 if not revs:
614 612 raise util.Abort(_('%s is not an ancestor of working directory') %
615 613 node.short(root))
616 614
617 615 ctxs = [repo[r] for r in revs]
618 616 if not rules:
619 617 rules = '\n'.join([makedesc(c) for c in ctxs])
620 618 rules += '\n\n'
621 619 rules += editcomment % (node.short(root), node.short(topmost))
622 620 rules = ui.edit(rules, ui.username())
623 621 # Save edit rules in .hg/histedit-last-edit.txt in case
624 622 # the user needs to ask for help after something
625 623 # surprising happens.
626 624 f = open(repo.join('histedit-last-edit.txt'), 'w')
627 625 f.write(rules)
628 626 f.close()
629 627 else:
630 628 if rules == '-':
631 629 f = sys.stdin
632 630 else:
633 631 f = open(rules)
634 632 rules = f.read()
635 633 f.close()
636 634 rules = [l for l in (r.strip() for r in rules.splitlines())
637 635 if l and not l.startswith('#')]
638 636 rules = verifyrules(rules, repo, ctxs)
639 637
640 638 parentctx = repo[root].parents()[0]
641 639
642 state = histeditstate(repo, parentctx.node(), rules, keep,
640 state = histeditstate(repo, parentctx, rules, keep,
643 641 topmost, replacements)
644 642
645 643 while state.rules:
646 644 state.write()
647 645 action, ha = state.rules.pop(0)
648 646 ui.debug('histedit: processing %s %s\n' % (action, ha))
649 647 actfunc = actiontable[action]
650 parentctx = repo[state.parentctxnode]
651 parentctx, replacement_ = actfunc(ui, repo, parentctx,
648 state.parentctx, replacement_ = actfunc(ui, repo, state.parentctx,
652 649 ha, opts)
653 state.parentctxnode = parentctx.node()
654 650 state.replacements.extend(replacement_)
655 651
656 hg.update(repo, state.parentctxnode)
652 hg.update(repo, state.parentctx.node())
657 653
658 654 mapping, tmpnodes, created, ntm = processreplacement(repo,
659 655 state.replacements)
660 656 if mapping:
661 657 for prec, succs in mapping.iteritems():
662 658 if not succs:
663 659 ui.debug('histedit: %s is dropped\n' % node.short(prec))
664 660 else:
665 661 ui.debug('histedit: %s is replaced by %s\n' % (
666 662 node.short(prec), node.short(succs[0])))
667 663 if len(succs) > 1:
668 664 m = 'histedit: %s'
669 665 for n in succs[1:]:
670 666 ui.debug(m % node.short(n))
671 667
672 668 if not keep:
673 669 if mapping:
674 670 movebookmarks(ui, repo, mapping, state.topmost, ntm)
675 671 # TODO update mq state
676 672 if obsolete.isenabled(repo, obsolete.createmarkersopt):
677 673 markers = []
678 674 # sort by revision number because it sound "right"
679 675 for prec in sorted(mapping, key=repo.changelog.rev):
680 676 succs = mapping[prec]
681 677 markers.append((repo[prec],
682 678 tuple(repo[s] for s in succs)))
683 679 if markers:
684 680 obsolete.createmarkers(repo, markers)
685 681 else:
686 682 cleanupnode(ui, repo, 'replaced', mapping)
687 683
688 684 cleanupnode(ui, repo, 'temp', tmpnodes)
689 685 state.clear()
690 686 if os.path.exists(repo.sjoin('undo')):
691 687 os.unlink(repo.sjoin('undo'))
692 688
693 689 def gatherchildren(repo, ctx):
694 690 # is there any new commit between the expected parent and "."
695 691 #
696 692 # note: does not take non linear new change in account (but previous
697 693 # implementation didn't used them anyway (issue3655)
698 694 newchildren = [c.node() for c in repo.set('(%d::.)', ctx)]
699 695 if ctx.node() != node.nullid:
700 696 if not newchildren:
701 697 # `ctx` should match but no result. This means that
702 698 # currentnode is not a descendant from ctx.
703 699 msg = _('%s is not an ancestor of working directory')
704 700 hint = _('use "histedit --abort" to clear broken state')
705 701 raise util.Abort(msg % ctx, hint=hint)
706 702 newchildren.pop(0) # remove ctx
707 703 return newchildren
708 704
709 705 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
710 706 action, currentnode = rules.pop(0)
711 707 ctx = repo[currentnode]
712 708
713 709 newchildren = gatherchildren(repo, parentctx)
714 710
715 711 # Commit dirty working directory if necessary
716 712 new = None
717 713 s = repo.status()
718 714 if s.modified or s.added or s.removed or s.deleted:
719 715 # prepare the message for the commit to comes
720 716 if action in ('f', 'fold', 'r', 'roll'):
721 717 message = 'fold-temp-revision %s' % currentnode
722 718 else:
723 719 message = ctx.description()
724 720 editopt = action in ('e', 'edit', 'm', 'mess')
725 721 canonaction = {'e': 'edit', 'm': 'mess', 'p': 'pick'}
726 722 editform = 'histedit.%s' % canonaction.get(action, action)
727 723 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
728 724 commit = commitfuncfor(repo, ctx)
729 725 new = commit(text=message, user=ctx.user(),
730 726 date=ctx.date(), extra=ctx.extra(),
731 727 editor=editor)
732 728 if new is not None:
733 729 newchildren.append(new)
734 730
735 731 replacements = []
736 732 # track replacements
737 733 if ctx.node() not in newchildren:
738 734 # note: new children may be empty when the changeset is dropped.
739 735 # this happen e.g during conflicting pick where we revert content
740 736 # to parent.
741 737 replacements.append((ctx.node(), tuple(newchildren)))
742 738
743 739 if action in ('f', 'fold', 'r', 'roll'):
744 740 if newchildren:
745 741 # finalize fold operation if applicable
746 742 if new is None:
747 743 new = newchildren[-1]
748 744 else:
749 745 newchildren.pop() # remove new from internal changes
750 746 foldopts = opts
751 747 if action in ('r', 'roll'):
752 748 foldopts = foldopts.copy()
753 749 foldopts['rollup'] = True
754 750 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new,
755 751 foldopts, newchildren)
756 752 replacements.extend(repl)
757 753 else:
758 754 # newchildren is empty if the fold did not result in any commit
759 755 # this happen when all folded change are discarded during the
760 756 # merge.
761 757 replacements.append((ctx.node(), (parentctx.node(),)))
762 758 elif newchildren:
763 759 # otherwise update "parentctx" before proceeding to further operation
764 760 parentctx = repo[newchildren[-1]]
765 761 return parentctx, replacements
766 762
767 763
768 764 def between(repo, old, new, keep):
769 765 """select and validate the set of revision to edit
770 766
771 767 When keep is false, the specified set can't have children."""
772 768 ctxs = list(repo.set('%n::%n', old, new))
773 769 if ctxs and not keep:
774 770 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
775 771 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
776 772 raise util.Abort(_('cannot edit history that would orphan nodes'))
777 773 if repo.revs('(%ld) and merge()', ctxs):
778 774 raise util.Abort(_('cannot edit history that contains merges'))
779 775 root = ctxs[0] # list is already sorted by repo.set
780 776 if not root.mutable():
781 777 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
782 778 return [c.node() for c in ctxs]
783 779
784 780 def readstate(repo):
785 781 """Reads a state from file and returns a histeditstate object
786 782 """
787 783 try:
788 784 fp = repo.vfs('histedit-state', 'r')
789 785 except IOError, err:
790 786 if err.errno != errno.ENOENT:
791 787 raise
792 788 raise util.Abort(_('no histedit in progress'))
793 789
794 790 (parentctxnode, rules, keep, topmost, replacements) = pickle.load(fp)
795 791
796 return histeditstate(repo, parentctxnode, rules,
792 return histeditstate(repo, repo[parentctxnode], rules,
797 793 keep, topmost, replacements)
798 794
799 795 def makedesc(c):
800 796 """build a initial action line for a ctx `c`
801 797
802 798 line are in the form:
803 799
804 800 pick <hash> <rev> <summary>
805 801 """
806 802 summary = ''
807 803 if c.description():
808 804 summary = c.description().splitlines()[0]
809 805 line = 'pick %s %d %s' % (c, c.rev(), summary)
810 806 # trim to 80 columns so it's not stupidly wide in my editor
811 807 return util.ellipsis(line, 80)
812 808
813 809 def verifyrules(rules, repo, ctxs):
814 810 """Verify that there exists exactly one edit rule per given changeset.
815 811
816 812 Will abort if there are to many or too few rules, a malformed rule,
817 813 or a rule on a changeset outside of the user-given range.
818 814 """
819 815 parsed = []
820 816 expected = set(str(c) for c in ctxs)
821 817 seen = set()
822 818 for r in rules:
823 819 if ' ' not in r:
824 820 raise util.Abort(_('malformed line "%s"') % r)
825 821 action, rest = r.split(' ', 1)
826 822 ha = rest.strip().split(' ', 1)[0]
827 823 try:
828 824 ha = str(repo[ha]) # ensure its a short hash
829 825 except error.RepoError:
830 826 raise util.Abort(_('unknown changeset %s listed') % ha)
831 827 if ha not in expected:
832 828 raise util.Abort(
833 829 _('may not use changesets other than the ones listed'))
834 830 if ha in seen:
835 831 raise util.Abort(_('duplicated command for changeset %s') % ha)
836 832 seen.add(ha)
837 833 if action not in actiontable:
838 834 raise util.Abort(_('unknown action "%s"') % action)
839 835 parsed.append([action, ha])
840 836 missing = sorted(expected - seen) # sort to stabilize output
841 837 if missing:
842 838 raise util.Abort(_('missing rules for changeset %s') % missing[0],
843 839 hint=_('do you want to use the drop action?'))
844 840 return parsed
845 841
846 842 def processreplacement(repo, replacements):
847 843 """process the list of replacements to return
848 844
849 845 1) the final mapping between original and created nodes
850 846 2) the list of temporary node created by histedit
851 847 3) the list of new commit created by histedit"""
852 848 allsuccs = set()
853 849 replaced = set()
854 850 fullmapping = {}
855 851 # initialise basic set
856 852 # fullmapping record all operation recorded in replacement
857 853 for rep in replacements:
858 854 allsuccs.update(rep[1])
859 855 replaced.add(rep[0])
860 856 fullmapping.setdefault(rep[0], set()).update(rep[1])
861 857 new = allsuccs - replaced
862 858 tmpnodes = allsuccs & replaced
863 859 # Reduce content fullmapping into direct relation between original nodes
864 860 # and final node created during history edition
865 861 # Dropped changeset are replaced by an empty list
866 862 toproceed = set(fullmapping)
867 863 final = {}
868 864 while toproceed:
869 865 for x in list(toproceed):
870 866 succs = fullmapping[x]
871 867 for s in list(succs):
872 868 if s in toproceed:
873 869 # non final node with unknown closure
874 870 # We can't process this now
875 871 break
876 872 elif s in final:
877 873 # non final node, replace with closure
878 874 succs.remove(s)
879 875 succs.update(final[s])
880 876 else:
881 877 final[x] = succs
882 878 toproceed.remove(x)
883 879 # remove tmpnodes from final mapping
884 880 for n in tmpnodes:
885 881 del final[n]
886 882 # we expect all changes involved in final to exist in the repo
887 883 # turn `final` into list (topologically sorted)
888 884 nm = repo.changelog.nodemap
889 885 for prec, succs in final.items():
890 886 final[prec] = sorted(succs, key=nm.get)
891 887
892 888 # computed topmost element (necessary for bookmark)
893 889 if new:
894 890 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
895 891 elif not final:
896 892 # Nothing rewritten at all. we won't need `newtopmost`
897 893 # It is the same as `oldtopmost` and `processreplacement` know it
898 894 newtopmost = None
899 895 else:
900 896 # every body died. The newtopmost is the parent of the root.
901 897 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
902 898
903 899 return final, tmpnodes, new, newtopmost
904 900
905 901 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
906 902 """Move bookmark from old to newly created node"""
907 903 if not mapping:
908 904 # if nothing got rewritten there is not purpose for this function
909 905 return
910 906 moves = []
911 907 for bk, old in sorted(repo._bookmarks.iteritems()):
912 908 if old == oldtopmost:
913 909 # special case ensure bookmark stay on tip.
914 910 #
915 911 # This is arguably a feature and we may only want that for the
916 912 # active bookmark. But the behavior is kept compatible with the old
917 913 # version for now.
918 914 moves.append((bk, newtopmost))
919 915 continue
920 916 base = old
921 917 new = mapping.get(base, None)
922 918 if new is None:
923 919 continue
924 920 while not new:
925 921 # base is killed, trying with parent
926 922 base = repo[base].p1().node()
927 923 new = mapping.get(base, (base,))
928 924 # nothing to move
929 925 moves.append((bk, new[-1]))
930 926 if moves:
931 927 marks = repo._bookmarks
932 928 for mark, new in moves:
933 929 old = marks[mark]
934 930 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
935 931 % (mark, node.short(old), node.short(new)))
936 932 marks[mark] = new
937 933 marks.write()
938 934
939 935 def cleanupnode(ui, repo, name, nodes):
940 936 """strip a group of nodes from the repository
941 937
942 938 The set of node to strip may contains unknown nodes."""
943 939 ui.debug('should strip %s nodes %s\n' %
944 940 (name, ', '.join([node.short(n) for n in nodes])))
945 941 lock = None
946 942 try:
947 943 lock = repo.lock()
948 944 # Find all node that need to be stripped
949 945 # (we hg %lr instead of %ln to silently ignore unknown item
950 946 nm = repo.changelog.nodemap
951 947 nodes = sorted(n for n in nodes if n in nm)
952 948 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
953 949 for c in roots:
954 950 # We should process node in reverse order to strip tip most first.
955 951 # but this trigger a bug in changegroup hook.
956 952 # This would reduce bundle overhead
957 953 repair.strip(ui, repo, c)
958 954 finally:
959 955 release(lock)
960 956
961 957 def summaryhook(ui, repo):
962 958 if not os.path.exists(repo.join('histedit-state')):
963 959 return
964 960 state = readstate(repo)
965 961 if state.rules:
966 962 # i18n: column positioning for "hg summary"
967 963 ui.write(_('hist: %s (histedit --continue)\n') %
968 964 (ui.label(_('%d remaining'), 'histedit.remaining') %
969 965 len(state.rules)))
970 966
971 967 def extsetup(ui):
972 968 cmdutil.summaryhooks.add('histedit', summaryhook)
973 969 cmdutil.unfinishedstates.append(
974 970 ['histedit-state', False, True, _('histedit in progress'),
975 971 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
General Comments 0
You need to be logged in to leave comments. Login now