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