This instance will be upgraded to Heptapod 0.21.0rc2 on 2021-04-16 at 14:00 UTC+2 (a few minutes of down time)

crecord.py 70.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
# stuff related specifically to patch manipulation / parsing
#
# Copyright 2008 Mark Edgington <edgimar@gmail.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
#
# This code is based on the Mark Edgington's crecord extension.
# (Itself based on Bryan O'Sullivan's record extension.)

Gregory Szorc's avatar
Gregory Szorc committed
11
from __future__ import absolute_import
12

Gregory Szorc's avatar
Gregory Szorc committed
13 14 15 16 17
import os
import re
import signal

from .i18n import _
18 19 20 21
from .pycompat import (
    getattr,
    open,
)
Gregory Szorc's avatar
Gregory Szorc committed
22
from . import (
23
    diffhelper,
Gregory Szorc's avatar
Gregory Szorc committed
24
    encoding,
25
    error,
Gregory Szorc's avatar
Gregory Szorc committed
26
    patch as patchmod,
27
    pycompat,
28
    scmutil,
29
    util,
Gregory Szorc's avatar
Gregory Szorc committed
30
)
31 32
from .utils import stringutil

33
stringio = util.stringio
Matt Mackall's avatar
Matt Mackall committed
34

35
# patch comments based on the git one
36 37
diffhelptext = _(
    """# To remove '-' lines, make them ' ' lines (context).
38 39
# To remove '+' lines, delete them.
# Lines starting with # will be removed from the patch.
40 41
"""
)
42

43 44
hunkhelptext = _(
    """#
45 46 47 48 49
# If the patch applies cleanly, the edited hunk will immediately be
# added to the record list. If it does not apply cleanly, a rejects file
# will be generated. You can use that when you try again. If all lines
# of the hunk are removed, then the edit is aborted and the hunk is left
# unchanged.
50 51
"""
)
52

53 54
patchhelptext = _(
    """#
55 56 57
# If the patch applies cleanly, the edited patch will immediately
# be finalised. If it does not apply cleanly, rejects files will be
# generated. You can use those when you try again.
58 59
"""
)
60

61
try:
62
    import curses
63
    import curses.ascii
64

65
    curses.error
66
except (ImportError, AttributeError):
67
    curses = False
68

69

70 71
class fallbackerror(error.Abort):
    """Error that indicates the client should try to fallback to text mode."""
72

73 74 75
    # Inherits from error.Abort so that existing behavior is preserved if the
    # calling code does not know how to fallback.

76

77 78 79 80 81 82
def checkcurses(ui):
    """Return True if the user wants to use curses

    This method returns True if curses is found (and that python is built with
    it) and that the user has the correct flag for the ui.
    """
83
    return curses and ui.interface(b"chunkselector") == b"curses"
84

85

86 87 88 89 90 91
class patchnode(object):
    """abstract class for patch graph nodes
    (i.e. patchroot, header, hunk, hunkline)
    """

    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 102 103 104 105 106
    def nextsibling(self):
        """
        Return the closest next item of the same type where there are no items
        of different types between the current item and this closest item.
        If no such item exists, return None.
        """
107
        raise NotImplementedError(b"method must be implemented by subclass")
108 109 110 111 112 113 114

    def prevsibling(self):
        """
        Return the closest previous item of the same type where there are no
        items of different types between the current item and this closest item.
        If no such item exists, return None.
        """
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 123
        Try to return the next item closest to this item, regardless of item's
        type (header, hunk, or hunkline).
124

125
        If skipfolded == True, and the current item is folded, then the child
126 127 128 129 130 131 132 133 134
        items that are hidden due to folding will be skipped when determining
        the next item.

        If it is not possible to get the next item, return None.
        """
        try:
            itemfolded = self.folded
        except AttributeError:
            itemfolded = False
135
        if skipfolded and itemfolded:
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
            nextitem = self.nextsibling()
            if nextitem is None:
                try:
                    nextitem = self.parentitem().nextsibling()
                except AttributeError:
                    nextitem = None
            return nextitem
        else:
            # try child
            item = self.firstchild()
            if item is not None:
                return item

            # else try next sibling
            item = self.nextsibling()
            if item is not None:
                return item

            try:
                # else try parent's next sibling
                item = self.parentitem().nextsibling()
                if item is not None:
                    return item

                # else return grandparent's next sibling (or None)
                return self.parentitem().parentitem().nextsibling()

163
            except AttributeError:  # parent and/or grandparent was None
164 165
                return None

166
    def previtem(self):
167
        """
