comparison hgext/histedit.py @ 43076:2372284d9457

formatting: blacken the codebase This is using my patch to black (https://github.com/psf/black/pull/826) so we don't un-wrap collection literals. Done with: hg files 'set:**.py - mercurial/thirdparty/** - "contrib/python-zstandard/**"' | xargs black -S # skip-blame mass-reformatting only # no-check-commit reformats foo_bar functions Differential Revision: https://phab.mercurial-scm.org/D6971
author Augie Fackler <augie@google.com>
date Sun, 06 Oct 2019 09:45:02 -0400
parents b4093d1d3b18
children 687b865b95ad
comparison
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
238 cmdtable = {} 238 cmdtable = {}
239 command = registrar.command(cmdtable) 239 command = registrar.command(cmdtable)
240 240
241 configtable = {} 241 configtable = {}
242 configitem = registrar.configitem(configtable) 242 configitem = registrar.configitem(configtable)
243 configitem('experimental', 'histedit.autoverb', 243 configitem(
244 default=False, 244 'experimental', 'histedit.autoverb', default=False,
245 ) 245 )
246 configitem('histedit', 'defaultrev', 246 configitem(
247 default=None, 247 'histedit', 'defaultrev', default=None,
248 ) 248 )
249 configitem('histedit', 'dropmissing', 249 configitem(
250 default=False, 250 'histedit', 'dropmissing', default=False,
251 ) 251 )
252 configitem('histedit', 'linelen', 252 configitem(
253 default=80, 253 'histedit', 'linelen', default=80,
254 ) 254 )
255 configitem('histedit', 'singletransaction', 255 configitem(
256 default=False, 256 'histedit', 'singletransaction', default=False,
257 ) 257 )
258 configitem('ui', 'interface.histedit', 258 configitem(
259 default=None, 259 'ui', 'interface.histedit', default=None,
260 ) 260 )
261 configitem('histedit', 'summary-template', 261 configitem('histedit', 'summary-template', default='{rev} {desc|firstline}')
262 default='{rev} {desc|firstline}')
263 262
264 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for 263 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
265 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should 264 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
266 # be specifying the version(s) of Mercurial they are tested with, or 265 # be specifying the version(s) of Mercurial they are tested with, or
267 # leave the attribute unspecified. 266 # leave the attribute unspecified.
270 actiontable = {} 269 actiontable = {}
271 primaryactions = set() 270 primaryactions = set()
272 secondaryactions = set() 271 secondaryactions = set()
273 tertiaryactions = set() 272 tertiaryactions = set()
274 internalactions = set() 273 internalactions = set()
274
275 275
276 def geteditcomment(ui, first, last): 276 def geteditcomment(ui, first, last):
277 """ construct the editor comment 277 """ construct the editor comment
278 The comment includes:: 278 The comment includes::
279 - an intro 279 - an intro
282 - sorted long commands 282 - sorted long commands
283 - additional hints 283 - additional hints
284 284
285 Commands are only included once. 285 Commands are only included once.
286 """ 286 """
287 intro = _("""Edit history between %s and %s 287 intro = _(
288 """Edit history between %s and %s
288 289
289 Commits are listed from least to most recent 290 Commits are listed from least to most recent
290 291
291 You can reorder changesets by reordering the lines 292 You can reorder changesets by reordering the lines
292 293
293 Commands: 294 Commands:
294 """) 295 """
296 )
295 actions = [] 297 actions = []
298
296 def addverb(v): 299 def addverb(v):
297 a = actiontable[v] 300 a = actiontable[v]
298 lines = a.message.split("\n") 301 lines = a.message.split("\n")
299 if len(a.verbs): 302 if len(a.verbs):
300 v = ', '.join(sorted(a.verbs, key=lambda v: len(v))) 303 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
301 actions.append(" %s = %s" % (v, lines[0])) 304 actions.append(" %s = %s" % (v, lines[0]))
302 actions.extend([' %s' for l in lines[1:]]) 305 actions.extend([' %s' for l in lines[1:]])
303 306
304 for v in ( 307 for v in (
305 sorted(primaryactions) + 308 sorted(primaryactions)
306 sorted(secondaryactions) + 309 + sorted(secondaryactions)
307 sorted(tertiaryactions) 310 + sorted(tertiaryactions)
308 ): 311 ):
309 addverb(v) 312 addverb(v)
310 actions.append('') 313 actions.append('')
311 314
312 hints = [] 315 hints = []
313 if ui.configbool('histedit', 'dropmissing'): 316 if ui.configbool('histedit', 'dropmissing'):
314 hints.append("Deleting a changeset from the list " 317 hints.append(
315 "will DISCARD it from the edited history!") 318 "Deleting a changeset from the list "
319 "will DISCARD it from the edited history!"
320 )
316 321
317 lines = (intro % (first, last)).split('\n') + actions + hints 322 lines = (intro % (first, last)).split('\n') + actions + hints
318 323
319 return ''.join(['# %s\n' % l if l else '#\n' for l in lines]) 324 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
325
320 326
321 class histeditstate(object): 327 class histeditstate(object):
322 def __init__(self, repo): 328 def __init__(self, repo):
323 self.repo = repo 329 self.repo = repo
324 self.actions = None 330 self.actions = None
355 data = pickle.loads(fp) 361 data = pickle.loads(fp)
356 parentctxnode, rules, keep, topmost, replacements = data 362 parentctxnode, rules, keep, topmost, replacements = data
357 backupfile = None 363 backupfile = None
358 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules]) 364 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
359 365
360 return {'parentctxnode': parentctxnode, "rules": rules, "keep": keep, 366 return {
361 "topmost": topmost, "replacements": replacements, 367 'parentctxnode': parentctxnode,
362 "backupfile": backupfile} 368 "rules": rules,
369 "keep": keep,
370 "topmost": topmost,
371 "replacements": replacements,
372 "backupfile": backupfile,
373 }
363 374
364 def write(self, tr=None): 375 def write(self, tr=None):
365 if tr: 376 if tr:
366 tr.addfilegenerator('histedit-state', ('histedit-state',), 377 tr.addfilegenerator(
367 self._write, location='plain') 378 'histedit-state',
379 ('histedit-state',),
380 self._write,
381 location='plain',
382 )
368 else: 383 else:
369 with self.repo.vfs("histedit-state", "w") as f: 384 with self.repo.vfs("histedit-state", "w") as f:
370 self._write(f) 385 self._write(f)
371 386
372 def _write(self, fp): 387 def _write(self, fp):
377 fp.write('%d\n' % len(self.actions)) 392 fp.write('%d\n' % len(self.actions))
378 for action in self.actions: 393 for action in self.actions:
379 fp.write('%s\n' % action.tostate()) 394 fp.write('%s\n' % action.tostate())
380 fp.write('%d\n' % len(self.replacements)) 395 fp.write('%d\n' % len(self.replacements))
381 for replacement in self.replacements: 396 for replacement in self.replacements:
382 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r) 397 fp.write(
383 for r in replacement[1]))) 398 '%s%s\n'
399 % (
400 node.hex(replacement[0]),
401 ''.join(node.hex(r) for r in replacement[1]),
402 )
403 )
384 backupfile = self.backupfile 404 backupfile = self.backupfile
385 if not backupfile: 405 if not backupfile:
386 backupfile = '' 406 backupfile = ''
387 fp.write('%s\n' % backupfile) 407 fp.write('%s\n' % backupfile)
388 408
389 def _load(self): 409 def _load(self):
390 fp = self.repo.vfs('histedit-state', 'r') 410 fp = self.repo.vfs('histedit-state', 'r')
391 lines = [l[:-1] for l in fp.readlines()] 411 lines = [l[:-1] for l in fp.readlines()]
392 412
393 index = 0 413 index = 0
394 lines[index] # version number 414 lines[index] # version number
395 index += 1 415 index += 1
396 416
397 parentctxnode = node.bin(lines[index]) 417 parentctxnode = node.bin(lines[index])
398 index += 1 418 index += 1
399 419
419 replacementlen = int(lines[index]) 439 replacementlen = int(lines[index])
420 index += 1 440 index += 1
421 for i in pycompat.xrange(replacementlen): 441 for i in pycompat.xrange(replacementlen):
422 replacement = lines[index] 442 replacement = lines[index]
423 original = node.bin(replacement[:40]) 443 original = node.bin(replacement[:40])
424 succ = [node.bin(replacement[i:i + 40]) for i in 444 succ = [
425 range(40, len(replacement), 40)] 445 node.bin(replacement[i : i + 40])
446 for i in range(40, len(replacement), 40)
447 ]
426 replacements.append((original, succ)) 448 replacements.append((original, succ))
427 index += 1 449 index += 1
428 450
429 backupfile = lines[index] 451 backupfile = lines[index]
430 index += 1 452 index += 1
475 self._verifynodeconstraints(prev, expected, seen) 497 self._verifynodeconstraints(prev, expected, seen)
476 498
477 def _verifynodeconstraints(self, prev, expected, seen): 499 def _verifynodeconstraints(self, prev, expected, seen):
478 # by default command need a node in the edited list 500 # by default command need a node in the edited list
479 if self.node not in expected: 501 if self.node not in expected:
480 raise error.ParseError(_('%s "%s" changeset was not a candidate') 502 raise error.ParseError(
481 % (self.verb, node.short(self.node)), 503 _('%s "%s" changeset was not a candidate')
482 hint=_('only use listed changesets')) 504 % (self.verb, node.short(self.node)),
505 hint=_('only use listed changesets'),
506 )
483 # and only one command per node 507 # and only one command per node
484 if self.node in seen: 508 if self.node in seen:
485 raise error.ParseError(_('duplicated command for changeset %s') % 509 raise error.ParseError(
486 node.short(self.node)) 510 _('duplicated command for changeset %s') % node.short(self.node)
511 )
487 512
488 def torule(self): 513 def torule(self):
489 """build a histedit rule line for an action 514 """build a histedit rule line for an action
490 515
491 by default lines are in the form: 516 by default lines are in the form:
492 <hash> <rev> <summary> 517 <hash> <rev> <summary>
493 """ 518 """
494 ctx = self.repo[self.node] 519 ctx = self.repo[self.node]
495 ui = self.repo.ui 520 ui = self.repo.ui
496 summary = cmdutil.rendertemplate( 521 summary = (
497 ctx, ui.config('histedit', 'summary-template')) or '' 522 cmdutil.rendertemplate(
523 ctx, ui.config('histedit', 'summary-template')
524 )
525 or ''
526 )
498 summary = summary.splitlines()[0] 527 summary = summary.splitlines()[0]
499 line = '%s %s %s' % (self.verb, ctx, summary) 528 line = '%s %s %s' % (self.verb, ctx, summary)
500 # trim to 75 columns by default so it's not stupidly wide in my editor 529 # trim to 75 columns by default so it's not stupidly wide in my editor
501 # (the 5 more are left for verb) 530 # (the 5 more are left for verb)
502 maxlen = self.repo.ui.configint('histedit', 'linelen') 531 maxlen = self.repo.ui.configint('histedit', 'linelen')
503 maxlen = max(maxlen, 22) # avoid truncating hash 532 maxlen = max(maxlen, 22) # avoid truncating hash
504 return stringutil.ellipsis(line, maxlen) 533 return stringutil.ellipsis(line, maxlen)
505 534
506 def tostate(self): 535 def tostate(self):
507 """Print an action in format used by histedit state files 536 """Print an action in format used by histedit state files
508 (the first line is a verb, the remainder is the second) 537 (the first line is a verb, the remainder is the second)
526 repo.ui.popbuffer() 555 repo.ui.popbuffer()
527 stats = applychanges(repo.ui, repo, rulectx, {}) 556 stats = applychanges(repo.ui, repo, rulectx, {})
528 repo.dirstate.setbranch(rulectx.branch()) 557 repo.dirstate.setbranch(rulectx.branch())
529 if stats.unresolvedcount: 558 if stats.unresolvedcount:
530 raise error.InterventionRequired( 559 raise error.InterventionRequired(
531 _('Fix up the change (%s %s)') % 560 _('Fix up the change (%s %s)')
532 (self.verb, node.short(self.node)), 561 % (self.verb, node.short(self.node)),
533 hint=_('hg histedit --continue to resume')) 562 hint=_('hg histedit --continue to resume'),
563 )
534 564
535 def continuedirty(self): 565 def continuedirty(self):
536 """Continues the action when changes have been applied to the working 566 """Continues the action when changes have been applied to the working
537 copy. The default behavior is to commit the dirty changes.""" 567 copy. The default behavior is to commit the dirty changes."""
538 repo = self.repo 568 repo = self.repo
542 commit = commitfuncfor(repo, rulectx) 572 commit = commitfuncfor(repo, rulectx)
543 if repo.ui.configbool('rewrite', 'update-timestamp'): 573 if repo.ui.configbool('rewrite', 'update-timestamp'):
544 date = dateutil.makedate() 574 date = dateutil.makedate()
545 else: 575 else:
546 date = rulectx.date() 576 date = rulectx.date()
547 commit(text=rulectx.description(), user=rulectx.user(), 577 commit(
548 date=date, extra=rulectx.extra(), editor=editor) 578 text=rulectx.description(),
579 user=rulectx.user(),
580 date=date,
581 extra=rulectx.extra(),
582 editor=editor,
583 )
549 584
550 def commiteditor(self): 585 def commiteditor(self):
551 """The editor to be used to edit the commit message.""" 586 """The editor to be used to edit the commit message."""
552 return False 587 return False
553 588
555 """Continues the action when the working copy is clean. The default 590 """Continues the action when the working copy is clean. The default
556 behavior is to accept the current commit as the new version of the 591 behavior is to accept the current commit as the new version of the
557 rulectx.""" 592 rulectx."""
558 ctx = self.repo['.'] 593 ctx = self.repo['.']
559 if ctx.node() == self.state.parentctxnode: 594 if ctx.node() == self.state.parentctxnode:
560 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') % 595 self.repo.ui.warn(
561 node.short(self.node)) 596 _('%s: skipping changeset (no changes)\n')
597 % node.short(self.node)
598 )
562 return ctx, [(self.node, tuple())] 599 return ctx, [(self.node, tuple())]
563 if ctx.node() == self.node: 600 if ctx.node() == self.node:
564 # Nothing changed 601 # Nothing changed
565 return ctx, [] 602 return ctx, []
566 return ctx, [(self.node, (ctx.node(),))] 603 return ctx, [(self.node, (ctx.node(),))]
567 604
605
568 def commitfuncfor(repo, src): 606 def commitfuncfor(repo, src):
569 """Build a commit function for the replacement of <src> 607 """Build a commit function for the replacement of <src>
570 608
571 This function ensure we apply the same treatment to all changesets. 609 This function ensure we apply the same treatment to all changesets.
572 610
574 612
575 Note that fold has its own separated logic because its handling is a bit 613 Note that fold has its own separated logic because its handling is a bit
576 different and not easily factored out of the fold method. 614 different and not easily factored out of the fold method.
577 """ 615 """
578 phasemin = src.phase() 616 phasemin = src.phase()
617
579 def commitfunc(**kwargs): 618 def commitfunc(**kwargs):
580 overrides = {('phases', 'new-commit'): phasemin} 619 overrides = {('phases', 'new-commit'): phasemin}
581 with repo.ui.configoverride(overrides, 'histedit'): 620 with repo.ui.configoverride(overrides, 'histedit'):
582 extra = kwargs.get(r'extra', {}).copy() 621 extra = kwargs.get(r'extra', {}).copy()
583 extra['histedit_source'] = src.hex() 622 extra['histedit_source'] = src.hex()
584 kwargs[r'extra'] = extra 623 kwargs[r'extra'] = extra
585 return repo.commit(**kwargs) 624 return repo.commit(**kwargs)
625
586 return commitfunc 626 return commitfunc
627
587 628
588 def applychanges(ui, repo, ctx, opts): 629 def applychanges(ui, repo, ctx, opts):
589 """Merge changeset from ctx (only) in the current working directory""" 630 """Merge changeset from ctx (only) in the current working directory"""
590 wcpar = repo.dirstate.p1() 631 wcpar = repo.dirstate.p1()
591 if ctx.p1().node() == wcpar: 632 if ctx.p1().node() == wcpar:
596 stats = mergemod.updateresult(0, 0, 0, 0) 637 stats = mergemod.updateresult(0, 0, 0, 0)
597 ui.popbuffer() 638 ui.popbuffer()
598 else: 639 else:
599 try: 640 try:
600 # ui.forcemerge is an internal variable, do not document 641 # ui.forcemerge is an internal variable, do not document
601 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''), 642 repo.ui.setconfig(
602 'histedit') 643 'ui', 'forcemerge', opts.get('tool', ''), 'histedit'
644 )
603 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit']) 645 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
604 finally: 646 finally:
605 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit') 647 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
606 return stats 648 return stats
649
607 650
608 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False): 651 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
609 """collapse the set of revisions from first to last as new one. 652 """collapse the set of revisions from first to last as new one.
610 653
611 Expected commit options are: 654 Expected commit options are:
619 if not ctxs: 662 if not ctxs:
620 return None 663 return None
621 for c in ctxs: 664 for c in ctxs:
622 if not c.mutable(): 665 if not c.mutable():
623 raise error.ParseError( 666 raise error.ParseError(
624 _("cannot fold into public change %s") % node.short(c.node())) 667 _("cannot fold into public change %s") % node.short(c.node())
668 )
625 base = firstctx.p1() 669 base = firstctx.p1()
626 670
627 # commit a new version of the old changeset, including the update 671 # commit a new version of the old changeset, including the update
628 # collect all files which might be affected 672 # collect all files which might be affected
629 files = set() 673 files = set()
635 679
636 # prune files which were reverted by the updates 680 # prune files which were reverted by the updates
637 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)] 681 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
638 # commit version of these files as defined by head 682 # commit version of these files as defined by head
639 headmf = lastctx.manifest() 683 headmf = lastctx.manifest()
684
640 def filectxfn(repo, ctx, path): 685 def filectxfn(repo, ctx, path):
641 if path in headmf: 686 if path in headmf:
642 fctx = lastctx[path] 687 fctx = lastctx[path]
643 flags = fctx.flags() 688 flags = fctx.flags()
644 mctx = context.memfilectx(repo, ctx, 689 mctx = context.memfilectx(
645 fctx.path(), fctx.data(), 690 repo,
646 islink='l' in flags, 691 ctx,
647 isexec='x' in flags, 692 fctx.path(),
648 copysource=copied.get(path)) 693 fctx.data(),
694 islink='l' in flags,
695 isexec='x' in flags,
696 copysource=copied.get(path),
697 )
649 return mctx 698 return mctx
650 return None 699 return None
651 700
652 if commitopts.get('message'): 701 if commitopts.get('message'):
653 message = commitopts['message'] 702 message = commitopts['message']
659 708
660 parents = (firstctx.p1().node(), firstctx.p2().node()) 709 parents = (firstctx.p1().node(), firstctx.p2().node())
661 editor = None 710 editor = None
662 if not skipprompt: 711 if not skipprompt:
663 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold') 712 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
664 new = context.memctx(repo, 713 new = context.memctx(
665 parents=parents, 714 repo,
666 text=message, 715 parents=parents,
667 files=files, 716 text=message,
668 filectxfn=filectxfn, 717 files=files,
669 user=user, 718 filectxfn=filectxfn,
670 date=date, 719 user=user,
671 extra=extra, 720 date=date,
672 editor=editor) 721 extra=extra,
722 editor=editor,
723 )
673 return repo.commitctx(new) 724 return repo.commitctx(new)
725
674 726
675 def _isdirtywc(repo): 727 def _isdirtywc(repo):
676 return repo[None].dirty(missing=True) 728 return repo[None].dirty(missing=True)
677 729
730
678 def abortdirty(): 731 def abortdirty():
679 raise error.Abort(_('working copy has pending changes'), 732 raise error.Abort(
680 hint=_('amend, commit, or revert them and run histedit ' 733 _('working copy has pending changes'),
681 '--continue, or abort with histedit --abort')) 734 hint=_(
735 'amend, commit, or revert them and run histedit '
736 '--continue, or abort with histedit --abort'
737 ),
738 )
739
682 740
683 def action(verbs, message, priority=False, internal=False): 741 def action(verbs, message, priority=False, internal=False):
684 def wrap(cls): 742 def wrap(cls):
685 assert not priority or not internal 743 assert not priority or not internal
686 verb = verbs[0] 744 verb = verbs[0]
697 cls.verbs = verbs 755 cls.verbs = verbs
698 cls.message = message 756 cls.message = message
699 for verb in verbs: 757 for verb in verbs:
700 actiontable[verb] = cls 758 actiontable[verb] = cls
701 return cls 759 return cls
760
702 return wrap 761 return wrap
703 762
704 @action(['pick', 'p'], 763
705 _('use commit'), 764 @action(['pick', 'p'], _('use commit'), priority=True)
706 priority=True)
707 class pick(histeditaction): 765 class pick(histeditaction):
708 def run(self): 766 def run(self):
709 rulectx = self.repo[self.node] 767 rulectx = self.repo[self.node]
710 if rulectx.p1().node() == self.state.parentctxnode: 768 if rulectx.p1().node() == self.state.parentctxnode:
711 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node)) 769 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
712 return rulectx, [] 770 return rulectx, []
713 771
714 return super(pick, self).run() 772 return super(pick, self).run()
715 773
716 @action(['edit', 'e'], 774
717 _('use commit, but stop for amending'), 775 @action(['edit', 'e'], _('use commit, but stop for amending'), priority=True)
718 priority=True)
719 class edit(histeditaction): 776 class edit(histeditaction):
720 def run(self): 777 def run(self):
721 repo = self.repo 778 repo = self.repo
722 rulectx = repo[self.node] 779 rulectx = repo[self.node]
723 hg.update(repo, self.state.parentctxnode, quietempty=True) 780 hg.update(repo, self.state.parentctxnode, quietempty=True)
724 applychanges(repo.ui, repo, rulectx, {}) 781 applychanges(repo.ui, repo, rulectx, {})
725 raise error.InterventionRequired( 782 raise error.InterventionRequired(
726 _('Editing (%s), you may commit or record as needed now.') 783 _('Editing (%s), you may commit or record as needed now.')
727 % node.short(self.node), 784 % node.short(self.node),
728 hint=_('hg histedit --continue to resume')) 785 hint=_('hg histedit --continue to resume'),
786 )
729 787
730 def commiteditor(self): 788 def commiteditor(self):
731 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit') 789 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
732 790
733 @action(['fold', 'f'], 791
734 _('use commit, but combine it with the one above')) 792 @action(['fold', 'f'], _('use commit, but combine it with the one above'))
735 class fold(histeditaction): 793 class fold(histeditaction):
736 def verify(self, prev, expected, seen): 794 def verify(self, prev, expected, seen):
737 """ Verifies semantic correctness of the fold rule""" 795 """ Verifies semantic correctness of the fold rule"""
738 super(fold, self).verify(prev, expected, seen) 796 super(fold, self).verify(prev, expected, seen)
739 repo = self.repo 797 repo = self.repo
743 return 801 return
744 else: 802 else:
745 c = repo[prev.node] 803 c = repo[prev.node]
746 if not c.mutable(): 804 if not c.mutable():
747 raise error.ParseError( 805 raise error.ParseError(
748 _("cannot fold into public change %s") % node.short(c.node())) 806 _("cannot fold into public change %s") % node.short(c.node())
749 807 )
750 808
751 def continuedirty(self): 809 def continuedirty(self):
752 repo = self.repo 810 repo = self.repo
753 rulectx = repo[self.node] 811 rulectx = repo[self.node]
754 812
755 commit = commitfuncfor(repo, rulectx) 813 commit = commitfuncfor(repo, rulectx)
756 commit(text='fold-temp-revision %s' % node.short(self.node), 814 commit(
757 user=rulectx.user(), date=rulectx.date(), 815 text='fold-temp-revision %s' % node.short(self.node),
758 extra=rulectx.extra()) 816 user=rulectx.user(),
817 date=rulectx.date(),
818 extra=rulectx.extra(),
819 )
759 820
760 def continueclean(self): 821 def continueclean(self):
761 repo = self.repo 822 repo = self.repo
762 ctx = repo['.'] 823 ctx = repo['.']
763 rulectx = repo[self.node] 824 rulectx = repo[self.node]
764 parentctxnode = self.state.parentctxnode 825 parentctxnode = self.state.parentctxnode
765 if ctx.node() == parentctxnode: 826 if ctx.node() == parentctxnode:
766 repo.ui.warn(_('%s: empty changeset\n') % 827 repo.ui.warn(_('%s: empty changeset\n') % node.short(self.node))
767 node.short(self.node))
768 return ctx, [(self.node, (parentctxnode,))] 828 return ctx, [(self.node, (parentctxnode,))]
769 829
770 parentctx = repo[parentctxnode] 830 parentctx = repo[parentctxnode]
771 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', 831 newcommits = set(
772 parentctx.rev(), 832 c.node()
773 parentctx.rev())) 833 for c in repo.set('(%d::. - %d)', parentctx.rev(), parentctx.rev())
834 )
774 if not newcommits: 835 if not newcommits:
775 repo.ui.warn(_('%s: cannot fold - working copy is not a ' 836 repo.ui.warn(
776 'descendant of previous commit %s\n') % 837 _(
777 (node.short(self.node), node.short(parentctxnode))) 838 '%s: cannot fold - working copy is not a '
839 'descendant of previous commit %s\n'
840 )
841 % (node.short(self.node), node.short(parentctxnode))
842 )
778 return ctx, [(self.node, (ctx.node(),))] 843 return ctx, [(self.node, (ctx.node(),))]
779 844
780 middlecommits = newcommits.copy() 845 middlecommits = newcommits.copy()
781 middlecommits.discard(ctx.node()) 846 middlecommits.discard(ctx.node())
782 847
783 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(), 848 return self.finishfold(
784 middlecommits) 849 repo.ui, repo, parentctx, rulectx, ctx.node(), middlecommits
850 )
785 851
786 def skipprompt(self): 852 def skipprompt(self):
787 """Returns true if the rule should skip the message editor. 853 """Returns true if the rule should skip the message editor.
788 854
789 For example, 'fold' wants to show an editor, but 'rollup' 855 For example, 'fold' wants to show an editor, but 'rollup'
816 commitopts['user'] = ctx.user() 882 commitopts['user'] = ctx.user()
817 # commit message 883 # commit message
818 if not self.mergedescs(): 884 if not self.mergedescs():
819 newmessage = ctx.description() 885 newmessage = ctx.description()
820 else: 886 else:
821 newmessage = '\n***\n'.join( 887 newmessage = (
822 [ctx.description()] + 888 '\n***\n'.join(
823 [repo[r].description() for r in internalchanges] + 889 [ctx.description()]
824 [oldctx.description()]) + '\n' 890 + [repo[r].description() for r in internalchanges]
891 + [oldctx.description()]
892 )
893 + '\n'
894 )
825 commitopts['message'] = newmessage 895 commitopts['message'] = newmessage
826 # date 896 # date
827 if self.firstdate(): 897 if self.firstdate():
828 commitopts['date'] = ctx.date() 898 commitopts['date'] = ctx.date()
829 else: 899 else:
839 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex()) 909 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
840 commitopts['extra'] = extra 910 commitopts['extra'] = extra
841 phasemin = max(ctx.phase(), oldctx.phase()) 911 phasemin = max(ctx.phase(), oldctx.phase())
842 overrides = {('phases', 'new-commit'): phasemin} 912 overrides = {('phases', 'new-commit'): phasemin}
843 with repo.ui.configoverride(overrides, 'histedit'): 913 with repo.ui.configoverride(overrides, 'histedit'):
844 n = collapse(repo, ctx, repo[newnode], commitopts, 914 n = collapse(
845 skipprompt=self.skipprompt()) 915 repo,
916 ctx,
917 repo[newnode],
918 commitopts,
919 skipprompt=self.skipprompt(),
920 )
846 if n is None: 921 if n is None:
847 return ctx, [] 922 return ctx, []
848 hg.updaterepo(repo, n, overwrite=False) 923 hg.updaterepo(repo, n, overwrite=False)
849 replacements = [(oldctx.node(), (newnode,)), 924 replacements = [
850 (ctx.node(), (n,)), 925 (oldctx.node(), (newnode,)),
851 (newnode, (n,)), 926 (ctx.node(), (n,)),
852 ] 927 (newnode, (n,)),
928 ]
853 for ich in internalchanges: 929 for ich in internalchanges:
854 replacements.append((ich, (n,))) 930 replacements.append((ich, (n,)))
855 return repo[n], replacements 931 return repo[n], replacements
856 932
857 @action(['base', 'b'], 933
858 _('checkout changeset and apply further changesets from there')) 934 @action(
935 ['base', 'b'],
936 _('checkout changeset and apply further changesets from there'),
937 )
859 class base(histeditaction): 938 class base(histeditaction):
860
861 def run(self): 939 def run(self):
862 if self.repo['.'].node() != self.node: 940 if self.repo['.'].node() != self.node:
863 mergemod.update(self.repo, self.node, branchmerge=False, force=True) 941 mergemod.update(self.repo, self.node, branchmerge=False, force=True)
864 return self.continueclean() 942 return self.continueclean()
865 943
874 # base can only be use with a node not in the edited set 952 # base can only be use with a node not in the edited set
875 if self.node in expected: 953 if self.node in expected:
876 msg = _('%s "%s" changeset was an edited list candidate') 954 msg = _('%s "%s" changeset was an edited list candidate')
877 raise error.ParseError( 955 raise error.ParseError(
878 msg % (self.verb, node.short(self.node)), 956 msg % (self.verb, node.short(self.node)),
879 hint=_('base must only use unlisted changesets')) 957 hint=_('base must only use unlisted changesets'),
880 958 )
881 @action(['_multifold'], 959
882 _( 960
883 """fold subclass used for when multiple folds happen in a row 961 @action(
962 ['_multifold'],
963 _(
964 """fold subclass used for when multiple folds happen in a row
884 965
885 We only want to fire the editor for the folded message once when 966 We only want to fire the editor for the folded message once when
886 (say) four changes are folded down into a single change. This is 967 (say) four changes are folded down into a single change. This is
887 similar to rollup, but we should preserve both messages so that 968 similar to rollup, but we should preserve both messages so that
888 when the last fold operation runs we can show the user all the 969 when the last fold operation runs we can show the user all the
889 commit messages in their editor. 970 commit messages in their editor.
890 """), 971 """
891 internal=True) 972 ),
973 internal=True,
974 )
892 class _multifold(fold): 975 class _multifold(fold):
893 def skipprompt(self): 976 def skipprompt(self):
894 return True 977 return True
895 978
896 @action(["roll", "r"], 979
897 _("like fold, but discard this commit's description and date")) 980 @action(
981 ["roll", "r"],
982 _("like fold, but discard this commit's description and date"),
983 )
898 class rollup(fold): 984 class rollup(fold):
899 def mergedescs(self): 985 def mergedescs(self):
900 return False 986 return False
901 987
902 def skipprompt(self): 988 def skipprompt(self):
903 return True 989 return True
904 990
905 def firstdate(self): 991 def firstdate(self):
906 return True 992 return True
907 993
908 @action(["drop", "d"], 994
909 _('remove commit from history')) 995 @action(["drop", "d"], _('remove commit from history'))
910 class drop(histeditaction): 996 class drop(histeditaction):
911 def run(self): 997 def run(self):
912 parentctx = self.repo[self.state.parentctxnode] 998 parentctx = self.repo[self.state.parentctxnode]
913 return parentctx, [(self.node, tuple())] 999 return parentctx, [(self.node, tuple())]
914 1000
915 @action(["mess", "m"], 1001
916 _('edit commit message without changing commit content'), 1002 @action(
917 priority=True) 1003 ["mess", "m"],
1004 _('edit commit message without changing commit content'),
1005 priority=True,
1006 )
918 class message(histeditaction): 1007 class message(histeditaction):
919 def commiteditor(self): 1008 def commiteditor(self):
920 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess') 1009 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
1010
921 1011
922 def findoutgoing(ui, repo, remote=None, force=False, opts=None): 1012 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
923 """utility function to find the first outgoing changeset 1013 """utility function to find the first outgoing changeset
924 1014
925 Used by initialization code""" 1015 Used by initialization code"""
943 msg = _('there are ambiguous outgoing revisions') 1033 msg = _('there are ambiguous outgoing revisions')
944 hint = _("see 'hg help histedit' for more detail") 1034 hint = _("see 'hg help histedit' for more detail")
945 raise error.Abort(msg, hint=hint) 1035 raise error.Abort(msg, hint=hint)
946 return repo[roots[0]].node() 1036 return repo[roots[0]].node()
947 1037
1038
948 # Curses Support 1039 # Curses Support
949 try: 1040 try:
950 import curses 1041 import curses
951 except ImportError: 1042 except ImportError:
952 curses = None 1043 curses = None
955 ACTION_LABELS = { 1046 ACTION_LABELS = {
956 'fold': '^fold', 1047 'fold': '^fold',
957 'roll': '^roll', 1048 'roll': '^roll',
958 } 1049 }
959 1050
960 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT = 1, 2, 3, 4, 5 1051 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT = 1, 2, 3, 4, 5
961 COLOR_DIFF_ADD_LINE, COLOR_DIFF_DEL_LINE, COLOR_DIFF_OFFSET = 6, 7, 8 1052 COLOR_DIFF_ADD_LINE, COLOR_DIFF_DEL_LINE, COLOR_DIFF_OFFSET = 6, 7, 8
962 1053
963 E_QUIT, E_HISTEDIT = 1, 2 1054 E_QUIT, E_HISTEDIT = 1, 2
964 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7 1055 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
965 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3 1056 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
966 1057
967 KEYTABLE = { 1058 KEYTABLE = {
968 'global': { 1059 'global': {
969 'h': 'next-action', 1060 'h': 'next-action',
970 'KEY_RIGHT': 'next-action', 1061 'KEY_RIGHT': 'next-action',
971 'l': 'prev-action', 1062 'l': 'prev-action',
972 'KEY_LEFT': 'prev-action', 1063 'KEY_LEFT': 'prev-action',
973 'q': 'quit', 1064 'q': 'quit',
974 'c': 'histedit', 1065 'c': 'histedit',
975 'C': 'histedit', 1066 'C': 'histedit',
976 'v': 'showpatch', 1067 'v': 'showpatch',
977 '?': 'help', 1068 '?': 'help',
978 }, 1069 },
979 MODE_RULES: { 1070 MODE_RULES: {
980 'd': 'action-drop', 1071 'd': 'action-drop',
981 'e': 'action-edit', 1072 'e': 'action-edit',
982 'f': 'action-fold', 1073 'f': 'action-fold',
983 'm': 'action-mess', 1074 'm': 'action-mess',
984 'p': 'action-pick', 1075 'p': 'action-pick',
985 'r': 'action-roll', 1076 'r': 'action-roll',
986 ' ': 'select', 1077 ' ': 'select',
987 'j': 'down', 1078 'j': 'down',
988 'k': 'up', 1079 'k': 'up',
989 'KEY_DOWN': 'down', 1080 'KEY_DOWN': 'down',
990 'KEY_UP': 'up', 1081 'KEY_UP': 'up',
991 'J': 'move-down', 1082 'J': 'move-down',
992 'K': 'move-up', 1083 'K': 'move-up',
993 'KEY_NPAGE': 'move-down', 1084 'KEY_NPAGE': 'move-down',
994 'KEY_PPAGE': 'move-up', 1085 'KEY_PPAGE': 'move-up',
995 '0': 'goto', # Used for 0..9 1086 '0': 'goto', # Used for 0..9
996 }, 1087 },
997 MODE_PATCH: { 1088 MODE_PATCH: {
998 ' ': 'page-down', 1089 ' ': 'page-down',
999 'KEY_NPAGE': 'page-down', 1090 'KEY_NPAGE': 'page-down',
1000 'KEY_PPAGE': 'page-up', 1091 'KEY_PPAGE': 'page-up',
1001 'j': 'line-down', 1092 'j': 'line-down',
1002 'k': 'line-up', 1093 'k': 'line-up',
1003 'KEY_DOWN': 'line-down', 1094 'KEY_DOWN': 'line-down',
1004 'KEY_UP': 'line-up', 1095 'KEY_UP': 'line-up',
1005 'J': 'down', 1096 'J': 'down',
1006 'K': 'up', 1097 'K': 'up',
1007 }, 1098 },
1008 MODE_HELP: { 1099 MODE_HELP: {},
1009 },
1010 } 1100 }
1101
1011 1102
1012 def screen_size(): 1103 def screen_size():
1013 return struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, ' ')) 1104 return struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, ' '))
1105
1014 1106
1015 class histeditrule(object): 1107 class histeditrule(object):
1016 def __init__(self, ctx, pos, action='pick'): 1108 def __init__(self, ctx, pos, action='pick'):
1017 self.ctx = ctx 1109 self.ctx = ctx
1018 self.action = action 1110 self.action = action
1037 r = self.ctx.rev() 1129 r = self.ctx.rev()
1038 desc = self.ctx.description().splitlines()[0].strip() 1130 desc = self.ctx.description().splitlines()[0].strip()
1039 if self.action == 'roll': 1131 if self.action == 'roll':
1040 desc = '' 1132 desc = ''
1041 return "#{0:<2} {1:<6} {2}:{3} {4}".format( 1133 return "#{0:<2} {1:<6} {2}:{3} {4}".format(
1042 self.origpos, action, r, h, desc) 1134 self.origpos, action, r, h, desc
1135 )
1043 1136
1044 def checkconflicts(self, other): 1137 def checkconflicts(self, other):
1045 if other.pos > self.pos and other.origpos <= self.origpos: 1138 if other.pos > self.pos and other.origpos <= self.origpos:
1046 if set(other.ctx.files()) & set(self.ctx.files()) != set(): 1139 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1047 self.conflicts.append(other) 1140 self.conflicts.append(other)
1048 return self.conflicts 1141 return self.conflicts
1049 1142
1050 if other in self.conflicts: 1143 if other in self.conflicts:
1051 self.conflicts.remove(other) 1144 self.conflicts.remove(other)
1052 return self.conflicts 1145 return self.conflicts
1146
1053 1147
1054 # ============ EVENTS =============== 1148 # ============ EVENTS ===============
1055 def movecursor(state, oldpos, newpos): 1149 def movecursor(state, oldpos, newpos):
1056 '''Change the rule/changeset that the cursor is pointing to, regardless of 1150 '''Change the rule/changeset that the cursor is pointing to, regardless of
1057 current mode (you can switch between patches from the view patch window).''' 1151 current mode (you can switch between patches from the view patch window).'''
1069 modestate['line_offset'] = newpos - state['page_height'] + 1 1163 modestate['line_offset'] = newpos - state['page_height'] + 1
1070 1164
1071 # Reset the patch view region to the top of the new patch. 1165 # Reset the patch view region to the top of the new patch.
1072 state['modes'][MODE_PATCH]['line_offset'] = 0 1166 state['modes'][MODE_PATCH]['line_offset'] = 0
1073 1167
1168
1074 def changemode(state, mode): 1169 def changemode(state, mode):
1075 curmode, _ = state['mode'] 1170 curmode, _ = state['mode']
1076 state['mode'] = (mode, curmode) 1171 state['mode'] = (mode, curmode)
1077 if mode == MODE_PATCH: 1172 if mode == MODE_PATCH:
1078 state['modes'][MODE_PATCH]['patchcontents'] = patchcontents(state) 1173 state['modes'][MODE_PATCH]['patchcontents'] = patchcontents(state)
1079 1174
1175
1080 def makeselection(state, pos): 1176 def makeselection(state, pos):
1081 state['selected'] = pos 1177 state['selected'] = pos
1178
1082 1179
1083 def swap(state, oldpos, newpos): 1180 def swap(state, oldpos, newpos):
1084 """Swap two positions and calculate necessary conflicts in 1181 """Swap two positions and calculate necessary conflicts in
1085 O(|newpos-oldpos|) time""" 1182 O(|newpos-oldpos|) time"""
1086 1183
1100 rules[oldpos].checkconflicts(rules[r]) 1197 rules[oldpos].checkconflicts(rules[r])
1101 1198
1102 if state['selected']: 1199 if state['selected']:
1103 makeselection(state, newpos) 1200 makeselection(state, newpos)
1104 1201
1202
1105 def changeaction(state, pos, action): 1203 def changeaction(state, pos, action):
1106 """Change the action state on the given position to the new action""" 1204 """Change the action state on the given position to the new action"""
1107 rules = state['rules'] 1205 rules = state['rules']
1108 assert 0 <= pos < len(rules) 1206 assert 0 <= pos < len(rules)
1109 rules[pos].action = action 1207 rules[pos].action = action
1208
1110 1209
1111 def cycleaction(state, pos, next=False): 1210 def cycleaction(state, pos, next=False):
1112 """Changes the action state the next or the previous action from 1211 """Changes the action state the next or the previous action from
1113 the action list""" 1212 the action list"""
1114 rules = state['rules'] 1213 rules = state['rules']
1121 if next: 1220 if next:
1122 index += 1 1221 index += 1
1123 else: 1222 else:
1124 index -= 1 1223 index -= 1
1125 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)]) 1224 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1225
1126 1226
1127 def changeview(state, delta, unit): 1227 def changeview(state, delta, unit):
1128 '''Change the region of whatever is being viewed (a patch or the list of 1228 '''Change the region of whatever is being viewed (a patch or the list of
1129 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.''' 1229 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.'''
1130 mode, _ = state['mode'] 1230 mode, _ = state['mode']
1136 unit = page_height if unit == 'page' else 1 1236 unit = page_height if unit == 'page' else 1
1137 num_pages = 1 + (num_lines - 1) / page_height 1237 num_pages = 1 + (num_lines - 1) / page_height
1138 max_offset = (num_pages - 1) * page_height 1238 max_offset = (num_pages - 1) * page_height
1139 newline = mode_state['line_offset'] + delta * unit 1239 newline = mode_state['line_offset'] + delta * unit
1140 mode_state['line_offset'] = max(0, min(max_offset, newline)) 1240 mode_state['line_offset'] = max(0, min(max_offset, newline))
1241
1141 1242
1142 def event(state, ch): 1243 def event(state, ch):
1143 """Change state based on the current character input 1244 """Change state based on the current character input
1144 1245
1145 This takes the current state and based on the current character input from 1246 This takes the current state and based on the current character input from
1199 elif action == 'line-down': 1300 elif action == 'line-down':
1200 return E_LINEDOWN 1301 return E_LINEDOWN
1201 elif action == 'line-up': 1302 elif action == 'line-up':
1202 return E_LINEUP 1303 return E_LINEUP
1203 1304
1305
1204 def makecommands(rules): 1306 def makecommands(rules):
1205 """Returns a list of commands consumable by histedit --commands based on 1307 """Returns a list of commands consumable by histedit --commands based on
1206 our list of rules""" 1308 our list of rules"""
1207 commands = [] 1309 commands = []
1208 for rules in rules: 1310 for rules in rules:
1209 commands.append("{0} {1}\n".format(rules.action, rules.ctx)) 1311 commands.append("{0} {1}\n".format(rules.action, rules.ctx))
1210 return commands 1312 return commands
1313
1211 1314
1212 def addln(win, y, x, line, color=None): 1315 def addln(win, y, x, line, color=None):
1213 """Add a line to the given window left padding but 100% filled with 1316 """Add a line to the given window left padding but 100% filled with
1214 whitespace characters, so that the color appears on the whole line""" 1317 whitespace characters, so that the color appears on the whole line"""
1215 maxy, maxx = win.getmaxyx() 1318 maxy, maxx = win.getmaxyx()
1222 if color: 1325 if color:
1223 win.addstr(y, x, line, color) 1326 win.addstr(y, x, line, color)
1224 else: 1327 else:
1225 win.addstr(y, x, line) 1328 win.addstr(y, x, line)
1226 1329
1330
1227 def _trunc_head(line, n): 1331 def _trunc_head(line, n):
1228 if len(line) <= n: 1332 if len(line) <= n:
1229 return line 1333 return line
1230 return '> ' + line[-(n - 2):] 1334 return '> ' + line[-(n - 2) :]
1335
1336
1231 def _trunc_tail(line, n): 1337 def _trunc_tail(line, n):
1232 if len(line) <= n: 1338 if len(line) <= n:
1233 return line 1339 return line
1234 return line[:n - 2] + ' >' 1340 return line[: n - 2] + ' >'
1341
1235 1342
1236 def patchcontents(state): 1343 def patchcontents(state):
1237 repo = state['repo'] 1344 repo = state['repo']
1238 rule = state['rules'][state['pos']] 1345 rule = state['rules'][state['pos']]
1239 displayer = logcmdutil.changesetdisplayer(repo.ui, repo, { 1346 displayer = logcmdutil.changesetdisplayer(
1240 "patch": True, "template": "status" 1347 repo.ui, repo, {"patch": True, "template": "status"}, buffered=True
1241 }, buffered=True) 1348 )
1242 overrides = {('ui', 'verbose'): True} 1349 overrides = {('ui', 'verbose'): True}
1243 with repo.ui.configoverride(overrides, source='histedit'): 1350 with repo.ui.configoverride(overrides, source='histedit'):
1244 displayer.show(rule.ctx) 1351 displayer.show(rule.ctx)
1245 displayer.close() 1352 displayer.close()
1246 return displayer.hunk[rule.ctx.rev()].splitlines() 1353 return displayer.hunk[rule.ctx.rev()].splitlines()
1354
1247 1355
1248 def _chisteditmain(repo, rules, stdscr): 1356 def _chisteditmain(repo, rules, stdscr):
1249 try: 1357 try:
1250 curses.use_default_colors() 1358 curses.use_default_colors()
1251 except curses.error: 1359 except curses.error:
1360 else: 1468 else:
1361 rulesscr.addstr(y, 0, " ", curses.COLOR_BLACK) 1469 rulesscr.addstr(y, 0, " ", curses.COLOR_BLACK)
1362 if y + start == selected: 1470 if y + start == selected:
1363 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED)) 1471 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1364 elif y + start == pos: 1472 elif y + start == pos:
1365 addln(rulesscr, y, 2, rule, 1473 addln(
1366 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD) 1474 rulesscr,
1475 y,
1476 2,
1477 rule,
1478 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD,
1479 )
1367 else: 1480 else:
1368 addln(rulesscr, y, 2, rule) 1481 addln(rulesscr, y, 2, rule)
1369 rulesscr.noutrefresh() 1482 rulesscr.noutrefresh()
1370 1483
1371 def renderstring(win, state, output, diffcolors=False): 1484 def renderstring(win, state, output, diffcolors=False):
1374 for y in range(0, length): 1487 for y in range(0, length):
1375 line = output[y] 1488 line = output[y]
1376 if diffcolors: 1489 if diffcolors:
1377 if line and line[0] == '+': 1490 if line and line[0] == '+':
1378 win.addstr( 1491 win.addstr(
1379 y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE)) 1492 y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE)
1493 )
1380 elif line and line[0] == '-': 1494 elif line and line[0] == '-':
1381 win.addstr( 1495 win.addstr(
1382 y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE)) 1496 y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE)
1497 )
1383 elif line.startswith('@@ '): 1498 elif line.startswith('@@ '):
1384 win.addstr( 1499 win.addstr(y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
1385 y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
1386 else: 1500 else:
1387 win.addstr(y, 0, line) 1501 win.addstr(y, 0, line)
1388 else: 1502 else:
1389 win.addstr(y, 0, line) 1503 win.addstr(y, 0, line)
1390 win.noutrefresh() 1504 win.noutrefresh()
1413 'rules': rules, 1527 'rules': rules,
1414 'selected': None, 1528 'selected': None,
1415 'mode': (MODE_INIT, MODE_INIT), 1529 'mode': (MODE_INIT, MODE_INIT),
1416 'page_height': None, 1530 'page_height': None,
1417 'modes': { 1531 'modes': {
1418 MODE_RULES: { 1532 MODE_RULES: {'line_offset': 0,},
1419 'line_offset': 0, 1533 MODE_PATCH: {'line_offset': 0,},
1420 },
1421 MODE_PATCH: {
1422 'line_offset': 0,
1423 }
1424 }, 1534 },
1425 'repo': repo, 1535 'repo': repo,
1426 } 1536 }
1427 1537
1428 # eventloop 1538 # eventloop
1486 # done rendering 1596 # done rendering
1487 ch = stdscr.getkey() 1597 ch = stdscr.getkey()
1488 except curses.error: 1598 except curses.error:
1489 pass 1599 pass
1490 1600
1601
1491 def _chistedit(ui, repo, *freeargs, **opts): 1602 def _chistedit(ui, repo, *freeargs, **opts):
1492 """interactively edit changeset history via a curses interface 1603 """interactively edit changeset history via a curses interface
1493 1604
1494 Provides a ncurses interface to histedit. Press ? in chistedit mode 1605 Provides a ncurses interface to histedit. Press ? in chistedit mode
1495 to see an extensive help. Requires python-curses to be installed.""" 1606 to see an extensive help. Requires python-curses to be installed."""
1505 revs = opts.get('rev', [])[:] 1616 revs = opts.get('rev', [])[:]
1506 cmdutil.checkunfinished(repo) 1617 cmdutil.checkunfinished(repo)
1507 cmdutil.bailifchanged(repo) 1618 cmdutil.bailifchanged(repo)
1508 1619
1509 if os.path.exists(os.path.join(repo.path, 'histedit-state')): 1620 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1510 raise error.Abort(_('history edit already in progress, try ' 1621 raise error.Abort(
1511 '--continue or --abort')) 1622 _(
1623 'history edit already in progress, try '
1624 '--continue or --abort'
1625 )
1626 )
1512 revs.extend(freeargs) 1627 revs.extend(freeargs)
1513 if not revs: 1628 if not revs:
1514 defaultrev = destutil.desthistedit(ui, repo) 1629 defaultrev = destutil.desthistedit(ui, repo)
1515 if defaultrev is not None: 1630 if defaultrev is not None:
1516 revs.append(defaultrev) 1631 revs.append(defaultrev)
1517 if len(revs) != 1: 1632 if len(revs) != 1:
1518 raise error.Abort( 1633 raise error.Abort(
1519 _('histedit requires exactly one ancestor revision')) 1634 _('histedit requires exactly one ancestor revision')
1635 )
1520 1636
1521 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs))) 1637 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1522 if len(rr) != 1: 1638 if len(rr) != 1:
1523 raise error.Abort(_('The specified revisions must have ' 1639 raise error.Abort(
1524 'exactly one common root')) 1640 _(
1641 'The specified revisions must have '
1642 'exactly one common root'
1643 )
1644 )
1525 root = rr[0].node() 1645 root = rr[0].node()
1526 1646
1527 topmost = repo.dirstate.p1() 1647 topmost = repo.dirstate.p1()
1528 revs = between(repo, root, topmost, keep) 1648 revs = between(repo, root, topmost, keep)
1529 if not revs: 1649 if not revs:
1530 raise error.Abort(_('%s is not an ancestor of working directory') % 1650 raise error.Abort(
1531 node.short(root)) 1651 _('%s is not an ancestor of working directory')
1652 % node.short(root)
1653 )
1532 1654
1533 ctxs = [] 1655 ctxs = []
1534 for i, r in enumerate(revs): 1656 for i, r in enumerate(revs):
1535 ctxs.append(histeditrule(repo[r], i)) 1657 ctxs.append(histeditrule(repo[r], i))
1536 # Curses requires setting the locale or it will default to the C 1658 # Curses requires setting the locale or it will default to the C
1554 return _texthistedit(ui, repo, *freeargs, **opts) 1676 return _texthistedit(ui, repo, *freeargs, **opts)
1555 except KeyboardInterrupt: 1677 except KeyboardInterrupt:
1556 pass 1678 pass
1557 return -1 1679 return -1
1558 1680
1559 @command('histedit', 1681
1560 [('', 'commands', '', 1682 @command(
1561 _('read history edits from the specified file'), _('FILE')), 1683 'histedit',
1562 ('c', 'continue', False, _('continue an edit already in progress')), 1684 [
1563 ('', 'edit-plan', False, _('edit remaining actions list')), 1685 (
1564 ('k', 'keep', False, 1686 '',
1565 _("don't strip old nodes after edit is complete")), 1687 'commands',
1566 ('', 'abort', False, _('abort an edit in progress')), 1688 '',
1567 ('o', 'outgoing', False, _('changesets not found in destination')), 1689 _('read history edits from the specified file'),
1568 ('f', 'force', False, 1690 _('FILE'),
1569 _('force outgoing even for unrelated repositories')), 1691 ),
1570 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] + 1692 ('c', 'continue', False, _('continue an edit already in progress')),
1571 cmdutil.formatteropts, 1693 ('', 'edit-plan', False, _('edit remaining actions list')),
1572 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"), 1694 ('k', 'keep', False, _("don't strip old nodes after edit is complete")),
1573 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT) 1695 ('', 'abort', False, _('abort an edit in progress')),
1696 ('o', 'outgoing', False, _('changesets not found in destination')),
1697 (
1698 'f',
1699 'force',
1700 False,
1701 _('force outgoing even for unrelated repositories'),
1702 ),
1703 ('r', 'rev', [], _('first revision to be edited'), _('REV')),
1704 ]
1705 + cmdutil.formatteropts,
1706 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1707 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
1708 )
1574 def histedit(ui, repo, *freeargs, **opts): 1709 def histedit(ui, repo, *freeargs, **opts):
1575 """interactively edit changeset history 1710 """interactively edit changeset history
1576 1711
1577 This command lets you edit a linear series of changesets (up to 1712 This command lets you edit a linear series of changesets (up to
1578 and including the working directory, which should be clean). 1713 and including the working directory, which should be clean).
1671 conflicts). 1806 conflicts).
1672 """ 1807 """
1673 # kludge: _chistedit only works for starting an edit, not aborting 1808 # kludge: _chistedit only works for starting an edit, not aborting
1674 # or continuing, so fall back to regular _texthistedit for those 1809 # or continuing, so fall back to regular _texthistedit for those
1675 # operations. 1810 # operations.
1676 if ui.interface('histedit') == 'curses' and _getgoal( 1811 if (
1677 pycompat.byteskwargs(opts)) == goalnew: 1812 ui.interface('histedit') == 'curses'
1813 and _getgoal(pycompat.byteskwargs(opts)) == goalnew
1814 ):
1678 return _chistedit(ui, repo, *freeargs, **opts) 1815 return _chistedit(ui, repo, *freeargs, **opts)
1679 return _texthistedit(ui, repo, *freeargs, **opts) 1816 return _texthistedit(ui, repo, *freeargs, **opts)
1817
1680 1818
1681 def _texthistedit(ui, repo, *freeargs, **opts): 1819 def _texthistedit(ui, repo, *freeargs, **opts):
1682 state = histeditstate(repo) 1820 state = histeditstate(repo)
1683 with repo.wlock() as wlock, repo.lock() as lock: 1821 with repo.wlock() as wlock, repo.lock() as lock:
1684 state.wlock = wlock 1822 state.wlock = wlock
1685 state.lock = lock 1823 state.lock = lock
1686 _histedit(ui, repo, state, *freeargs, **opts) 1824 _histedit(ui, repo, state, *freeargs, **opts)
1687 1825
1826
1688 goalcontinue = 'continue' 1827 goalcontinue = 'continue'
1689 goalabort = 'abort' 1828 goalabort = 'abort'
1690 goaleditplan = 'edit-plan' 1829 goaleditplan = 'edit-plan'
1691 goalnew = 'new' 1830 goalnew = 'new'
1831
1692 1832
1693 def _getgoal(opts): 1833 def _getgoal(opts):
1694 if opts.get(b'continue'): 1834 if opts.get(b'continue'):
1695 return goalcontinue 1835 return goalcontinue
1696 if opts.get(b'abort'): 1836 if opts.get(b'abort'):
1697 return goalabort 1837 return goalabort
1698 if opts.get(b'edit_plan'): 1838 if opts.get(b'edit_plan'):
1699 return goaleditplan 1839 return goaleditplan
1700 return goalnew 1840 return goalnew
1701 1841
1842
1702 def _readfile(ui, path): 1843 def _readfile(ui, path):
1703 if path == '-': 1844 if path == '-':
1704 with ui.timeblockedsection('histedit'): 1845 with ui.timeblockedsection('histedit'):
1705 return ui.fin.read() 1846 return ui.fin.read()
1706 else: 1847 else:
1707 with open(path, 'rb') as f: 1848 with open(path, 'rb') as f:
1708 return f.read() 1849 return f.read()
1850
1709 1851
1710 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs): 1852 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1711 # TODO only abort if we try to histedit mq patches, not just 1853 # TODO only abort if we try to histedit mq patches, not just
1712 # blanket if mq patches are applied somewhere 1854 # blanket if mq patches are applied somewhere
1713 mq = getattr(repo, 'mq', None) 1855 mq = getattr(repo, 'mq', None)
1727 elif goal == 'abort': 1869 elif goal == 'abort':
1728 if any((outg, revs, freeargs, rules, editplan)): 1870 if any((outg, revs, freeargs, rules, editplan)):
1729 raise error.Abort(_('no arguments allowed with --abort')) 1871 raise error.Abort(_('no arguments allowed with --abort'))
1730 elif goal == 'edit-plan': 1872 elif goal == 'edit-plan':
1731 if any((outg, revs, freeargs)): 1873 if any((outg, revs, freeargs)):
1732 raise error.Abort(_('only --commands argument allowed with ' 1874 raise error.Abort(
1733 '--edit-plan')) 1875 _('only --commands argument allowed with ' '--edit-plan')
1876 )
1734 else: 1877 else:
1735 if state.inprogress(): 1878 if state.inprogress():
1736 raise error.Abort(_('history edit already in progress, try ' 1879 raise error.Abort(
1737 '--continue or --abort')) 1880 _(
1881 'history edit already in progress, try '
1882 '--continue or --abort'
1883 )
1884 )
1738 if outg: 1885 if outg:
1739 if revs: 1886 if revs:
1740 raise error.Abort(_('no revisions allowed with --outgoing')) 1887 raise error.Abort(_('no revisions allowed with --outgoing'))
1741 if len(freeargs) > 1: 1888 if len(freeargs) > 1:
1742 raise error.Abort( 1889 raise error.Abort(
1743 _('only one repo argument allowed with --outgoing')) 1890 _('only one repo argument allowed with --outgoing')
1891 )
1744 else: 1892 else:
1745 revs.extend(freeargs) 1893 revs.extend(freeargs)
1746 if len(revs) == 0: 1894 if len(revs) == 0:
1747 defaultrev = destutil.desthistedit(ui, repo) 1895 defaultrev = destutil.desthistedit(ui, repo)
1748 if defaultrev is not None: 1896 if defaultrev is not None:
1749 revs.append(defaultrev) 1897 revs.append(defaultrev)
1750 1898
1751 if len(revs) != 1: 1899 if len(revs) != 1:
1752 raise error.Abort( 1900 raise error.Abort(
1753 _('histedit requires exactly one ancestor revision')) 1901 _('histedit requires exactly one ancestor revision')
1902 )
1903
1754 1904
1755 def _histedit(ui, repo, state, *freeargs, **opts): 1905 def _histedit(ui, repo, state, *freeargs, **opts):
1756 opts = pycompat.byteskwargs(opts) 1906 opts = pycompat.byteskwargs(opts)
1757 fm = ui.formatter('histedit', opts) 1907 fm = ui.formatter('histedit', opts)
1758 fm.startitem() 1908 fm.startitem()
1771 for ctx in ctxs: 1921 for ctx in ctxs:
1772 tags = [tag for tag in ctx.tags() if tag != 'tip'] 1922 tags = [tag for tag in ctx.tags() if tag != 'tip']
1773 if not hastags: 1923 if not hastags:
1774 hastags = len(tags) 1924 hastags = len(tags)
1775 if hastags: 1925 if hastags:
1776 if ui.promptchoice(_('warning: tags associated with the given' 1926 if ui.promptchoice(
1777 ' changeset will be lost after histedit.\n' 1927 _(
1778 'do you want to continue (yN)? $$ &Yes $$ &No'), 1928 'warning: tags associated with the given'
1779 default=1): 1929 ' changeset will be lost after histedit.\n'
1930 'do you want to continue (yN)? $$ &Yes $$ &No'
1931 ),
1932 default=1,
1933 ):
1780 raise error.Abort(_('histedit cancelled\n')) 1934 raise error.Abort(_('histedit cancelled\n'))
1781 # rebuild state 1935 # rebuild state
1782 if goal == goalcontinue: 1936 if goal == goalcontinue:
1783 state.read() 1937 state.read()
1784 state = bootstrapcontinue(ui, state, opts) 1938 state = bootstrapcontinue(ui, state, opts)
1794 1948
1795 _continuehistedit(ui, repo, state) 1949 _continuehistedit(ui, repo, state)
1796 _finishhistedit(ui, repo, state, fm) 1950 _finishhistedit(ui, repo, state, fm)
1797 fm.end() 1951 fm.end()
1798 1952
1953
1799 def _continuehistedit(ui, repo, state): 1954 def _continuehistedit(ui, repo, state):
1800 """This function runs after either: 1955 """This function runs after either:
1801 - bootstrapcontinue (if the goal is 'continue') 1956 - bootstrapcontinue (if the goal is 'continue')
1802 - _newhistedit (if the goal is 'new') 1957 - _newhistedit (if the goal is 'new')
1803 """ 1958 """
1804 # preprocess rules so that we can hide inner folds from the user 1959 # preprocess rules so that we can hide inner folds from the user
1805 # and only show one editor 1960 # and only show one editor
1806 actions = state.actions[:] 1961 actions = state.actions[:]
1807 for idx, (action, nextact) in enumerate( 1962 for idx, (action, nextact) in enumerate(zip(actions, actions[1:] + [None])):
1808 zip(actions, actions[1:] + [None])):
1809 if action.verb == 'fold' and nextact and nextact.verb == 'fold': 1963 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1810 state.actions[idx].__class__ = _multifold 1964 state.actions[idx].__class__ = _multifold
1811 1965
1812 # Force an initial state file write, so the user can run --abort/continue 1966 # Force an initial state file write, so the user can run --abort/continue
1813 # even if there's an exception before the first transaction serialize. 1967 # even if there's an exception before the first transaction serialize.
1820 if ui.configbool("histedit", "singletransaction"): 1974 if ui.configbool("histedit", "singletransaction"):
1821 # Don't use a 'with' for the transaction, since actions may close 1975 # Don't use a 'with' for the transaction, since actions may close
1822 # and reopen a transaction. For example, if the action executes an 1976 # and reopen a transaction. For example, if the action executes an
1823 # external process it may choose to commit the transaction first. 1977 # external process it may choose to commit the transaction first.
1824 tr = repo.transaction('histedit') 1978 tr = repo.transaction('histedit')
1825 progress = ui.makeprogress(_("editing"), unit=_('changes'), 1979 progress = ui.makeprogress(
1826 total=len(state.actions)) 1980 _("editing"), unit=_('changes'), total=len(state.actions)
1981 )
1827 with progress, util.acceptintervention(tr): 1982 with progress, util.acceptintervention(tr):
1828 while state.actions: 1983 while state.actions:
1829 state.write(tr=tr) 1984 state.write(tr=tr)
1830 actobj = state.actions[0] 1985 actobj = state.actions[0]
1831 progress.increment(item=actobj.torule()) 1986 progress.increment(item=actobj.torule())
1832 ui.debug('histedit: processing %s %s\n' % (actobj.verb, 1987 ui.debug(
1833 actobj.torule())) 1988 'histedit: processing %s %s\n' % (actobj.verb, actobj.torule())
1989 )
1834 parentctx, replacement_ = actobj.run() 1990 parentctx, replacement_ = actobj.run()
1835 state.parentctxnode = parentctx.node() 1991 state.parentctxnode = parentctx.node()
1836 state.replacements.extend(replacement_) 1992 state.replacements.extend(replacement_)
1837 state.actions.pop(0) 1993 state.actions.pop(0)
1838 1994
1839 state.write() 1995 state.write()
1996
1840 1997
1841 def _finishhistedit(ui, repo, state, fm): 1998 def _finishhistedit(ui, repo, state, fm):
1842 """This action runs when histedit is finishing its session""" 1999 """This action runs when histedit is finishing its session"""
1843 hg.updaterepo(repo, state.parentctxnode, overwrite=False) 2000 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
1844 2001
1846 if mapping: 2003 if mapping:
1847 for prec, succs in mapping.iteritems(): 2004 for prec, succs in mapping.iteritems():
1848 if not succs: 2005 if not succs:
1849 ui.debug('histedit: %s is dropped\n' % node.short(prec)) 2006 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1850 else: 2007 else:
1851 ui.debug('histedit: %s is replaced by %s\n' % ( 2008 ui.debug(
1852 node.short(prec), node.short(succs[0]))) 2009 'histedit: %s is replaced by %s\n'
2010 % (node.short(prec), node.short(succs[0]))
2011 )
1853 if len(succs) > 1: 2012 if len(succs) > 1:
1854 m = 'histedit: %s' 2013 m = 'histedit: %s'
1855 for n in succs[1:]: 2014 for n in succs[1:]:
1856 ui.debug(m % node.short(n)) 2015 ui.debug(m % node.short(n))
1857 2016
1866 if n in repo: 2025 if n in repo:
1867 mapping[n] = () 2026 mapping[n] = ()
1868 2027
1869 # remove entries about unknown nodes 2028 # remove entries about unknown nodes
1870 nodemap = repo.unfiltered().changelog.nodemap 2029 nodemap = repo.unfiltered().changelog.nodemap
1871 mapping = {k: v for k, v in mapping.items() 2030 mapping = {
1872 if k in nodemap and all(n in nodemap for n in v)} 2031 k: v
2032 for k, v in mapping.items()
2033 if k in nodemap and all(n in nodemap for n in v)
2034 }
1873 scmutil.cleanupnodes(repo, mapping, 'histedit') 2035 scmutil.cleanupnodes(repo, mapping, 'histedit')
1874 hf = fm.hexfunc 2036 hf = fm.hexfunc
1875 fl = fm.formatlist 2037 fl = fm.formatlist
1876 fd = fm.formatdict 2038 fd = fm.formatdict
1877 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node') 2039 nodechanges = fd(
1878 for oldn, newn in mapping.iteritems()}, 2040 {
1879 key="oldnode", value="newnodes") 2041 hf(oldn): fl([hf(n) for n in newn], name='node')
2042 for oldn, newn in mapping.iteritems()
2043 },
2044 key="oldnode",
2045 value="newnodes",
2046 )
1880 fm.data(nodechanges=nodechanges) 2047 fm.data(nodechanges=nodechanges)
1881 2048
1882 state.clear() 2049 state.clear()
1883 if os.path.exists(repo.sjoin('undo')): 2050 if os.path.exists(repo.sjoin('undo')):
1884 os.unlink(repo.sjoin('undo')) 2051 os.unlink(repo.sjoin('undo'))
1885 if repo.vfs.exists('histedit-last-edit.txt'): 2052 if repo.vfs.exists('histedit-last-edit.txt'):
1886 repo.vfs.unlink('histedit-last-edit.txt') 2053 repo.vfs.unlink('histedit-last-edit.txt')
1887 2054
2055
1888 def _aborthistedit(ui, repo, state, nobackup=False): 2056 def _aborthistedit(ui, repo, state, nobackup=False):
1889 try: 2057 try:
1890 state.read() 2058 state.read()
1891 __, leafs, tmpnodes, __ = processreplacement(state) 2059 __, leafs, tmpnodes, __ = processreplacement(state)
1892 ui.debug('restore wc to old parent %s\n' 2060 ui.debug('restore wc to old parent %s\n' % node.short(state.topmost))
1893 % node.short(state.topmost))
1894 2061
1895 # Recover our old commits if necessary 2062 # Recover our old commits if necessary
1896 if not state.topmost in repo and state.backupfile: 2063 if not state.topmost in repo and state.backupfile:
1897 backupfile = repo.vfs.join(state.backupfile) 2064 backupfile = repo.vfs.join(state.backupfile)
1898 f = hg.openpath(ui, backupfile) 2065 f = hg.openpath(ui, backupfile)
1899 gen = exchange.readbundle(ui, f, backupfile) 2066 gen = exchange.readbundle(ui, f, backupfile)
1900 with repo.transaction('histedit.abort') as tr: 2067 with repo.transaction('histedit.abort') as tr:
1901 bundle2.applybundle(repo, gen, tr, source='histedit', 2068 bundle2.applybundle(
1902 url='bundle:' + backupfile) 2069 repo, gen, tr, source='histedit', url='bundle:' + backupfile
2070 )
1903 2071
1904 os.remove(backupfile) 2072 os.remove(backupfile)
1905 2073
1906 # check whether we should update away 2074 # check whether we should update away
1907 if repo.unfiltered().revs('parents() and (%n or %ln::)', 2075 if repo.unfiltered().revs(
1908 state.parentctxnode, leafs | tmpnodes): 2076 'parents() and (%n or %ln::)',
2077 state.parentctxnode,
2078 leafs | tmpnodes,
2079 ):
1909 hg.clean(repo, state.topmost, show_stats=True, quietempty=True) 2080 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1910 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup) 2081 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
1911 cleanupnode(ui, repo, leafs, nobackup=nobackup) 2082 cleanupnode(ui, repo, leafs, nobackup=nobackup)
1912 except Exception: 2083 except Exception:
1913 if state.inprogress(): 2084 if state.inprogress():
1914 ui.warn(_('warning: encountered an exception during histedit ' 2085 ui.warn(
1915 '--abort; the repository may not have been completely ' 2086 _(
1916 'cleaned up\n')) 2087 'warning: encountered an exception during histedit '
2088 '--abort; the repository may not have been completely '
2089 'cleaned up\n'
2090 )
2091 )
1917 raise 2092 raise
1918 finally: 2093 finally:
1919 state.clear() 2094 state.clear()
2095
1920 2096
1921 def hgaborthistedit(ui, repo): 2097 def hgaborthistedit(ui, repo):
1922 state = histeditstate(repo) 2098 state = histeditstate(repo)
1923 nobackup = not ui.configbool('rewrite', 'backup-bundle') 2099 nobackup = not ui.configbool('rewrite', 'backup-bundle')
1924 with repo.wlock() as wlock, repo.lock() as lock: 2100 with repo.wlock() as wlock, repo.lock() as lock:
1925 state.wlock = wlock 2101 state.wlock = wlock
1926 state.lock = lock 2102 state.lock = lock
1927 _aborthistedit(ui, repo, state, nobackup=nobackup) 2103 _aborthistedit(ui, repo, state, nobackup=nobackup)
1928 2104
2105
1929 def _edithisteditplan(ui, repo, state, rules): 2106 def _edithisteditplan(ui, repo, state, rules):
1930 state.read() 2107 state.read()
1931 if not rules: 2108 if not rules:
1932 comment = geteditcomment(ui, 2109 comment = geteditcomment(
1933 node.short(state.parentctxnode), 2110 ui, node.short(state.parentctxnode), node.short(state.topmost)
1934 node.short(state.topmost)) 2111 )
1935 rules = ruleeditor(repo, ui, state.actions, comment) 2112 rules = ruleeditor(repo, ui, state.actions, comment)
1936 else: 2113 else:
1937 rules = _readfile(ui, rules) 2114 rules = _readfile(ui, rules)
1938 actions = parserules(rules, state) 2115 actions = parserules(rules, state)
1939 ctxs = [repo[act.node] 2116 ctxs = [repo[act.node] for act in state.actions if act.node]
1940 for act in state.actions if act.node]
1941 warnverifyactions(ui, repo, actions, state, ctxs) 2117 warnverifyactions(ui, repo, actions, state, ctxs)
1942 state.actions = actions 2118 state.actions = actions
1943 state.write() 2119 state.write()
2120
1944 2121
1945 def _newhistedit(ui, repo, state, revs, freeargs, opts): 2122 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1946 outg = opts.get('outgoing') 2123 outg = opts.get('outgoing')
1947 rules = opts.get('commands', '') 2124 rules = opts.get('commands', '')
1948 force = opts.get('force') 2125 force = opts.get('force')
1958 remote = None 2135 remote = None
1959 root = findoutgoing(ui, repo, remote, force, opts) 2136 root = findoutgoing(ui, repo, remote, force, opts)
1960 else: 2137 else:
1961 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs))) 2138 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1962 if len(rr) != 1: 2139 if len(rr) != 1:
1963 raise error.Abort(_('The specified revisions must have ' 2140 raise error.Abort(
1964 'exactly one common root')) 2141 _(
2142 'The specified revisions must have '
2143 'exactly one common root'
2144 )
2145 )
1965 root = rr[0].node() 2146 root = rr[0].node()
1966 2147
1967 revs = between(repo, root, topmost, state.keep) 2148 revs = between(repo, root, topmost, state.keep)
1968 if not revs: 2149 if not revs:
1969 raise error.Abort(_('%s is not an ancestor of working directory') % 2150 raise error.Abort(
1970 node.short(root)) 2151 _('%s is not an ancestor of working directory') % node.short(root)
2152 )
1971 2153
1972 ctxs = [repo[r] for r in revs] 2154 ctxs = [repo[r] for r in revs]
1973 2155
1974 wctx = repo[None] 2156 wctx = repo[None]
1975 # Please don't ask me why `ancestors` is this value. I figured it 2157 # Please don't ask me why `ancestors` is this value. I figured it
1981 # collision after we've started histedit and backing out gets ugly 2163 # collision after we've started histedit and backing out gets ugly
1982 # for everyone, especially the user. 2164 # for everyone, especially the user.
1983 for c in [ctxs[0].p1()] + ctxs: 2165 for c in [ctxs[0].p1()] + ctxs:
1984 try: 2166 try:
1985 mergemod.calculateupdates( 2167 mergemod.calculateupdates(
1986 repo, wctx, c, ancs, 2168 repo,
2169 wctx,
2170 c,
2171 ancs,
1987 # These parameters were determined by print-debugging 2172 # These parameters were determined by print-debugging
1988 # what happens later on inside histedit. 2173 # what happens later on inside histedit.
1989 branchmerge=False, force=False, acceptremote=False, 2174 branchmerge=False,
1990 followcopies=False) 2175 force=False,
2176 acceptremote=False,
2177 followcopies=False,
2178 )
1991 except error.Abort: 2179 except error.Abort:
1992 raise error.Abort( 2180 raise error.Abort(
1993 _("untracked files in working directory conflict with files in %s") % ( 2181 _(
1994 c)) 2182 "untracked files in working directory conflict with files in %s"
2183 )
2184 % c
2185 )
1995 2186
1996 if not rules: 2187 if not rules:
1997 comment = geteditcomment(ui, node.short(root), node.short(topmost)) 2188 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1998 actions = [pick(state, r) for r in revs] 2189 actions = [pick(state, r) for r in revs]
1999 rules = ruleeditor(repo, ui, actions, comment) 2190 rules = ruleeditor(repo, ui, actions, comment)
2007 state.parentctxnode = parentctxnode 2198 state.parentctxnode = parentctxnode
2008 state.actions = actions 2199 state.actions = actions
2009 state.topmost = topmost 2200 state.topmost = topmost
2010 state.replacements = [] 2201 state.replacements = []
2011 2202
2012 ui.log("histedit", "%d actions to histedit\n", len(actions), 2203 ui.log(
2013 histedit_num_actions=len(actions)) 2204 "histedit",
2205 "%d actions to histedit\n",
2206 len(actions),
2207 histedit_num_actions=len(actions),
2208 )
2014 2209
2015 # Create a backup so we can always abort completely. 2210 # Create a backup so we can always abort completely.
2016 backupfile = None 2211 backupfile = None
2017 if not obsolete.isenabled(repo, obsolete.createmarkersopt): 2212 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2018 backupfile = repair.backupbundle(repo, [parentctxnode], 2213 backupfile = repair.backupbundle(
2019 [topmost], root, 'histedit') 2214 repo, [parentctxnode], [topmost], root, 'histedit'
2215 )
2020 state.backupfile = backupfile 2216 state.backupfile = backupfile
2217
2021 2218
2022 def _getsummary(ctx): 2219 def _getsummary(ctx):
2023 # a common pattern is to extract the summary but default to the empty 2220 # a common pattern is to extract the summary but default to the empty
2024 # string 2221 # string
2025 summary = ctx.description() or '' 2222 summary = ctx.description() or ''
2026 if summary: 2223 if summary:
2027 summary = summary.splitlines()[0] 2224 summary = summary.splitlines()[0]
2028 return summary 2225 return summary
2029 2226
2227
2030 def bootstrapcontinue(ui, state, opts): 2228 def bootstrapcontinue(ui, state, opts):
2031 repo = state.repo 2229 repo = state.repo
2032 2230
2033 ms = mergemod.mergestate.read(repo) 2231 ms = mergemod.mergestate.read(repo)
2034 mergeutil.checkunresolved(ms) 2232 mergeutil.checkunresolved(ms)
2046 state.parentctxnode = parentctx.node() 2244 state.parentctxnode = parentctx.node()
2047 state.replacements.extend(replacements) 2245 state.replacements.extend(replacements)
2048 2246
2049 return state 2247 return state
2050 2248
2249
2051 def between(repo, old, new, keep): 2250 def between(repo, old, new, keep):
2052 """select and validate the set of revision to edit 2251 """select and validate the set of revision to edit
2053 2252
2054 When keep is false, the specified set can't have children.""" 2253 When keep is false, the specified set can't have children."""
2055 revs = repo.revs('%n::%n', old, new) 2254 revs = repo.revs('%n::%n', old, new)
2056 if revs and not keep: 2255 if revs and not keep:
2057 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and 2256 if not obsolete.isenabled(
2058 repo.revs('(%ld::) - (%ld)', revs, revs)): 2257 repo, obsolete.allowunstableopt
2059 raise error.Abort(_('can only histedit a changeset together ' 2258 ) and repo.revs('(%ld::) - (%ld)', revs, revs):
2060 'with all its descendants')) 2259 raise error.Abort(
2260 _(
2261 'can only histedit a changeset together '
2262 'with all its descendants'
2263 )
2264 )
2061 if repo.revs('(%ld) and merge()', revs): 2265 if repo.revs('(%ld) and merge()', revs):
2062 raise error.Abort(_('cannot edit history that contains merges')) 2266 raise error.Abort(_('cannot edit history that contains merges'))
2063 root = repo[revs.first()] # list is already sorted by repo.revs() 2267 root = repo[revs.first()] # list is already sorted by repo.revs()
2064 if not root.mutable(): 2268 if not root.mutable():
2065 raise error.Abort(_('cannot edit public changeset: %s') % root, 2269 raise error.Abort(
2066 hint=_("see 'hg help phases' for details")) 2270 _('cannot edit public changeset: %s') % root,
2271 hint=_("see 'hg help phases' for details"),
2272 )
2067 return pycompat.maplist(repo.changelog.node, revs) 2273 return pycompat.maplist(repo.changelog.node, revs)
2274
2068 2275
2069 def ruleeditor(repo, ui, actions, editcomment=""): 2276 def ruleeditor(repo, ui, actions, editcomment=""):
2070 """open an editor to edit rules 2277 """open an editor to edit rules
2071 2278
2072 rules are in the format [ [act, ctx], ...] like in state.rules 2279 rules are in the format [ [act, ctx], ...] like in state.rules
2083 if fword.endswith('!'): 2290 if fword.endswith('!'):
2084 fword = fword[:-1] 2291 fword = fword[:-1]
2085 if fword in primaryactions | secondaryactions | tertiaryactions: 2292 if fword in primaryactions | secondaryactions | tertiaryactions:
2086 act.verb = fword 2293 act.verb = fword
2087 # get the target summary 2294 # get the target summary
2088 tsum = summary[len(fword) + 1:].lstrip() 2295 tsum = summary[len(fword) + 1 :].lstrip()
2089 # safe but slow: reverse iterate over the actions so we 2296 # safe but slow: reverse iterate over the actions so we
2090 # don't clash on two commits having the same summary 2297 # don't clash on two commits having the same summary
2091 for na, l in reversed(list(newact.iteritems())): 2298 for na, l in reversed(list(newact.iteritems())):
2092 actx = repo[na.node] 2299 actx = repo[na.node]
2093 asum = _getsummary(actx) 2300 asum = _getsummary(actx)
2106 actions += l 2313 actions += l
2107 2314
2108 rules = '\n'.join([act.torule() for act in actions]) 2315 rules = '\n'.join([act.torule() for act in actions])
2109 rules += '\n\n' 2316 rules += '\n\n'
2110 rules += editcomment 2317 rules += editcomment
2111 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'}, 2318 rules = ui.edit(
2112 repopath=repo.path, action='histedit') 2319 rules,
2320 ui.username(),
2321 {'prefix': 'histedit'},
2322 repopath=repo.path,
2323 action='histedit',
2324 )
2113 2325
2114 # Save edit rules in .hg/histedit-last-edit.txt in case 2326 # Save edit rules in .hg/histedit-last-edit.txt in case
2115 # the user needs to ask for help after something 2327 # the user needs to ask for help after something
2116 # surprising happens. 2328 # surprising happens.
2117 with repo.vfs('histedit-last-edit.txt', 'wb') as f: 2329 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
2118 f.write(rules) 2330 f.write(rules)
2119 2331
2120 return rules 2332 return rules
2121 2333
2334
2122 def parserules(rules, state): 2335 def parserules(rules, state):
2123 """Read the histedit rules string and return list of action objects """ 2336 """Read the histedit rules string and return list of action objects """
2124 rules = [l for l in (r.strip() for r in rules.splitlines()) 2337 rules = [
2125 if l and not l.startswith('#')] 2338 l
2339 for l in (r.strip() for r in rules.splitlines())
2340 if l and not l.startswith('#')
2341 ]
2126 actions = [] 2342 actions = []
2127 for r in rules: 2343 for r in rules:
2128 if ' ' not in r: 2344 if ' ' not in r:
2129 raise error.ParseError(_('malformed line "%s"') % r) 2345 raise error.ParseError(_('malformed line "%s"') % r)
2130 verb, rest = r.split(' ', 1) 2346 verb, rest = r.split(' ', 1)
2134 2350
2135 action = actiontable[verb].fromrule(state, rest) 2351 action = actiontable[verb].fromrule(state, rest)
2136 actions.append(action) 2352 actions.append(action)
2137 return actions 2353 return actions
2138 2354
2355
2139 def warnverifyactions(ui, repo, actions, state, ctxs): 2356 def warnverifyactions(ui, repo, actions, state, ctxs):
2140 try: 2357 try:
2141 verifyactions(actions, state, ctxs) 2358 verifyactions(actions, state, ctxs)
2142 except error.ParseError: 2359 except error.ParseError:
2143 if repo.vfs.exists('histedit-last-edit.txt'): 2360 if repo.vfs.exists('histedit-last-edit.txt'):
2144 ui.warn(_('warning: histedit rules saved ' 2361 ui.warn(
2145 'to: .hg/histedit-last-edit.txt\n')) 2362 _(
2363 'warning: histedit rules saved '
2364 'to: .hg/histedit-last-edit.txt\n'
2365 )
2366 )
2146 raise 2367 raise
2368
2147 2369
2148 def verifyactions(actions, state, ctxs): 2370 def verifyactions(actions, state, ctxs):
2149 """Verify that there exists exactly one action per given changeset and 2371 """Verify that there exists exactly one action per given changeset and
2150 other constraints. 2372 other constraints.
2151 2373
2155 expected = set(c.node() for c in ctxs) 2377 expected = set(c.node() for c in ctxs)
2156 seen = set() 2378 seen = set()
2157 prev = None 2379 prev = None
2158 2380
2159 if actions and actions[0].verb in ['roll', 'fold']: 2381 if actions and actions[0].verb in ['roll', 'fold']:
2160 raise error.ParseError(_('first changeset cannot use verb "%s"') % 2382 raise error.ParseError(
2161 actions[0].verb) 2383 _('first changeset cannot use verb "%s"') % actions[0].verb
2384 )
2162 2385
2163 for action in actions: 2386 for action in actions:
2164 action.verify(prev, expected, seen) 2387 action.verify(prev, expected, seen)
2165 prev = action 2388 prev = action
2166 if action.node is not None: 2389 if action.node is not None:
2167 seen.add(action.node) 2390 seen.add(action.node)
2168 missing = sorted(expected - seen) # sort to stabilize output 2391 missing = sorted(expected - seen) # sort to stabilize output
2169 2392
2170 if state.repo.ui.configbool('histedit', 'dropmissing'): 2393 if state.repo.ui.configbool('histedit', 'dropmissing'):
2171 if len(actions) == 0: 2394 if len(actions) == 0:
2172 raise error.ParseError(_('no rules provided'), 2395 raise error.ParseError(
2173 hint=_('use strip extension to remove commits')) 2396 _('no rules provided'),
2397 hint=_('use strip extension to remove commits'),
2398 )
2174 2399
2175 drops = [drop(state, n) for n in missing] 2400 drops = [drop(state, n) for n in missing]
2176 # put the in the beginning so they execute immediately and 2401 # put the in the beginning so they execute immediately and
2177 # don't show in the edit-plan in the future 2402 # don't show in the edit-plan in the future
2178 actions[:0] = drops 2403 actions[:0] = drops
2179 elif missing: 2404 elif missing:
2180 raise error.ParseError(_('missing rules for changeset %s') % 2405 raise error.ParseError(
2181 node.short(missing[0]), 2406 _('missing rules for changeset %s') % node.short(missing[0]),
2182 hint=_('use "drop %s" to discard, see also: ' 2407 hint=_(
2183 "'hg help -e histedit.config'") 2408 'use "drop %s" to discard, see also: '
2184 % node.short(missing[0])) 2409 "'hg help -e histedit.config'"
2410 )
2411 % node.short(missing[0]),
2412 )
2413
2185 2414
2186 def adjustreplacementsfrommarkers(repo, oldreplacements): 2415 def adjustreplacementsfrommarkers(repo, oldreplacements):
2187 """Adjust replacements from obsolescence markers 2416 """Adjust replacements from obsolescence markers
2188 2417
2189 Replacements structure is originally generated based on 2418 Replacements structure is originally generated based on
2197 nm = unfi.changelog.nodemap 2426 nm = unfi.changelog.nodemap
2198 obsstore = repo.obsstore 2427 obsstore = repo.obsstore
2199 newreplacements = list(oldreplacements) 2428 newreplacements = list(oldreplacements)
2200 oldsuccs = [r[1] for r in oldreplacements] 2429 oldsuccs = [r[1] for r in oldreplacements]
2201 # successors that have already been added to succstocheck once 2430 # successors that have already been added to succstocheck once
2202 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples 2431 seensuccs = set().union(
2432 *oldsuccs
2433 ) # create a set from an iterable of tuples
2203 succstocheck = list(seensuccs) 2434 succstocheck = list(seensuccs)
2204 while succstocheck: 2435 while succstocheck:
2205 n = succstocheck.pop() 2436 n = succstocheck.pop()
2206 missing = nm.get(n) is None 2437 missing = nm.get(n) is None
2207 markers = obsstore.successors.get(n, ()) 2438 markers = obsstore.successors.get(n, ())
2215 if nsucc not in seensuccs: 2446 if nsucc not in seensuccs:
2216 seensuccs.add(nsucc) 2447 seensuccs.add(nsucc)
2217 succstocheck.append(nsucc) 2448 succstocheck.append(nsucc)
2218 2449
2219 return newreplacements 2450 return newreplacements
2451
2220 2452
2221 def processreplacement(state): 2453 def processreplacement(state):
2222 """process the list of replacements to return 2454 """process the list of replacements to return
2223 2455
2224 1) the final mapping between original and created nodes 2456 1) the final mapping between original and created nodes
2277 r = state.repo.changelog.rev 2509 r = state.repo.changelog.rev
2278 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node() 2510 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2279 2511
2280 return final, tmpnodes, new, newtopmost 2512 return final, tmpnodes, new, newtopmost
2281 2513
2514
2282 def movetopmostbookmarks(repo, oldtopmost, newtopmost): 2515 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2283 """Move bookmark from oldtopmost to newly created topmost 2516 """Move bookmark from oldtopmost to newly created topmost
2284 2517
2285 This is arguably a feature and we may only want that for the active 2518 This is arguably a feature and we may only want that for the active
2286 bookmark. But the behavior is kept compatible with the old version for now. 2519 bookmark. But the behavior is kept compatible with the old version for now.
2293 marks = repo._bookmarks 2526 marks = repo._bookmarks
2294 changes = [] 2527 changes = []
2295 for name in oldbmarks: 2528 for name in oldbmarks:
2296 changes.append((name, newtopmost)) 2529 changes.append((name, newtopmost))
2297 marks.applychanges(repo, tr, changes) 2530 marks.applychanges(repo, tr, changes)
2531
2298 2532
2299 def cleanupnode(ui, repo, nodes, nobackup=False): 2533 def cleanupnode(ui, repo, nodes, nobackup=False):
2300 """strip a group of nodes from the repository 2534 """strip a group of nodes from the repository
2301 2535
2302 The set of node to strip may contains unknown nodes.""" 2536 The set of node to strip may contains unknown nodes."""
2312 roots = [c.node() for c in repo.set("roots(%ln)", nodes)] 2546 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
2313 if roots: 2547 if roots:
2314 backup = not nobackup 2548 backup = not nobackup
2315 repair.strip(ui, repo, roots, backup=backup) 2549 repair.strip(ui, repo, roots, backup=backup)
2316 2550
2551
2317 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs): 2552 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2318 if isinstance(nodelist, str): 2553 if isinstance(nodelist, str):
2319 nodelist = [nodelist] 2554 nodelist = [nodelist]
2320 state = histeditstate(repo) 2555 state = histeditstate(repo)
2321 if state.inprogress(): 2556 if state.inprogress():
2322 state.read() 2557 state.read()
2323 histedit_nodes = {action.node for action 2558 histedit_nodes = {
2324 in state.actions if action.node} 2559 action.node for action in state.actions if action.node
2560 }
2325 common_nodes = histedit_nodes & set(nodelist) 2561 common_nodes = histedit_nodes & set(nodelist)
2326 if common_nodes: 2562 if common_nodes:
2327 raise error.Abort(_("histedit in progress, can't strip %s") 2563 raise error.Abort(
2328 % ', '.join(node.short(x) for x in common_nodes)) 2564 _("histedit in progress, can't strip %s")
2565 % ', '.join(node.short(x) for x in common_nodes)
2566 )
2329 return orig(ui, repo, nodelist, *args, **kwargs) 2567 return orig(ui, repo, nodelist, *args, **kwargs)
2330 2568
2569
2331 extensions.wrapfunction(repair, 'strip', stripwrapper) 2570 extensions.wrapfunction(repair, 'strip', stripwrapper)
2571
2332 2572
2333 def summaryhook(ui, repo): 2573 def summaryhook(ui, repo):
2334 state = histeditstate(repo) 2574 state = histeditstate(repo)
2335 if not state.inprogress(): 2575 if not state.inprogress():
2336 return 2576 return
2337 state.read() 2577 state.read()
2338 if state.actions: 2578 if state.actions:
2339 # i18n: column positioning for "hg summary" 2579 # i18n: column positioning for "hg summary"
2340 ui.write(_('hist: %s (histedit --continue)\n') % 2580 ui.write(
2341 (ui.label(_('%d remaining'), 'histedit.remaining') % 2581 _('hist: %s (histedit --continue)\n')
2342 len(state.actions))) 2582 % (
2583 ui.label(_('%d remaining'), 'histedit.remaining')
2584 % len(state.actions)
2585 )
2586 )
2587
2343 2588
2344 def extsetup(ui): 2589 def extsetup(ui):
2345 cmdutil.summaryhooks.add('histedit', summaryhook) 2590 cmdutil.summaryhooks.add('histedit', summaryhook)
2346 statemod.addunfinished('histedit', fname='histedit-state', allowcommit=True, 2591 statemod.addunfinished(
2347 continueflag=True, abortfunc=hgaborthistedit) 2592 'histedit',
2593 fname='histedit-state',
2594 allowcommit=True,
2595 continueflag=True,
2596 abortfunc=hgaborthistedit,
2597 )