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