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