mercurial/crecord.py
changeset 43076 2372284d9457
parent 42659 701341f57ceb
child 43077 687b865b95ad
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
    22     patch as patchmod,
    22     patch as patchmod,
    23     pycompat,
    23     pycompat,
    24     scmutil,
    24     scmutil,
    25     util,
    25     util,
    26 )
    26 )
    27 from .utils import (
    27 from .utils import stringutil
    28     stringutil,
    28 
    29 )
       
    30 stringio = util.stringio
    29 stringio = util.stringio
    31 
    30 
    32 # patch comments based on the git one
    31 # patch comments based on the git one
    33 diffhelptext = _("""# To remove '-' lines, make them ' ' lines (context).
    32 diffhelptext = _(
       
    33     """# To remove '-' lines, make them ' ' lines (context).
    34 # To remove '+' lines, delete them.
    34 # To remove '+' lines, delete them.
    35 # Lines starting with # will be removed from the patch.
    35 # Lines starting with # will be removed from the patch.
    36 """)
    36 """
    37 
    37 )
    38 hunkhelptext = _("""#
    38 
       
    39 hunkhelptext = _(
       
    40     """#
    39 # If the patch applies cleanly, the edited hunk will immediately be
    41 # If the patch applies cleanly, the edited hunk will immediately be
    40 # added to the record list. If it does not apply cleanly, a rejects file
    42 # added to the record list. If it does not apply cleanly, a rejects file
    41 # will be generated. You can use that when you try again. If all lines
    43 # will be generated. You can use that when you try again. If all lines
    42 # of the hunk are removed, then the edit is aborted and the hunk is left
    44 # of the hunk are removed, then the edit is aborted and the hunk is left
    43 # unchanged.
    45 # unchanged.
    44 """)
    46 """
    45 
    47 )
    46 patchhelptext = _("""#
    48 
       
    49 patchhelptext = _(
       
    50     """#
    47 # If the patch applies cleanly, the edited patch will immediately
    51 # If the patch applies cleanly, the edited patch will immediately
    48 # be finalised. If it does not apply cleanly, rejects files will be
    52 # be finalised. If it does not apply cleanly, rejects files will be
    49 # generated. You can use those when you try again.
    53 # generated. You can use those when you try again.
    50 """)
    54 """
       
    55 )
    51 
    56 
    52 try:
    57 try:
    53     import curses
    58     import curses
       
    59 
    54     curses.error
    60     curses.error
    55 except ImportError:
    61 except ImportError:
    56     # I have no idea if wcurses works with crecord...
    62     # I have no idea if wcurses works with crecord...
    57     try:
    63     try:
    58         import wcurses as curses
    64         import wcurses as curses
       
    65 
    59         curses.error
    66         curses.error
    60     except ImportError:
    67     except ImportError:
    61         # wcurses is not shipped on Windows by default, or python is not
    68         # wcurses is not shipped on Windows by default, or python is not
    62         # compiled with curses
    69         # compiled with curses
    63         curses = False
    70         curses = False
    64 
    71 
       
    72 
    65 class fallbackerror(error.Abort):
    73 class fallbackerror(error.Abort):
    66     """Error that indicates the client should try to fallback to text mode."""
    74     """Error that indicates the client should try to fallback to text mode."""
       
    75 
    67     # Inherits from error.Abort so that existing behavior is preserved if the
    76     # Inherits from error.Abort so that existing behavior is preserved if the
    68     # calling code does not know how to fallback.
    77     # calling code does not know how to fallback.
       
    78 
    69 
    79 
    70 def checkcurses(ui):
    80 def checkcurses(ui):
    71     """Return True if the user wants to use curses
    81     """Return True if the user wants to use curses
    72 
    82 
    73     This method returns True if curses is found (and that python is built with
    83     This method returns True if curses is found (and that python is built with
    74     it) and that the user has the correct flag for the ui.
    84     it) and that the user has the correct flag for the ui.
    75     """
    85     """
    76     return curses and ui.interface("chunkselector") == "curses"
    86     return curses and ui.interface("chunkselector") == "curses"
       
    87 
    77 
    88 
    78 class patchnode(object):
    89 class patchnode(object):
    79     """abstract class for patch graph nodes
    90     """abstract class for patch graph nodes
    80     (i.e. patchroot, header, hunk, hunkline)
    91     (i.e. patchroot, header, hunk, hunkline)
    81     """
    92     """
   150                     return item
   161                     return item
   151 
   162 
   152                 # else return grandparent's next sibling (or None)
   163                 # else return grandparent's next sibling (or None)
   153                 return self.parentitem().parentitem().nextsibling()
   164                 return self.parentitem().parentitem().nextsibling()
   154 
   165 
   155             except AttributeError: # parent and/or grandparent was None
   166             except AttributeError:  # parent and/or grandparent was None
   156                 return None
   167                 return None
   157 
   168 
   158     def previtem(self):
   169     def previtem(self):
   159         """
   170         """
   160         Try to return the previous item closest to this item, regardless of
   171         Try to return the previous item closest to this item, regardless of
   165         # try previous sibling's last child's last child,
   176         # try previous sibling's last child's last child,
   166         # else try previous sibling's last child, else try previous sibling
   177         # else try previous sibling's last child, else try previous sibling
   167         prevsibling = self.prevsibling()
   178         prevsibling = self.prevsibling()
   168         if prevsibling is not None:
   179         if prevsibling is not None:
   169             prevsiblinglastchild = prevsibling.lastchild()
   180             prevsiblinglastchild = prevsibling.lastchild()
   170             if ((prevsiblinglastchild is not None) and
   181             if (prevsiblinglastchild is not None) and not prevsibling.folded:
   171                 not prevsibling.folded):
       
   172                 prevsiblinglclc = prevsiblinglastchild.lastchild()
   182                 prevsiblinglclc = prevsiblinglastchild.lastchild()
   173                 if ((prevsiblinglclc is not None) and
   183                 if (
   174                     not prevsiblinglastchild.folded):
   184                     prevsiblinglclc is not None
       
   185                 ) and not prevsiblinglastchild.folded:
   175                     return prevsiblinglclc
   186                     return prevsiblinglclc
   176                 else:
   187                 else:
   177                     return prevsiblinglastchild
   188                     return prevsiblinglastchild
   178             else:
   189             else:
   179                 return prevsibling
   190                 return prevsibling
   180 
   191 
   181         # try parent (or None)
   192         # try parent (or None)
   182         return self.parentitem()
   193         return self.parentitem()
   183 
   194 
   184 class patch(patchnode, list): # todo: rename patchroot
   195 
       
   196 class patch(patchnode, list):  # todo: rename patchroot
   185     """
   197     """
   186     list of header objects representing the patch.
   198     list of header objects representing the patch.
   187     """
   199     """
       
   200 
   188     def __init__(self, headerlist):
   201     def __init__(self, headerlist):
   189         self.extend(headerlist)
   202         self.extend(headerlist)
   190         # add parent patch object reference to each header
   203         # add parent patch object reference to each header
   191         for header in self:
   204         for header in self:
   192             header.patch = self
   205             header.patch = self
       
   206 
   193 
   207 
   194 class uiheader(patchnode):
   208 class uiheader(patchnode):
   195     """patch header
   209     """patch header
   196 
   210 
   197     xxx shouldn't we move this to mercurial/patch.py ?
   211     xxx shouldn't we move this to mercurial/patch.py ?
   264         return self.hunks
   278         return self.hunks
   265 
   279 
   266     def __getattr__(self, name):
   280     def __getattr__(self, name):
   267         return getattr(self.nonuiheader, name)
   281         return getattr(self.nonuiheader, name)
   268 
   282 
       
   283 
   269 class uihunkline(patchnode):
   284 class uihunkline(patchnode):
   270     "represents a changed line in a hunk"
   285     "represents a changed line in a hunk"
       
   286 
   271     def __init__(self, linetext, hunk):
   287     def __init__(self, linetext, hunk):
   272         self.linetext = linetext
   288         self.linetext = linetext
   273         self.applied = True
   289         self.applied = True
   274         # the parent hunk to which this line belongs
   290         # the parent hunk to which this line belongs
   275         self.hunk = hunk
   291         self.hunk = hunk
   282 
   298 
   283     def nextsibling(self):
   299     def nextsibling(self):
   284         numlinesinhunk = len(self.hunk.changedlines)
   300         numlinesinhunk = len(self.hunk.changedlines)
   285         indexofthisline = self.hunk.changedlines.index(self)
   301         indexofthisline = self.hunk.changedlines.index(self)
   286 
   302 
   287         if (indexofthisline < numlinesinhunk - 1):
   303         if indexofthisline < numlinesinhunk - 1:
   288             nextline = self.hunk.changedlines[indexofthisline + 1]
   304             nextline = self.hunk.changedlines[indexofthisline + 1]
   289             return nextline
   305             return nextline
   290         else:
   306         else:
   291             return None
   307             return None
   292 
   308 
   310     def lastchild(self):
   326     def lastchild(self):
   311         "return the last child of this item, if one exists.  otherwise None."
   327         "return the last child of this item, if one exists.  otherwise None."
   312         # hunk-lines don't have children
   328         # hunk-lines don't have children
   313         return None
   329         return None
   314 
   330 
       
   331 
   315 class uihunk(patchnode):
   332 class uihunk(patchnode):
   316     """ui patch hunk, wraps a hunk and keep track of ui behavior """
   333     """ui patch hunk, wraps a hunk and keep track of ui behavior """
       
   334 
   317     maxcontext = 3
   335     maxcontext = 3
   318 
   336 
   319     def __init__(self, hunk, header):
   337     def __init__(self, hunk, header):
   320         self._hunk = hunk
   338         self._hunk = hunk
   321         self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
   339         self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
   333 
   351 
   334     def nextsibling(self):
   352     def nextsibling(self):
   335         numhunksinheader = len(self.header.hunks)
   353         numhunksinheader = len(self.header.hunks)
   336         indexofthishunk = self.header.hunks.index(self)
   354         indexofthishunk = self.header.hunks.index(self)
   337 
   355 
   338         if (indexofthishunk < numhunksinheader - 1):
   356         if indexofthishunk < numhunksinheader - 1:
   339             nexthunk = self.header.hunks[indexofthishunk + 1]
   357             nexthunk = self.header.hunks[indexofthishunk + 1]
   340             return nexthunk
   358             return nexthunk
   341         else:
   359         else:
   342             return None
   360             return None
   343 
   361 
   371         "return a list of all of the direct children of this node"
   389         "return a list of all of the direct children of this node"
   372         return self.changedlines
   390         return self.changedlines
   373 
   391 
   374     def countchanges(self):
   392     def countchanges(self):
   375         """changedlines -> (n+,n-)"""
   393         """changedlines -> (n+,n-)"""
   376         add = len([l for l in self.changedlines if l.applied
   394         add = len(
   377                     and l.prettystr().startswith('+')])
   395             [
   378         rem = len([l for l in self.changedlines if l.applied
   396                 l
   379                     and l.prettystr().startswith('-')])
   397                 for l in self.changedlines
       
   398                 if l.applied and l.prettystr().startswith('+')
       
   399             ]
       
   400         )
       
   401         rem = len(
       
   402             [
       
   403                 l
       
   404                 for l in self.changedlines
       
   405                 if l.applied and l.prettystr().startswith('-')
       
   406             ]
       
   407         )
   380         return add, rem
   408         return add, rem
   381 
   409 
   382     def getfromtoline(self):
   410     def getfromtoline(self):
   383         # calculate the number of removed lines converted to context lines
   411         # calculate the number of removed lines converted to context lines
   384         removedconvertedtocontext = self.originalremoved - self.removed
   412         removedconvertedtocontext = self.originalremoved - self.removed
   385 
   413 
   386         contextlen = (len(self.before) + len(self.after) +
   414         contextlen = (
   387                       removedconvertedtocontext)
   415             len(self.before) + len(self.after) + removedconvertedtocontext
       
   416         )
   388         if self.after and self.after[-1] == '\\ No newline at end of file\n':
   417         if self.after and self.after[-1] == '\\ No newline at end of file\n':
   389             contextlen -= 1
   418             contextlen -= 1
   390         fromlen = contextlen + self.removed
   419         fromlen = contextlen + self.removed
   391         tolen = contextlen + self.added
   420         tolen = contextlen + self.added
   392 
   421 
   402                 fromline -= 1
   431                 fromline -= 1
   403             if tolen == 0 and toline > 0:
   432             if tolen == 0 and toline > 0:
   404                 toline -= 1
   433                 toline -= 1
   405 
   434 
   406         fromtoline = '@@ -%d,%d +%d,%d @@%s\n' % (
   435         fromtoline = '@@ -%d,%d +%d,%d @@%s\n' % (
   407             fromline, fromlen, toline, tolen,
   436             fromline,
   408             self.proc and (' ' + self.proc))
   437             fromlen,
       
   438             toline,
       
   439             tolen,
       
   440             self.proc and (' ' + self.proc),
       
   441         )
   409         return fromtoline
   442         return fromtoline
   410 
   443 
   411     def write(self, fp):
   444     def write(self, fp):
   412         # updated self.added/removed, which are used by getfromtoline()
   445         # updated self.added/removed, which are used by getfromtoline()
   413         self.added, self.removed = self.countchanges()
   446         self.added, self.removed = self.countchanges()
   475             elif text.startswith('+'):
   508             elif text.startswith('+'):
   476                 dels.append(text[1:])
   509                 dels.append(text[1:])
   477                 adds.append(text[1:])
   510                 adds.append(text[1:])
   478         hunk = ['-%s' % l for l in dels] + ['+%s' % l for l in adds]
   511         hunk = ['-%s' % l for l in dels] + ['+%s' % l for l in adds]
   479         h = self._hunk
   512         h = self._hunk
   480         return patchmod.recordhunk(h.header, h.toline, h.fromline, h.proc,
   513         return patchmod.recordhunk(
   481                                    h.before, hunk, h.after)
   514             h.header, h.toline, h.fromline, h.proc, h.before, hunk, h.after
       
   515         )
   482 
   516 
   483     def __getattr__(self, name):
   517     def __getattr__(self, name):
   484         return getattr(self._hunk, name)
   518         return getattr(self._hunk, name)
   485 
   519 
   486     def __repr__(self):
   520     def __repr__(self):
   487         return r'<hunk %r@%d>' % (self.filename(), self.fromline)
   521         return r'<hunk %r@%d>' % (self.filename(), self.fromline)
       
   522 
   488 
   523 
   489 def filterpatch(ui, chunks, chunkselector, operation=None):
   524 def filterpatch(ui, chunks, chunkselector, operation=None):
   490     """interactively filter patch chunks into applied-only chunks"""
   525     """interactively filter patch chunks into applied-only chunks"""
   491     chunks = list(chunks)
   526     chunks = list(chunks)
   492     # convert chunks list into structure suitable for displaying/modifying
   527     # convert chunks list into structure suitable for displaying/modifying
   500     # let user choose headers/hunks/lines, and mark their applied flags
   535     # let user choose headers/hunks/lines, and mark their applied flags
   501     # accordingly
   536     # accordingly
   502     ret = chunkselector(ui, uiheaders, operation=operation)
   537     ret = chunkselector(ui, uiheaders, operation=operation)
   503     appliedhunklist = []
   538     appliedhunklist = []
   504     for hdr in uiheaders:
   539     for hdr in uiheaders:
   505         if (hdr.applied and
   540         if hdr.applied and (
   506             (hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0)):
   541             hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0
       
   542         ):
   507             appliedhunklist.append(hdr)
   543             appliedhunklist.append(hdr)
   508             fixoffset = 0
   544             fixoffset = 0
   509             for hnk in hdr.hunks:
   545             for hnk in hdr.hunks:
   510                 if hnk.applied:
   546                 if hnk.applied:
   511                     appliedhunklist.append(hnk)
   547                     appliedhunklist.append(hnk)
   512                     # adjust the 'to'-line offset of the hunk to be correct
   548                     # adjust the 'to'-line offset of the hunk to be correct
   513                     # after de-activating some of the other hunks for this file
   549                     # after de-activating some of the other hunks for this file
   514                     if fixoffset:
   550                     if fixoffset:
   515                         #hnk = copy.copy(hnk) # necessary??
   551                         # hnk = copy.copy(hnk) # necessary??
   516                         hnk.toline += fixoffset
   552                         hnk.toline += fixoffset
   517                 else:
   553                 else:
   518                     fixoffset += hnk.removed - hnk.added
   554                     fixoffset += hnk.removed - hnk.added
   519 
   555 
   520     return (appliedhunklist, ret)
   556     return (appliedhunklist, ret)
       
   557 
   521 
   558 
   522 def chunkselector(ui, headerlist, operation=None):
   559 def chunkselector(ui, headerlist, operation=None):
   523     """
   560     """
   524     curses interface to get selection of chunks, and mark the applied flags
   561     curses interface to get selection of chunks, and mark the applied flags
   525     of the chosen chunks.
   562     of the chosen chunks.
   540     finally:
   577     finally:
   541         if origsigtstp is not sentinel:
   578         if origsigtstp is not sentinel:
   542             signal.signal(signal.SIGTSTP, origsigtstp)
   579             signal.signal(signal.SIGTSTP, origsigtstp)
   543     return chunkselector.opts
   580     return chunkselector.opts
   544 
   581 
       
   582 
   545 def testdecorator(testfn, f):
   583 def testdecorator(testfn, f):
   546     def u(*args, **kwargs):
   584     def u(*args, **kwargs):
   547         return f(testfn, *args, **kwargs)
   585         return f(testfn, *args, **kwargs)
       
   586 
   548     return u
   587     return u
       
   588 
   549 
   589 
   550 def testchunkselector(testfn, ui, headerlist, operation=None):
   590 def testchunkselector(testfn, ui, headerlist, operation=None):
   551     """
   591     """
   552     test interface to get selection of chunks, and mark the applied flags
   592     test interface to get selection of chunks, and mark the applied flags
   553     of the chosen chunks.
   593     of the chosen chunks.
   555     chunkselector = curseschunkselector(headerlist, ui, operation)
   595     chunkselector = curseschunkselector(headerlist, ui, operation)
   556 
   596 
   557     class dummystdscr(object):
   597     class dummystdscr(object):
   558         def clear(self):
   598         def clear(self):
   559             pass
   599             pass
       
   600 
   560         def refresh(self):
   601         def refresh(self):
   561             pass
   602             pass
   562 
   603 
   563     chunkselector.stdscr = dummystdscr()
   604     chunkselector.stdscr = dummystdscr()
   564     if testfn and os.path.exists(testfn):
   605     if testfn and os.path.exists(testfn):
   568         while True:
   609         while True:
   569             if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
   610             if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
   570                 break
   611                 break
   571     return chunkselector.opts
   612     return chunkselector.opts
   572 
   613 
   573 _headermessages = { # {operation: text}
   614 
       
   615 _headermessages = {  # {operation: text}
   574     'apply': _('Select hunks to apply'),
   616     'apply': _('Select hunks to apply'),
   575     'discard': _('Select hunks to discard'),
   617     'discard': _('Select hunks to discard'),
   576     'keep': _('Select hunks to keep'),
   618     'keep': _('Select hunks to keep'),
   577     None: _('Select hunks to record'),
   619     None: _('Select hunks to record'),
   578 }
   620 }
       
   621 
   579 
   622 
   580 class curseschunkselector(object):
   623 class curseschunkselector(object):
   581     def __init__(self, headerlist, ui, operation=None):
   624     def __init__(self, headerlist, ui, operation=None):
   582         # put the headers into a patch object
   625         # put the headers into a patch object
   583         self.headerlist = patch(headerlist)
   626         self.headerlist = patch(headerlist)
   689         most-indented level.  for example, if a hunk is selected, select
   732         most-indented level.  for example, if a hunk is selected, select
   690         the first hunkline of the selected hunk.  or, if the last hunkline of
   733         the first hunkline of the selected hunk.  or, if the last hunkline of
   691         a hunk is currently selected, then select the next hunk, if one exists,
   734         a hunk is currently selected, then select the next hunk, if one exists,
   692         or if not, the next header if one exists.
   735         or if not, the next header if one exists.
   693         """
   736         """
   694         #self.startprintline += 1 #debug
   737         # self.startprintline += 1 #debug
   695         currentitem = self.currentselecteditem
   738         currentitem = self.currentselecteditem
   696 
   739 
   697         nextitem = currentitem.nextitem()
   740         nextitem = currentitem.nextitem()
   698         # if there's no next item, keep the selection as-is
   741         # if there's no next item, keep the selection as-is
   699         if nextitem is None:
   742         if nextitem is None:
   870             siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
   913             siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
   871             allsiblingsapplied = not (False in siblingappliedstatus)
   914             allsiblingsapplied = not (False in siblingappliedstatus)
   872             nosiblingsapplied = not (True in siblingappliedstatus)
   915             nosiblingsapplied = not (True in siblingappliedstatus)
   873 
   916 
   874             siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
   917             siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
   875             somesiblingspartial = (True in siblingspartialstatus)
   918             somesiblingspartial = True in siblingspartialstatus
   876 
   919 
   877             #cases where applied or partial should be removed from header
   920             # cases where applied or partial should be removed from header
   878 
   921 
   879             # if no 'sibling' hunks are applied (including this hunk)
   922             # if no 'sibling' hunks are applied (including this hunk)
   880             if nosiblingsapplied:
   923             if nosiblingsapplied:
   881                 if not item.header.special():
   924                 if not item.header.special():
   882                     item.header.applied = False
   925                     item.header.applied = False
   883                     item.header.partial = False
   926                     item.header.partial = False
   884             else: # some/all parent siblings are applied
   927             else:  # some/all parent siblings are applied
   885                 item.header.applied = True
   928                 item.header.applied = True
   886                 item.header.partial = (somesiblingspartial or
   929                 item.header.partial = (
   887                                         not allsiblingsapplied)
   930                     somesiblingspartial or not allsiblingsapplied
       
   931                 )
   888 
   932 
   889         elif isinstance(item, uihunkline):
   933         elif isinstance(item, uihunkline):
   890             siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
   934             siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
   891             allsiblingsapplied = not (False in siblingappliedstatus)
   935             allsiblingsapplied = not (False in siblingappliedstatus)
   892             nosiblingsapplied = not (True in siblingappliedstatus)
   936             nosiblingsapplied = not (True in siblingappliedstatus)
   896                 item.hunk.applied = False
   940                 item.hunk.applied = False
   897                 item.hunk.partial = False
   941                 item.hunk.partial = False
   898             elif allsiblingsapplied:
   942             elif allsiblingsapplied:
   899                 item.hunk.applied = True
   943                 item.hunk.applied = True
   900                 item.hunk.partial = False
   944                 item.hunk.partial = False
   901             else: # some siblings applied
   945             else:  # some siblings applied
   902                 item.hunk.applied = True
   946                 item.hunk.applied = True
   903                 item.hunk.partial = True
   947                 item.hunk.partial = True
   904 
   948 
   905             parentsiblingsapplied = [hnk.applied for hnk
   949             parentsiblingsapplied = [
   906                                      in item.hunk.header.hunks]
   950                 hnk.applied for hnk in item.hunk.header.hunks
       
   951             ]
   907             noparentsiblingsapplied = not (True in parentsiblingsapplied)
   952             noparentsiblingsapplied = not (True in parentsiblingsapplied)
   908             allparentsiblingsapplied = not (False in parentsiblingsapplied)
   953             allparentsiblingsapplied = not (False in parentsiblingsapplied)
   909 
   954 
   910             parentsiblingspartial = [hnk.partial for hnk
   955             parentsiblingspartial = [
   911                                      in item.hunk.header.hunks]
   956                 hnk.partial for hnk in item.hunk.header.hunks
   912             someparentsiblingspartial = (True in parentsiblingspartial)
   957             ]
       
   958             someparentsiblingspartial = True in parentsiblingspartial
   913 
   959 
   914             # if all parent hunks are not applied, un-apply header
   960             # if all parent hunks are not applied, un-apply header
   915             if noparentsiblingsapplied:
   961             if noparentsiblingsapplied:
   916                 if not item.hunk.header.special():
   962                 if not item.hunk.header.special():
   917                     item.hunk.header.applied = False
   963                     item.hunk.header.applied = False
   918                     item.hunk.header.partial = False
   964                     item.hunk.header.partial = False
   919             # set the applied and partial status of the header if needed
   965             # set the applied and partial status of the header if needed
   920             else: # some/all parent siblings are applied
   966             else:  # some/all parent siblings are applied
   921                 item.hunk.header.applied = True
   967                 item.hunk.header.applied = True
   922                 item.hunk.header.partial = (someparentsiblingspartial or
   968                 item.hunk.header.partial = (
   923                                             not allparentsiblingsapplied)
   969                     someparentsiblingspartial or not allparentsiblingsapplied
       
   970                 )
   924 
   971 
   925     def toggleall(self):
   972     def toggleall(self):
   926         "toggle the applied flag of all items."
   973         "toggle the applied flag of all items."
   927         if self.waslasttoggleallapplied: # then unapply them this time
   974         if self.waslasttoggleallapplied:  # then unapply them this time
   928             for item in self.headerlist:
   975             for item in self.headerlist:
   929                 if item.applied:
   976                 if item.applied:
   930                     self.toggleapply(item)
   977                     self.toggleapply(item)
   931         else:
   978         else:
   932             for item in self.headerlist:
   979             for item in self.headerlist:
   934                     self.toggleapply(item)
   981                     self.toggleapply(item)
   935         self.waslasttoggleallapplied = not self.waslasttoggleallapplied
   982         self.waslasttoggleallapplied = not self.waslasttoggleallapplied
   936 
   983 
   937     def toggleallbetween(self):
   984     def toggleallbetween(self):
   938         "toggle applied on or off for all items in range [lastapplied,current]."
   985         "toggle applied on or off for all items in range [lastapplied,current]."
   939         if (not self.lastapplieditem or
   986         if (
   940             self.currentselecteditem == self.lastapplieditem):
   987             not self.lastapplieditem
       
   988             or self.currentselecteditem == self.lastapplieditem
       
   989         ):
   941             # Treat this like a normal 'x'/' '
   990             # Treat this like a normal 'x'/' '
   942             self.toggleapply()
   991             self.toggleapply()
   943             return
   992             return
   944 
   993 
   945         startitem = self.lastapplieditem
   994         startitem = self.lastapplieditem
   983                 self.currentselecteditem = item = item.parentitem()
  1032                 self.currentselecteditem = item = item.parentitem()
   984             elif item.neverunfolded:
  1033             elif item.neverunfolded:
   985                 item.neverunfolded = False
  1034                 item.neverunfolded = False
   986 
  1035 
   987             # also fold any foldable children of the parent/current item
  1036             # also fold any foldable children of the parent/current item
   988             if isinstance(item, uiheader): # the original or 'new' item
  1037             if isinstance(item, uiheader):  # the original or 'new' item
   989                 for child in item.allchildren():
  1038                 for child in item.allchildren():
   990                     child.folded = not item.folded
  1039                     child.folded = not item.folded
   991 
  1040 
   992         if isinstance(item, (uiheader, uihunk)):
  1041         if isinstance(item, (uiheader, uihunk)):
   993             item.folded = not item.folded
  1042             item.folded = not item.folded
  1002         y, xstart = window.getyx()
  1051         y, xstart = window.getyx()
  1003         width = self.xscreensize
  1052         width = self.xscreensize
  1004         # turn tabs into spaces
  1053         # turn tabs into spaces
  1005         instr = instr.expandtabs(4)
  1054         instr = instr.expandtabs(4)
  1006         strwidth = encoding.colwidth(instr)
  1055         strwidth = encoding.colwidth(instr)
  1007         numspaces = (width - ((strwidth + xstart) % width))
  1056         numspaces = width - ((strwidth + xstart) % width)
  1008         return instr + " " * numspaces
  1057         return instr + " " * numspaces
  1009 
  1058 
  1010     def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None,
  1059     def printstring(
  1011         pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False):
  1060         self,
       
  1061         window,
       
  1062         text,
       
  1063         fgcolor=None,
       
  1064         bgcolor=None,
       
  1065         pair=None,
       
  1066         pairname=None,
       
  1067         attrlist=None,
       
  1068         towin=True,
       
  1069         align=True,
       
  1070         showwhtspc=False,
       
  1071     ):
  1012         """
  1072         """
  1013         print the string, text, with the specified colors and attributes, to
  1073         print the string, text, with the specified colors and attributes, to
  1014         the specified curses window object.
  1074         the specified curses window object.
  1015 
  1075 
  1016         the foreground and background colors are of the form
  1076         the foreground and background colors are of the form
  1028         if showwhtspc == True, trailing whitespace of a string is highlighted.
  1088         if showwhtspc == True, trailing whitespace of a string is highlighted.
  1029         """
  1089         """
  1030         # preprocess the text, converting tabs to spaces
  1090         # preprocess the text, converting tabs to spaces
  1031         text = text.expandtabs(4)
  1091         text = text.expandtabs(4)
  1032         # strip \n, and convert control characters to ^[char] representation
  1092         # strip \n, and convert control characters to ^[char] representation
  1033         text = re.sub(br'[\x00-\x08\x0a-\x1f]',
  1093         text = re.sub(
  1034                 lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n'))
  1094             br'[\x00-\x08\x0a-\x1f]',
       
  1095             lambda m: '^' + chr(ord(m.group()) + 64),
       
  1096             text.strip('\n'),
       
  1097         )
  1035 
  1098 
  1036         if pair is not None:
  1099         if pair is not None:
  1037             colorpair = pair
  1100             colorpair = pair
  1038         elif pairname is not None:
  1101         elif pairname is not None:
  1039             colorpair = self.colorpairnames[pairname]
  1102             colorpair = self.colorpairnames[pairname]
  1058             for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
  1121             for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
  1059                 if textattr in attrlist:
  1122                 if textattr in attrlist:
  1060                     colorpair |= textattr
  1123                     colorpair |= textattr
  1061 
  1124 
  1062         y, xstart = self.chunkpad.getyx()
  1125         y, xstart = self.chunkpad.getyx()
  1063         t = "" # variable for counting lines printed
  1126         t = ""  # variable for counting lines printed
  1064         # if requested, show trailing whitespace
  1127         # if requested, show trailing whitespace
  1065         if showwhtspc:
  1128         if showwhtspc:
  1066             origlen = len(text)
  1129             origlen = len(text)
  1067             text = text.rstrip(' \n') # tabs have already been expanded
  1130             text = text.rstrip(' \n')  # tabs have already been expanded
  1068             strippedlen = len(text)
  1131             strippedlen = len(text)
  1069             numtrailingspaces = origlen - strippedlen
  1132             numtrailingspaces = origlen - strippedlen
  1070 
  1133 
  1071         if towin:
  1134         if towin:
  1072             window.addstr(text, colorpair)
  1135             window.addstr(text, colorpair)
  1073         t += text
  1136         t += text
  1074 
  1137 
  1075         if showwhtspc:
  1138         if showwhtspc:
  1076                 wscolorpair = colorpair | curses.A_REVERSE
  1139             wscolorpair = colorpair | curses.A_REVERSE
  1077                 if towin:
  1140             if towin:
  1078                     for i in range(numtrailingspaces):
  1141                 for i in range(numtrailingspaces):
  1079                         window.addch(curses.ACS_CKBOARD, wscolorpair)
  1142                     window.addch(curses.ACS_CKBOARD, wscolorpair)
  1080                 t += " " * numtrailingspaces
  1143             t += " " * numtrailingspaces
  1081 
  1144 
  1082         if align:
  1145         if align:
  1083             if towin:
  1146             if towin:
  1084                 extrawhitespace = self.alignstring("", window)
  1147                 extrawhitespace = self.alignstring("", window)
  1085                 window.addstr(extrawhitespace, colorpair)
  1148                 window.addstr(extrawhitespace, colorpair)
  1100         spaceselect = _('space/enter: select')
  1163         spaceselect = _('space/enter: select')
  1101         spacedeselect = _('space/enter: deselect')
  1164         spacedeselect = _('space/enter: deselect')
  1102         # Format the selected label into a place as long as the longer of the
  1165         # Format the selected label into a place as long as the longer of the
  1103         # two possible labels.  This may vary by language.
  1166         # two possible labels.  This may vary by language.
  1104         spacelen = max(len(spaceselect), len(spacedeselect))
  1167         spacelen = max(len(spaceselect), len(spacedeselect))
  1105         selectedlabel = '%-*s' % (spacelen,
  1168         selectedlabel = '%-*s' % (
  1106                                   spacedeselect if selected else spaceselect)
  1169             spacelen,
       
  1170             spacedeselect if selected else spaceselect,
       
  1171         )
  1107         segments = [
  1172         segments = [
  1108             _headermessages[self.operation],
  1173             _headermessages[self.operation],
  1109             '-',
  1174             '-',
  1110             _('[x]=selected **=collapsed'),
  1175             _('[x]=selected **=collapsed'),
  1111             _('c: confirm'),
  1176             _('c: confirm'),
  1158 
  1223 
  1159         # print out the patch in the remaining part of the window
  1224         # print out the patch in the remaining part of the window
  1160         try:
  1225         try:
  1161             self.printitem()
  1226             self.printitem()
  1162             self.updatescroll()
  1227             self.updatescroll()
  1163             self.chunkpad.refresh(self.firstlineofpadtoprint, 0,
  1228             self.chunkpad.refresh(
  1164                                   self.numstatuslines, 0,
  1229                 self.firstlineofpadtoprint,
  1165                                   self.yscreensize - self.numstatuslines,
  1230                 0,
  1166                                   self.xscreensize)
  1231                 self.numstatuslines,
       
  1232                 0,
       
  1233                 self.yscreensize - self.numstatuslines,
       
  1234                 self.xscreensize,
       
  1235             )
  1167         except curses.error:
  1236         except curses.error:
  1168             pass
  1237             pass
  1169 
  1238 
  1170     def getstatusprefixstring(self, item):
  1239     def getstatusprefixstring(self, item):
  1171         """
  1240         """
  1193             else:
  1262             else:
  1194                 checkbox += "  "
  1263                 checkbox += "  "
  1195                 if isinstance(item, uiheader):
  1264                 if isinstance(item, uiheader):
  1196                     # add two more spaces for headers
  1265                     # add two more spaces for headers
  1197                     checkbox += "  "
  1266                     checkbox += "  "
  1198         except AttributeError: # not foldable
  1267         except AttributeError:  # not foldable
  1199             checkbox += "  "
  1268             checkbox += "  "
  1200 
  1269 
  1201         return checkbox
  1270         return checkbox
  1202 
  1271 
  1203     def printheader(self, header, selected=False, towin=True,
  1272     def printheader(
  1204                     ignorefolding=False):
  1273         self, header, selected=False, towin=True, ignorefolding=False
       
  1274     ):
  1205         """
  1275         """
  1206         print the header to the pad.  if countlines is True, don't print
  1276         print the header to the pad.  if countlines is True, don't print
  1207         anything, but just count the number of lines which would be printed.
  1277         anything, but just count the number of lines which would be printed.
  1208         """
  1278         """
  1209 
  1279 
  1211         text = header.prettystr()
  1281         text = header.prettystr()
  1212         chunkindex = self.chunklist.index(header)
  1282         chunkindex = self.chunklist.index(header)
  1213 
  1283 
  1214         if chunkindex != 0 and not header.folded:
  1284         if chunkindex != 0 and not header.folded:
  1215             # add separating line before headers
  1285             # add separating line before headers
  1216             outstr += self.printstring(self.chunkpad, '_' * self.xscreensize,
  1286             outstr += self.printstring(
  1217                                        towin=towin, align=False)
  1287                 self.chunkpad, '_' * self.xscreensize, towin=towin, align=False
       
  1288             )
  1218         # select color-pair based on if the header is selected
  1289         # select color-pair based on if the header is selected
  1219         colorpair = self.getcolorpair(name=selected and "selected" or "normal",
  1290         colorpair = self.getcolorpair(
  1220                                       attrlist=[curses.A_BOLD])
  1291             name=selected and "selected" or "normal", attrlist=[curses.A_BOLD]
       
  1292         )
  1221 
  1293 
  1222         # print out each line of the chunk, expanding it to screen width
  1294         # print out each line of the chunk, expanding it to screen width
  1223 
  1295 
  1224         # number of characters to indent lines on this level by
  1296         # number of characters to indent lines on this level by
  1225         indentnumchars = 0
  1297         indentnumchars = 0
  1227         if not header.folded or ignorefolding:
  1299         if not header.folded or ignorefolding:
  1228             textlist = text.split("\n")
  1300             textlist = text.split("\n")
  1229             linestr = checkbox + textlist[0]
  1301             linestr = checkbox + textlist[0]
  1230         else:
  1302         else:
  1231             linestr = checkbox + header.filename()
  1303             linestr = checkbox + header.filename()
  1232         outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
  1304         outstr += self.printstring(
  1233                                    towin=towin)
  1305             self.chunkpad, linestr, pair=colorpair, towin=towin
       
  1306         )
  1234         if not header.folded or ignorefolding:
  1307         if not header.folded or ignorefolding:
  1235             if len(textlist) > 1:
  1308             if len(textlist) > 1:
  1236                 for line in textlist[1:]:
  1309                 for line in textlist[1:]:
  1237                     linestr = " "*(indentnumchars + len(checkbox)) + line
  1310                     linestr = " " * (indentnumchars + len(checkbox)) + line
  1238                     outstr += self.printstring(self.chunkpad, linestr,
  1311                     outstr += self.printstring(
  1239                                                pair=colorpair, towin=towin)
  1312                         self.chunkpad, linestr, pair=colorpair, towin=towin
       
  1313                     )
  1240 
  1314 
  1241         return outstr
  1315         return outstr
  1242 
  1316 
  1243     def printhunklinesbefore(self, hunk, selected=False, towin=True,
  1317     def printhunklinesbefore(
  1244                              ignorefolding=False):
  1318         self, hunk, selected=False, towin=True, ignorefolding=False
       
  1319     ):
  1245         "includes start/end line indicator"
  1320         "includes start/end line indicator"
  1246         outstr = ""
  1321         outstr = ""
  1247         # where hunk is in list of siblings
  1322         # where hunk is in list of siblings
  1248         hunkindex = hunk.header.hunks.index(hunk)
  1323         hunkindex = hunk.header.hunks.index(hunk)
  1249 
  1324 
  1250         if hunkindex != 0:
  1325         if hunkindex != 0:
  1251             # add separating line before headers
  1326             # add separating line before headers
  1252             outstr += self.printstring(self.chunkpad, ' '*self.xscreensize,
  1327             outstr += self.printstring(
  1253                                        towin=towin, align=False)
  1328                 self.chunkpad, ' ' * self.xscreensize, towin=towin, align=False
  1254 
  1329             )
  1255         colorpair = self.getcolorpair(name=selected and "selected" or "normal",
  1330 
  1256                                       attrlist=[curses.A_BOLD])
  1331         colorpair = self.getcolorpair(
       
  1332             name=selected and "selected" or "normal", attrlist=[curses.A_BOLD]
       
  1333         )
  1257 
  1334 
  1258         # print out from-to line with checkbox
  1335         # print out from-to line with checkbox
  1259         checkbox = self.getstatusprefixstring(hunk)
  1336         checkbox = self.getstatusprefixstring(hunk)
  1260 
  1337 
  1261         lineprefix = " "*self.hunkindentnumchars + checkbox
  1338         lineprefix = " " * self.hunkindentnumchars + checkbox
  1262         frtoline = "   " + hunk.getfromtoline().strip("\n")
  1339         frtoline = "   " + hunk.getfromtoline().strip("\n")
  1263 
  1340 
  1264         outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
  1341         outstr += self.printstring(
  1265                                    align=False) # add uncolored checkbox/indent
  1342             self.chunkpad, lineprefix, towin=towin, align=False
  1266         outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair,
  1343         )  # add uncolored checkbox/indent
  1267                                    towin=towin)
  1344         outstr += self.printstring(
       
  1345             self.chunkpad, frtoline, pair=colorpair, towin=towin
       
  1346         )
  1268 
  1347 
  1269         if hunk.folded and not ignorefolding:
  1348         if hunk.folded and not ignorefolding:
  1270             # skip remainder of output
  1349             # skip remainder of output
  1271             return outstr
  1350             return outstr
  1272 
  1351 
  1273         # print out lines of the chunk preceeding changed-lines
  1352         # print out lines of the chunk preceeding changed-lines
  1274         for line in hunk.before:
  1353         for line in hunk.before:
  1275             linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
  1354             linestr = " " * (self.hunklineindentnumchars + len(checkbox)) + line
  1276             outstr += self.printstring(self.chunkpad, linestr, towin=towin)
  1355             outstr += self.printstring(self.chunkpad, linestr, towin=towin)
  1277 
  1356 
  1278         return outstr
  1357         return outstr
  1279 
  1358 
  1280     def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
  1359     def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
  1283             return outstr
  1362             return outstr
  1284 
  1363 
  1285         # a bit superfluous, but to avoid hard-coding indent amount
  1364         # a bit superfluous, but to avoid hard-coding indent amount
  1286         checkbox = self.getstatusprefixstring(hunk)
  1365         checkbox = self.getstatusprefixstring(hunk)
  1287         for line in hunk.after:
  1366         for line in hunk.after:
  1288             linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
  1367             linestr = " " * (self.hunklineindentnumchars + len(checkbox)) + line
  1289             outstr += self.printstring(self.chunkpad, linestr, towin=towin)
  1368             outstr += self.printstring(self.chunkpad, linestr, towin=towin)
  1290 
  1369 
  1291         return outstr
  1370         return outstr
  1292 
  1371 
  1293     def printhunkchangedline(self, hunkline, selected=False, towin=True):
  1372     def printhunkchangedline(self, hunkline, selected=False, towin=True):
  1304         elif linestr.startswith("-"):
  1383         elif linestr.startswith("-"):
  1305             colorpair = self.getcolorpair(name="deletion")
  1384             colorpair = self.getcolorpair(name="deletion")
  1306         elif linestr.startswith("\\"):
  1385         elif linestr.startswith("\\"):
  1307             colorpair = self.getcolorpair(name="normal")
  1386             colorpair = self.getcolorpair(name="normal")
  1308 
  1387 
  1309         lineprefix = " "*self.hunklineindentnumchars + checkbox
  1388         lineprefix = " " * self.hunklineindentnumchars + checkbox
  1310         outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
  1389         outstr += self.printstring(
  1311                                    align=False) # add uncolored checkbox/indent
  1390             self.chunkpad, lineprefix, towin=towin, align=False
  1312         outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
  1391         )  # add uncolored checkbox/indent
  1313                                    towin=towin, showwhtspc=True)
  1392         outstr += self.printstring(
       
  1393             self.chunkpad, linestr, pair=colorpair, towin=towin, showwhtspc=True
       
  1394         )
  1314         return outstr
  1395         return outstr
  1315 
  1396 
  1316     def printitem(self, item=None, ignorefolding=False, recursechildren=True,
  1397     def printitem(
  1317                   towin=True):
  1398         self, item=None, ignorefolding=False, recursechildren=True, towin=True
       
  1399     ):
  1318         """
  1400         """
  1319         use __printitem() to print the the specified item.applied.
  1401         use __printitem() to print the the specified item.applied.
  1320         if item is not specified, then print the entire patch.
  1402         if item is not specified, then print the entire patch.
  1321         (hiding folded elements, etc. -- see __printitem() docstring)
  1403         (hiding folded elements, etc. -- see __printitem() docstring)
  1322         """
  1404         """
  1325             item = self.headerlist
  1407             item = self.headerlist
  1326         if recursechildren:
  1408         if recursechildren:
  1327             self.linesprintedtopadsofar = 0
  1409             self.linesprintedtopadsofar = 0
  1328 
  1410 
  1329         outstr = []
  1411         outstr = []
  1330         self.__printitem(item, ignorefolding, recursechildren, outstr,
  1412         self.__printitem(
  1331                                   towin=towin)
  1413             item, ignorefolding, recursechildren, outstr, towin=towin
       
  1414         )
  1332         return ''.join(outstr)
  1415         return ''.join(outstr)
  1333 
  1416 
  1334     def outofdisplayedarea(self):
  1417     def outofdisplayedarea(self):
  1335         y, _ = self.chunkpad.getyx() # cursor location
  1418         y, _ = self.chunkpad.getyx()  # cursor location
  1336         # * 2 here works but an optimization would be the max number of
  1419         # * 2 here works but an optimization would be the max number of
  1337         # consecutive non selectable lines
  1420         # consecutive non selectable lines
  1338         # i.e the max number of context line for any hunk in the patch
  1421         # i.e the max number of context line for any hunk in the patch
  1339         miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
  1422         miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
  1340         maxy = self.firstlineofpadtoprint + self.yscreensize * 2
  1423         maxy = self.firstlineofpadtoprint + self.yscreensize * 2
  1341         return y < miny or y > maxy
  1424         return y < miny or y > maxy
  1342 
  1425 
  1343     def handleselection(self, item, recursechildren):
  1426     def handleselection(self, item, recursechildren):
  1344         selected = (item is self.currentselecteditem)
  1427         selected = item is self.currentselecteditem
  1345         if selected and recursechildren:
  1428         if selected and recursechildren:
  1346             # assumes line numbering starting from line 0
  1429             # assumes line numbering starting from line 0
  1347             self.selecteditemstartline = self.linesprintedtopadsofar
  1430             self.selecteditemstartline = self.linesprintedtopadsofar
  1348             selecteditemlines = self.getnumlinesdisplayed(item,
  1431             selecteditemlines = self.getnumlinesdisplayed(
  1349                                                           recursechildren=False)
  1432                 item, recursechildren=False
  1350             self.selecteditemendline = (self.selecteditemstartline +
  1433             )
  1351                                         selecteditemlines - 1)
  1434             self.selecteditemendline = (
       
  1435                 self.selecteditemstartline + selecteditemlines - 1
       
  1436             )
  1352         return selected
  1437         return selected
  1353 
  1438 
  1354     def __printitem(self, item, ignorefolding, recursechildren, outstr,
  1439     def __printitem(
  1355                     towin=True):
  1440         self, item, ignorefolding, recursechildren, outstr, towin=True
       
  1441     ):
  1356         """
  1442         """
  1357         recursive method for printing out patch/header/hunk/hunk-line data to
  1443         recursive method for printing out patch/header/hunk/hunk-line data to
  1358         screen.  also returns a string with all of the content of the displayed
  1444         screen.  also returns a string with all of the content of the displayed
  1359         patch (not including coloring, etc.).
  1445         patch (not including coloring, etc.).
  1360 
  1446 
  1371 
  1457 
  1372         # patch object is a list of headers
  1458         # patch object is a list of headers
  1373         if isinstance(item, patch):
  1459         if isinstance(item, patch):
  1374             if recursechildren:
  1460             if recursechildren:
  1375                 for hdr in item:
  1461                 for hdr in item:
  1376                     self.__printitem(hdr, ignorefolding,
  1462                     self.__printitem(
  1377                             recursechildren, outstr, towin)
  1463                         hdr, ignorefolding, recursechildren, outstr, towin
       
  1464                     )
  1378         # todo: eliminate all isinstance() calls
  1465         # todo: eliminate all isinstance() calls
  1379         if isinstance(item, uiheader):
  1466         if isinstance(item, uiheader):
  1380             outstr.append(self.printheader(item, selected, towin=towin,
  1467             outstr.append(
  1381                                        ignorefolding=ignorefolding))
  1468                 self.printheader(
       
  1469                     item, selected, towin=towin, ignorefolding=ignorefolding
       
  1470                 )
       
  1471             )
  1382             if recursechildren:
  1472             if recursechildren:
  1383                 for hnk in item.hunks:
  1473                 for hnk in item.hunks:
  1384                     self.__printitem(hnk, ignorefolding,
  1474                     self.__printitem(
  1385                             recursechildren, outstr, towin)
  1475                         hnk, ignorefolding, recursechildren, outstr, towin
  1386         elif (isinstance(item, uihunk) and
  1476                     )
  1387               ((not item.header.folded) or ignorefolding)):
  1477         elif isinstance(item, uihunk) and (
       
  1478             (not item.header.folded) or ignorefolding
       
  1479         ):
  1388             # print the hunk data which comes before the changed-lines
  1480             # print the hunk data which comes before the changed-lines
  1389             outstr.append(self.printhunklinesbefore(item, selected, towin=towin,
  1481             outstr.append(
  1390                                                 ignorefolding=ignorefolding))
  1482                 self.printhunklinesbefore(
       
  1483                     item, selected, towin=towin, ignorefolding=ignorefolding
       
  1484                 )
       
  1485             )
  1391             if recursechildren:
  1486             if recursechildren:
  1392                 for l in item.changedlines:
  1487                 for l in item.changedlines:
  1393                     self.__printitem(l, ignorefolding,
  1488                     self.__printitem(
  1394                             recursechildren, outstr, towin)
  1489                         l, ignorefolding, recursechildren, outstr, towin
  1395                 outstr.append(self.printhunklinesafter(item, towin=towin,
  1490                     )
  1396                                                 ignorefolding=ignorefolding))
  1491                 outstr.append(
  1397         elif (isinstance(item, uihunkline) and
  1492                     self.printhunklinesafter(
  1398               ((not item.hunk.folded) or ignorefolding)):
  1493                         item, towin=towin, ignorefolding=ignorefolding
  1399             outstr.append(self.printhunkchangedline(item, selected,
  1494                     )
  1400                 towin=towin))
  1495                 )
       
  1496         elif isinstance(item, uihunkline) and (
       
  1497             (not item.hunk.folded) or ignorefolding
       
  1498         ):
       
  1499             outstr.append(
       
  1500                 self.printhunkchangedline(item, selected, towin=towin)
       
  1501             )
  1401 
  1502 
  1402         return outstr
  1503         return outstr
  1403 
  1504 
  1404     def getnumlinesdisplayed(self, item=None, ignorefolding=False,
  1505     def getnumlinesdisplayed(
  1405                              recursechildren=True):
  1506         self, item=None, ignorefolding=False, recursechildren=True
       
  1507     ):
  1406         """
  1508         """
  1407         return the number of lines which would be displayed if the item were
  1509         return the number of lines which would be displayed if the item were
  1408         to be printed to the display.  the item will not be printed to the
  1510         to be printed to the display.  the item will not be printed to the
  1409         display (pad).
  1511         display (pad).
  1410         if no item is given, assume the entire patch.
  1512         if no item is given, assume the entire patch.
  1411         if ignorefolding is True, folded items will be unfolded when counting
  1513         if ignorefolding is True, folded items will be unfolded when counting
  1412         the number of lines.
  1514         the number of lines.
  1413         """
  1515         """
  1414 
  1516 
  1415         # temporarily disable printing to windows by printstring
  1517         # temporarily disable printing to windows by printstring
  1416         patchdisplaystring = self.printitem(item, ignorefolding,
  1518         patchdisplaystring = self.printitem(
  1417                                             recursechildren, towin=False)
  1519             item, ignorefolding, recursechildren, towin=False
       
  1520         )
  1418         numlines = len(patchdisplaystring) // self.xscreensize
  1521         numlines = len(patchdisplaystring) // self.xscreensize
  1419         return numlines
  1522         return numlines
  1420 
  1523 
  1421     def sigwinchhandler(self, n, frame):
  1524     def sigwinchhandler(self, n, frame):
  1422         "handle window resizing"
  1525         "handle window resizing"
  1427             self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
  1530             self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
  1428             self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
  1531             self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
  1429         except curses.error:
  1532         except curses.error:
  1430             pass
  1533             pass
  1431 
  1534 
  1432     def getcolorpair(self, fgcolor=None, bgcolor=None, name=None,
  1535     def getcolorpair(
  1433                      attrlist=None):
  1536         self, fgcolor=None, bgcolor=None, name=None, attrlist=None
       
  1537     ):
  1434         """
  1538         """
  1435         get a curses color pair, adding it to self.colorpairs if it is not
  1539         get a curses color pair, adding it to self.colorpairs if it is not
  1436         already defined.  an optional string, name, can be passed as a shortcut
  1540         already defined.  an optional string, name, can be passed as a shortcut
  1437         for referring to the color-pair.  by default, if no arguments are
  1541         for referring to the color-pair.  by default, if no arguments are
  1438         specified, the white foreground / black background color-pair is
  1542         specified, the white foreground / black background color-pair is
  1458                 colorpair = self.colorpairs[(fgcolor, bgcolor)]
  1562                 colorpair = self.colorpairs[(fgcolor, bgcolor)]
  1459             else:
  1563             else:
  1460                 pairindex = len(self.colorpairs) + 1
  1564                 pairindex = len(self.colorpairs) + 1
  1461                 if self.usecolor:
  1565                 if self.usecolor:
  1462                     curses.init_pair(pairindex, fgcolor, bgcolor)
  1566                     curses.init_pair(pairindex, fgcolor, bgcolor)
  1463                     colorpair = self.colorpairs[(fgcolor, bgcolor)] = (
  1567                     colorpair = self.colorpairs[
  1464                         curses.color_pair(pairindex))
  1568                         (fgcolor, bgcolor)
       
  1569                     ] = curses.color_pair(pairindex)
  1465                     if name is not None:
  1570                     if name is not None:
  1466                         self.colorpairnames[name] = curses.color_pair(pairindex)
  1571                         self.colorpairnames[name] = curses.color_pair(pairindex)
  1467                 else:
  1572                 else:
  1468                     cval = 0
  1573                     cval = 0
  1469                     if name is not None:
  1574                     if name is not None:
  1519                       e : edit the currently selected hunk
  1624                       e : edit the currently selected hunk
  1520                       a : toggle amend mode, only with commit -i
  1625                       a : toggle amend mode, only with commit -i
  1521                       c : confirm selected changes
  1626                       c : confirm selected changes
  1522                       r : review/edit and confirm selected changes
  1627                       r : review/edit and confirm selected changes
  1523                       q : quit without confirming (no changes will be made)
  1628                       q : quit without confirming (no changes will be made)
  1524                       ? : help (what you're currently reading)""")
  1629                       ? : help (what you're currently reading)"""
       
  1630         )
  1525 
  1631 
  1526         helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
  1632         helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
  1527         helplines = helptext.split("\n")
  1633         helplines = helptext.split("\n")
  1528         helplines = helplines + [" "]*(
  1634         helplines = helplines + [" "] * (
  1529             self.yscreensize - self.numstatuslines - len(helplines) - 1)
  1635             self.yscreensize - self.numstatuslines - len(helplines) - 1
       
  1636         )
  1530         try:
  1637         try:
  1531             for line in helplines:
  1638             for line in helplines:
  1532                 self.printstring(helpwin, line, pairname="legend")
  1639                 self.printstring(helpwin, line, pairname="legend")
  1533         except curses.error:
  1640         except curses.error:
  1534             pass
  1641             pass
  1546         curses.def_prog_mode()
  1653         curses.def_prog_mode()
  1547         curses.endwin()
  1654         curses.endwin()
  1548         self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
  1655         self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
  1549         curses.cbreak()
  1656         curses.cbreak()
  1550         self.stdscr.refresh()
  1657         self.stdscr.refresh()
  1551         self.stdscr.keypad(1) # allow arrow-keys to continue to function
  1658         self.stdscr.keypad(1)  # allow arrow-keys to continue to function
  1552 
  1659 
  1553     def handlefirstlineevent(self):
  1660     def handlefirstlineevent(self):
  1554         """
  1661         """
  1555         Handle 'g' to navigate to the top most file in the ncurses window.
  1662         Handle 'g' to navigate to the top most file in the ncurses window.
  1556         """
  1663         """
  1611 
  1718 
  1612     def reviewcommit(self):
  1719     def reviewcommit(self):
  1613         """ask for 'y' to be pressed to confirm selected. return True if
  1720         """ask for 'y' to be pressed to confirm selected. return True if
  1614         confirmed."""
  1721         confirmed."""
  1615         confirmtext = _(
  1722         confirmtext = _(
  1616 """If you answer yes to the following, your currently chosen patch chunks
  1723             """If you answer yes to the following, your currently chosen patch chunks
  1617 will be loaded into an editor. To modify the patch, make the changes in your
  1724 will be loaded into an editor. To modify the patch, make the changes in your
  1618 editor and save. To accept the current patch as-is, close the editor without
  1725 editor and save. To accept the current patch as-is, close the editor without
  1619 saving.
  1726 saving.
  1620 
  1727 
  1621 note: don't add/remove lines unless you also modify the range information.
  1728 note: don't add/remove lines unless you also modify the range information.
  1622       failing to follow this rule will result in the commit aborting.
  1729       failing to follow this rule will result in the commit aborting.
  1623 
  1730 
  1624 are you sure you want to review/edit and confirm the selected changes [yn]?
  1731 are you sure you want to review/edit and confirm the selected changes [yn]?
  1625 """)
  1732 """
       
  1733         )
  1626         with self.ui.timeblockedsection('crecord'):
  1734         with self.ui.timeblockedsection('crecord'):
  1627             response = self.confirmationwindow(confirmtext)
  1735             response = self.confirmationwindow(confirmtext)
  1628         if response is None:
  1736         if response is None:
  1629             response = "n"
  1737             response = "n"
  1630         if response.lower().startswith("y"):
  1738         if response.lower().startswith("y"):
  1640         new changeset will be created (the normal commit behavior).
  1748         new changeset will be created (the normal commit behavior).
  1641         """
  1749         """
  1642 
  1750 
  1643         if opts.get('amend') is None:
  1751         if opts.get('amend') is None:
  1644             opts['amend'] = True
  1752             opts['amend'] = True
  1645             msg = _("Amend option is turned on -- committing the currently "
  1753             msg = _(
  1646                     "selected changes will not create a new changeset, but "
  1754                 "Amend option is turned on -- committing the currently "
  1647                     "instead update the most recently committed changeset.\n\n"
  1755                 "selected changes will not create a new changeset, but "
  1648                     "Press any key to continue.")
  1756                 "instead update the most recently committed changeset.\n\n"
       
  1757                 "Press any key to continue."
       
  1758             )
  1649         elif opts.get('amend') is True:
  1759         elif opts.get('amend') is True:
  1650             opts['amend'] = None
  1760             opts['amend'] = None
  1651             msg = _("Amend option is turned off -- committing the currently "
  1761             msg = _(
  1652                     "selected changes will create a new changeset.\n\n"
  1762                 "Amend option is turned off -- committing the currently "
  1653                     "Press any key to continue.")
  1763                 "selected changes will create a new changeset.\n\n"
       
  1764                 "Press any key to continue."
       
  1765             )
  1654         if not test:
  1766         if not test:
  1655             self.confirmationwindow(msg)
  1767             self.confirmationwindow(msg)
  1656 
  1768 
  1657     def recenterdisplayedarea(self):
  1769     def recenterdisplayedarea(self):
  1658         """
  1770         """
  1666 
  1778 
  1667     def toggleedit(self, item=None, test=False):
  1779     def toggleedit(self, item=None, test=False):
  1668         """
  1780         """
  1669         edit the currently selected chunk
  1781         edit the currently selected chunk
  1670         """
  1782         """
       
  1783 
  1671         def updateui(self):
  1784         def updateui(self):
  1672             self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
  1785             self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
  1673             self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
  1786             self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
  1674             self.updatescroll()
  1787             self.updatescroll()
  1675             self.stdscr.refresh()
  1788             self.stdscr.refresh()
  1701             finally:
  1814             finally:
  1702                 self.stdscr.clear()
  1815                 self.stdscr.clear()
  1703                 self.stdscr.refresh()
  1816                 self.stdscr.refresh()
  1704 
  1817 
  1705             # remove comment lines
  1818             # remove comment lines
  1706             patch = [line + '\n' for line in patch.splitlines()
  1819             patch = [
  1707                      if not line.startswith('#')]
  1820                 line + '\n'
       
  1821                 for line in patch.splitlines()
       
  1822                 if not line.startswith('#')
       
  1823             ]
  1708             return patchmod.parsepatch(patch)
  1824             return patchmod.parsepatch(patch)
  1709 
  1825 
  1710         if item is None:
  1826         if item is None:
  1711             item = self.currentselecteditem
  1827             item = self.currentselecteditem
  1712         if isinstance(item, uiheader):
  1828         if isinstance(item, uiheader):
  1726                 updateui(self)
  1842                 updateui(self)
  1727             return
  1843             return
  1728         header = item.header
  1844         header = item.header
  1729         editedhunkindex = header.hunks.index(item)
  1845         editedhunkindex = header.hunks.index(item)
  1730         hunksbefore = header.hunks[:editedhunkindex]
  1846         hunksbefore = header.hunks[:editedhunkindex]
  1731         hunksafter = header.hunks[editedhunkindex + 1:]
  1847         hunksafter = header.hunks[editedhunkindex + 1 :]
  1732         newpatchheader = newpatches[0]
  1848         newpatchheader = newpatches[0]
  1733         newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
  1849         newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
  1734         newadded = sum([h.added for h in newhunks])
  1850         newadded = sum([h.added for h in newhunks])
  1735         newremoved = sum([h.removed for h in newhunks])
  1851         newremoved = sum([h.removed for h in newhunks])
  1736         offset = (newadded - beforeadded) - (newremoved - beforeremoved)
  1852         offset = (newadded - beforeadded) - (newremoved - beforeremoved)
  1829         method to be wrapped by curses.wrapper() for selecting chunks.
  1945         method to be wrapped by curses.wrapper() for selecting chunks.
  1830         """
  1946         """
  1831 
  1947 
  1832         origsigwinch = sentinel = object()
  1948         origsigwinch = sentinel = object()
  1833         if util.safehasattr(signal, 'SIGWINCH'):
  1949         if util.safehasattr(signal, 'SIGWINCH'):
  1834             origsigwinch = signal.signal(signal.SIGWINCH,
  1950             origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
  1835                                          self.sigwinchhandler)
       
  1836         try:
  1951         try:
  1837             return self._main(stdscr)
  1952             return self._main(stdscr)
  1838         finally:
  1953         finally:
  1839             if origsigwinch is not sentinel:
  1954             if origsigwinch is not sentinel:
  1840                 signal.signal(signal.SIGWINCH, origsigwinch)
  1955                 signal.signal(signal.SIGWINCH, origsigwinch)
  1865             pass
  1980             pass
  1866 
  1981 
  1867         # available colors: black, blue, cyan, green, magenta, white, yellow
  1982         # available colors: black, blue, cyan, green, magenta, white, yellow
  1868         # init_pair(color_id, foreground_color, background_color)
  1983         # init_pair(color_id, foreground_color, background_color)
  1869         self.initcolorpair(None, None, name="normal")
  1984         self.initcolorpair(None, None, name="normal")
  1870         self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA,
  1985         self.initcolorpair(
  1871                            name="selected")
  1986             curses.COLOR_WHITE, curses.COLOR_MAGENTA, name="selected"
       
  1987         )
  1872         self.initcolorpair(curses.COLOR_RED, None, name="deletion")
  1988         self.initcolorpair(curses.COLOR_RED, None, name="deletion")
  1873         self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
  1989         self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
  1874         self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
  1990         self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
  1875         # newwin([height, width,] begin_y, begin_x)
  1991         # newwin([height, width,] begin_y, begin_x)
  1876         self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
  1992         self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
  1877         self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
  1993         self.statuswin.keypad(1)  # interpret arrow-key, etc. esc sequences
  1878 
  1994 
  1879         # figure out how much space to allocate for the chunk-pad which is
  1995         # figure out how much space to allocate for the chunk-pad which is
  1880         # used for displaying the patch
  1996         # used for displaying the patch
  1881 
  1997 
  1882         # stupid hack to prevent getnumlinesdisplayed from failing
  1998         # stupid hack to prevent getnumlinesdisplayed from failing
  1887 
  2003 
  1888         try:
  2004         try:
  1889             self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
  2005             self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
  1890         except curses.error:
  2006         except curses.error:
  1891             self.initexc = fallbackerror(
  2007             self.initexc = fallbackerror(
  1892                 _('this diff is too large to be displayed'))
  2008                 _('this diff is too large to be displayed')
       
  2009             )
  1893             return
  2010             return
  1894         # initialize selecteditemendline (initial start-line is 0)
  2011         # initialize selecteditemendline (initial start-line is 0)
  1895         self.selecteditemendline = self.getnumlinesdisplayed(
  2012         self.selecteditemendline = self.getnumlinesdisplayed(
  1896             self.currentselecteditem, recursechildren=False)
  2013             self.currentselecteditem, recursechildren=False
       
  2014         )
  1897 
  2015 
  1898         while True:
  2016         while True:
  1899             self.updatescreen()
  2017             self.updatescreen()
  1900             try:
  2018             try:
  1901                 with self.ui.timeblockedsection('crecord'):
  2019                 with self.ui.timeblockedsection('crecord'):
  1907                 keypressed = "foobar"
  2025                 keypressed = "foobar"
  1908             if self.handlekeypressed(keypressed):
  2026             if self.handlekeypressed(keypressed):
  1909                 break
  2027                 break
  1910 
  2028 
  1911         if self.commenttext != "":
  2029         if self.commenttext != "":
  1912             whitespaceremoved = re.sub(br"(?m)^\s.*(\n|$)", b"",
  2030             whitespaceremoved = re.sub(
  1913                                        self.commenttext)
  2031                 br"(?m)^\s.*(\n|$)", b"", self.commenttext
       
  2032             )
  1914             if whitespaceremoved != "":
  2033             if whitespaceremoved != "":
  1915                 self.opts['message'] = self.commenttext
  2034                 self.opts['message'] = self.commenttext