##// END OF EJS Templates
histedit: edit with custom filename...
Mykola Nikishov -
r27154:3bc7919f default
parent child Browse files
Show More
@@ -1,1367 +1,1367 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 commit 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 commit 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 Histedit rule lines are truncated to 80 characters by default. You
147 147 can customize this behavior by setting a different length in your
148 148 configuration file::
149 149
150 150 [histedit]
151 151 linelen = 120 # truncate rule lines at 120 characters
152 152 """
153 153
154 154 try:
155 155 import cPickle as pickle
156 156 pickle.dump # import now
157 157 except ImportError:
158 158 import pickle
159 159 import errno
160 160 import os
161 161 import sys
162 162
163 163 from mercurial import bundle2
164 164 from mercurial import cmdutil
165 165 from mercurial import discovery
166 166 from mercurial import error
167 167 from mercurial import copies
168 168 from mercurial import context
169 169 from mercurial import exchange
170 170 from mercurial import extensions
171 171 from mercurial import hg
172 172 from mercurial import node
173 173 from mercurial import repair
174 174 from mercurial import scmutil
175 175 from mercurial import util
176 176 from mercurial import obsolete
177 177 from mercurial import merge as mergemod
178 178 from mercurial.lock import release
179 179 from mercurial.i18n import _
180 180
181 181 cmdtable = {}
182 182 command = cmdutil.command(cmdtable)
183 183
184 184 class _constraints(object):
185 185 # aborts if there are multiple rules for one node
186 186 noduplicates = 'noduplicates'
187 187 # abort if the node does belong to edited stack
188 188 forceother = 'forceother'
189 189 # abort if the node doesn't belong to edited stack
190 190 noother = 'noother'
191 191
192 192 @classmethod
193 193 def known(cls):
194 194 return set([v for k, v in cls.__dict__.items() if k[0] != '_'])
195 195
196 196 # Note for extension authors: ONLY specify testedwith = 'internal' for
197 197 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
198 198 # be specifying the version(s) of Mercurial they are tested with, or
199 199 # leave the attribute unspecified.
200 200 testedwith = 'internal'
201 201
202 202 # i18n: command names and abbreviations must remain untranslated
203 203 editcomment = _("""# Edit history between %s and %s
204 204 #
205 205 # Commits are listed from least to most recent
206 206 #
207 207 # Commands:
208 208 # p, pick = use commit
209 209 # e, edit = use commit, but stop for amending
210 210 # f, fold = use commit, but combine it with the one above
211 211 # r, roll = like fold, but discard this commit's description
212 212 # d, drop = remove commit from history
213 213 # m, mess = edit commit message without changing commit content
214 214 #
215 215 """)
216 216
217 217 class histeditstate(object):
218 218 def __init__(self, repo, parentctxnode=None, rules=None, keep=None,
219 219 topmost=None, replacements=None, lock=None, wlock=None):
220 220 self.repo = repo
221 221 self.rules = rules
222 222 self.keep = keep
223 223 self.topmost = topmost
224 224 self.parentctxnode = parentctxnode
225 225 self.lock = lock
226 226 self.wlock = wlock
227 227 self.backupfile = None
228 228 if replacements is None:
229 229 self.replacements = []
230 230 else:
231 231 self.replacements = replacements
232 232
233 233 def read(self):
234 234 """Load histedit state from disk and set fields appropriately."""
235 235 try:
236 236 fp = self.repo.vfs('histedit-state', 'r')
237 237 except IOError as err:
238 238 if err.errno != errno.ENOENT:
239 239 raise
240 240 raise error.Abort(_('no histedit in progress'))
241 241
242 242 try:
243 243 data = pickle.load(fp)
244 244 parentctxnode, rules, keep, topmost, replacements = data
245 245 backupfile = None
246 246 except pickle.UnpicklingError:
247 247 data = self._load()
248 248 parentctxnode, rules, keep, topmost, replacements, backupfile = data
249 249
250 250 self.parentctxnode = parentctxnode
251 251 self.rules = rules
252 252 self.keep = keep
253 253 self.topmost = topmost
254 254 self.replacements = replacements
255 255 self.backupfile = backupfile
256 256
257 257 def write(self):
258 258 fp = self.repo.vfs('histedit-state', 'w')
259 259 fp.write('v1\n')
260 260 fp.write('%s\n' % node.hex(self.parentctxnode))
261 261 fp.write('%s\n' % node.hex(self.topmost))
262 262 fp.write('%s\n' % self.keep)
263 263 fp.write('%d\n' % len(self.rules))
264 264 for rule in self.rules:
265 265 fp.write('%s\n' % rule[0]) # action
266 266 fp.write('%s\n' % rule[1]) # remainder
267 267 fp.write('%d\n' % len(self.replacements))
268 268 for replacement in self.replacements:
269 269 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
270 270 for r in replacement[1])))
271 271 backupfile = self.backupfile
272 272 if not backupfile:
273 273 backupfile = ''
274 274 fp.write('%s\n' % backupfile)
275 275 fp.close()
276 276
277 277 def _load(self):
278 278 fp = self.repo.vfs('histedit-state', 'r')
279 279 lines = [l[:-1] for l in fp.readlines()]
280 280
281 281 index = 0
282 282 lines[index] # version number
283 283 index += 1
284 284
285 285 parentctxnode = node.bin(lines[index])
286 286 index += 1
287 287
288 288 topmost = node.bin(lines[index])
289 289 index += 1
290 290
291 291 keep = lines[index] == 'True'
292 292 index += 1
293 293
294 294 # Rules
295 295 rules = []
296 296 rulelen = int(lines[index])
297 297 index += 1
298 298 for i in xrange(rulelen):
299 299 ruleaction = lines[index]
300 300 index += 1
301 301 rule = lines[index]
302 302 index += 1
303 303 rules.append((ruleaction, rule))
304 304
305 305 # Replacements
306 306 replacements = []
307 307 replacementlen = int(lines[index])
308 308 index += 1
309 309 for i in xrange(replacementlen):
310 310 replacement = lines[index]
311 311 original = node.bin(replacement[:40])
312 312 succ = [node.bin(replacement[i:i + 40]) for i in
313 313 range(40, len(replacement), 40)]
314 314 replacements.append((original, succ))
315 315 index += 1
316 316
317 317 backupfile = lines[index]
318 318 index += 1
319 319
320 320 fp.close()
321 321
322 322 return parentctxnode, rules, keep, topmost, replacements, backupfile
323 323
324 324 def clear(self):
325 325 if self.inprogress():
326 326 self.repo.vfs.unlink('histedit-state')
327 327
328 328 def inprogress(self):
329 329 return self.repo.vfs.exists('histedit-state')
330 330
331 331 class histeditaction(object):
332 332 def __init__(self, state, node):
333 333 self.state = state
334 334 self.repo = state.repo
335 335 self.node = node
336 336
337 337 @classmethod
338 338 def fromrule(cls, state, rule):
339 339 """Parses the given rule, returning an instance of the histeditaction.
340 340 """
341 341 repo = state.repo
342 342 rulehash = rule.strip().split(' ', 1)[0]
343 343 try:
344 344 node = repo[rulehash].node()
345 345 except error.RepoError:
346 346 raise error.Abort(_('unknown changeset %s listed') % rulehash[:12])
347 347 return cls(state, node)
348 348
349 349 def constraints(self):
350 350 """Return a set of constrains that this action should be verified for
351 351 """
352 352 return set([_constraints.noduplicates, _constraints.noother])
353 353
354 354 def nodetoverify(self):
355 355 """Returns a node associated with the action that will be used for
356 356 verification purposes.
357 357
358 358 If the action doesn't correspond to node it should return None
359 359 """
360 360 return self.node
361 361
362 362 def run(self):
363 363 """Runs the action. The default behavior is simply apply the action's
364 364 rulectx onto the current parentctx."""
365 365 self.applychange()
366 366 self.continuedirty()
367 367 return self.continueclean()
368 368
369 369 def applychange(self):
370 370 """Applies the changes from this action's rulectx onto the current
371 371 parentctx, but does not commit them."""
372 372 repo = self.repo
373 373 rulectx = repo[self.node]
374 374 hg.update(repo, self.state.parentctxnode)
375 375 stats = applychanges(repo.ui, repo, rulectx, {})
376 376 if stats and stats[3] > 0:
377 377 raise error.InterventionRequired(_('Fix up the change and run '
378 378 'hg histedit --continue'))
379 379
380 380 def continuedirty(self):
381 381 """Continues the action when changes have been applied to the working
382 382 copy. The default behavior is to commit the dirty changes."""
383 383 repo = self.repo
384 384 rulectx = repo[self.node]
385 385
386 386 editor = self.commiteditor()
387 387 commit = commitfuncfor(repo, rulectx)
388 388
389 389 commit(text=rulectx.description(), user=rulectx.user(),
390 390 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
391 391
392 392 def commiteditor(self):
393 393 """The editor to be used to edit the commit message."""
394 394 return False
395 395
396 396 def continueclean(self):
397 397 """Continues the action when the working copy is clean. The default
398 398 behavior is to accept the current commit as the new version of the
399 399 rulectx."""
400 400 ctx = self.repo['.']
401 401 if ctx.node() == self.state.parentctxnode:
402 402 self.repo.ui.warn(_('%s: empty changeset\n') %
403 403 node.short(self.node))
404 404 return ctx, [(self.node, tuple())]
405 405 if ctx.node() == self.node:
406 406 # Nothing changed
407 407 return ctx, []
408 408 return ctx, [(self.node, (ctx.node(),))]
409 409
410 410 def commitfuncfor(repo, src):
411 411 """Build a commit function for the replacement of <src>
412 412
413 413 This function ensure we apply the same treatment to all changesets.
414 414
415 415 - Add a 'histedit_source' entry in extra.
416 416
417 417 Note that fold has its own separated logic because its handling is a bit
418 418 different and not easily factored out of the fold method.
419 419 """
420 420 phasemin = src.phase()
421 421 def commitfunc(**kwargs):
422 422 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
423 423 try:
424 424 repo.ui.setconfig('phases', 'new-commit', phasemin,
425 425 'histedit')
426 426 extra = kwargs.get('extra', {}).copy()
427 427 extra['histedit_source'] = src.hex()
428 428 kwargs['extra'] = extra
429 429 return repo.commit(**kwargs)
430 430 finally:
431 431 repo.ui.restoreconfig(phasebackup)
432 432 return commitfunc
433 433
434 434 def applychanges(ui, repo, ctx, opts):
435 435 """Merge changeset from ctx (only) in the current working directory"""
436 436 wcpar = repo.dirstate.parents()[0]
437 437 if ctx.p1().node() == wcpar:
438 438 # edits are "in place" we do not need to make any merge,
439 439 # just applies changes on parent for edition
440 440 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
441 441 stats = None
442 442 else:
443 443 try:
444 444 # ui.forcemerge is an internal variable, do not document
445 445 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
446 446 'histedit')
447 447 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
448 448 finally:
449 449 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
450 450 return stats
451 451
452 452 def collapse(repo, first, last, commitopts, skipprompt=False):
453 453 """collapse the set of revisions from first to last as new one.
454 454
455 455 Expected commit options are:
456 456 - message
457 457 - date
458 458 - username
459 459 Commit message is edited in all cases.
460 460
461 461 This function works in memory."""
462 462 ctxs = list(repo.set('%d::%d', first, last))
463 463 if not ctxs:
464 464 return None
465 465 for c in ctxs:
466 466 if not c.mutable():
467 467 raise error.Abort(
468 468 _("cannot fold into public change %s") % node.short(c.node()))
469 469 base = first.parents()[0]
470 470
471 471 # commit a new version of the old changeset, including the update
472 472 # collect all files which might be affected
473 473 files = set()
474 474 for ctx in ctxs:
475 475 files.update(ctx.files())
476 476
477 477 # Recompute copies (avoid recording a -> b -> a)
478 478 copied = copies.pathcopies(base, last)
479 479
480 480 # prune files which were reverted by the updates
481 481 def samefile(f):
482 482 if f in last.manifest():
483 483 a = last.filectx(f)
484 484 if f in base.manifest():
485 485 b = base.filectx(f)
486 486 return (a.data() == b.data()
487 487 and a.flags() == b.flags())
488 488 else:
489 489 return False
490 490 else:
491 491 return f not in base.manifest()
492 492 files = [f for f in files if not samefile(f)]
493 493 # commit version of these files as defined by head
494 494 headmf = last.manifest()
495 495 def filectxfn(repo, ctx, path):
496 496 if path in headmf:
497 497 fctx = last[path]
498 498 flags = fctx.flags()
499 499 mctx = context.memfilectx(repo,
500 500 fctx.path(), fctx.data(),
501 501 islink='l' in flags,
502 502 isexec='x' in flags,
503 503 copied=copied.get(path))
504 504 return mctx
505 505 return None
506 506
507 507 if commitopts.get('message'):
508 508 message = commitopts['message']
509 509 else:
510 510 message = first.description()
511 511 user = commitopts.get('user')
512 512 date = commitopts.get('date')
513 513 extra = commitopts.get('extra')
514 514
515 515 parents = (first.p1().node(), first.p2().node())
516 516 editor = None
517 517 if not skipprompt:
518 518 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
519 519 new = context.memctx(repo,
520 520 parents=parents,
521 521 text=message,
522 522 files=files,
523 523 filectxfn=filectxfn,
524 524 user=user,
525 525 date=date,
526 526 extra=extra,
527 527 editor=editor)
528 528 return repo.commitctx(new)
529 529
530 530 def _isdirtywc(repo):
531 531 return repo[None].dirty(missing=True)
532 532
533 533 def abortdirty():
534 534 raise error.Abort(_('working copy has pending changes'),
535 535 hint=_('amend, commit, or revert them and run histedit '
536 536 '--continue, or abort with histedit --abort'))
537 537
538 538
539 539 class pick(histeditaction):
540 540 def run(self):
541 541 rulectx = self.repo[self.node]
542 542 if rulectx.parents()[0].node() == self.state.parentctxnode:
543 543 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
544 544 return rulectx, []
545 545
546 546 return super(pick, self).run()
547 547
548 548 class edit(histeditaction):
549 549 def run(self):
550 550 repo = self.repo
551 551 rulectx = repo[self.node]
552 552 hg.update(repo, self.state.parentctxnode)
553 553 applychanges(repo.ui, repo, rulectx, {})
554 554 raise error.InterventionRequired(
555 555 _('Make changes as needed, you may commit or record as needed '
556 556 'now.\nWhen you are finished, run hg histedit --continue to '
557 557 'resume.'))
558 558
559 559 def commiteditor(self):
560 560 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
561 561
562 562 class fold(histeditaction):
563 563 def continuedirty(self):
564 564 repo = self.repo
565 565 rulectx = repo[self.node]
566 566
567 567 commit = commitfuncfor(repo, rulectx)
568 568 commit(text='fold-temp-revision %s' % node.short(self.node),
569 569 user=rulectx.user(), date=rulectx.date(),
570 570 extra=rulectx.extra())
571 571
572 572 def continueclean(self):
573 573 repo = self.repo
574 574 ctx = repo['.']
575 575 rulectx = repo[self.node]
576 576 parentctxnode = self.state.parentctxnode
577 577 if ctx.node() == parentctxnode:
578 578 repo.ui.warn(_('%s: empty changeset\n') %
579 579 node.short(self.node))
580 580 return ctx, [(self.node, (parentctxnode,))]
581 581
582 582 parentctx = repo[parentctxnode]
583 583 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
584 584 parentctx))
585 585 if not newcommits:
586 586 repo.ui.warn(_('%s: cannot fold - working copy is not a '
587 587 'descendant of previous commit %s\n') %
588 588 (node.short(self.node), node.short(parentctxnode)))
589 589 return ctx, [(self.node, (ctx.node(),))]
590 590
591 591 middlecommits = newcommits.copy()
592 592 middlecommits.discard(ctx.node())
593 593
594 594 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
595 595 middlecommits)
596 596
597 597 def skipprompt(self):
598 598 """Returns true if the rule should skip the message editor.
599 599
600 600 For example, 'fold' wants to show an editor, but 'rollup'
601 601 doesn't want to.
602 602 """
603 603 return False
604 604
605 605 def mergedescs(self):
606 606 """Returns true if the rule should merge messages of multiple changes.
607 607
608 608 This exists mainly so that 'rollup' rules can be a subclass of
609 609 'fold'.
610 610 """
611 611 return True
612 612
613 613 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
614 614 parent = ctx.parents()[0].node()
615 615 hg.update(repo, parent)
616 616 ### prepare new commit data
617 617 commitopts = {}
618 618 commitopts['user'] = ctx.user()
619 619 # commit message
620 620 if not self.mergedescs():
621 621 newmessage = ctx.description()
622 622 else:
623 623 newmessage = '\n***\n'.join(
624 624 [ctx.description()] +
625 625 [repo[r].description() for r in internalchanges] +
626 626 [oldctx.description()]) + '\n'
627 627 commitopts['message'] = newmessage
628 628 # date
629 629 commitopts['date'] = max(ctx.date(), oldctx.date())
630 630 extra = ctx.extra().copy()
631 631 # histedit_source
632 632 # note: ctx is likely a temporary commit but that the best we can do
633 633 # here. This is sufficient to solve issue3681 anyway.
634 634 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
635 635 commitopts['extra'] = extra
636 636 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
637 637 try:
638 638 phasemin = max(ctx.phase(), oldctx.phase())
639 639 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
640 640 n = collapse(repo, ctx, repo[newnode], commitopts,
641 641 skipprompt=self.skipprompt())
642 642 finally:
643 643 repo.ui.restoreconfig(phasebackup)
644 644 if n is None:
645 645 return ctx, []
646 646 hg.update(repo, n)
647 647 replacements = [(oldctx.node(), (newnode,)),
648 648 (ctx.node(), (n,)),
649 649 (newnode, (n,)),
650 650 ]
651 651 for ich in internalchanges:
652 652 replacements.append((ich, (n,)))
653 653 return repo[n], replacements
654 654
655 655 class base(histeditaction):
656 656 def constraints(self):
657 657 return set([_constraints.forceother])
658 658
659 659 def run(self):
660 660 if self.repo['.'].node() != self.node:
661 661 mergemod.update(self.repo, self.node, False, True, False)
662 662 # branchmerge, force, partial)
663 663 return self.continueclean()
664 664
665 665 def continuedirty(self):
666 666 abortdirty()
667 667
668 668 def continueclean(self):
669 669 basectx = self.repo['.']
670 670 return basectx, []
671 671
672 672 class _multifold(fold):
673 673 """fold subclass used for when multiple folds happen in a row
674 674
675 675 We only want to fire the editor for the folded message once when
676 676 (say) four changes are folded down into a single change. This is
677 677 similar to rollup, but we should preserve both messages so that
678 678 when the last fold operation runs we can show the user all the
679 679 commit messages in their editor.
680 680 """
681 681 def skipprompt(self):
682 682 return True
683 683
684 684 class rollup(fold):
685 685 def mergedescs(self):
686 686 return False
687 687
688 688 def skipprompt(self):
689 689 return True
690 690
691 691 class drop(histeditaction):
692 692 def run(self):
693 693 parentctx = self.repo[self.state.parentctxnode]
694 694 return parentctx, [(self.node, tuple())]
695 695
696 696 class message(histeditaction):
697 697 def commiteditor(self):
698 698 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
699 699
700 700 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
701 701 """utility function to find the first outgoing changeset
702 702
703 703 Used by initialization code"""
704 704 if opts is None:
705 705 opts = {}
706 706 dest = ui.expandpath(remote or 'default-push', remote or 'default')
707 707 dest, revs = hg.parseurl(dest, None)[:2]
708 708 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
709 709
710 710 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
711 711 other = hg.peer(repo, opts, dest)
712 712
713 713 if revs:
714 714 revs = [repo.lookup(rev) for rev in revs]
715 715
716 716 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
717 717 if not outgoing.missing:
718 718 raise error.Abort(_('no outgoing ancestors'))
719 719 roots = list(repo.revs("roots(%ln)", outgoing.missing))
720 720 if 1 < len(roots):
721 721 msg = _('there are ambiguous outgoing revisions')
722 722 hint = _('see "hg help histedit" for more detail')
723 723 raise error.Abort(msg, hint=hint)
724 724 return repo.lookup(roots[0])
725 725
726 726 actiontable = {'p': pick,
727 727 'pick': pick,
728 728 'e': edit,
729 729 'edit': edit,
730 730 'f': fold,
731 731 'fold': fold,
732 732 '_multifold': _multifold,
733 733 'r': rollup,
734 734 'roll': rollup,
735 735 'd': drop,
736 736 'drop': drop,
737 737 'm': message,
738 738 'mess': message,
739 739 }
740 740
741 741 @command('histedit',
742 742 [('', 'commands', '',
743 743 _('read history edits from the specified file'), _('FILE')),
744 744 ('c', 'continue', False, _('continue an edit already in progress')),
745 745 ('', 'edit-plan', False, _('edit remaining actions list')),
746 746 ('k', 'keep', False,
747 747 _("don't strip old nodes after edit is complete")),
748 748 ('', 'abort', False, _('abort an edit in progress')),
749 749 ('o', 'outgoing', False, _('changesets not found in destination')),
750 750 ('f', 'force', False,
751 751 _('force outgoing even for unrelated repositories')),
752 752 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
753 753 _("ANCESTOR | --outgoing [URL]"))
754 754 def histedit(ui, repo, *freeargs, **opts):
755 755 """interactively edit changeset history
756 756
757 757 This command edits changesets between ANCESTOR and the parent of
758 758 the working directory.
759 759
760 760 With --outgoing, this edits changesets not found in the
761 761 destination repository. If URL of the destination is omitted, the
762 762 'default-push' (or 'default') path will be used.
763 763
764 764 For safety, this command is also aborted if there are ambiguous
765 765 outgoing revisions which may confuse users: for example, if there
766 766 are multiple branches containing outgoing revisions.
767 767
768 768 Use "min(outgoing() and ::.)" or similar revset specification
769 769 instead of --outgoing to specify edit target revision exactly in
770 770 such ambiguous situation. See :hg:`help revsets` for detail about
771 771 selecting revisions.
772 772
773 773 .. container:: verbose
774 774
775 775 Examples:
776 776
777 777 - A number of changes have been made.
778 778 Revision 3 is no longer needed.
779 779
780 780 Start history editing from revision 3::
781 781
782 782 hg histedit -r 3
783 783
784 784 An editor opens, containing the list of revisions,
785 785 with specific actions specified::
786 786
787 787 pick 5339bf82f0ca 3 Zworgle the foobar
788 788 pick 8ef592ce7cc4 4 Bedazzle the zerlog
789 789 pick 0a9639fcda9d 5 Morgify the cromulancy
790 790
791 791 Additional information about the possible actions
792 792 to take appears below the list of revisions.
793 793
794 794 To remove revision 3 from the history,
795 795 its action (at the beginning of the relevant line)
796 796 is changed to 'drop'::
797 797
798 798 drop 5339bf82f0ca 3 Zworgle the foobar
799 799 pick 8ef592ce7cc4 4 Bedazzle the zerlog
800 800 pick 0a9639fcda9d 5 Morgify the cromulancy
801 801
802 802 - A number of changes have been made.
803 803 Revision 2 and 4 need to be swapped.
804 804
805 805 Start history editing from revision 2::
806 806
807 807 hg histedit -r 2
808 808
809 809 An editor opens, containing the list of revisions,
810 810 with specific actions specified::
811 811
812 812 pick 252a1af424ad 2 Blorb a morgwazzle
813 813 pick 5339bf82f0ca 3 Zworgle the foobar
814 814 pick 8ef592ce7cc4 4 Bedazzle the zerlog
815 815
816 816 To swap revision 2 and 4, its lines are swapped
817 817 in the editor::
818 818
819 819 pick 8ef592ce7cc4 4 Bedazzle the zerlog
820 820 pick 5339bf82f0ca 3 Zworgle the foobar
821 821 pick 252a1af424ad 2 Blorb a morgwazzle
822 822
823 823 Returns 0 on success, 1 if user intervention is required (not only
824 824 for intentional "edit" command, but also for resolving unexpected
825 825 conflicts).
826 826 """
827 827 state = histeditstate(repo)
828 828 try:
829 829 state.wlock = repo.wlock()
830 830 state.lock = repo.lock()
831 831 _histedit(ui, repo, state, *freeargs, **opts)
832 832 finally:
833 833 release(state.lock, state.wlock)
834 834
835 835 def _histedit(ui, repo, state, *freeargs, **opts):
836 836 # TODO only abort if we try and histedit mq patches, not just
837 837 # blanket if mq patches are applied somewhere
838 838 mq = getattr(repo, 'mq', None)
839 839 if mq and mq.applied:
840 840 raise error.Abort(_('source has mq patches applied'))
841 841
842 842 # basic argument incompatibility processing
843 843 outg = opts.get('outgoing')
844 844 cont = opts.get('continue')
845 845 editplan = opts.get('edit_plan')
846 846 abort = opts.get('abort')
847 847 force = opts.get('force')
848 848 rules = opts.get('commands', '')
849 849 revs = opts.get('rev', [])
850 850 goal = 'new' # This invocation goal, in new, continue, abort
851 851 if force and not outg:
852 852 raise error.Abort(_('--force only allowed with --outgoing'))
853 853 if cont:
854 854 if any((outg, abort, revs, freeargs, rules, editplan)):
855 855 raise error.Abort(_('no arguments allowed with --continue'))
856 856 goal = 'continue'
857 857 elif abort:
858 858 if any((outg, revs, freeargs, rules, editplan)):
859 859 raise error.Abort(_('no arguments allowed with --abort'))
860 860 goal = 'abort'
861 861 elif editplan:
862 862 if any((outg, revs, freeargs)):
863 863 raise error.Abort(_('only --commands argument allowed with '
864 864 '--edit-plan'))
865 865 goal = 'edit-plan'
866 866 else:
867 867 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
868 868 raise error.Abort(_('history edit already in progress, try '
869 869 '--continue or --abort'))
870 870 if outg:
871 871 if revs:
872 872 raise error.Abort(_('no revisions allowed with --outgoing'))
873 873 if len(freeargs) > 1:
874 874 raise error.Abort(
875 875 _('only one repo argument allowed with --outgoing'))
876 876 else:
877 877 revs.extend(freeargs)
878 878 if len(revs) == 0:
879 879 # experimental config: histedit.defaultrev
880 880 histeditdefault = ui.config('histedit', 'defaultrev')
881 881 if histeditdefault:
882 882 revs.append(histeditdefault)
883 883 if len(revs) != 1:
884 884 raise error.Abort(
885 885 _('histedit requires exactly one ancestor revision'))
886 886
887 887
888 888 replacements = []
889 889 state.keep = opts.get('keep', False)
890 890 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
891 891
892 892 # rebuild state
893 893 if goal == 'continue':
894 894 state.read()
895 895 state = bootstrapcontinue(ui, state, opts)
896 896 elif goal == 'edit-plan':
897 897 state.read()
898 898 if not rules:
899 899 comment = editcomment % (node.short(state.parentctxnode),
900 900 node.short(state.topmost))
901 901 rules = ruleeditor(repo, ui, state.rules, comment)
902 902 else:
903 903 if rules == '-':
904 904 f = sys.stdin
905 905 else:
906 906 f = open(rules)
907 907 rules = f.read()
908 908 f.close()
909 909 rules = [l for l in (r.strip() for r in rules.splitlines())
910 910 if l and not l.startswith('#')]
911 911 rules = verifyrules(rules, state, [repo[c] for [_a, c] in state.rules])
912 912 state.rules = rules
913 913 state.write()
914 914 return
915 915 elif goal == 'abort':
916 916 try:
917 917 state.read()
918 918 tmpnodes, leafs = newnodestoabort(state)
919 919 ui.debug('restore wc to old parent %s\n'
920 920 % node.short(state.topmost))
921 921
922 922 # Recover our old commits if necessary
923 923 if not state.topmost in repo and state.backupfile:
924 924 backupfile = repo.join(state.backupfile)
925 925 f = hg.openpath(ui, backupfile)
926 926 gen = exchange.readbundle(ui, f, backupfile)
927 927 tr = repo.transaction('histedit.abort')
928 928 try:
929 929 if not isinstance(gen, bundle2.unbundle20):
930 930 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
931 931 if isinstance(gen, bundle2.unbundle20):
932 932 bundle2.applybundle(repo, gen, tr,
933 933 source='histedit',
934 934 url='bundle:' + backupfile)
935 935 tr.close()
936 936 finally:
937 937 tr.release()
938 938
939 939 os.remove(backupfile)
940 940
941 941 # check whether we should update away
942 942 if repo.unfiltered().revs('parents() and (%n or %ln::)',
943 943 state.parentctxnode, leafs | tmpnodes):
944 944 hg.clean(repo, state.topmost)
945 945 cleanupnode(ui, repo, 'created', tmpnodes)
946 946 cleanupnode(ui, repo, 'temp', leafs)
947 947 except Exception:
948 948 if state.inprogress():
949 949 ui.warn(_('warning: encountered an exception during histedit '
950 950 '--abort; the repository may not have been completely '
951 951 'cleaned up\n'))
952 952 raise
953 953 finally:
954 954 state.clear()
955 955 return
956 956 else:
957 957 cmdutil.checkunfinished(repo)
958 958 cmdutil.bailifchanged(repo)
959 959
960 960 topmost, empty = repo.dirstate.parents()
961 961 if outg:
962 962 if freeargs:
963 963 remote = freeargs[0]
964 964 else:
965 965 remote = None
966 966 root = findoutgoing(ui, repo, remote, force, opts)
967 967 else:
968 968 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
969 969 if len(rr) != 1:
970 970 raise error.Abort(_('The specified revisions must have '
971 971 'exactly one common root'))
972 972 root = rr[0].node()
973 973
974 974 revs = between(repo, root, topmost, state.keep)
975 975 if not revs:
976 976 raise error.Abort(_('%s is not an ancestor of working directory') %
977 977 node.short(root))
978 978
979 979 ctxs = [repo[r] for r in revs]
980 980 if not rules:
981 981 comment = editcomment % (node.short(root), node.short(topmost))
982 982 rules = ruleeditor(repo, ui, [['pick', c] for c in ctxs], comment)
983 983 else:
984 984 if rules == '-':
985 985 f = sys.stdin
986 986 else:
987 987 f = open(rules)
988 988 rules = f.read()
989 989 f.close()
990 990 rules = [l for l in (r.strip() for r in rules.splitlines())
991 991 if l and not l.startswith('#')]
992 992 rules = verifyrules(rules, state, ctxs)
993 993
994 994 parentctxnode = repo[root].parents()[0].node()
995 995
996 996 state.parentctxnode = parentctxnode
997 997 state.rules = rules
998 998 state.topmost = topmost
999 999 state.replacements = replacements
1000 1000
1001 1001 # Create a backup so we can always abort completely.
1002 1002 backupfile = None
1003 1003 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1004 1004 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1005 1005 'histedit')
1006 1006 state.backupfile = backupfile
1007 1007
1008 1008 # preprocess rules so that we can hide inner folds from the user
1009 1009 # and only show one editor
1010 1010 rules = state.rules[:]
1011 1011 for idx, ((action, ha), (nextact, unused)) in enumerate(
1012 1012 zip(rules, rules[1:] + [(None, None)])):
1013 1013 if action == 'fold' and nextact == 'fold':
1014 1014 state.rules[idx] = '_multifold', ha
1015 1015
1016 1016 while state.rules:
1017 1017 state.write()
1018 1018 action, ha = state.rules.pop(0)
1019 1019 ui.debug('histedit: processing %s %s\n' % (action, ha[:12]))
1020 1020 actobj = actiontable[action].fromrule(state, ha)
1021 1021 parentctx, replacement_ = actobj.run()
1022 1022 state.parentctxnode = parentctx.node()
1023 1023 state.replacements.extend(replacement_)
1024 1024 state.write()
1025 1025
1026 1026 hg.update(repo, state.parentctxnode)
1027 1027
1028 1028 mapping, tmpnodes, created, ntm = processreplacement(state)
1029 1029 if mapping:
1030 1030 for prec, succs in mapping.iteritems():
1031 1031 if not succs:
1032 1032 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1033 1033 else:
1034 1034 ui.debug('histedit: %s is replaced by %s\n' % (
1035 1035 node.short(prec), node.short(succs[0])))
1036 1036 if len(succs) > 1:
1037 1037 m = 'histedit: %s'
1038 1038 for n in succs[1:]:
1039 1039 ui.debug(m % node.short(n))
1040 1040
1041 1041 if supportsmarkers:
1042 1042 # Only create markers if the temp nodes weren't already removed.
1043 1043 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1044 1044 if t in repo))
1045 1045 else:
1046 1046 cleanupnode(ui, repo, 'temp', tmpnodes)
1047 1047
1048 1048 if not state.keep:
1049 1049 if mapping:
1050 1050 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1051 1051 # TODO update mq state
1052 1052 if supportsmarkers:
1053 1053 markers = []
1054 1054 # sort by revision number because it sound "right"
1055 1055 for prec in sorted(mapping, key=repo.changelog.rev):
1056 1056 succs = mapping[prec]
1057 1057 markers.append((repo[prec],
1058 1058 tuple(repo[s] for s in succs)))
1059 1059 if markers:
1060 1060 obsolete.createmarkers(repo, markers)
1061 1061 else:
1062 1062 cleanupnode(ui, repo, 'replaced', mapping)
1063 1063
1064 1064 state.clear()
1065 1065 if os.path.exists(repo.sjoin('undo')):
1066 1066 os.unlink(repo.sjoin('undo'))
1067 1067
1068 1068 def bootstrapcontinue(ui, state, opts):
1069 1069 repo = state.repo
1070 1070 if state.rules:
1071 1071 action, currentnode = state.rules.pop(0)
1072 1072
1073 1073 actobj = actiontable[action].fromrule(state, currentnode)
1074 1074
1075 1075 if _isdirtywc(repo):
1076 1076 actobj.continuedirty()
1077 1077 if _isdirtywc(repo):
1078 1078 abortdirty()
1079 1079
1080 1080 parentctx, replacements = actobj.continueclean()
1081 1081
1082 1082 state.parentctxnode = parentctx.node()
1083 1083 state.replacements.extend(replacements)
1084 1084
1085 1085 return state
1086 1086
1087 1087 def between(repo, old, new, keep):
1088 1088 """select and validate the set of revision to edit
1089 1089
1090 1090 When keep is false, the specified set can't have children."""
1091 1091 ctxs = list(repo.set('%n::%n', old, new))
1092 1092 if ctxs and not keep:
1093 1093 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1094 1094 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1095 1095 raise error.Abort(_('cannot edit history that would orphan nodes'))
1096 1096 if repo.revs('(%ld) and merge()', ctxs):
1097 1097 raise error.Abort(_('cannot edit history that contains merges'))
1098 1098 root = ctxs[0] # list is already sorted by repo.set
1099 1099 if not root.mutable():
1100 1100 raise error.Abort(_('cannot edit public changeset: %s') % root,
1101 1101 hint=_('see "hg help phases" for details'))
1102 1102 return [c.node() for c in ctxs]
1103 1103
1104 1104 def makedesc(repo, action, rev):
1105 1105 """build a initial action line for a ctx
1106 1106
1107 1107 line are in the form:
1108 1108
1109 1109 <action> <hash> <rev> <summary>
1110 1110 """
1111 1111 ctx = repo[rev]
1112 1112 summary = ''
1113 1113 if ctx.description():
1114 1114 summary = ctx.description().splitlines()[0]
1115 1115 line = '%s %s %d %s' % (action, ctx, ctx.rev(), summary)
1116 1116 # trim to 80 columns so it's not stupidly wide in my editor
1117 1117 maxlen = repo.ui.configint('histedit', 'linelen', default=80)
1118 1118 maxlen = max(maxlen, 22) # avoid truncating hash
1119 1119 return util.ellipsis(line, maxlen)
1120 1120
1121 1121 def ruleeditor(repo, ui, rules, editcomment=""):
1122 1122 """open an editor to edit rules
1123 1123
1124 1124 rules are in the format [ [act, ctx], ...] like in state.rules
1125 1125 """
1126 1126 rules = '\n'.join([makedesc(repo, act, rev) for [act, rev] in rules])
1127 1127 rules += '\n\n'
1128 1128 rules += editcomment
1129 rules = ui.edit(rules, ui.username())
1129 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'})
1130 1130
1131 1131 # Save edit rules in .hg/histedit-last-edit.txt in case
1132 1132 # the user needs to ask for help after something
1133 1133 # surprising happens.
1134 1134 f = open(repo.join('histedit-last-edit.txt'), 'w')
1135 1135 f.write(rules)
1136 1136 f.close()
1137 1137
1138 1138 return rules
1139 1139
1140 1140 def verifyrules(rules, state, ctxs):
1141 1141 """Verify that there exists exactly one edit rule per given changeset.
1142 1142
1143 1143 Will abort if there are to many or too few rules, a malformed rule,
1144 1144 or a rule on a changeset outside of the user-given range.
1145 1145 """
1146 1146 parsed = []
1147 1147 expected = set(c.hex() for c in ctxs)
1148 1148 seen = set()
1149 1149 for r in rules:
1150 1150 if ' ' not in r:
1151 1151 raise error.Abort(_('malformed line "%s"') % r)
1152 1152 verb, rest = r.split(' ', 1)
1153 1153
1154 1154 if verb not in actiontable or verb.startswith('_'):
1155 1155 raise error.Abort(_('unknown action "%s"') % verb)
1156 1156 action = actiontable[verb].fromrule(state, rest)
1157 1157 constraints = action.constraints()
1158 1158 for constraint in constraints:
1159 1159 if constraint not in _constraints.known():
1160 1160 error.Abort(_('unknown constraint "%s"') % constraint)
1161 1161
1162 1162 nodetoverify = action.nodetoverify()
1163 1163 if nodetoverify is not None:
1164 1164 ha = node.hex(nodetoverify)
1165 1165 if _constraints.noother in constraints and ha not in expected:
1166 1166 raise error.Abort(
1167 1167 _('may not use "%s" with changesets '
1168 1168 'other than the ones listed') % verb)
1169 1169 if _constraints.forceother in constraints and ha in expected:
1170 1170 raise error.Abort(
1171 1171 _('may not use "%s" with changesets '
1172 1172 'within the edited list') % verb)
1173 1173 if _constraints.noduplicates in constraints and ha in seen:
1174 1174 raise error.Abort(_('duplicated command for changeset %s') %
1175 1175 ha[:12])
1176 1176 seen.add(ha)
1177 1177 rest = ha
1178 1178 parsed.append([verb, rest])
1179 1179 missing = sorted(expected - seen) # sort to stabilize output
1180 1180 if missing:
1181 1181 raise error.Abort(_('missing rules for changeset %s') %
1182 1182 missing[0][:12],
1183 1183 hint=_('do you want to use the drop action?'))
1184 1184 return parsed
1185 1185
1186 1186 def newnodestoabort(state):
1187 1187 """process the list of replacements to return
1188 1188
1189 1189 1) the list of final node
1190 1190 2) the list of temporary node
1191 1191
1192 1192 This meant to be used on abort as less data are required in this case.
1193 1193 """
1194 1194 replacements = state.replacements
1195 1195 allsuccs = set()
1196 1196 replaced = set()
1197 1197 for rep in replacements:
1198 1198 allsuccs.update(rep[1])
1199 1199 replaced.add(rep[0])
1200 1200 newnodes = allsuccs - replaced
1201 1201 tmpnodes = allsuccs & replaced
1202 1202 return newnodes, tmpnodes
1203 1203
1204 1204
1205 1205 def processreplacement(state):
1206 1206 """process the list of replacements to return
1207 1207
1208 1208 1) the final mapping between original and created nodes
1209 1209 2) the list of temporary node created by histedit
1210 1210 3) the list of new commit created by histedit"""
1211 1211 replacements = state.replacements
1212 1212 allsuccs = set()
1213 1213 replaced = set()
1214 1214 fullmapping = {}
1215 1215 # initialize basic set
1216 1216 # fullmapping records all operations recorded in replacement
1217 1217 for rep in replacements:
1218 1218 allsuccs.update(rep[1])
1219 1219 replaced.add(rep[0])
1220 1220 fullmapping.setdefault(rep[0], set()).update(rep[1])
1221 1221 new = allsuccs - replaced
1222 1222 tmpnodes = allsuccs & replaced
1223 1223 # Reduce content fullmapping into direct relation between original nodes
1224 1224 # and final node created during history edition
1225 1225 # Dropped changeset are replaced by an empty list
1226 1226 toproceed = set(fullmapping)
1227 1227 final = {}
1228 1228 while toproceed:
1229 1229 for x in list(toproceed):
1230 1230 succs = fullmapping[x]
1231 1231 for s in list(succs):
1232 1232 if s in toproceed:
1233 1233 # non final node with unknown closure
1234 1234 # We can't process this now
1235 1235 break
1236 1236 elif s in final:
1237 1237 # non final node, replace with closure
1238 1238 succs.remove(s)
1239 1239 succs.update(final[s])
1240 1240 else:
1241 1241 final[x] = succs
1242 1242 toproceed.remove(x)
1243 1243 # remove tmpnodes from final mapping
1244 1244 for n in tmpnodes:
1245 1245 del final[n]
1246 1246 # we expect all changes involved in final to exist in the repo
1247 1247 # turn `final` into list (topologically sorted)
1248 1248 nm = state.repo.changelog.nodemap
1249 1249 for prec, succs in final.items():
1250 1250 final[prec] = sorted(succs, key=nm.get)
1251 1251
1252 1252 # computed topmost element (necessary for bookmark)
1253 1253 if new:
1254 1254 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1255 1255 elif not final:
1256 1256 # Nothing rewritten at all. we won't need `newtopmost`
1257 1257 # It is the same as `oldtopmost` and `processreplacement` know it
1258 1258 newtopmost = None
1259 1259 else:
1260 1260 # every body died. The newtopmost is the parent of the root.
1261 1261 r = state.repo.changelog.rev
1262 1262 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1263 1263
1264 1264 return final, tmpnodes, new, newtopmost
1265 1265
1266 1266 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1267 1267 """Move bookmark from old to newly created node"""
1268 1268 if not mapping:
1269 1269 # if nothing got rewritten there is not purpose for this function
1270 1270 return
1271 1271 moves = []
1272 1272 for bk, old in sorted(repo._bookmarks.iteritems()):
1273 1273 if old == oldtopmost:
1274 1274 # special case ensure bookmark stay on tip.
1275 1275 #
1276 1276 # This is arguably a feature and we may only want that for the
1277 1277 # active bookmark. But the behavior is kept compatible with the old
1278 1278 # version for now.
1279 1279 moves.append((bk, newtopmost))
1280 1280 continue
1281 1281 base = old
1282 1282 new = mapping.get(base, None)
1283 1283 if new is None:
1284 1284 continue
1285 1285 while not new:
1286 1286 # base is killed, trying with parent
1287 1287 base = repo[base].p1().node()
1288 1288 new = mapping.get(base, (base,))
1289 1289 # nothing to move
1290 1290 moves.append((bk, new[-1]))
1291 1291 if moves:
1292 1292 lock = tr = None
1293 1293 try:
1294 1294 lock = repo.lock()
1295 1295 tr = repo.transaction('histedit')
1296 1296 marks = repo._bookmarks
1297 1297 for mark, new in moves:
1298 1298 old = marks[mark]
1299 1299 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1300 1300 % (mark, node.short(old), node.short(new)))
1301 1301 marks[mark] = new
1302 1302 marks.recordchange(tr)
1303 1303 tr.close()
1304 1304 finally:
1305 1305 release(tr, lock)
1306 1306
1307 1307 def cleanupnode(ui, repo, name, nodes):
1308 1308 """strip a group of nodes from the repository
1309 1309
1310 1310 The set of node to strip may contains unknown nodes."""
1311 1311 ui.debug('should strip %s nodes %s\n' %
1312 1312 (name, ', '.join([node.short(n) for n in nodes])))
1313 1313 lock = None
1314 1314 try:
1315 1315 lock = repo.lock()
1316 1316 # do not let filtering get in the way of the cleanse
1317 1317 # we should probably get rid of obsolescence marker created during the
1318 1318 # histedit, but we currently do not have such information.
1319 1319 repo = repo.unfiltered()
1320 1320 # Find all nodes that need to be stripped
1321 1321 # (we use %lr instead of %ln to silently ignore unknown items)
1322 1322 nm = repo.changelog.nodemap
1323 1323 nodes = sorted(n for n in nodes if n in nm)
1324 1324 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1325 1325 for c in roots:
1326 1326 # We should process node in reverse order to strip tip most first.
1327 1327 # but this trigger a bug in changegroup hook.
1328 1328 # This would reduce bundle overhead
1329 1329 repair.strip(ui, repo, c)
1330 1330 finally:
1331 1331 release(lock)
1332 1332
1333 1333 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1334 1334 if isinstance(nodelist, str):
1335 1335 nodelist = [nodelist]
1336 1336 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1337 1337 state = histeditstate(repo)
1338 1338 state.read()
1339 1339 histedit_nodes = set([repo[rulehash].node() for (action, rulehash)
1340 1340 in state.rules if rulehash in repo])
1341 1341 strip_nodes = set([repo[n].node() for n in nodelist])
1342 1342 common_nodes = histedit_nodes & strip_nodes
1343 1343 if common_nodes:
1344 1344 raise error.Abort(_("histedit in progress, can't strip %s")
1345 1345 % ', '.join(node.short(x) for x in common_nodes))
1346 1346 return orig(ui, repo, nodelist, *args, **kwargs)
1347 1347
1348 1348 extensions.wrapfunction(repair, 'strip', stripwrapper)
1349 1349
1350 1350 def summaryhook(ui, repo):
1351 1351 if not os.path.exists(repo.join('histedit-state')):
1352 1352 return
1353 1353 state = histeditstate(repo)
1354 1354 state.read()
1355 1355 if state.rules:
1356 1356 # i18n: column positioning for "hg summary"
1357 1357 ui.write(_('hist: %s (histedit --continue)\n') %
1358 1358 (ui.label(_('%d remaining'), 'histedit.remaining') %
1359 1359 len(state.rules)))
1360 1360
1361 1361 def extsetup(ui):
1362 1362 cmdutil.summaryhooks.add('histedit', summaryhook)
1363 1363 cmdutil.unfinishedstates.append(
1364 1364 ['histedit-state', False, True, _('histedit in progress'),
1365 1365 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1366 1366 if ui.configbool("experimental", "histeditng"):
1367 1367 actiontable.update({'b': base, 'base': base})
General Comments 0
You need to be logged in to leave comments. Login now