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