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