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