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