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