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