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