Mercurial > hg
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 ) |