168 169
        Try to return the previous item closest to this item, regardless of
        item's type (header, hunk, or hunkline).
170 171 172

        If it is not possible to get the previous item, return None.
        """
173 174 175 176 177
        # try previous sibling's last child's last child,
        # else try previous sibling's last child, else try previous sibling
        prevsibling = self.prevsibling()
        if prevsibling is not None:
            prevsiblinglastchild = prevsibling.lastchild()
178
            if (prevsiblinglastchild is not None) and not prevsibling.folded:
179
                prevsiblinglclc = prevsiblinglastchild.lastchild()
180 181 182
                if (
                    prevsiblinglclc is not None
                ) and not prevsiblinglastchild.folded:
183
                    return prevsiblinglclc
184
                else:
185 186 187
                    return prevsiblinglastchild
            else:
                return prevsibling
188

189 190
        # try parent (or None)
        return self.parentitem()
191

192 193

class patch(patchnode, list):  # todo: rename patchroot
194 195 196
    """
    list of header objects representing the patch.
    """
197

198 199 200 201 202 203
    def __init__(self, headerlist):
        self.extend(headerlist)
        # add parent patch object reference to each header
        for header in self:
            header.patch = self

204

205 206 207
class uiheader(patchnode):
    """patch header

208
    xxx shouldn't we move this to mercurial/patch.py ?
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
    """

    def __init__(self, header):
        self.nonuiheader = header
        # flag to indicate whether to apply this chunk
        self.applied = True
        # flag which only affects the status display indicating if a node's
        # children are partially applied (i.e. some applied, some not).
        self.partial = False

        # flag to indicate whether to display as folded/unfolded to user
        self.folded = True

        # list of all headers in patch
        self.patch = None

        # flag is False if this header was ever unfolded from initial state
        self.neverunfolded = True
        self.hunks = [uihunk(h, self) for h in self.hunks]

    def prettystr(self):
230
        x = stringio()
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
        self.pretty(x)
        return x.getvalue()

    def nextsibling(self):
        numheadersinpatch = len(self.patch)
        indexofthisheader = self.patch.index(self)

        if indexofthisheader < numheadersinpatch - 1:
            nextheader = self.patch[indexofthisheader + 1]
            return nextheader
        else:
            return None

    def prevsibling(self):
        indexofthisheader = self.patch.index(self)
        if indexofthisheader > 0:
            previousheader = self.patch[indexofthisheader - 1]
            return previousheader
        else:
            return None

    def parentitem(self):
        """
        there is no 'real' parent item of a header that can be selected,
        so return None.
        """
        return None

    def firstchild(self):
260 261
        """return the first child of this item, if one exists.  otherwise
        None."""
262 263 264 265 266 267
        if len(self.hunks) > 0:
            return self.hunks[0]
        else:
            return None

    def lastchild(self):
268 269
        """return the last child of this item, if one exists.  otherwise
        None."""
270 271 272 273 274 275
        if len(self.hunks) > 0:
            return self.hunks[-1]
        else:
            return None

    def allchildren(self):
276
        """return a list of all of the direct children of this node"""
277 278 279 280 281
        return self.hunks

    def __getattr__(self, name):
        return getattr(self.nonuiheader, name)

282

283
class uihunkline(patchnode):
284
    """represents a changed line in a hunk"""
285

286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
    def __init__(self, linetext, hunk):
        self.linetext = linetext
        self.applied = True
        # the parent hunk to which this line belongs
        self.hunk = hunk
        # folding lines currently is not used/needed, but this flag is needed
        # in the previtem method.
        self.folded = False

    def prettystr(self):
        return self.linetext

    def nextsibling(self):
        numlinesinhunk = len(self.hunk.changedlines)
        indexofthisline = self.hunk.changedlines.index(self)

302
        if indexofthisline < numlinesinhunk - 1:
303 304 305 306 307 308 309 310 311 312 313 314 315 316
            nextline = self.hunk.changedlines[indexofthisline + 1]
            return nextline
        else:
            return None

    def prevsibling(self):
        indexofthisline = self.hunk.changedlines.index(self)
        if indexofthisline > 0:
            previousline = self.hunk.changedlines[indexofthisline - 1]
            return previousline
        else:
            return None

    def parentitem(self):
317
        """return the parent to the current item"""
318 319 320
        return self.hunk

    def firstchild(self):
321 322
        """return the first child of this item, if one exists.  otherwise
        None."""
323 324 325 326
        # hunk-lines don't have children
        return None

    def lastchild(self):
327 328
        """return the last child of this item, if one exists.  otherwise
        None."""
329 330 331
        # hunk-lines don't have children
        return None

332

333 334
class uihunk(patchnode):
    """ui patch hunk, wraps a hunk and keep track of ui behavior """
335

336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
    maxcontext = 3

    def __init__(self, hunk, header):
        self._hunk = hunk
        self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
        self.header = header
        # used at end for detecting how many removed lines were un-applied
        self.originalremoved = self.removed

        # flag to indicate whether to display as folded/unfolded to user
        self.folded = True
        # flag to indicate whether to apply this chunk
        self.applied = True
        # flag which only affects the status display indicating if a node's
        # children are partially applied (i.e. some applied, some not).
        self.partial = False

    def nextsibling(self):
        numhunksinheader = len(self.header.hunks)
        indexofthishunk = self.header.hunks.index(self)

357
        if indexofthishunk < numhunksinheader - 1:
358 359 360 361 362 363 364 365 366 367 368 369 370 371
            nexthunk = self.header.hunks[indexofthishunk + 1]
            return nexthunk
        else:
            return None

    def prevsibling(self):
        indexofthishunk = self.header.hunks.index(self)
        if indexofthishunk > 0:
            previoushunk = self.header.hunks[indexofthishunk - 1]
            return previoushunk
        else:
            return None

    def parentitem(self):
372
        """return the parent to the current item"""
373 374 375
        return self.header

    def firstchild(self):
376 377
        """return the first child of this item, if one exists.  otherwise
        None."""
378 379 380 381 382 383
        if len(self.changedlines) > 0:
            return self.changedlines[0]
        else:
            return None

    def lastchild(self):
384 385
        """return the last child of this item, if one exists.  otherwise
        None."""
386 387 388 389 390 391
        if len(self.changedlines) > 0:
            return self.changedlines[-1]
        else:
            return None

    def allchildren(self):
392
        """return a list of all of the direct children of this node"""
393
        return self.changedlines
394

395 396
    def countchanges(self):
        """changedlines -> (n+,n-)"""
397 398 399 400
        add = len(
            [
                l
                for l in self.changedlines
401
                if l.applied and l.prettystr().startswith(b'+')
402 403 404 405 406 407
            ]
        )
        rem = len(
            [
                l
                for l in self.changedlines
408
                if l.applied and l.prettystr().startswith(b'-')
409 410
            ]
        )
411 412 413 414 415 416
        return add, rem

    def getfromtoline(self):
        # calculate the number of removed lines converted to context lines
        removedconvertedtocontext = self.originalremoved - self.removed

417 418 419
        contextlen = (
            len(self.before) + len(self.after) + removedconvertedtocontext
        )
420
        if self.after and self.after[-1] == diffhelper.MISSING_NEWLINE_MARKER:
421 422 423 424 425 426 427 428 429 430 431 432 433 434
            contextlen -= 1
        fromlen = contextlen + self.removed
        tolen = contextlen + self.added

        # diffutils manual, section "2.2.2.2 detailed description of unified
        # format": "an empty hunk is considered to end at the line that
        # precedes the hunk."
        #
        # so, if either of hunks is empty, decrease its line start. --immerrr
        # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
        fromline, toline = self.fromline, self.toline
        if fromline != 0:
            if fromlen == 0:
                fromline -= 1
435
            if tolen == 0 and toline > 0:
436 437
                toline -= 1

438
        fromtoline = b'@@ -%d,%d +%d,%d @@%s\n' % (
439 440 441 442
            fromline,
            fromlen,
            toline,
            tolen,
443
            self.proc and (b' ' + self.proc),
444
        )
445 446 447 448 449 450 451 452 453 454 455 456 457 458
        return fromtoline

    def write(self, fp):
        # updated self.added/removed, which are used by getfromtoline()
        self.added, self.removed = self.countchanges()
        fp.write(self.getfromtoline())

        hunklinelist = []
        # add the following to the list: (1) all applied lines, and
        # (2) all unapplied removal lines (convert these to context lines)
        for changedline in self.changedlines:
            changedlinestr = changedline.prettystr()
            if changedline.applied:
                hunklinelist.append(changedlinestr)
459 460
            elif changedlinestr.startswith(b"-"):
                hunklinelist.append(b" " + changedlinestr[1:])
461

462
        fp.write(b''.join(self.before + hunklinelist + self.after))
463 464 465 466

    pretty = write

    def prettystr(self):
467
        x = stringio()
468 469 470
        self.pretty(x)
        return x.getvalue()

471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503
    def reversehunk(self):
        """return a recordhunk which is the reverse of the hunk

        Assuming the displayed patch is diff(A, B) result. The returned hunk is
        intended to be applied to B, instead of A.

        For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and
        the user made the following selection:

                 0
            [x] -1           [x]: selected
            [ ] -2           [ ]: not selected
            [x] +3
            [ ] +4
            [x] +5
                 6

        This function returns a hunk like:

                 0
                -3
                -4
                -5
                +1
                +4
                 6

        Note "4" was first deleted then added. That's because "4" exists in B
        side and "-4" must exist between "-3" and "-5" to make the patch
        applicable to B.
        """
        dels = []
        adds = []
504
        noeol = False
505 506
        for line in self.changedlines:
            text = line.linetext
507
            if line.linetext == diffhelper.MISSING_NEWLINE_MARKER:
508 509
                noeol = True
                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 517
                dels.append(text[1:])
                adds.append(text[1:])
518
        hunk = [b'-%s' % l for l in dels] + [b'+%s' % l for l in adds]
519 520 521
        if noeol and hunk:
            # Remove the newline from the end of the hunk.
            hunk[-1] = hunk[-1][:-1]
522
        h = self._hunk
523 524 525
        return patchmod.recordhunk(
            h.header, h.toline, h.fromline, h.proc, h.before, hunk, h.after
        )
526

527 528
    def __getattr__(self, name):
        return getattr(self._hunk, name)
529

530
    def __repr__(self):
531
        return '<hunk %r@%d>' % (self.filename(), self.fromline)
532

533

Jun Wu's avatar
Jun Wu committed
534
def filterpatch(ui, chunks, chunkselector, operation=None):
535 536 537 538 539 540 541 542
    """interactively filter patch chunks into applied-only chunks"""
    chunks = list(chunks)
    # convert chunks list into structure suitable for displaying/modifying
    # with curses.  create a list of headers only.
    headers = [c for c in chunks if isinstance(c, patchmod.header)]

    # if there are no changed files
    if len(headers) == 0:
543
        return [], {}
544 545 546
    uiheaders = [uiheader(h) for h in headers]
    # let user choose headers/hunks/lines, and mark their applied flags
    # accordingly
Jun Wu's avatar
Jun Wu committed
547
    ret = chunkselector(ui, uiheaders, operation=operation)
548 549
    appliedhunklist = []
    for hdr in uiheaders:
550 551 552
        if hdr.applied and (
            hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0
        ):
553 554 555 556 557 558 559 560
            appliedhunklist.append(hdr)
            fixoffset = 0
            for hnk in hdr.hunks:
                if hnk.applied:
                    appliedhunklist.append(hnk)
                    # adjust the 'to'-line offset of the hunk to be correct
                    # after de-activating some of the other hunks for this file
                    if fixoffset:
561
                        # hnk = copy.copy(hnk) # necessary??
562 563 564 565
                        hnk.toline += fixoffset
                else:
                    fixoffset += hnk.removed - hnk.added

566
    return (appliedhunklist, ret)
567

568

Jun Wu's avatar
Jun Wu committed
569
def chunkselector(ui, headerlist, operation=None):
570 571 572 573
    """
    curses interface to get selection of chunks, and mark the applied flags
    of the chosen chunks.
    """
574
    ui.write(_(b'starting interactive selection\n'))
Jun Wu's avatar
Jun Wu committed
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 581
        with util.with_lc_ctype():
            curses.wrapper(chunkselector.main)
582 583
        if chunkselector.initexc is not None:
            raise chunkselector.initexc
584 585
        # ncurses does not restore signal handler for SIGTSTP
    finally:
586 587
        if origsigtstp is not sentinel:
            signal.signal(signal.SIGTSTP, origsigtstp)
588
    return chunkselector.opts
589

590

591 592 593
def testdecorator(testfn, f):
    def u(*args, **kwargs):
        return f(testfn, *args, **kwargs)
594

595 596
    return u

597

Jun Wu's avatar
Jun Wu committed
598
def testchunkselector(testfn, ui, headerlist, operation=None):
599 600 601 602
    """
    test interface to get selection of chunks, and mark the applied flags
    of the chosen chunks.
    """
Jun Wu's avatar
Jun Wu committed
603
    chunkselector = curseschunkselector(headerlist, ui, operation)
604 605 606 607

    class dummystdscr(object):
        def clear(self):
            pass
608

609 610 611 612
        def refresh(self):
            pass

    chunkselector.stdscr = dummystdscr()
613
    if testfn and os.path.exists(testfn):
614 615
        testf = open(testfn, 'r')
        testcommands = [x.rstrip('\n') for x in testf.readlines()]
616 617 618 619
        testf.close()
        while True:
            if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
                break
620
    return chunkselector.opts
621

622 623

_headermessages = {  # {operation: text}
624 625 626 627
    b'apply': _(b'Select hunks to apply'),
    b'discard': _(b'Select hunks to discard'),
    b'keep': _(b'Select hunks to keep'),
    None: _(b'Select hunks to record'),
628 629
}

630

631
class curseschunkselector(object):
Jun Wu's avatar
Jun Wu committed
632
    def __init__(self, headerlist, ui, operation=None):
633 634 635 636
        # put the headers into a patch object
        self.headerlist = patch(headerlist)

        self.ui = ui
637
        self.opts = {}
638

639
        self.errorstr = None
640 641 642 643 644 645 646 647 648 649 650 651
        # list of all chunks
        self.chunklist = []
        for h in headerlist:
            self.chunklist.append(h)
            self.chunklist.extend(h.hunks)

        # dictionary mapping (fgcolor, bgcolor) pairs to the
        # corresponding curses color-pair value.
        self.colorpairs = {}
        # maps custom nicknames of color-pairs to curses color-pair values
        self.colorpairnames = {}

652 653 654 655
        # Honor color setting of ui section. Keep colored setup as
        # long as not explicitly set to a falsy value - especially,
        # when not set at all. This is to stay most compatible with
        # previous (color only) behaviour.
656
        uicolor = stringutil.parsebool(self.ui.config(b'ui', b'color'))
657 658
        self.usecolor = uicolor is not False

659 660
        # the currently selected header, hunk, or hunk-line
        self.currentselecteditem = self.headerlist[0]
661
        self.lastapplieditem = None
662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678

        # updated when printing out patch-display -- the 'lines' here are the
        # line positions *in the pad*, not on the screen.
        self.selecteditemstartline = 0
        self.selecteditemendline = None

        # define indentation levels
        self.headerindentnumchars = 0
        self.hunkindentnumchars = 3
        self.hunklineindentnumchars = 6

        # the first line of the pad to print to the screen
        self.firstlineofpadtoprint = 0

        # keeps track of the number of lines in the pad
        self.numpadlines = None

679
        self.numstatuslines = 1
680 681 682 683 684 685

        # keep a running count of the number of lines printed to the pad
        # (used for determining when the selected item begins/ends)
        self.linesprintedtopadsofar = 0

        # stores optional text for a commit comment provided by the user
686
        self.commenttext = b""
687 688 689 690

        # if the last 'toggle all' command caused all changes to be applied
        self.waslasttoggleallapplied = True

Jun Wu's avatar
Jun Wu committed
691
        # affects some ui text
692
        if operation not in _headermessages:
693 694 695
            raise error.ProgrammingError(
                b'unexpected operation: %s' % operation
            )
Jun Wu's avatar
Jun Wu committed
696 697
        self.operation = operation

698 699 700 701 702 703 704 705 706 707
    def uparrowevent(self):
        """
        try to select the previous item to the current item that has the
        most-indented level.  for example, if a hunk is selected, try to select
        the last hunkline of the hunk prior to the selected hunk.  or, if
        the first hunkline of a hunk is currently selected, then select the
        hunk itself.
        """
        currentitem = self.currentselecteditem

708
        nextitem = currentitem.previtem()
709 710 711 712 713 714 715 716 717 718 719 720 721 722 723

        if nextitem is None:
            # if no parent item (i.e. currentitem is the first header), then
            # no change...
            nextitem = currentitem

        self.currentselecteditem = nextitem

    def uparrowshiftevent(self):
        """
        select (if possible) the previous item on the same level as the
        currently selected item.  otherwise, select (if possible) the
        parent-item of the currently selected item.
        """
        currentitem = self.currentselecteditem
724 725
        nextitem = currentitem.prevsibling()
        # if there's no previous sibling, try choosing the parent
726 727 728 729 730 731 732 733
        if nextitem is None:
            nextitem = currentitem.parentitem()
        if nextitem is None:
            # if no parent item (i.e. currentitem is the first header), then
            # no change...
            nextitem = currentitem

        self.currentselecteditem = nextitem
734
        self.recenterdisplayedarea()
735 736 737 738 739 740 741 742 743

    def downarrowevent(self):
        """
        try to select the next item to the current item that has the
        most-indented level.  for example, if a hunk is selected, select
        the first hunkline of the selected hunk.  or, if the last hunkline of
        a hunk is currently selected, then select the next hunk, if one exists,
        or if not, the next header if one exists.
        """
744
        # self.startprintline += 1 #debug
745 746
        currentitem = self.currentselecteditem

747
        nextitem = currentitem.nextitem()
748 749 750 751 752 753 754 755
        # if there's no next item, keep the selection as-is
        if nextitem is None:
            nextitem = currentitem

        self.currentselecteditem = nextitem

    def downarrowshiftevent(self):
        """
