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