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