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