##// END OF EJS Templates
histedit: move logic for finding child nodes to new function...
Olle Lundberg -
r20648:0838bd2f default
parent child Browse files
Show More
@@ -1,919 +1,924 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 # d, drop = remove commit from history
40 40 # m, mess = edit message without changing commit content
41 41 #
42 42
43 43 In this file, lines beginning with ``#`` are ignored. You must specify a rule
44 44 for each revision in your history. For example, if you had meant to add gamma
45 45 before beta, and then wanted to add delta in the same revision as beta, you
46 46 would reorganize the file to look like this::
47 47
48 48 pick 030b686bedc4 Add gamma
49 49 pick c561b4e977df Add beta
50 50 fold 7c2fd3b9020c Add delta
51 51
52 52 # Edit history between c561b4e977df and 7c2fd3b9020c
53 53 #
54 54 # Commits are listed from least to most recent
55 55 #
56 56 # Commands:
57 57 # p, pick = use commit
58 58 # e, edit = use commit, but stop for amending
59 59 # f, fold = use commit, but combine it with the one above
60 60 # d, drop = remove commit from history
61 61 # m, mess = edit message without changing commit content
62 62 #
63 63
64 64 At which point you close the editor and ``histedit`` starts working. When you
65 65 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
66 66 those revisions together, offering you a chance to clean up the commit message::
67 67
68 68 Add beta
69 69 ***
70 70 Add delta
71 71
72 72 Edit the commit message to your liking, then close the editor. For
73 73 this example, let's assume that the commit message was changed to
74 74 ``Add beta and delta.`` After histedit has run and had a chance to
75 75 remove any old or temporary revisions it needed, the history looks
76 76 like this::
77 77
78 78 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
79 79 | Add beta and delta.
80 80 |
81 81 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
82 82 | Add gamma
83 83 |
84 84 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
85 85 Add alpha
86 86
87 87 Note that ``histedit`` does *not* remove any revisions (even its own temporary
88 88 ones) until after it has completed all the editing operations, so it will
89 89 probably perform several strip operations when it's done. For the above example,
90 90 it had to run strip twice. Strip can be slow depending on a variety of factors,
91 91 so you might need to be a little patient. You can choose to keep the original
92 92 revisions by passing the ``--keep`` flag.
93 93
94 94 The ``edit`` operation will drop you back to a command prompt,
95 95 allowing you to edit files freely, or even use ``hg record`` to commit
96 96 some changes as a separate commit. When you're done, any remaining
97 97 uncommitted changes will be committed as well. When done, run ``hg
98 98 histedit --continue`` to finish this step. You'll be prompted for a
99 99 new commit message, but the default commit message will be the
100 100 original message for the ``edit`` ed revision.
101 101
102 102 The ``message`` operation will give you a chance to revise a commit
103 103 message without changing the contents. It's a shortcut for doing
104 104 ``edit`` immediately followed by `hg histedit --continue``.
105 105
106 106 If ``histedit`` encounters a conflict when moving a revision (while
107 107 handling ``pick`` or ``fold``), it'll stop in a similar manner to
108 108 ``edit`` with the difference that it won't prompt you for a commit
109 109 message when done. If you decide at this point that you don't like how
110 110 much work it will be to rearrange history, or that you made a mistake,
111 111 you can use ``hg histedit --abort`` to abandon the new changes you
112 112 have made and return to the state before you attempted to edit your
113 113 history.
114 114
115 115 If we clone the histedit-ed example repository above and add four more
116 116 changes, such that we have the following history::
117 117
118 118 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
119 119 | Add theta
120 120 |
121 121 o 5 140988835471 2009-04-27 18:04 -0500 stefan
122 122 | Add eta
123 123 |
124 124 o 4 122930637314 2009-04-27 18:04 -0500 stefan
125 125 | Add zeta
126 126 |
127 127 o 3 836302820282 2009-04-27 18:04 -0500 stefan
128 128 | Add epsilon
129 129 |
130 130 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
131 131 | Add beta and delta.
132 132 |
133 133 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
134 134 | Add gamma
135 135 |
136 136 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
137 137 Add alpha
138 138
139 139 If you run ``hg histedit --outgoing`` on the clone then it is the same
140 140 as running ``hg histedit 836302820282``. If you need plan to push to a
141 141 repository that Mercurial does not detect to be related to the source
142 142 repo, you can add a ``--force`` option.
143 143 """
144 144
145 145 try:
146 146 import cPickle as pickle
147 147 pickle.dump # import now
148 148 except ImportError:
149 149 import pickle
150 150 import os
151 151 import sys
152 152
153 153 from mercurial import cmdutil
154 154 from mercurial import discovery
155 155 from mercurial import error
156 156 from mercurial import copies
157 157 from mercurial import context
158 158 from mercurial import hg
159 159 from mercurial import node
160 160 from mercurial import repair
161 161 from mercurial import scmutil
162 162 from mercurial import util
163 163 from mercurial import obsolete
164 164 from mercurial import merge as mergemod
165 165 from mercurial.lock import release
166 166 from mercurial.i18n import _
167 167
168 168 cmdtable = {}
169 169 command = cmdutil.command(cmdtable)
170 170
171 171 testedwith = 'internal'
172 172
173 173 # i18n: command names and abbreviations must remain untranslated
174 174 editcomment = _("""# Edit history between %s and %s
175 175 #
176 176 # Commits are listed from least to most recent
177 177 #
178 178 # Commands:
179 179 # p, pick = use commit
180 180 # e, edit = use commit, but stop for amending
181 181 # f, fold = use commit, but combine it with the one above
182 182 # d, drop = remove commit from history
183 183 # m, mess = edit message without changing commit content
184 184 #
185 185 """)
186 186
187 187 def commitfuncfor(repo, src):
188 188 """Build a commit function for the replacement of <src>
189 189
190 190 This function ensure we apply the same treatment to all changesets.
191 191
192 192 - Add a 'histedit_source' entry in extra.
193 193
194 194 Note that fold have its own separated logic because its handling is a bit
195 195 different and not easily factored out of the fold method.
196 196 """
197 197 phasemin = src.phase()
198 198 def commitfunc(**kwargs):
199 199 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
200 200 try:
201 201 repo.ui.setconfig('phases', 'new-commit', phasemin)
202 202 extra = kwargs.get('extra', {}).copy()
203 203 extra['histedit_source'] = src.hex()
204 204 kwargs['extra'] = extra
205 205 return repo.commit(**kwargs)
206 206 finally:
207 207 repo.ui.restoreconfig(phasebackup)
208 208 return commitfunc
209 209
210 210
211 211
212 212 def applychanges(ui, repo, ctx, opts):
213 213 """Merge changeset from ctx (only) in the current working directory"""
214 214 wcpar = repo.dirstate.parents()[0]
215 215 if ctx.p1().node() == wcpar:
216 216 # edition ar "in place" we do not need to make any merge,
217 217 # just applies changes on parent for edition
218 218 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
219 219 stats = None
220 220 else:
221 221 try:
222 222 # ui.forcemerge is an internal variable, do not document
223 223 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
224 224 stats = mergemod.update(repo, ctx.node(), True, True, False,
225 225 ctx.p1().node())
226 226 finally:
227 227 repo.ui.setconfig('ui', 'forcemerge', '')
228 228 repo.setparents(wcpar, node.nullid)
229 229 repo.dirstate.write()
230 230 # fix up dirstate for copies and renames
231 231 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
232 232 return stats
233 233
234 234 def collapse(repo, first, last, commitopts):
235 235 """collapse the set of revisions from first to last as new one.
236 236
237 237 Expected commit options are:
238 238 - message
239 239 - date
240 240 - username
241 241 Commit message is edited in all cases.
242 242
243 243 This function works in memory."""
244 244 ctxs = list(repo.set('%d::%d', first, last))
245 245 if not ctxs:
246 246 return None
247 247 base = first.parents()[0]
248 248
249 249 # commit a new version of the old changeset, including the update
250 250 # collect all files which might be affected
251 251 files = set()
252 252 for ctx in ctxs:
253 253 files.update(ctx.files())
254 254
255 255 # Recompute copies (avoid recording a -> b -> a)
256 256 copied = copies.pathcopies(base, last)
257 257
258 258 # prune files which were reverted by the updates
259 259 def samefile(f):
260 260 if f in last.manifest():
261 261 a = last.filectx(f)
262 262 if f in base.manifest():
263 263 b = base.filectx(f)
264 264 return (a.data() == b.data()
265 265 and a.flags() == b.flags())
266 266 else:
267 267 return False
268 268 else:
269 269 return f not in base.manifest()
270 270 files = [f for f in files if not samefile(f)]
271 271 # commit version of these files as defined by head
272 272 headmf = last.manifest()
273 273 def filectxfn(repo, ctx, path):
274 274 if path in headmf:
275 275 fctx = last[path]
276 276 flags = fctx.flags()
277 277 mctx = context.memfilectx(fctx.path(), fctx.data(),
278 278 islink='l' in flags,
279 279 isexec='x' in flags,
280 280 copied=copied.get(path))
281 281 return mctx
282 282 raise IOError()
283 283
284 284 if commitopts.get('message'):
285 285 message = commitopts['message']
286 286 else:
287 287 message = first.description()
288 288 user = commitopts.get('user')
289 289 date = commitopts.get('date')
290 290 extra = commitopts.get('extra')
291 291
292 292 parents = (first.p1().node(), first.p2().node())
293 293 new = context.memctx(repo,
294 294 parents=parents,
295 295 text=message,
296 296 files=files,
297 297 filectxfn=filectxfn,
298 298 user=user,
299 299 date=date,
300 300 extra=extra)
301 301 new._text = cmdutil.commitforceeditor(repo, new, [])
302 302 return repo.commitctx(new)
303 303
304 304 def pick(ui, repo, ctx, ha, opts):
305 305 oldctx = repo[ha]
306 306 if oldctx.parents()[0] == ctx:
307 307 ui.debug('node %s unchanged\n' % ha)
308 308 return oldctx, []
309 309 hg.update(repo, ctx.node())
310 310 stats = applychanges(ui, repo, oldctx, opts)
311 311 if stats and stats[3] > 0:
312 312 raise error.InterventionRequired(_('Fix up the change and run '
313 313 'hg histedit --continue'))
314 314 # drop the second merge parent
315 315 commit = commitfuncfor(repo, oldctx)
316 316 n = commit(text=oldctx.description(), user=oldctx.user(),
317 317 date=oldctx.date(), extra=oldctx.extra())
318 318 if n is None:
319 319 ui.warn(_('%s: empty changeset\n')
320 320 % node.hex(ha))
321 321 return ctx, []
322 322 new = repo[n]
323 323 return new, [(oldctx.node(), (n,))]
324 324
325 325
326 326 def edit(ui, repo, ctx, ha, opts):
327 327 oldctx = repo[ha]
328 328 hg.update(repo, ctx.node())
329 329 applychanges(ui, repo, oldctx, opts)
330 330 raise error.InterventionRequired(
331 331 _('Make changes as needed, you may commit or record as needed now.\n'
332 332 'When you are finished, run hg histedit --continue to resume.'))
333 333
334 334 def fold(ui, repo, ctx, ha, opts):
335 335 oldctx = repo[ha]
336 336 hg.update(repo, ctx.node())
337 337 stats = applychanges(ui, repo, oldctx, opts)
338 338 if stats and stats[3] > 0:
339 339 raise error.InterventionRequired(
340 340 _('Fix up the change and run hg histedit --continue'))
341 341 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
342 342 date=oldctx.date(), extra=oldctx.extra())
343 343 if n is None:
344 344 ui.warn(_('%s: empty changeset')
345 345 % node.hex(ha))
346 346 return ctx, []
347 347 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
348 348
349 349 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
350 350 parent = ctx.parents()[0].node()
351 351 hg.update(repo, parent)
352 352 ### prepare new commit data
353 353 commitopts = opts.copy()
354 354 # username
355 355 if ctx.user() == oldctx.user():
356 356 username = ctx.user()
357 357 else:
358 358 username = ui.username()
359 359 commitopts['user'] = username
360 360 # commit message
361 361 newmessage = '\n***\n'.join(
362 362 [ctx.description()] +
363 363 [repo[r].description() for r in internalchanges] +
364 364 [oldctx.description()]) + '\n'
365 365 commitopts['message'] = newmessage
366 366 # date
367 367 commitopts['date'] = max(ctx.date(), oldctx.date())
368 368 extra = ctx.extra().copy()
369 369 # histedit_source
370 370 # note: ctx is likely a temporary commit but that the best we can do here
371 371 # This is sufficient to solve issue3681 anyway
372 372 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
373 373 commitopts['extra'] = extra
374 374 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
375 375 try:
376 376 phasemin = max(ctx.phase(), oldctx.phase())
377 377 repo.ui.setconfig('phases', 'new-commit', phasemin)
378 378 n = collapse(repo, ctx, repo[newnode], commitopts)
379 379 finally:
380 380 repo.ui.restoreconfig(phasebackup)
381 381 if n is None:
382 382 return ctx, []
383 383 hg.update(repo, n)
384 384 replacements = [(oldctx.node(), (newnode,)),
385 385 (ctx.node(), (n,)),
386 386 (newnode, (n,)),
387 387 ]
388 388 for ich in internalchanges:
389 389 replacements.append((ich, (n,)))
390 390 return repo[n], replacements
391 391
392 392 def drop(ui, repo, ctx, ha, opts):
393 393 return ctx, [(repo[ha].node(), ())]
394 394
395 395
396 396 def message(ui, repo, ctx, ha, opts):
397 397 oldctx = repo[ha]
398 398 hg.update(repo, ctx.node())
399 399 stats = applychanges(ui, repo, oldctx, opts)
400 400 if stats and stats[3] > 0:
401 401 raise error.InterventionRequired(
402 402 _('Fix up the change and run hg histedit --continue'))
403 403 message = oldctx.description() + '\n'
404 404 message = ui.edit(message, ui.username())
405 405 commit = commitfuncfor(repo, oldctx)
406 406 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
407 407 extra=oldctx.extra())
408 408 newctx = repo[new]
409 409 if oldctx.node() != newctx.node():
410 410 return newctx, [(oldctx.node(), (new,))]
411 411 # We didn't make an edit, so just indicate no replaced nodes
412 412 return newctx, []
413 413
414 414 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
415 415 """utility function to find the first outgoing changeset
416 416
417 417 Used by initialisation code"""
418 418 dest = ui.expandpath(remote or 'default-push', remote or 'default')
419 419 dest, revs = hg.parseurl(dest, None)[:2]
420 420 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
421 421
422 422 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
423 423 other = hg.peer(repo, opts, dest)
424 424
425 425 if revs:
426 426 revs = [repo.lookup(rev) for rev in revs]
427 427
428 428 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
429 429 if not outgoing.missing:
430 430 raise util.Abort(_('no outgoing ancestors'))
431 431 roots = list(repo.revs("roots(%ln)", outgoing.missing))
432 432 if 1 < len(roots):
433 433 msg = _('there are ambiguous outgoing revisions')
434 434 hint = _('see "hg help histedit" for more detail')
435 435 raise util.Abort(msg, hint=hint)
436 436 return repo.lookup(roots[0])
437 437
438 438 actiontable = {'p': pick,
439 439 'pick': pick,
440 440 'e': edit,
441 441 'edit': edit,
442 442 'f': fold,
443 443 'fold': fold,
444 444 'd': drop,
445 445 'drop': drop,
446 446 'm': message,
447 447 'mess': message,
448 448 }
449 449
450 450 @command('histedit',
451 451 [('', 'commands', '',
452 452 _('Read history edits from the specified file.')),
453 453 ('c', 'continue', False, _('continue an edit already in progress')),
454 454 ('k', 'keep', False,
455 455 _("don't strip old nodes after edit is complete")),
456 456 ('', 'abort', False, _('abort an edit in progress')),
457 457 ('o', 'outgoing', False, _('changesets not found in destination')),
458 458 ('f', 'force', False,
459 459 _('force outgoing even for unrelated repositories')),
460 460 ('r', 'rev', [], _('first revision to be edited'))],
461 461 _("ANCESTOR | --outgoing [URL]"))
462 462 def histedit(ui, repo, *freeargs, **opts):
463 463 """interactively edit changeset history
464 464
465 465 This command edits changesets between ANCESTOR and the parent of
466 466 the working directory.
467 467
468 468 With --outgoing, this edits changesets not found in the
469 469 destination repository. If URL of the destination is omitted, the
470 470 'default-push' (or 'default') path will be used.
471 471
472 472 For safety, this command is aborted, also if there are ambiguous
473 473 outgoing revisions which may confuse users: for example, there are
474 474 multiple branches containing outgoing revisions.
475 475
476 476 Use "min(outgoing() and ::.)" or similar revset specification
477 477 instead of --outgoing to specify edit target revision exactly in
478 478 such ambiguous situation. See :hg:`help revsets` for detail about
479 479 selecting revisions.
480 480
481 481 Returns 0 on success, 1 if user intervention is required (not only
482 482 for intentional "edit" command, but also for resolving unexpected
483 483 conflicts).
484 484 """
485 485 lock = wlock = None
486 486 try:
487 487 wlock = repo.wlock()
488 488 lock = repo.lock()
489 489 _histedit(ui, repo, *freeargs, **opts)
490 490 finally:
491 491 release(lock, wlock)
492 492
493 493 def _histedit(ui, repo, *freeargs, **opts):
494 494 # TODO only abort if we try and histedit mq patches, not just
495 495 # blanket if mq patches are applied somewhere
496 496 mq = getattr(repo, 'mq', None)
497 497 if mq and mq.applied:
498 498 raise util.Abort(_('source has mq patches applied'))
499 499
500 500 # basic argument incompatibility processing
501 501 outg = opts.get('outgoing')
502 502 cont = opts.get('continue')
503 503 abort = opts.get('abort')
504 504 force = opts.get('force')
505 505 rules = opts.get('commands', '')
506 506 revs = opts.get('rev', [])
507 507 goal = 'new' # This invocation goal, in new, continue, abort
508 508 if force and not outg:
509 509 raise util.Abort(_('--force only allowed with --outgoing'))
510 510 if cont:
511 511 if util.any((outg, abort, revs, freeargs, rules)):
512 512 raise util.Abort(_('no arguments allowed with --continue'))
513 513 goal = 'continue'
514 514 elif abort:
515 515 if util.any((outg, revs, freeargs, rules)):
516 516 raise util.Abort(_('no arguments allowed with --abort'))
517 517 goal = 'abort'
518 518 else:
519 519 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
520 520 raise util.Abort(_('history edit already in progress, try '
521 521 '--continue or --abort'))
522 522 if outg:
523 523 if revs:
524 524 raise util.Abort(_('no revisions allowed with --outgoing'))
525 525 if len(freeargs) > 1:
526 526 raise util.Abort(
527 527 _('only one repo argument allowed with --outgoing'))
528 528 else:
529 529 revs.extend(freeargs)
530 530 if len(revs) != 1:
531 531 raise util.Abort(
532 532 _('histedit requires exactly one ancestor revision'))
533 533
534 534
535 535 if goal == 'continue':
536 536 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
537 537 parentctx = repo[parentctxnode]
538 538 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
539 539 replacements.extend(repl)
540 540 elif goal == 'abort':
541 541 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
542 542 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
543 543 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
544 544 # check whether we should update away
545 545 parentnodes = [c.node() for c in repo[None].parents()]
546 546 for n in leafs | set([parentctxnode]):
547 547 if n in parentnodes:
548 548 hg.clean(repo, topmost)
549 549 break
550 550 else:
551 551 pass
552 552 cleanupnode(ui, repo, 'created', tmpnodes)
553 553 cleanupnode(ui, repo, 'temp', leafs)
554 554 os.unlink(os.path.join(repo.path, 'histedit-state'))
555 555 return
556 556 else:
557 557 cmdutil.checkunfinished(repo)
558 558 cmdutil.bailifchanged(repo)
559 559
560 560 topmost, empty = repo.dirstate.parents()
561 561 if outg:
562 562 if freeargs:
563 563 remote = freeargs[0]
564 564 else:
565 565 remote = None
566 566 root = findoutgoing(ui, repo, remote, force, opts)
567 567 else:
568 568 root = revs[0]
569 569 root = scmutil.revsingle(repo, root).node()
570 570
571 571 keep = opts.get('keep', False)
572 572 revs = between(repo, root, topmost, keep)
573 573 if not revs:
574 574 raise util.Abort(_('%s is not an ancestor of working directory') %
575 575 node.short(root))
576 576
577 577 ctxs = [repo[r] for r in revs]
578 578 if not rules:
579 579 rules = '\n'.join([makedesc(c) for c in ctxs])
580 580 rules += '\n\n'
581 581 rules += editcomment % (node.short(root), node.short(topmost))
582 582 rules = ui.edit(rules, ui.username())
583 583 # Save edit rules in .hg/histedit-last-edit.txt in case
584 584 # the user needs to ask for help after something
585 585 # surprising happens.
586 586 f = open(repo.join('histedit-last-edit.txt'), 'w')
587 587 f.write(rules)
588 588 f.close()
589 589 else:
590 590 if rules == '-':
591 591 f = sys.stdin
592 592 else:
593 593 f = open(rules)
594 594 rules = f.read()
595 595 f.close()
596 596 rules = [l for l in (r.strip() for r in rules.splitlines())
597 597 if l and not l[0] == '#']
598 598 rules = verifyrules(rules, repo, ctxs)
599 599
600 600 parentctx = repo[root].parents()[0]
601 601 keep = opts.get('keep', False)
602 602 replacements = []
603 603
604 604
605 605 while rules:
606 606 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
607 607 action, ha = rules.pop(0)
608 608 ui.debug('histedit: processing %s %s\n' % (action, ha))
609 609 actfunc = actiontable[action]
610 610 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
611 611 replacements.extend(replacement_)
612 612
613 613 hg.update(repo, parentctx.node())
614 614
615 615 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
616 616 if mapping:
617 617 for prec, succs in mapping.iteritems():
618 618 if not succs:
619 619 ui.debug('histedit: %s is dropped\n' % node.short(prec))
620 620 else:
621 621 ui.debug('histedit: %s is replaced by %s\n' % (
622 622 node.short(prec), node.short(succs[0])))
623 623 if len(succs) > 1:
624 624 m = 'histedit: %s'
625 625 for n in succs[1:]:
626 626 ui.debug(m % node.short(n))
627 627
628 628 if not keep:
629 629 if mapping:
630 630 movebookmarks(ui, repo, mapping, topmost, ntm)
631 631 # TODO update mq state
632 632 if obsolete._enabled:
633 633 markers = []
634 634 # sort by revision number because it sound "right"
635 635 for prec in sorted(mapping, key=repo.changelog.rev):
636 636 succs = mapping[prec]
637 637 markers.append((repo[prec],
638 638 tuple(repo[s] for s in succs)))
639 639 if markers:
640 640 obsolete.createmarkers(repo, markers)
641 641 else:
642 642 cleanupnode(ui, repo, 'replaced', mapping)
643 643
644 644 cleanupnode(ui, repo, 'temp', tmpnodes)
645 645 os.unlink(os.path.join(repo.path, 'histedit-state'))
646 646 if os.path.exists(repo.sjoin('undo')):
647 647 os.unlink(repo.sjoin('undo'))
648 648
649
650 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
651 action, currentnode = rules.pop(0)
652 ctx = repo[currentnode]
649 def gatherchildren(repo, ctx):
653 650 # is there any new commit between the expected parent and "."
654 651 #
655 652 # note: does not take non linear new change in account (but previous
656 653 # implementation didn't used them anyway (issue3655)
657 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
658 if parentctx.node() != node.nullid:
654 newchildren = [c.node() for c in repo.set('(%d::.)', ctx)]
655 if ctx.node() != node.nullid:
659 656 if not newchildren:
660 # `parentctxnode` should match but no result. This means that
661 # currentnode is not a descendant from parentctxnode.
657 # `ctx` should match but no result. This means that
658 # currentnode is not a descendant from ctx.
662 659 msg = _('%s is not an ancestor of working directory')
663 660 hint = _('use "histedit --abort" to clear broken state')
664 raise util.Abort(msg % parentctx, hint=hint)
665 newchildren.pop(0) # remove parentctxnode
661 raise util.Abort(msg % ctx, hint=hint)
662 newchildren.pop(0) # remove ctx
663 return newchildren
664
665 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
666 action, currentnode = rules.pop(0)
667 ctx = repo[currentnode]
668
669 newchildren = gatherchildren(repo, parentctx)
670
666 671 # Commit dirty working directory if necessary
667 672 new = None
668 673 m, a, r, d = repo.status()[:4]
669 674 if m or a or r or d:
670 675 # prepare the message for the commit to comes
671 676 if action in ('f', 'fold'):
672 677 message = 'fold-temp-revision %s' % currentnode
673 678 else:
674 679 message = ctx.description() + '\n'
675 680 if action in ('e', 'edit', 'm', 'mess'):
676 681 editor = cmdutil.commitforceeditor
677 682 else:
678 683 editor = False
679 684 commit = commitfuncfor(repo, ctx)
680 685 new = commit(text=message, user=ctx.user(),
681 686 date=ctx.date(), extra=ctx.extra(),
682 687 editor=editor)
683 688 if new is not None:
684 689 newchildren.append(new)
685 690
686 691 replacements = []
687 692 # track replacements
688 693 if ctx.node() not in newchildren:
689 694 # note: new children may be empty when the changeset is dropped.
690 695 # this happen e.g during conflicting pick where we revert content
691 696 # to parent.
692 697 replacements.append((ctx.node(), tuple(newchildren)))
693 698
694 699 if action in ('f', 'fold'):
695 700 if newchildren:
696 701 # finalize fold operation if applicable
697 702 if new is None:
698 703 new = newchildren[-1]
699 704 else:
700 705 newchildren.pop() # remove new from internal changes
701 706 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
702 707 newchildren)
703 708 replacements.extend(repl)
704 709 else:
705 710 # newchildren is empty if the fold did not result in any commit
706 711 # this happen when all folded change are discarded during the
707 712 # merge.
708 713 replacements.append((ctx.node(), (parentctx.node(),)))
709 714 elif newchildren:
710 715 # otherwise update "parentctx" before proceeding to further operation
711 716 parentctx = repo[newchildren[-1]]
712 717 return parentctx, replacements
713 718
714 719
715 720 def between(repo, old, new, keep):
716 721 """select and validate the set of revision to edit
717 722
718 723 When keep is false, the specified set can't have children."""
719 724 ctxs = list(repo.set('%n::%n', old, new))
720 725 if ctxs and not keep:
721 726 if (not obsolete._enabled and
722 727 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
723 728 raise util.Abort(_('cannot edit history that would orphan nodes'))
724 729 if repo.revs('(%ld) and merge()', ctxs):
725 730 raise util.Abort(_('cannot edit history that contains merges'))
726 731 root = ctxs[0] # list is already sorted by repo.set
727 732 if not root.phase():
728 733 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
729 734 return [c.node() for c in ctxs]
730 735
731 736
732 737 def writestate(repo, parentnode, rules, keep, topmost, replacements):
733 738 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
734 739 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
735 740 fp.close()
736 741
737 742 def readstate(repo):
738 743 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
739 744 """
740 745 fp = open(os.path.join(repo.path, 'histedit-state'))
741 746 return pickle.load(fp)
742 747
743 748
744 749 def makedesc(c):
745 750 """build a initial action line for a ctx `c`
746 751
747 752 line are in the form:
748 753
749 754 pick <hash> <rev> <summary>
750 755 """
751 756 summary = ''
752 757 if c.description():
753 758 summary = c.description().splitlines()[0]
754 759 line = 'pick %s %d %s' % (c, c.rev(), summary)
755 760 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
756 761
757 762 def verifyrules(rules, repo, ctxs):
758 763 """Verify that there exists exactly one edit rule per given changeset.
759 764
760 765 Will abort if there are to many or too few rules, a malformed rule,
761 766 or a rule on a changeset outside of the user-given range.
762 767 """
763 768 parsed = []
764 769 expected = set(str(c) for c in ctxs)
765 770 seen = set()
766 771 for r in rules:
767 772 if ' ' not in r:
768 773 raise util.Abort(_('malformed line "%s"') % r)
769 774 action, rest = r.split(' ', 1)
770 775 ha = rest.strip().split(' ', 1)[0]
771 776 try:
772 777 ha = str(repo[ha]) # ensure its a short hash
773 778 except error.RepoError:
774 779 raise util.Abort(_('unknown changeset %s listed') % ha)
775 780 if ha not in expected:
776 781 raise util.Abort(
777 782 _('may not use changesets other than the ones listed'))
778 783 if ha in seen:
779 784 raise util.Abort(_('duplicated command for changeset %s') % ha)
780 785 seen.add(ha)
781 786 if action not in actiontable:
782 787 raise util.Abort(_('unknown action "%s"') % action)
783 788 parsed.append([action, ha])
784 789 missing = sorted(expected - seen) # sort to stabilize output
785 790 if missing:
786 791 raise util.Abort(_('missing rules for changeset %s') % missing[0],
787 792 hint=_('do you want to use the drop action?'))
788 793 return parsed
789 794
790 795 def processreplacement(repo, replacements):
791 796 """process the list of replacements to return
792 797
793 798 1) the final mapping between original and created nodes
794 799 2) the list of temporary node created by histedit
795 800 3) the list of new commit created by histedit"""
796 801 allsuccs = set()
797 802 replaced = set()
798 803 fullmapping = {}
799 804 # initialise basic set
800 805 # fullmapping record all operation recorded in replacement
801 806 for rep in replacements:
802 807 allsuccs.update(rep[1])
803 808 replaced.add(rep[0])
804 809 fullmapping.setdefault(rep[0], set()).update(rep[1])
805 810 new = allsuccs - replaced
806 811 tmpnodes = allsuccs & replaced
807 812 # Reduce content fullmapping into direct relation between original nodes
808 813 # and final node created during history edition
809 814 # Dropped changeset are replaced by an empty list
810 815 toproceed = set(fullmapping)
811 816 final = {}
812 817 while toproceed:
813 818 for x in list(toproceed):
814 819 succs = fullmapping[x]
815 820 for s in list(succs):
816 821 if s in toproceed:
817 822 # non final node with unknown closure
818 823 # We can't process this now
819 824 break
820 825 elif s in final:
821 826 # non final node, replace with closure
822 827 succs.remove(s)
823 828 succs.update(final[s])
824 829 else:
825 830 final[x] = succs
826 831 toproceed.remove(x)
827 832 # remove tmpnodes from final mapping
828 833 for n in tmpnodes:
829 834 del final[n]
830 835 # we expect all changes involved in final to exist in the repo
831 836 # turn `final` into list (topologically sorted)
832 837 nm = repo.changelog.nodemap
833 838 for prec, succs in final.items():
834 839 final[prec] = sorted(succs, key=nm.get)
835 840
836 841 # computed topmost element (necessary for bookmark)
837 842 if new:
838 843 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
839 844 elif not final:
840 845 # Nothing rewritten at all. we won't need `newtopmost`
841 846 # It is the same as `oldtopmost` and `processreplacement` know it
842 847 newtopmost = None
843 848 else:
844 849 # every body died. The newtopmost is the parent of the root.
845 850 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
846 851
847 852 return final, tmpnodes, new, newtopmost
848 853
849 854 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
850 855 """Move bookmark from old to newly created node"""
851 856 if not mapping:
852 857 # if nothing got rewritten there is not purpose for this function
853 858 return
854 859 moves = []
855 860 for bk, old in sorted(repo._bookmarks.iteritems()):
856 861 if old == oldtopmost:
857 862 # special case ensure bookmark stay on tip.
858 863 #
859 864 # This is arguably a feature and we may only want that for the
860 865 # active bookmark. But the behavior is kept compatible with the old
861 866 # version for now.
862 867 moves.append((bk, newtopmost))
863 868 continue
864 869 base = old
865 870 new = mapping.get(base, None)
866 871 if new is None:
867 872 continue
868 873 while not new:
869 874 # base is killed, trying with parent
870 875 base = repo[base].p1().node()
871 876 new = mapping.get(base, (base,))
872 877 # nothing to move
873 878 moves.append((bk, new[-1]))
874 879 if moves:
875 880 marks = repo._bookmarks
876 881 for mark, new in moves:
877 882 old = marks[mark]
878 883 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
879 884 % (mark, node.short(old), node.short(new)))
880 885 marks[mark] = new
881 886 marks.write()
882 887
883 888 def cleanupnode(ui, repo, name, nodes):
884 889 """strip a group of nodes from the repository
885 890
886 891 The set of node to strip may contains unknown nodes."""
887 892 ui.debug('should strip %s nodes %s\n' %
888 893 (name, ', '.join([node.short(n) for n in nodes])))
889 894 lock = None
890 895 try:
891 896 lock = repo.lock()
892 897 # Find all node that need to be stripped
893 898 # (we hg %lr instead of %ln to silently ignore unknown item
894 899 nm = repo.changelog.nodemap
895 900 nodes = [n for n in nodes if n in nm]
896 901 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
897 902 for c in roots:
898 903 # We should process node in reverse order to strip tip most first.
899 904 # but this trigger a bug in changegroup hook.
900 905 # This would reduce bundle overhead
901 906 repair.strip(ui, repo, c)
902 907 finally:
903 908 release(lock)
904 909
905 910 def summaryhook(ui, repo):
906 911 if not os.path.exists(repo.join('histedit-state')):
907 912 return
908 913 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
909 914 if rules:
910 915 # i18n: column positioning for "hg summary"
911 916 ui.write(_('hist: %s (histedit --continue)\n') %
912 917 (ui.label(_('%d remaining'), 'histedit.remaining') %
913 918 len(rules)))
914 919
915 920 def extsetup(ui):
916 921 cmdutil.summaryhooks.add('histedit', summaryhook)
917 922 cmdutil.unfinishedstates.append(
918 923 ['histedit-state', False, True, _('histedit in progress'),
919 924 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
General Comments 0
You need to be logged in to leave comments. Login now