1# stuff related specifically to patch manipulation / parsing 2# 3# Copyright 2008 Mark Edgington <edgimar@gmail.com> 4# 5# This software may be used and distributed according to the terms of the 6# GNU General Public License version 2 or any later version. 7# 8# This code is based on the Mark Edgington's crecord extension. 9# (Itself based on Bryan O'Sullivan's record extension.) 10 11from __future__ import absolute_import 12 13import os 14import re 15import signal 16 17from .i18n import _ 18from .pycompat import ( 19 getattr, 20 open, 21) 22from . import ( 23 diffhelper, 24 encoding, 25 error, 26 patch as patchmod, 27 pycompat, 28 scmutil, 29 util, 30) 31from .utils import stringutil 32 33stringio = util.stringio 34 35# patch comments based on the git one 36diffhelptext = _( 37 b"""# To remove '-' lines, make them ' ' lines (context). 38# To remove '+' lines, delete them. 39# Lines starting with # will be removed from the patch. 40""" 41) 42 43hunkhelptext = _( 44 b"""# 45# If the patch applies cleanly, the edited hunk will immediately be 46# added to the record list. If it does not apply cleanly, a rejects file 47# will be generated. You can use that when you try again. If all lines 48# of the hunk are removed, then the edit is aborted and the hunk is left 49# unchanged. 50""" 51) 52 53patchhelptext = _( 54 b"""# 55# If the patch applies cleanly, the edited patch will immediately 56# be finalised. If it does not apply cleanly, rejects files will be 57# generated. You can use those when you try again. 58""" 59) 60 61try: 62 import curses 63 import curses.ascii 64 65 curses.error 66except (ImportError, AttributeError): 67 curses = None 68 69 70class fallbackerror(error.Abort): 71 """Error that indicates the client should try to fallback to text mode.""" 72 73 # Inherits from error.Abort so that existing behavior is preserved if the 74 # calling code does not know how to fallback. 75 76 77def checkcurses(ui): 78 """Return True if the user wants to use curses 79 80 This method returns True if curses is found (and that python is built with 81 it) and that the user has the correct flag for the ui. 82 """ 83 return curses and ui.interface(b"chunkselector") == b"curses" 84 85 86class patchnode(object): 87 """abstract class for patch graph nodes 88 (i.e. patchroot, header, hunk, hunkline) 89 """ 90 91 def firstchild(self): 92 raise NotImplementedError(b"method must be implemented by subclass") 93 94 def lastchild(self): 95 raise NotImplementedError(b"method must be implemented by subclass") 96 97 def allchildren(self): 98 """Return a list of all of the direct children of this node""" 99 raise NotImplementedError(b"method must be implemented by subclass") 100 101 def nextsibling(self): 102 """ 103 Return the closest next item of the same type where there are no items 104 of different types between the current item and this closest item. 105 If no such item exists, return None. 106 """ 107 raise NotImplementedError(b"method must be implemented by subclass") 108 109 def prevsibling(self): 110 """ 111 Return the closest previous item of the same type where there are no 112 items of different types between the current item and this closest item. 113 If no such item exists, return None. 114 """ 115 raise NotImplementedError(b"method must be implemented by subclass") 116 117 def parentitem(self): 118 raise NotImplementedError(b"method must be implemented by subclass") 119 120 def nextitem(self, skipfolded=True): 121 """ 122 Try to return the next item closest to this item, regardless of item's 123 type (header, hunk, or hunkline). 124 125 If skipfolded == True, and the current item is folded, then the child 126 items that are hidden due to folding will be skipped when determining 127 the next item. 128 129 If it is not possible to get the next item, return None. 130 """ 131 try: 132 itemfolded = self.folded 133 except AttributeError: 134 itemfolded = False 135 if skipfolded and itemfolded: 136 nextitem = self.nextsibling() 137 if nextitem is None: 138 try: 139 nextitem = self.parentitem().nextsibling() 140 except AttributeError: 141 nextitem = None 142 return nextitem 143 else: 144 # try child 145 item = self.firstchild() 146 if item is not None: 147 return item 148 149 # else try next sibling 150 item = self.nextsibling() 151 if item is not None: 152 return item 153 154 try: 155 # else try parent's next sibling 156 item = self.parentitem().nextsibling() 157 if item is not None: 158 return item 159 160 # else return grandparent's next sibling (or None) 161 return self.parentitem().parentitem().nextsibling() 162 163 except AttributeError: # parent and/or grandparent was None 164 return None 165 166 def previtem(self): 167 """ 168 Try to return the previous item closest to this item, regardless of 169 item's type (header, hunk, or hunkline). 170 171 If it is not possible to get the previous item, return None. 172 """ 173 # try previous sibling's last child's last child, 174 # else try previous sibling's last child, else try previous sibling 175 prevsibling = self.prevsibling() 176 if prevsibling is not None: 177 prevsiblinglastchild = prevsibling.lastchild() 178 if (prevsiblinglastchild is not None) and not prevsibling.folded: 179 prevsiblinglclc = prevsiblinglastchild.lastchild() 180 if ( 181 prevsiblinglclc is not None 182 ) and not prevsiblinglastchild.folded: 183 return prevsiblinglclc 184 else: 185 return prevsiblinglastchild 186 else: 187 return prevsibling 188 189 # try parent (or None) 190 return self.parentitem() 191 192 193class patch(patchnode, list): # todo: rename patchroot 194 """ 195 list of header objects representing the patch. 196 """ 197 198 def __init__(self, headerlist): 199 self.extend(headerlist) 200 # add parent patch object reference to each header 201 for header in self: 202 header.patch = self 203 204 205class uiheader(patchnode): 206 """patch header 207 208 xxx shouldn't we move this to mercurial/patch.py ? 209 """ 210 211 def __init__(self, header): 212 self.nonuiheader = header 213 # flag to indicate whether to apply this chunk 214 self.applied = True 215 # flag which only affects the status display indicating if a node's 216 # children are partially applied (i.e. some applied, some not). 217 self.partial = False 218 219 # flag to indicate whether to display as folded/unfolded to user 220 self.folded = True 221 222 # list of all headers in patch 223 self.patch = None 224 225 # flag is False if this header was ever unfolded from initial state 226 self.neverunfolded = True 227 self.hunks = [uihunk(h, self) for h in self.hunks] 228 229 def prettystr(self): 230 x = stringio() 231 self.pretty(x) 232 return x.getvalue() 233 234 def nextsibling(self): 235 numheadersinpatch = len(self.patch) 236 indexofthisheader = self.patch.index(self) 237 238 if indexofthisheader < numheadersinpatch - 1: 239 nextheader = self.patch[indexofthisheader + 1] 240 return nextheader 241 else: 242 return None 243 244 def prevsibling(self): 245 indexofthisheader = self.patch.index(self) 246 if indexofthisheader > 0: 247 previousheader = self.patch[indexofthisheader - 1] 248 return previousheader 249 else: 250 return None 251 252 def parentitem(self): 253 """ 254 there is no 'real' parent item of a header that can be selected, 255 so return None. 256 """ 257 return None 258 259 def firstchild(self): 260 """return the first child of this item, if one exists. otherwise 261 None.""" 262 if len(self.hunks) > 0: 263 return self.hunks[0] 264 else: 265 return None 266 267 def lastchild(self): 268 """return the last child of this item, if one exists. otherwise 269 None.""" 270 if len(self.hunks) > 0: 271 return self.hunks[-1] 272 else: 273 return None 274 275 def allchildren(self): 276 """return a list of all of the direct children of this node""" 277 return self.hunks 278 279 def __getattr__(self, name): 280 return getattr(self.nonuiheader, name) 281 282 283class uihunkline(patchnode): 284 """represents a changed line in a hunk""" 285 286 def __init__(self, linetext, hunk): 287 self.linetext = linetext 288 self.applied = True 289 # the parent hunk to which this line belongs 290 self.hunk = hunk 291 # folding lines currently is not used/needed, but this flag is needed 292 # in the previtem method. 293 self.folded = False 294 295 def prettystr(self): 296 return self.linetext 297 298 def nextsibling(self): 299 numlinesinhunk = len(self.hunk.changedlines) 300 indexofthisline = self.hunk.changedlines.index(self) 301 302 if indexofthisline < numlinesinhunk - 1: 303 nextline = self.hunk.changedlines[indexofthisline + 1] 304 return nextline 305 else: 306 return None 307 308 def prevsibling(self): 309 indexofthisline = self.hunk.changedlines.index(self) 310 if indexofthisline > 0: 311 previousline = self.hunk.changedlines[indexofthisline - 1] 312 return previousline 313 else: 314 return None 315 316 def parentitem(self): 317 """return the parent to the current item""" 318 return self.hunk 319 320 def firstchild(self): 321 """return the first child of this item, if one exists. otherwise 322 None.""" 323 # hunk-lines don't have children 324 return None 325 326 def lastchild(self): 327 """return the last child of this item, if one exists. otherwise 328 None.""" 329 # hunk-lines don't have children 330 return None 331 332 333class uihunk(patchnode): 334 """ui patch hunk, wraps a hunk and keep track of ui behavior""" 335 336 maxcontext = 3 337 338 def __init__(self, hunk, header): 339 self._hunk = hunk 340 self.changedlines = [uihunkline(line, self) for line in hunk.hunk] 341 self.header = header 342 # used at end for detecting how many removed lines were un-applied 343 self.originalremoved = self.removed 344 345 # flag to indicate whether to display as folded/unfolded to user 346 self.folded = True 347 # flag to indicate whether to apply this chunk 348 self.applied = True 349 # flag which only affects the status display indicating if a node's 350 # children are partially applied (i.e. some applied, some not). 351 self.partial = False 352 353 def nextsibling(self): 354 numhunksinheader = len(self.header.hunks) 355 indexofthishunk = self.header.hunks.index(self) 356 357 if indexofthishunk < numhunksinheader - 1: 358 nexthunk = self.header.hunks[indexofthishunk + 1] 359 return nexthunk 360 else: 361 return None 362 363 def prevsibling(self): 364 indexofthishunk = self.header.hunks.index(self) 365 if indexofthishunk > 0: 366 previoushunk = self.header.hunks[indexofthishunk - 1] 367 return previoushunk 368 else: 369 return None 370 371 def parentitem(self): 372 """return the parent to the current item""" 373 return self.header 374 375 def firstchild(self): 376 """return the first child of this item, if one exists. otherwise 377 None.""" 378 if len(self.changedlines) > 0: 379 return self.changedlines[0] 380 else: 381 return None 382 383 def lastchild(self): 384 """return the last child of this item, if one exists. otherwise 385 None.""" 386 if len(self.changedlines) > 0: 387 return self.changedlines[-1] 388 else: 389 return None 390 391 def allchildren(self): 392 """return a list of all of the direct children of this node""" 393 return self.changedlines 394 395 def countchanges(self): 396 """changedlines -> (n+,n-)""" 397 add = len( 398 [ 399 l 400 for l in self.changedlines 401 if l.applied and l.prettystr().startswith(b'+') 402 ] 403 ) 404 rem = len( 405 [ 406 l 407 for l in self.changedlines 408 if l.applied and l.prettystr().startswith(b'-') 409 ] 410 ) 411 return add, rem 412 413 def getfromtoline(self): 414 # calculate the number of removed lines converted to context lines 415 removedconvertedtocontext = self.originalremoved - self.removed 416 417 contextlen = ( 418 len(self.before) + len(self.after) + removedconvertedtocontext 419 ) 420 if self.after and self.after[-1] == diffhelper.MISSING_NEWLINE_MARKER: 421 contextlen -= 1 422 fromlen = contextlen + self.removed 423 tolen = contextlen + self.added 424 425 # diffutils manual, section "2.2.2.2 detailed description of unified 426 # format": "an empty hunk is considered to end at the line that 427 # precedes the hunk." 428 # 429 # so, if either of hunks is empty, decrease its line start. --immerrr 430 # but only do this if fromline > 0, to avoid having, e.g fromline=-1. 431 fromline, toline = self.fromline, self.toline 432 if fromline != 0: 433 if fromlen == 0: 434 fromline -= 1 435 if tolen == 0 and toline > 0: 436 toline -= 1 437 438 fromtoline = b'@@ -%d,%d +%d,%d @@%s\n' % ( 439 fromline, 440 fromlen, 441 toline, 442 tolen, 443 self.proc and (b' ' + self.proc), 444 ) 445 return fromtoline 446 447 def write(self, fp): 448 # updated self.added/removed, which are used by getfromtoline() 449 self.added, self.removed = self.countchanges() 450 fp.write(self.getfromtoline()) 451 452 hunklinelist = [] 453 # add the following to the list: (1) all applied lines, and 454 # (2) all unapplied removal lines (convert these to context lines) 455 for changedline in self.changedlines: 456 changedlinestr = changedline.prettystr() 457 if changedline.applied: 458 hunklinelist.append(changedlinestr) 459 elif changedlinestr.startswith(b"-"): 460 hunklinelist.append(b" " + changedlinestr[1:]) 461 462 fp.write(b''.join(self.before + hunklinelist + self.after)) 463 464 pretty = write 465 466 def prettystr(self): 467 x = stringio() 468 self.pretty(x) 469 return x.getvalue() 470 471 def reversehunk(self): 472 """return a recordhunk which is the reverse of the hunk 473 474 Assuming the displayed patch is diff(A, B) result. The returned hunk is 475 intended to be applied to B, instead of A. 476 477 For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and 478 the user made the following selection: 479 480 0 481 [x] -1 [x]: selected 482 [ ] -2 [ ]: not selected 483 [x] +3 484 [ ] +4 485 [x] +5 486 6 487 488 This function returns a hunk like: 489 490 0 491 -3 492 -4 493 -5 494 +1 495 +4 496 6 497 498 Note "4" was first deleted then added. That's because "4" exists in B 499 side and "-4" must exist between "-3" and "-5" to make the patch 500 applicable to B. 501 """ 502 dels = [] 503 adds = [] 504 noeol = False 505 for line in self.changedlines: 506 text = line.linetext 507 if line.linetext == diffhelper.MISSING_NEWLINE_MARKER: 508 noeol = True 509 break 510 if line.applied: 511 if text.startswith(b'+'): 512 dels.append(text[1:]) 513 elif text.startswith(b'-'): 514 adds.append(text[1:]) 515 elif text.startswith(b'+'): 516 dels.append(text[1:]) 517 adds.append(text[1:]) 518 hunk = [b'-%s' % l for l in dels] + [b'+%s' % l for l in adds] 519 if noeol and hunk: 520 # Remove the newline from the end of the hunk. 521 hunk[-1] = hunk[-1][:-1] 522 h = self._hunk 523 return patchmod.recordhunk( 524 h.header, h.toline, h.fromline, h.proc, h.before, hunk, h.after 525 ) 526 527 def __getattr__(self, name): 528 return getattr(self._hunk, name) 529 530 def __repr__(self): 531 return '<hunk %r@%d>' % (self.filename(), self.fromline) 532 533 534def filterpatch(ui, chunks, chunkselector, operation=None): 535 """interactively filter patch chunks into applied-only chunks""" 536 chunks = list(chunks) 537 # convert chunks list into structure suitable for displaying/modifying 538 # with curses. create a list of headers only. 539 headers = [c for c in chunks if isinstance(c, patchmod.header)] 540 541 # if there are no changed files 542 if len(headers) == 0: 543 return [], {} 544 uiheaders = [uiheader(h) for h in headers] 545 # let user choose headers/hunks/lines, and mark their applied flags 546 # accordingly 547 ret = chunkselector(ui, uiheaders, operation=operation) 548 appliedhunklist = [] 549 for hdr in uiheaders: 550 if hdr.applied and ( 551 hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0 552 ): 553 appliedhunklist.append(hdr) 554 fixoffset = 0 555 for hnk in hdr.hunks: 556 if hnk.applied: 557 appliedhunklist.append(hnk) 558 # adjust the 'to'-line offset of the hunk to be correct 559 # after de-activating some of the other hunks for this file 560 if fixoffset: 561 # hnk = copy.copy(hnk) # necessary?? 562 hnk.toline += fixoffset 563 else: 564 fixoffset += hnk.removed - hnk.added 565 566 return (appliedhunklist, ret) 567 568 569def chunkselector(ui, headerlist, operation=None): 570 """ 571 curses interface to get selection of chunks, and mark the applied flags 572 of the chosen chunks. 573 """ 574 ui.write(_(b'starting interactive selection\n')) 575 chunkselector = curseschunkselector(headerlist, ui, operation) 576 origsigtstp = sentinel = object() 577 if util.safehasattr(signal, b'SIGTSTP'): 578 origsigtstp = signal.getsignal(signal.SIGTSTP) 579 try: 580 with util.with_lc_ctype(): 581 curses.wrapper(chunkselector.main) 582 if chunkselector.initexc is not None: 583 raise chunkselector.initexc 584 # ncurses does not restore signal handler for SIGTSTP 585 finally: 586 if origsigtstp is not sentinel: 587 signal.signal(signal.SIGTSTP, origsigtstp) 588 return chunkselector.opts 589 590 591def testdecorator(testfn, f): 592 def u(*args, **kwargs): 593 return f(testfn, *args, **kwargs) 594 595 return u 596 597 598def testchunkselector(testfn, ui, headerlist, operation=None): 599 """ 600 test interface to get selection of chunks, and mark the applied flags 601 of the chosen chunks. 602 """ 603 chunkselector = curseschunkselector(headerlist, ui, operation) 604 605 class dummystdscr(object): 606 def clear(self): 607 pass 608 609 def refresh(self): 610 pass 611 612 chunkselector.stdscr = dummystdscr() 613 if testfn and os.path.exists(testfn): 614 testf = open(testfn, b'r') 615 # TODO: open in binary mode? 616 testcommands = [x.rstrip('\n') for x in testf.readlines()] 617 testf.close() 618 while True: 619 if chunkselector.handlekeypressed(testcommands.pop(0), test=True): 620 break 621 return chunkselector.opts 622 623 624_headermessages = { # {operation: text} 625 b'apply': _(b'Select hunks to apply'), 626 b'discard': _(b'Select hunks to discard'), 627 b'keep': _(b'Select hunks to keep'), 628 None: _(b'Select hunks to record'), 629} 630 631 632class curseschunkselector(object): 633 def __init__(self, headerlist, ui, operation=None): 634 # put the headers into a patch object 635 self.headerlist = patch(headerlist) 636 637 self.ui = ui 638 self.opts = {} 639 640 self.errorstr = None 641 # list of all chunks 642 self.chunklist = [] 643 for h in headerlist: 644 self.chunklist.append(h) 645 self.chunklist.extend(h.hunks) 646 647 # dictionary mapping (fgcolor, bgcolor) pairs to the 648 # corresponding curses color-pair value. 649 self.colorpairs = {} 650 # maps custom nicknames of color-pairs to curses color-pair values 651 self.colorpairnames = {} 652 653 # Honor color setting of ui section. Keep colored setup as 654 # long as not explicitly set to a falsy value - especially, 655 # when not set at all. This is to stay most compatible with 656 # previous (color only) behaviour. 657 uicolor = stringutil.parsebool(self.ui.config(b'ui', b'color')) 658 self.usecolor = uicolor is not False 659 660 # the currently selected header, hunk, or hunk-line 661 self.currentselecteditem = self.headerlist[0] 662 self.lastapplieditem = None 663 664 # updated when printing out patch-display -- the 'lines' here are the 665 # line positions *in the pad*, not on the screen. 666 self.selecteditemstartline = 0 667 self.selecteditemendline = None 668 669 # define indentation levels 670 self.headerindentnumchars = 0 671 self.hunkindentnumchars = 3 672 self.hunklineindentnumchars = 6 673 674 # the first line of the pad to print to the screen 675 self.firstlineofpadtoprint = 0 676 677 # keeps track of the number of lines in the pad 678 self.numpadlines = None 679 680 self.numstatuslines = 1 681 682 # keep a running count of the number of lines printed to the pad 683 # (used for determining when the selected item begins/ends) 684 self.linesprintedtopadsofar = 0 685 686 # stores optional text for a commit comment provided by the user 687 self.commenttext = b"" 688 689 # if the last 'toggle all' command caused all changes to be applied 690 self.waslasttoggleallapplied = True 691 692 # affects some ui text 693 if operation not in _headermessages: 694 raise error.ProgrammingError( 695 b'unexpected operation: %s' % operation 696 ) 697 self.operation = operation 698 699 def uparrowevent(self): 700 """ 701 try to select the previous item to the current item that has the 702 most-indented level. for example, if a hunk is selected, try to select 703 the last hunkline of the hunk prior to the selected hunk. or, if 704 the first hunkline of a hunk is currently selected, then select the 705 hunk itself. 706 """ 707 currentitem = self.currentselecteditem 708 709 nextitem = currentitem.previtem() 710 711 if nextitem is None: 712 # if no parent item (i.e. currentitem is the first header), then 713 # no change... 714 nextitem = currentitem 715 716 self.currentselecteditem = nextitem 717 718 def uparrowshiftevent(self): 719 """ 720 select (if possible) the previous item on the same level as the 721 currently selected item. otherwise, select (if possible) the 722 parent-item of the currently selected item. 723 """ 724 currentitem = self.currentselecteditem 725 nextitem = currentitem.prevsibling() 726 # if there's no previous sibling, try choosing the parent 727 if nextitem is None: 728 nextitem = currentitem.parentitem() 729 if nextitem is None: 730 # if no parent item (i.e. currentitem is the first header), then 731 # no change... 732 nextitem = currentitem 733 734 self.currentselecteditem = nextitem 735 self.recenterdisplayedarea() 736 737 def downarrowevent(self): 738 """ 739 try to select the next item to the current item that has the 740 most-indented level. for example, if a hunk is selected, select 741 the first hunkline of the selected hunk. or, if the last hunkline of 742 a hunk is currently selected, then select the next hunk, if one exists, 743 or if not, the next header if one exists. 744 """ 745 # self.startprintline += 1 #debug 746 currentitem = self.currentselecteditem 747 748 nextitem = currentitem.nextitem() 749 # if there's no next item, keep the selection as-is 750 if nextitem is None: 751 nextitem = currentitem 752 753 self.currentselecteditem = nextitem 754 755 def downarrowshiftevent(self): 756 """ 757 select (if possible) the next item on the same level as the currently 758 selected item. otherwise, select (if possible) the next item on the 759 same level as the parent item of the currently selected item. 760 """ 761 currentitem = self.currentselecteditem 762 nextitem = currentitem.nextsibling() 763 # if there's no next sibling, try choosing the parent's nextsibling 764 if nextitem is None: 765 try: 766 nextitem = currentitem.parentitem().nextsibling() 767 except AttributeError: 768 # parentitem returned None, so nextsibling() can't be called 769 nextitem = None 770 if nextitem is None: 771 # if parent has no next sibling, then no change... 772 nextitem = currentitem 773 774 self.currentselecteditem = nextitem 775 self.recenterdisplayedarea() 776 777 def nextsametype(self, test=False): 778 currentitem = self.currentselecteditem 779 sametype = lambda item: isinstance(item, type(currentitem)) 780 nextitem = currentitem.nextitem() 781 782 while nextitem is not None and not sametype(nextitem): 783 nextitem = nextitem.nextitem() 784 785 if nextitem is None: 786 nextitem = currentitem 787 else: 788 parent = nextitem.parentitem() 789 if parent is not None and parent.folded: 790 self.togglefolded(parent) 791 792 self.currentselecteditem = nextitem 793 if not test: 794 self.recenterdisplayedarea() 795 796 def rightarrowevent(self): 797 """ 798 select (if possible) the first of this item's child-items. 799 """ 800 currentitem = self.currentselecteditem 801 nextitem = currentitem.firstchild() 802 803 # turn off folding if we want to show a child-item 804 if currentitem.folded: 805 self.togglefolded(currentitem) 806 807 if nextitem is None: 808 # if no next item on parent-level, then no change... 809 nextitem = currentitem 810 811 self.currentselecteditem = nextitem 812 813 def leftarrowevent(self): 814 """ 815 if the current item can be folded (i.e. it is an unfolded header or 816 hunk), then fold it. otherwise try select (if possible) the parent 817 of this item. 818 """ 819 currentitem = self.currentselecteditem 820 821 # try to fold the item 822 if not isinstance(currentitem, uihunkline): 823 if not currentitem.folded: 824 self.togglefolded(item=currentitem) 825 return 826 827 # if it can't be folded, try to select the parent item 828 nextitem = currentitem.parentitem() 829 830 if nextitem is None: 831 # if no item on parent-level, then no change... 832 nextitem = currentitem 833 if not nextitem.folded: 834 self.togglefolded(item=nextitem) 835 836 self.currentselecteditem = nextitem 837 838 def leftarrowshiftevent(self): 839 """ 840 select the header of the current item (or fold current item if the 841 current item is already a header). 842 """ 843 currentitem = self.currentselecteditem 844 845 if isinstance(currentitem, uiheader): 846 if not currentitem.folded: 847 self.togglefolded(item=currentitem) 848 return 849 850 # select the parent item recursively until we're at a header 851 while True: 852 nextitem = currentitem.parentitem() 853 if nextitem is None: 854 break 855 else: 856 currentitem = nextitem 857 858 self.currentselecteditem = currentitem 859 860 def updatescroll(self): 861 """scroll the screen to fully show the currently-selected""" 862 selstart = self.selecteditemstartline 863 selend = self.selecteditemendline 864 865 padstart = self.firstlineofpadtoprint 866 padend = padstart + self.yscreensize - self.numstatuslines - 1 867 # 'buffered' pad start/end values which scroll with a certain 868 # top/bottom context margin 869 padstartbuffered = padstart + 3 870 padendbuffered = padend - 3 871 872 if selend > padendbuffered: 873 self.scrolllines(selend - padendbuffered) 874 elif selstart < padstartbuffered: 875 # negative values scroll in pgup direction 876 self.scrolllines(selstart - padstartbuffered) 877 878 def scrolllines(self, numlines): 879 """scroll the screen up (down) by numlines when numlines >0 (<0).""" 880 self.firstlineofpadtoprint += numlines 881 if self.firstlineofpadtoprint < 0: 882 self.firstlineofpadtoprint = 0 883 if self.firstlineofpadtoprint > self.numpadlines - 1: 884 self.firstlineofpadtoprint = self.numpadlines - 1 885 886 def toggleapply(self, item=None): 887 """ 888 toggle the applied flag of the specified item. if no item is specified, 889 toggle the flag of the currently selected item. 890 """ 891 if item is None: 892 item = self.currentselecteditem 893 # Only set this when NOT using 'toggleall' 894 self.lastapplieditem = item 895 896 item.applied = not item.applied 897 898 if isinstance(item, uiheader): 899 item.partial = False 900 if item.applied: 901 # apply all its hunks 902 for hnk in item.hunks: 903 hnk.applied = True 904 # apply all their hunklines 905 for hunkline in hnk.changedlines: 906 hunkline.applied = True 907 else: 908 # un-apply all its hunks 909 for hnk in item.hunks: 910 hnk.applied = False 911 hnk.partial = False 912 # un-apply all their hunklines 913 for hunkline in hnk.changedlines: 914 hunkline.applied = False 915 elif isinstance(item, uihunk): 916 item.partial = False 917 # apply all it's hunklines 918 for hunkline in item.changedlines: 919 hunkline.applied = item.applied 920 921 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks] 922 allsiblingsapplied = not (False in siblingappliedstatus) 923 nosiblingsapplied = not (True in siblingappliedstatus) 924 925 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks] 926 somesiblingspartial = True in siblingspartialstatus 927 928 # cases where applied or partial should be removed from header 929 930 # if no 'sibling' hunks are applied (including this hunk) 931 if nosiblingsapplied: 932 if not item.header.special(): 933 item.header.applied = False 934 item.header.partial = False 935 else: # some/all parent siblings are applied 936 item.header.applied = True 937 item.header.partial = ( 938 somesiblingspartial or not allsiblingsapplied 939 ) 940 941 elif isinstance(item, uihunkline): 942 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines] 943 allsiblingsapplied = not (False in siblingappliedstatus) 944 nosiblingsapplied = not (True in siblingappliedstatus) 945 946 # if no 'sibling' lines are applied 947 if nosiblingsapplied: 948 item.hunk.applied = False 949 item.hunk.partial = False 950 elif allsiblingsapplied: 951 item.hunk.applied = True 952 item.hunk.partial = False 953 else: # some siblings applied 954 item.hunk.applied = True 955 item.hunk.partial = True 956 957 parentsiblingsapplied = [ 958 hnk.applied for hnk in item.hunk.header.hunks 959 ] 960 noparentsiblingsapplied = not (True in parentsiblingsapplied) 961 allparentsiblingsapplied = not (False in parentsiblingsapplied) 962 963 parentsiblingspartial = [ 964 hnk.partial for hnk in item.hunk.header.hunks 965 ] 966 someparentsiblingspartial = True in parentsiblingspartial 967 968 # if all parent hunks are not applied, un-apply header 969 if noparentsiblingsapplied: 970 if not item.hunk.header.special(): 971 item.hunk.header.applied = False 972 item.hunk.header.partial = False 973 # set the applied and partial status of the header if needed 974 else: # some/all parent siblings are applied 975 item.hunk.header.applied = True 976 item.hunk.header.partial = ( 977 someparentsiblingspartial or not allparentsiblingsapplied 978 ) 979 980 def toggleall(self): 981 """toggle the applied flag of all items.""" 982 if self.waslasttoggleallapplied: # then unapply them this time 983 for item in self.headerlist: 984 if item.applied: 985 self.toggleapply(item) 986 else: 987 for item in self.headerlist: 988 if not item.applied: 989 self.toggleapply(item) 990 self.waslasttoggleallapplied = not self.waslasttoggleallapplied 991 992 def flipselections(self): 993 """ 994 Flip all selections. Every selected line is unselected and vice 995 versa. 996 """ 997 for header in self.headerlist: 998 for hunk in header.allchildren(): 999 for line in hunk.allchildren(): 1000 self.toggleapply(line) 1001 1002 def toggleallbetween(self): 1003 """toggle applied on or off for all items in range [lastapplied, 1004 current].""" 1005 if ( 1006 not self.lastapplieditem 1007 or self.currentselecteditem == self.lastapplieditem 1008 ): 1009 # Treat this like a normal 'x'/' ' 1010 self.toggleapply() 1011 return 1012 1013 startitem = self.lastapplieditem 1014 enditem = self.currentselecteditem 1015 # Verify that enditem is "after" startitem, otherwise swap them. 1016 for direction in [b'forward', b'reverse']: 1017 nextitem = startitem.nextitem() 1018 while nextitem and nextitem != enditem: 1019 nextitem = nextitem.nextitem() 1020 if nextitem: 1021 break 1022 # Looks like we went the wrong direction :) 1023 startitem, enditem = enditem, startitem 1024 1025 if not nextitem: 1026 # We didn't find a path going either forward or backward? Don't know 1027 # how this can happen, let's not crash though. 1028 return 1029 1030 nextitem = startitem 1031 # Switch all items to be the opposite state of the currently selected 1032 # item. Specifically: 1033 # [ ] startitem 1034 # [x] middleitem 1035 # [ ] enditem <-- currently selected 1036 # This will turn all three on, since the currently selected item is off. 1037 # This does *not* invert each item (i.e. middleitem stays marked/on) 1038 desiredstate = not self.currentselecteditem.applied 1039 while nextitem != enditem.nextitem(): 1040 if nextitem.applied != desiredstate: 1041 self.toggleapply(item=nextitem) 1042 nextitem = nextitem.nextitem() 1043 1044 def togglefolded(self, item=None, foldparent=False): 1045 """toggle folded flag of specified item (defaults to currently 1046 selected)""" 1047 if item is None: 1048 item = self.currentselecteditem 1049 if foldparent or (isinstance(item, uiheader) and item.neverunfolded): 1050 if not isinstance(item, uiheader): 1051 # we need to select the parent item in this case 1052 self.currentselecteditem = item = item.parentitem() 1053 elif item.neverunfolded: 1054 item.neverunfolded = False 1055 1056 # also fold any foldable children of the parent/current item 1057 if isinstance(item, uiheader): # the original or 'new' item 1058 for child in item.allchildren(): 1059 child.folded = not item.folded 1060 1061 if isinstance(item, (uiheader, uihunk)): 1062 item.folded = not item.folded 1063 1064 def alignstring(self, instr, window): 1065 """ 1066 add whitespace to the end of a string in order to make it fill 1067 the screen in the x direction. the current cursor position is 1068 taken into account when making this calculation. the string can span 1069 multiple lines. 1070 """ 1071 y, xstart = window.getyx() 1072 width = self.xscreensize 1073 # turn tabs into spaces 1074 instr = instr.expandtabs(4) 1075 strwidth = encoding.colwidth(instr) 1076 numspaces = width - ((strwidth + xstart) % width) 1077 return instr + b" " * numspaces 1078 1079 def printstring( 1080 self, 1081 window, 1082 text, 1083 fgcolor=None, 1084 bgcolor=None, 1085 pair=None, 1086 pairname=None, 1087 attrlist=None, 1088 towin=True, 1089 align=True, 1090 showwhtspc=False, 1091 ): 1092 """ 1093 print the string, text, with the specified colors and attributes, to 1094 the specified curses window object. 1095 1096 the foreground and background colors are of the form 1097 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green, 1098 magenta, red, white, yellow]. if pairname is provided, a color 1099 pair will be looked up in the self.colorpairnames dictionary. 1100 1101 attrlist is a list containing text attributes in the form of 1102 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout, 1103 underline]. 1104 1105 if align == True, whitespace is added to the printed string such that 1106 the string stretches to the right border of the window. 1107 1108 if showwhtspc == True, trailing whitespace of a string is highlighted. 1109 """ 1110 # preprocess the text, converting tabs to spaces 1111 text = text.expandtabs(4) 1112 # strip \n, and convert control characters to ^[char] representation 1113 text = re.sub( 1114 br'[\x00-\x08\x0a-\x1f]', 1115 lambda m: b'^' + pycompat.sysbytes(chr(ord(m.group()) + 64)), 1116 text.strip(b'\n'), 1117 ) 1118 1119 if pair is not None: 1120 colorpair = pair 1121 elif pairname is not None: 1122 colorpair = self.colorpairnames[pairname] 1123 else: 1124 if fgcolor is None: 1125 fgcolor = -1 1126 if bgcolor is None: 1127 bgcolor = -1 1128 if (fgcolor, bgcolor) in self.colorpairs: 1129 colorpair = self.colorpairs[(fgcolor, bgcolor)] 1130 else: 1131 colorpair = self.getcolorpair(fgcolor, bgcolor) 1132 # add attributes if possible 1133 if attrlist is None: 1134 attrlist = [] 1135 if colorpair < 256: 1136 # then it is safe to apply all attributes 1137 for textattr in attrlist: 1138 colorpair |= textattr 1139 else: 1140 # just apply a select few (safe?) attributes 1141 for textattr in (curses.A_UNDERLINE, curses.A_BOLD): 1142 if textattr in attrlist: 1143 colorpair |= textattr 1144 1145 y, xstart = self.chunkpad.getyx() 1146 t = b"" # variable for counting lines printed 1147 # if requested, show trailing whitespace 1148 if showwhtspc: 1149 origlen = len(text) 1150 text = text.rstrip(b' \n') # tabs have already been expanded 1151 strippedlen = len(text) 1152 numtrailingspaces = origlen - strippedlen 1153 1154 if towin: 1155 window.addstr(encoding.strfromlocal(text), colorpair) 1156 t += text 1157 1158 if showwhtspc: 1159 wscolorpair = colorpair | curses.A_REVERSE 1160 if towin: 1161 for i in range(numtrailingspaces): 1162 window.addch(curses.ACS_CKBOARD, wscolorpair) 1163 t += b" " * numtrailingspaces 1164 1165 if align: 1166 if towin: 1167 extrawhitespace = self.alignstring(b"", window) 1168 window.addstr(extrawhitespace, colorpair) 1169 else: 1170 # need to use t, since the x position hasn't incremented 1171 extrawhitespace = self.alignstring(t, window) 1172 t += extrawhitespace 1173 1174 # is reset to 0 at the beginning of printitem() 1175 1176 linesprinted = (xstart + len(t)) // self.xscreensize 1177 self.linesprintedtopadsofar += linesprinted 1178 return t 1179 1180 def _getstatuslinesegments(self): 1181 """-> [str]. return segments""" 1182 selected = self.currentselecteditem.applied 1183 spaceselect = _(b'space/enter: select') 1184 spacedeselect = _(b'space/enter: deselect') 1185 # Format the selected label into a place as long as the longer of the 1186 # two possible labels. This may vary by language. 1187 spacelen = max(len(spaceselect), len(spacedeselect)) 1188 selectedlabel = b'%-*s' % ( 1189 spacelen, 1190 spacedeselect if selected else spaceselect, 1191 ) 1192 segments = [ 1193 _headermessages[self.operation], 1194 b'-', 1195 _(b'[x]=selected **=collapsed'), 1196 _(b'c: confirm'), 1197 _(b'q: abort'), 1198 _(b'arrow keys: move/expand/collapse'), 1199 selectedlabel, 1200 _(b'?: help'), 1201 ] 1202 return segments 1203 1204 def _getstatuslines(self): 1205 """() -> [str]. return short help used in the top status window""" 1206 if self.errorstr is not None: 1207 lines = [self.errorstr, _(b'Press any key to continue')] 1208 else: 1209 # wrap segments to lines 1210 segments = self._getstatuslinesegments() 1211 width = self.xscreensize 1212 lines = [] 1213 lastwidth = width 1214 for s in segments: 1215 w = encoding.colwidth(s) 1216 sep = b' ' * (1 + (s and s[0] not in b'-[')) 1217 if lastwidth + w + len(sep) >= width: 1218 lines.append(s) 1219 lastwidth = w 1220 else: 1221 lines[-1] += sep + s 1222 lastwidth += w + len(sep) 1223 if len(lines) != self.numstatuslines: 1224 self.numstatuslines = len(lines) 1225 self.statuswin.resize(self.numstatuslines, self.xscreensize) 1226 return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines] 1227 1228 def updatescreen(self): 1229 self.statuswin.erase() 1230 self.chunkpad.erase() 1231 1232 printstring = self.printstring 1233 1234 # print out the status lines at the top 1235 try: 1236 for line in self._getstatuslines(): 1237 printstring(self.statuswin, line, pairname=b"legend") 1238 self.statuswin.refresh() 1239 except curses.error: 1240 pass 1241 if self.errorstr is not None: 1242 return 1243 1244 # print out the patch in the remaining part of the window 1245 try: 1246 self.printitem() 1247 self.updatescroll() 1248 self.chunkpad.refresh( 1249 self.firstlineofpadtoprint, 1250 0, 1251 self.numstatuslines, 1252 0, 1253 self.yscreensize - self.numstatuslines, 1254 self.xscreensize - 1, 1255 ) 1256 except curses.error: 1257 pass 1258 1259 def getstatusprefixstring(self, item): 1260 """ 1261 create a string to prefix a line with which indicates whether 'item' 1262 is applied and/or folded. 1263 """ 1264 1265 # create checkbox string 1266 if item.applied: 1267 if not isinstance(item, uihunkline) and item.partial: 1268 checkbox = b"[~]" 1269 else: 1270 checkbox = b"[x]" 1271 else: 1272 checkbox = b"[ ]" 1273 1274 try: 1275 if item.folded: 1276 checkbox += b"**" 1277 if isinstance(item, uiheader): 1278 # one of "m", "a", or "d" (modified, added, deleted) 1279 filestatus = item.changetype 1280 1281 checkbox += filestatus + b" " 1282 else: 1283 checkbox += b" " 1284 if isinstance(item, uiheader): 1285 # add two more spaces for headers 1286 checkbox += b" " 1287 except AttributeError: # not foldable 1288 checkbox += b" " 1289 1290 return checkbox 1291 1292 def printheader( 1293 self, header, selected=False, towin=True, ignorefolding=False 1294 ): 1295 """ 1296 print the header to the pad. if countlines is True, don't print 1297 anything, but just count the number of lines which would be printed. 1298 """ 1299 1300 outstr = b"" 1301 text = header.prettystr() 1302 chunkindex = self.chunklist.index(header) 1303 1304 if chunkindex != 0 and not header.folded: 1305 # add separating line before headers 1306 outstr += self.printstring( 1307 self.chunkpad, b'_' * self.xscreensize, towin=towin, align=False 1308 ) 1309 # select color-pair based on if the header is selected 1310 colorpair = self.getcolorpair( 1311 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD] 1312 ) 1313 1314 # print out each line of the chunk, expanding it to screen width 1315 1316 # number of characters to indent lines on this level by 1317 indentnumchars = 0 1318 checkbox = self.getstatusprefixstring(header) 1319 if not header.folded or ignorefolding: 1320 textlist = text.split(b"\n") 1321 linestr = checkbox + textlist[0] 1322 else: 1323 linestr = checkbox + header.filename() 1324 outstr += self.printstring( 1325 self.chunkpad, linestr, pair=colorpair, towin=towin 1326 ) 1327 if not header.folded or ignorefolding: 1328 if len(textlist) > 1: 1329 for line in textlist[1:]: 1330 linestr = b" " * (indentnumchars + len(checkbox)) + line 1331 outstr += self.printstring( 1332 self.chunkpad, linestr, pair=colorpair, towin=towin 1333 ) 1334 1335 return outstr 1336 1337 def printhunklinesbefore( 1338 self, hunk, selected=False, towin=True, ignorefolding=False 1339 ): 1340 """includes start/end line indicator""" 1341 outstr = b"" 1342 # where hunk is in list of siblings 1343 hunkindex = hunk.header.hunks.index(hunk) 1344 1345 if hunkindex != 0: 1346 # add separating line before headers 1347 outstr += self.printstring( 1348 self.chunkpad, b' ' * self.xscreensize, towin=towin, align=False 1349 ) 1350 1351 colorpair = self.getcolorpair( 1352 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD] 1353 ) 1354 1355 # print out from-to line with checkbox 1356 checkbox = self.getstatusprefixstring(hunk) 1357 1358 lineprefix = b" " * self.hunkindentnumchars + checkbox 1359 frtoline = b" " + hunk.getfromtoline().strip(b"\n") 1360 1361 outstr += self.printstring( 1362 self.chunkpad, lineprefix, towin=towin, align=False 1363 ) # add uncolored checkbox/indent 1364 outstr += self.printstring( 1365 self.chunkpad, frtoline, pair=colorpair, towin=towin 1366 ) 1367 1368 if hunk.folded and not ignorefolding: 1369 # skip remainder of output 1370 return outstr 1371 1372 # print out lines of the chunk preceeding changed-lines 1373 for line in hunk.before: 1374 linestr = ( 1375 b" " * (self.hunklineindentnumchars + len(checkbox)) + line 1376 ) 1377 outstr += self.printstring(self.chunkpad, linestr, towin=towin) 1378 1379 return outstr 1380 1381 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False): 1382 outstr = b"" 1383 if hunk.folded and not ignorefolding: 1384 return outstr 1385 1386 # a bit superfluous, but to avoid hard-coding indent amount 1387 checkbox = self.getstatusprefixstring(hunk) 1388 for line in hunk.after: 1389 linestr = ( 1390 b" " * (self.hunklineindentnumchars + len(checkbox)) + line 1391 ) 1392 outstr += self.printstring(self.chunkpad, linestr, towin=towin) 1393 1394 return outstr 1395 1396 def printhunkchangedline(self, hunkline, selected=False, towin=True): 1397 outstr = b"" 1398 checkbox = self.getstatusprefixstring(hunkline) 1399 1400 linestr = hunkline.prettystr().strip(b"\n") 1401 1402 # select color-pair based on whether line is an addition/removal 1403 if selected: 1404 colorpair = self.getcolorpair(name=b"selected") 1405 elif linestr.startswith(b"+"): 1406 colorpair = self.getcolorpair(name=b"addition") 1407 elif linestr.startswith(b"-"): 1408 colorpair = self.getcolorpair(name=b"deletion") 1409 elif linestr.startswith(b"\\"): 1410 colorpair = self.getcolorpair(name=b"normal") 1411 1412 lineprefix = b" " * self.hunklineindentnumchars + checkbox 1413 outstr += self.printstring( 1414 self.chunkpad, lineprefix, towin=towin, align=False 1415 ) # add uncolored checkbox/indent 1416 outstr += self.printstring( 1417 self.chunkpad, linestr, pair=colorpair, towin=towin, showwhtspc=True 1418 ) 1419 return outstr 1420 1421 def printitem( 1422 self, item=None, ignorefolding=False, recursechildren=True, towin=True 1423 ): 1424 """ 1425 use __printitem() to print the the specified item.applied. 1426 if item is not specified, then print the entire patch. 1427 (hiding folded elements, etc. -- see __printitem() docstring) 1428 """ 1429 1430 if item is None: 1431 item = self.headerlist 1432 if recursechildren: 1433 self.linesprintedtopadsofar = 0 1434 1435 outstr = [] 1436 self.__printitem( 1437 item, ignorefolding, recursechildren, outstr, towin=towin 1438 ) 1439 return b''.join(outstr) 1440 1441 def outofdisplayedarea(self): 1442 y, _ = self.chunkpad.getyx() # cursor location 1443 # * 2 here works but an optimization would be the max number of 1444 # consecutive non selectable lines 1445 # i.e the max number of context line for any hunk in the patch 1446 miny = min(0, self.firstlineofpadtoprint - self.yscreensize) 1447 maxy = self.firstlineofpadtoprint + self.yscreensize * 2 1448 return y < miny or y > maxy 1449 1450 def handleselection(self, item, recursechildren): 1451 selected = item is self.currentselecteditem 1452 if selected and recursechildren: 1453 # assumes line numbering starting from line 0 1454 self.selecteditemstartline = self.linesprintedtopadsofar 1455 selecteditemlines = self.getnumlinesdisplayed( 1456 item, recursechildren=False 1457 ) 1458 self.selecteditemendline = ( 1459 self.selecteditemstartline + selecteditemlines - 1 1460 ) 1461 return selected 1462 1463 def __printitem( 1464 self, item, ignorefolding, recursechildren, outstr, towin=True 1465 ): 1466 """ 1467 recursive method for printing out patch/header/hunk/hunk-line data to 1468 screen. also returns a string with all of the content of the displayed 1469 patch (not including coloring, etc.). 1470 1471 if ignorefolding is True, then folded items are printed out. 1472 1473 if recursechildren is False, then only print the item without its 1474 child items. 1475 """ 1476 1477 if towin and self.outofdisplayedarea(): 1478 return 1479 1480 selected = self.handleselection(item, recursechildren) 1481 1482 # patch object is a list of headers 1483 if isinstance(item, patch): 1484 if recursechildren: 1485 for hdr in item: 1486 self.__printitem( 1487 hdr, ignorefolding, recursechildren, outstr, towin 1488 ) 1489 # todo: eliminate all isinstance() calls 1490 if isinstance(item, uiheader): 1491 outstr.append( 1492 self.printheader( 1493 item, selected, towin=towin, ignorefolding=ignorefolding 1494 ) 1495 ) 1496 if recursechildren: 1497 for hnk in item.hunks: 1498 self.__printitem( 1499 hnk, ignorefolding, recursechildren, outstr, towin 1500 ) 1501 elif isinstance(item, uihunk) and ( 1502 (not item.header.folded) or ignorefolding 1503 ): 1504 # print the hunk data which comes before the changed-lines 1505 outstr.append( 1506 self.printhunklinesbefore( 1507 item, selected, towin=towin, ignorefolding=ignorefolding 1508 ) 1509 ) 1510 if recursechildren: 1511 for l in item.changedlines: 1512 self.__printitem( 1513 l, ignorefolding, recursechildren, outstr, towin 1514 ) 1515 outstr.append( 1516 self.printhunklinesafter( 1517 item, towin=towin, ignorefolding=ignorefolding 1518 ) 1519 ) 1520 elif isinstance(item, uihunkline) and ( 1521 (not item.hunk.folded) or ignorefolding 1522 ): 1523 outstr.append( 1524 self.printhunkchangedline(item, selected, towin=towin) 1525 ) 1526 1527 return outstr 1528 1529 def getnumlinesdisplayed( 1530 self, item=None, ignorefolding=False, recursechildren=True 1531 ): 1532 """ 1533 return the number of lines which would be displayed if the item were 1534 to be printed to the display. the item will not be printed to the 1535 display (pad). 1536 if no item is given, assume the entire patch. 1537 if ignorefolding is True, folded items will be unfolded when counting 1538 the number of lines. 1539 """ 1540 1541 # temporarily disable printing to windows by printstring 1542 patchdisplaystring = self.printitem( 1543 item, ignorefolding, recursechildren, towin=False 1544 ) 1545 numlines = len(patchdisplaystring) // self.xscreensize 1546 return numlines 1547 1548 def sigwinchhandler(self, n, frame): 1549 """handle window resizing""" 1550 try: 1551 curses.endwin() 1552 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui) 1553 self.statuswin.resize(self.numstatuslines, self.xscreensize) 1554 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1 1555 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize) 1556 except curses.error: 1557 pass 1558 1559 def getcolorpair( 1560 self, fgcolor=None, bgcolor=None, name=None, attrlist=None 1561 ): 1562 """ 1563 get a curses color pair, adding it to self.colorpairs if it is not 1564 already defined. an optional string, name, can be passed as a shortcut 1565 for referring to the color-pair. by default, if no arguments are 1566 specified, the white foreground / black background color-pair is 1567 returned. 1568 1569 it is expected that this function will be used exclusively for 1570 initializing color pairs, and not curses.init_pair(). 1571 1572 attrlist is used to 'flavor' the returned color-pair. this information 1573 is not stored in self.colorpairs. it contains attribute values like 1574 curses.A_BOLD. 1575 """ 1576 1577 if (name is not None) and name in self.colorpairnames: 1578 # then get the associated color pair and return it 1579 colorpair = self.colorpairnames[name] 1580 else: 1581 if fgcolor is None: 1582 fgcolor = -1 1583 if bgcolor is None: 1584 bgcolor = -1 1585 if (fgcolor, bgcolor) in self.colorpairs: 1586 colorpair = self.colorpairs[(fgcolor, bgcolor)] 1587 else: 1588 pairindex = len(self.colorpairs) + 1 1589 if self.usecolor: 1590 curses.init_pair(pairindex, fgcolor, bgcolor) 1591 colorpair = self.colorpairs[ 1592 (fgcolor, bgcolor) 1593 ] = curses.color_pair(pairindex) 1594 if name is not None: 1595 self.colorpairnames[name] = curses.color_pair(pairindex) 1596 else: 1597 cval = 0 1598 if name is not None: 1599 if name == b'selected': 1600 cval = curses.A_REVERSE 1601 self.colorpairnames[name] = cval 1602 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval 1603 1604 # add attributes if possible 1605 if attrlist is None: 1606 attrlist = [] 1607 if colorpair < 256: 1608 # then it is safe to apply all attributes 1609 for textattr in attrlist: 1610 colorpair |= textattr 1611 else: 1612 # just apply a select few (safe?) attributes 1613 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD): 1614 if textattrib in attrlist: 1615 colorpair |= textattrib 1616 return colorpair 1617 1618 def initcolorpair(self, *args, **kwargs): 1619 """same as getcolorpair.""" 1620 self.getcolorpair(*args, **kwargs) 1621 1622 def helpwindow(self): 1623 """print a help window to the screen. exit after any keypress.""" 1624 helptext = _( 1625 b""" [press any key to return to the patch-display] 1626 1627The curses hunk selector allows you to interactively choose among the 1628changes you have made, and confirm only those changes you select for 1629further processing by the command you are running (such as commit, 1630shelve, or revert). After confirming the selected changes, the 1631unselected changes are still present in your working copy, so you can 1632use the hunk selector multiple times to split large changes into 1633smaller changesets. the following are valid keystrokes: 1634 1635 x [space] : (un-)select item ([~]/[x] = partly/fully applied) 1636 [enter] : (un-)select item and go to next item of same type 1637 A : (un-)select all items 1638 X : (un-)select all items between current and most-recent 1639 up/down-arrow [k/j] : go to previous/next unfolded item 1640 pgup/pgdn [K/J] : go to previous/next item of same type 1641 right/left-arrow [l/h] : go to child item / parent item 1642 shift-left-arrow [H] : go to parent header / fold selected header 1643 g : go to the top 1644 G : go to the bottom 1645 f : fold / unfold item, hiding/revealing its children 1646 F : fold / unfold parent item and all of its ancestors 1647 ctrl-l : scroll the selected line to the top of the screen 1648 m : edit / resume editing the commit message 1649 e : edit the currently selected hunk 1650 a : toggle all selections 1651 c : confirm selected changes 1652 r : review/edit and confirm selected changes 1653 q : quit without confirming (no changes will be made) 1654 ? : help (what you're currently reading)""" 1655 ) 1656 1657 helpwin = curses.newwin(self.yscreensize, 0, 0, 0) 1658 helplines = helptext.split(b"\n") 1659 helplines = helplines + [b" "] * ( 1660 self.yscreensize - self.numstatuslines - len(helplines) - 1 1661 ) 1662 try: 1663 for line in helplines: 1664 self.printstring(helpwin, line, pairname=b"legend") 1665 except curses.error: 1666 pass 1667 helpwin.refresh() 1668 try: 1669 with self.ui.timeblockedsection(b'crecord'): 1670 helpwin.getkey() 1671 except curses.error: 1672 pass 1673 1674 def commitMessageWindow(self): 1675 """Create a temporary commit message editing window on the screen.""" 1676 1677 curses.raw() 1678 curses.def_prog_mode() 1679 curses.endwin() 1680 self.commenttext = self.ui.edit(self.commenttext, self.ui.username()) 1681 curses.cbreak() 1682 self.stdscr.refresh() 1683 self.stdscr.keypad(1) # allow arrow-keys to continue to function 1684 1685 def handlefirstlineevent(self): 1686 """ 1687 Handle 'g' to navigate to the top most file in the ncurses window. 1688 """ 1689 self.currentselecteditem = self.headerlist[0] 1690 currentitem = self.currentselecteditem 1691 # select the parent item recursively until we're at a header 1692 while True: 1693 nextitem = currentitem.parentitem() 1694 if nextitem is None: 1695 break 1696 else: 1697 currentitem = nextitem 1698 1699 self.currentselecteditem = currentitem 1700 1701 def handlelastlineevent(self): 1702 """ 1703 Handle 'G' to navigate to the bottom most file/hunk/line depending 1704 on the whether the fold is active or not. 1705 1706 If the bottom most file is folded, it navigates to that file and 1707 stops there. If the bottom most file is unfolded, it navigates to 1708 the bottom most hunk in that file and stops there. If the bottom most 1709 hunk is unfolded, it navigates to the bottom most line in that hunk. 1710 """ 1711 currentitem = self.currentselecteditem 1712 nextitem = currentitem.nextitem() 1713 # select the child item recursively until we're at a footer 1714 while nextitem is not None: 1715 nextitem = currentitem.nextitem() 1716 if nextitem is None: 1717 break 1718 else: 1719 currentitem = nextitem 1720 1721 self.currentselecteditem = currentitem 1722 self.recenterdisplayedarea() 1723 1724 def confirmationwindow(self, windowtext): 1725 """display an informational window, then wait for and return a 1726 keypress.""" 1727 1728 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0) 1729 try: 1730 lines = windowtext.split(b"\n") 1731 for line in lines: 1732 self.printstring(confirmwin, line, pairname=b"selected") 1733 except curses.error: 1734 pass 1735 self.stdscr.refresh() 1736 confirmwin.refresh() 1737 try: 1738 with self.ui.timeblockedsection(b'crecord'): 1739 response = chr(self.stdscr.getch()) 1740 except ValueError: 1741 response = None 1742 1743 return response 1744 1745 def reviewcommit(self): 1746 """ask for 'y' to be pressed to confirm selected. return True if 1747 confirmed.""" 1748 confirmtext = _( 1749 b"""If you answer yes to the following, your currently chosen patch chunks 1750will be loaded into an editor. To modify the patch, make the changes in your 1751editor and save. To accept the current patch as-is, close the editor without 1752saving. 1753 1754note: don't add/remove lines unless you also modify the range information. 1755 failing to follow this rule will result in the commit aborting. 1756 1757are you sure you want to review/edit and confirm the selected changes [yn]? 1758""" 1759 ) 1760 with self.ui.timeblockedsection(b'crecord'): 1761 response = self.confirmationwindow(confirmtext) 1762 if response is None: 1763 response = "n" 1764 if response.lower().startswith("y"): 1765 return True 1766 else: 1767 return False 1768 1769 def recenterdisplayedarea(self): 1770 """ 1771 once we scrolled with pg up pg down we can be pointing outside of the 1772 display zone. we print the patch with towin=False to compute the 1773 location of the selected item even though it is outside of the displayed 1774 zone and then update the scroll. 1775 """ 1776 self.printitem(towin=False) 1777 self.updatescroll() 1778 1779 def toggleedit(self, item=None, test=False): 1780 """ 1781 edit the currently selected chunk 1782 """ 1783 1784 def updateui(self): 1785 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1 1786 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize) 1787 self.updatescroll() 1788 self.stdscr.refresh() 1789 self.statuswin.refresh() 1790 self.stdscr.keypad(1) 1791 1792 def editpatchwitheditor(self, chunk): 1793 if chunk is None: 1794 self.ui.write(_(b'cannot edit patch for whole file')) 1795 self.ui.write(b"\n") 1796 return None 1797 if chunk.header.binary(): 1798 self.ui.write(_(b'cannot edit patch for binary file')) 1799 self.ui.write(b"\n") 1800 return None 1801 1802 # write the initial patch 1803 patch = stringio() 1804 patch.write(diffhelptext + hunkhelptext) 1805 chunk.header.write(patch) 1806 chunk.write(patch) 1807 1808 # start the editor and wait for it to complete 1809 try: 1810 patch = self.ui.edit(patch.getvalue(), b"", action=b"diff") 1811 except error.Abort as exc: 1812 self.errorstr = exc.message 1813 return None 1814 finally: 1815 self.stdscr.clear() 1816 self.stdscr.refresh() 1817 1818 # remove comment lines 1819 patch = [ 1820 line + b'\n' 1821 for line in patch.splitlines() 1822 if not line.startswith(b'#') 1823 ] 1824 return patchmod.parsepatch(patch) 1825 1826 if item is None: 1827 item = self.currentselecteditem 1828 if isinstance(item, uiheader): 1829 return 1830 if isinstance(item, uihunkline): 1831 item = item.parentitem() 1832 if not isinstance(item, uihunk): 1833 return 1834 1835 # To go back to that hunk or its replacement at the end of the edit 1836 itemindex = item.parentitem().hunks.index(item) 1837 1838 beforeadded, beforeremoved = item.added, item.removed 1839 newpatches = editpatchwitheditor(self, item) 1840 if newpatches is None: 1841 if not test: 1842 updateui(self) 1843 return 1844 header = item.header 1845 editedhunkindex = header.hunks.index(item) 1846 hunksbefore = header.hunks[:editedhunkindex] 1847 hunksafter = header.hunks[editedhunkindex + 1 :] 1848 newpatchheader = newpatches[0] 1849 newhunks = [uihunk(h, header) for h in newpatchheader.hunks] 1850 newadded = sum([h.added for h in newhunks]) 1851 newremoved = sum([h.removed for h in newhunks]) 1852 offset = (newadded - beforeadded) - (newremoved - beforeremoved) 1853 1854 for h in hunksafter: 1855 h.toline += offset 1856 for h in newhunks: 1857 h.folded = False 1858 header.hunks = hunksbefore + newhunks + hunksafter 1859 if self.emptypatch(): 1860 header.hunks = hunksbefore + [item] + hunksafter 1861 self.currentselecteditem = header 1862 if len(header.hunks) > itemindex: 1863 self.currentselecteditem = header.hunks[itemindex] 1864 1865 if not test: 1866 updateui(self) 1867 1868 def emptypatch(self): 1869 item = self.headerlist 1870 if not item: 1871 return True 1872 for header in item: 1873 if header.hunks: 1874 return False 1875 return True 1876 1877 def handlekeypressed(self, keypressed, test=False): 1878 """ 1879 Perform actions based on pressed keys. 1880 1881 Return true to exit the main loop. 1882 """ 1883 if keypressed in ["k", "KEY_UP"]: 1884 self.uparrowevent() 1885 elif keypressed in ["K", "KEY_PPAGE"]: 1886 self.uparrowshiftevent() 1887 elif keypressed in ["j", "KEY_DOWN"]: 1888 self.downarrowevent() 1889 elif keypressed in ["J", "KEY_NPAGE"]: 1890 self.downarrowshiftevent() 1891 elif keypressed in ["l", "KEY_RIGHT"]: 1892 self.rightarrowevent() 1893 elif keypressed in ["h", "KEY_LEFT"]: 1894 self.leftarrowevent() 1895 elif keypressed in ["H", "KEY_SLEFT"]: 1896 self.leftarrowshiftevent() 1897 elif keypressed in ["q"]: 1898 raise error.CanceledError(_(b'user quit')) 1899 elif keypressed in ['a']: 1900 self.flipselections() 1901 elif keypressed in ["c"]: 1902 return True 1903 elif keypressed in ["r"]: 1904 if self.reviewcommit(): 1905 self.opts[b'review'] = True 1906 return True 1907 elif test and keypressed in ["R"]: 1908 self.opts[b'review'] = True 1909 return True 1910 elif keypressed in [" ", "x"]: 1911 self.toggleapply() 1912 elif keypressed in ["\n", "KEY_ENTER"]: 1913 self.toggleapply() 1914 self.nextsametype(test=test) 1915 elif keypressed in ["X"]: 1916 self.toggleallbetween() 1917 elif keypressed in ["A"]: 1918 self.toggleall() 1919 elif keypressed in ["e"]: 1920 self.toggleedit(test=test) 1921 elif keypressed in ["f"]: 1922 self.togglefolded() 1923 elif keypressed in ["F"]: 1924 self.togglefolded(foldparent=True) 1925 elif keypressed in ["m"]: 1926 self.commitMessageWindow() 1927 elif keypressed in ["g", "KEY_HOME"]: 1928 self.handlefirstlineevent() 1929 elif keypressed in ["G", "KEY_END"]: 1930 self.handlelastlineevent() 1931 elif keypressed in ["?"]: 1932 self.helpwindow() 1933 self.stdscr.clear() 1934 self.stdscr.refresh() 1935 elif keypressed in [curses.ascii.ctrl("L")]: 1936 # scroll the current line to the top of the screen, and redraw 1937 # everything 1938 self.scrolllines(self.selecteditemstartline) 1939 self.stdscr.clear() 1940 self.stdscr.refresh() 1941 1942 def main(self, stdscr): 1943 """ 1944 method to be wrapped by curses.wrapper() for selecting chunks. 1945 """ 1946 1947 origsigwinch = sentinel = object() 1948 if util.safehasattr(signal, b'SIGWINCH'): 1949 origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler) 1950 try: 1951 return self._main(stdscr) 1952 finally: 1953 if origsigwinch is not sentinel: 1954 signal.signal(signal.SIGWINCH, origsigwinch) 1955 1956 def _main(self, stdscr): 1957 self.stdscr = stdscr 1958 # error during initialization, cannot be printed in the curses 1959 # interface, it should be printed by the calling code 1960 self.initexc = None 1961 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx() 1962 1963 curses.start_color() 1964 try: 1965 curses.use_default_colors() 1966 except curses.error: 1967 self.usecolor = False 1968 1969 # In some situations we may have some cruft left on the "alternate 1970 # screen" from another program (or previous iterations of ourself), and 1971 # we won't clear it if the scroll region is small enough to comfortably 1972 # fit on the terminal. 1973 self.stdscr.clear() 1974 1975 # don't display the cursor 1976 try: 1977 curses.curs_set(0) 1978 except curses.error: 1979 pass 1980 1981 # available colors: black, blue, cyan, green, magenta, white, yellow 1982 # init_pair(color_id, foreground_color, background_color) 1983 self.initcolorpair(None, None, name=b"normal") 1984 self.initcolorpair( 1985 curses.COLOR_WHITE, curses.COLOR_MAGENTA, name=b"selected" 1986 ) 1987 self.initcolorpair(curses.COLOR_RED, None, name=b"deletion") 1988 self.initcolorpair(curses.COLOR_GREEN, None, name=b"addition") 1989 self.initcolorpair( 1990 curses.COLOR_WHITE, curses.COLOR_BLUE, name=b"legend" 1991 ) 1992 # newwin([height, width,] begin_y, begin_x) 1993 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0) 1994 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences 1995 1996 # figure out how much space to allocate for the chunk-pad which is 1997 # used for displaying the patch 1998 1999 # stupid hack to prevent getnumlinesdisplayed from failing 2000 self.chunkpad = curses.newpad(1, self.xscreensize) 2001 2002 # add 1 so to account for last line text reaching end of line 2003 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1 2004 2005 try: 2006 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize) 2007 except curses.error: 2008 self.initexc = fallbackerror( 2009 _(b'this diff is too large to be displayed') 2010 ) 2011 return 2012 # initialize selecteditemendline (initial start-line is 0) 2013 self.selecteditemendline = self.getnumlinesdisplayed( 2014 self.currentselecteditem, recursechildren=False 2015 ) 2016 2017 while True: 2018 self.updatescreen() 2019 try: 2020 with self.ui.timeblockedsection(b'crecord'): 2021 keypressed = self.statuswin.getkey() 2022 if self.errorstr is not None: 2023 self.errorstr = None 2024 continue 2025 except curses.error: 2026 keypressed = b"foobar" 2027 if self.handlekeypressed(keypressed): 2028 break 2029 2030 if self.commenttext != b"": 2031 whitespaceremoved = re.sub( 2032 br"(?m)^\s.*(\n|$)", b"", self.commenttext 2033 ) 2034 if whitespaceremoved != b"": 2035 self.opts[b'message'] = self.commenttext 2036