756 757 758
        select (if possible) the next item on the same level as the currently
        selected item.  otherwise, select (if possible) the next item on the
        same level as the parent item of the currently selected item.
759 760
        """
        currentitem = self.currentselecteditem
761 762
        nextitem = currentitem.nextsibling()
        # if there's no next sibling, try choosing the parent's nextsibling
763 764
        if nextitem is None:
            try:
765
                nextitem = currentitem.parentitem().nextsibling()
766
            except AttributeError:
767
                # parentitem returned None, so nextsibling() can't be called
768 769
                nextitem = None
        if nextitem is None:
770
            # if parent has no next sibling, then no change...
771 772 773
            nextitem = currentitem

        self.currentselecteditem = nextitem
774
        self.recenterdisplayedarea()
775

776
    def nextsametype(self, test=False):
777 778 779 780 781 782 783 784 785 786 787
        currentitem = self.currentselecteditem
        sametype = lambda item: isinstance(item, type(currentitem))
        nextitem = currentitem.nextitem()

        while nextitem is not None and not sametype(nextitem):
            nextitem = nextitem.nextitem()

        if nextitem is None:
            nextitem = currentitem
        else:
            parent = nextitem.parentitem()
788
            if parent is not None and parent.folded:
789 790 791
                self.togglefolded(parent)

        self.currentselecteditem = nextitem
792 793
        if not test:
            self.recenterdisplayedarea()
794

795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859
    def rightarrowevent(self):
        """
        select (if possible) the first of this item's child-items.
        """
        currentitem = self.currentselecteditem
        nextitem = currentitem.firstchild()

        # turn off folding if we want to show a child-item
        if currentitem.folded:
            self.togglefolded(currentitem)

        if nextitem is None:
            # if no next item on parent-level, then no change...
            nextitem = currentitem

        self.currentselecteditem = nextitem

    def leftarrowevent(self):
        """
        if the current item can be folded (i.e. it is an unfolded header or
        hunk), then fold it.  otherwise try select (if possible) the parent
        of this item.
        """
        currentitem = self.currentselecteditem

        # try to fold the item
        if not isinstance(currentitem, uihunkline):
            if not currentitem.folded:
                self.togglefolded(item=currentitem)
                return

        # if it can't be folded, try to select the parent item
        nextitem = currentitem.parentitem()

        if nextitem is None:
            # if no item on parent-level, then no change...
            nextitem = currentitem
            if not nextitem.folded:
                self.togglefolded(item=nextitem)

        self.currentselecteditem = nextitem

    def leftarrowshiftevent(self):
        """
        select the header of the current item (or fold current item if the
        current item is already a header).
        """
        currentitem = self.currentselecteditem

        if isinstance(currentitem, uiheader):
            if not currentitem.folded:
                self.togglefolded(item=currentitem)
                return

        # select the parent item recursively until we're at a header
        while True:
            nextitem = currentitem.parentitem()
            if nextitem is None:
                break
            else:
                currentitem = nextitem

        self.currentselecteditem = currentitem

    def updatescroll(self):
860
        """scroll the screen to fully show the currently-selected"""
861 862
        selstart = self.selecteditemstartline
        selend = self.selecteditemendline
863

864 865 866 867 868 869 870 871 872 873 874 875 876 877
        padstart = self.firstlineofpadtoprint
        padend = padstart + self.yscreensize - self.numstatuslines - 1
        # 'buffered' pad start/end values which scroll with a certain
        # top/bottom context margin
        padstartbuffered = padstart + 3
        padendbuffered = padend - 3

        if selend > padendbuffered:
            self.scrolllines(selend - padendbuffered)
        elif selstart < padstartbuffered:
            # negative values scroll in pgup direction
            self.scrolllines(selstart - padstartbuffered)

    def scrolllines(self, numlines):
878
        """scroll the screen up (down) by numlines when numlines >0 (<0)."""
879 880 881 882 883 884 885 886 887 888 889 890 891
        self.firstlineofpadtoprint += numlines
        if self.firstlineofpadtoprint < 0:
            self.firstlineofpadtoprint = 0
        if self.firstlineofpadtoprint > self.numpadlines - 1:
            self.firstlineofpadtoprint = self.numpadlines - 1

    def toggleapply(self, item=None):
        """
        toggle the applied flag of the specified item.  if no item is specified,
        toggle the flag of the currently selected item.
        """
        if item is None:
            item = self.currentselecteditem
892 893
            # Only set this when NOT using 'toggleall'
            self.lastapplieditem = item
894 895 896 897 898 899

        item.applied = not item.applied

        if isinstance(item, uiheader):
            item.partial = False
            if item.applied:
900 901 902 903 904 905
                # apply all its hunks
                for hnk in item.hunks:
                    hnk.applied = True
                    # apply all their hunklines
                    for hunkline in hnk.changedlines:
                        hunkline.applied = True
906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924
            else:
                # un-apply all its hunks
                for hnk in item.hunks:
                    hnk.applied = False
                    hnk.partial = False
                    # un-apply all their hunklines
                    for hunkline in hnk.changedlines:
                        hunkline.applied = False
        elif isinstance(item, uihunk):
            item.partial = False
            # apply all it's hunklines
            for hunkline in item.changedlines:
                hunkline.applied = item.applied

            siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
            allsiblingsapplied = not (False in siblingappliedstatus)
            nosiblingsapplied = not (True in siblingappliedstatus)

            siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
925
            somesiblingspartial = True in siblingspartialstatus
926

927
            # cases where applied or partial should be removed from header
928 929 930 931 932 933

            # if no 'sibling' hunks are applied (including this hunk)
            if nosiblingsapplied:
                if not item.header.special():
                    item.header.applied = False
                    item.header.partial = False
934
            else:  # some/all parent siblings are applied
935
                item.header.applied = True
936 937 938
                item.header.partial = (
                    somesiblingspartial or not allsiblingsapplied
                )
939 940 941 942 943 944 945 946 947 948 949 950 951

        elif isinstance(item, uihunkline):
            siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
            allsiblingsapplied = not (False in siblingappliedstatus)
            nosiblingsapplied = not (True in siblingappliedstatus)

            # if no 'sibling' lines are applied
            if nosiblingsapplied:
                item.hunk.applied = False
                item.hunk.partial = False
            elif allsiblingsapplied:
                item.hunk.applied = True
                item.hunk.partial = False
952
            else:  # some siblings applied
953 954 955
                item.hunk.applied = True
                item.hunk.partial = True

956 957 958
            parentsiblingsapplied = [
                hnk.applied for hnk in item.hunk.header.hunks
            ]
959 960 961
            noparentsiblingsapplied = not (True in parentsiblingsapplied)
            allparentsiblingsapplied = not (False in parentsiblingsapplied)

962 963 964 965
            parentsiblingspartial = [
                hnk.partial for hnk in item.hunk.header.hunks
            ]
            someparentsiblingspartial = True in parentsiblingspartial
966 967 968 969 970 971 972

            # if all parent hunks are not applied, un-apply header
            if noparentsiblingsapplied:
                if not item.hunk.header.special():
                    item.hunk.header.applied = False
                    item.hunk.header.partial = False
            # set the applied and partial status of the header if needed
973
            else:  # some/all parent siblings are applied
974
                item.hunk.header.applied = True
975 976 977
                item.hunk.header.partial = (
                    someparentsiblingspartial or not allparentsiblingsapplied
                )
978 979

    def toggleall(self):
980
        """toggle the applied flag of all items."""
981
        if self.waslasttoggleallapplied:  # then unapply them this time
982 983 984 985 986 987 988 989 990
            for item in self.headerlist:
                if item.applied:
                    self.toggleapply(item)
        else:
            for item in self.headerlist:
                if not item.applied:
                    self.toggleapply(item)
        self.waslasttoggleallapplied = not self.waslasttoggleallapplied

991 992 993 994 995 996 997 998 999 1000
    def flipselections(self):
        """
        Flip all selections. Every selected line is unselected and vice
        versa.
        """
        for header in self.headerlist:
            for hunk in header.allchildren():
                for line in hunk.allchildren():
                    self.toggleapply(line)

1001
    def toggleallbetween(self):
1002 1003
        """toggle applied on or off for all items in range [lastapplied,
        current]. """
1004 1005 1006 1007
        if (
            not self.lastapplieditem
            or self.currentselecteditem == self.lastapplieditem
        ):
1008 1009 1010 1011 1012 1013 1014
            # Treat this like a normal 'x'/' '
            self.toggleapply()
            return

        startitem = self.lastapplieditem
        enditem = self.currentselecteditem
        # Verify that enditem is "after" startitem, otherwise swap them.
1015
        for direction in [b'forward', b'reverse']:
1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042
            nextitem = startitem.nextitem()
            while nextitem and nextitem != enditem:
                nextitem = nextitem.nextitem()
            if nextitem:
                break
            # Looks like we went the wrong direction :)
            startitem, enditem = enditem, startitem

        if not nextitem:
            # We didn't find a path going either forward or backward? Don't know
            # how this can happen, let's not crash though.
            return

        nextitem = startitem
        # Switch all items to be the opposite state of the currently selected
        # item. Specifically:
        #  [ ] startitem
        #  [x] middleitem
        #  [ ] enditem  <-- currently selected
        # This will turn all three on, since the currently selected item is off.
        # This does *not* invert each item (i.e. middleitem stays marked/on)
        desiredstate = not self.currentselecteditem.applied
        while nextitem != enditem.nextitem():
            if nextitem.applied != desiredstate:
                self.toggleapply(item=nextitem)
            nextitem = nextitem.nextitem()

1043
    def togglefolded(self, item=None, foldparent=False):
1044 1045
        """toggle folded flag of specified item (defaults to currently
        selected)"""
1046 1047 1048 1049 1050 1051 1052 1053 1054 1055
        if item is None:
            item = self.currentselecteditem
        if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
            if not isinstance(item, uiheader):
                # we need to select the parent item in this case
                self.currentselecteditem = item = item.parentitem()
            elif item.neverunfolded:
                item.neverunfolded = False

            # also fold any foldable children of the parent/current item
1056
            if isinstance(item, uiheader):  # the original or 'new' item
1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073
                for child in item.allchildren():
                    child.folded = not item.folded

        if isinstance(item, (uiheader, uihunk)):
            item.folded = not item.folded

    def alignstring(self, instr, window):
        """
        add whitespace to the end of a string in order to make it fill
        the screen in the x direction.  the current cursor position is
        taken into account when making this calculation.  the string can span
        multiple lines.
        """
        y, xstart = window.getyx()
        width = self.xscreensize
        # turn tabs into spaces
        instr = instr.expandtabs(4)
1074
        strwidth = encoding.colwidth(instr)
1075
        numspaces = width - ((strwidth + xstart) % width)
1076
        return instr + b" " * numspaces
1077

1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090
    def printstring(
        self,
        window,
        text,
        fgcolor=None,
        bgcolor=None,
        pair=None,
        pairname=None,
        attrlist=None,
        towin=True,
        align=True,
        showwhtspc=False,
    ):