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