mercurial/crecord.py
changeset 24310 6409fb6c934d
child 24313 ed535f2c15c3
equal deleted inserted replaced
24309:fefcafda10b8 24310:6409fb6c934d
       
     1 # stuff related specifically to patch manipulation / parsing
       
     2 #
       
     3 # Copyright 2008 Mark Edgington <edgimar@gmail.com>
       
     4 #
       
     5 # This software may be used and distributed according to the terms of the
       
     6 # GNU General Public License version 2 or any later version.
       
     7 #
       
     8 # This code is based on the Mark Edgington's crecord extension.
       
     9 # (Itself based on Bryan O'Sullivan's record extension.)
       
    10 
       
    11 from mercurial.i18n import _
       
    12 
       
    13 from mercurial import patch as patchmod
       
    14 from mercurial import util
       
    15 from mercurial import demandimport
       
    16 demandimport.ignore.append('mercurial.encoding')
       
    17 try:
       
    18     import mercurial.encoding as encoding
       
    19     code = encoding.encoding
       
    20 except ImportError:
       
    21     encoding = util
       
    22     code = encoding._encoding
       
    23 
       
    24 import os
       
    25 import re
       
    26 import sys
       
    27 import fcntl
       
    28 import struct
       
    29 import termios
       
    30 import signal
       
    31 import tempfile
       
    32 import locale
       
    33 import cStringIO
       
    34 # This is required for ncurses to display non-ASCII characters in default user
       
    35 # locale encoding correctly.  --immerrr
       
    36 locale.setlocale(locale.LC_ALL, '')
       
    37 # os.name is one of: 'posix', 'nt', 'dos', 'os2', 'mac', or 'ce'
       
    38 if os.name == 'posix':
       
    39     import curses
       
    40 else:
       
    41     # I have no idea if wcurses works with crecord...
       
    42     import wcurses as curses
       
    43 
       
    44 try:
       
    45     curses
       
    46 except NameError:
       
    47     raise util.Abort(
       
    48         _('the python curses/wcurses module is not available/installed'))
       
    49 
       
    50 
       
    51 orig_stdout = sys.__stdout__ # used by gethw()
       
    52 
       
    53 
       
    54 
       
    55 class patchnode(object):
       
    56     """abstract class for patch graph nodes
       
    57     (i.e. patchroot, header, hunk, hunkline)
       
    58     """
       
    59 
       
    60     def firstchild(self):
       
    61         raise NotImplementedError("method must be implemented by subclass")
       
    62 
       
    63     def lastchild(self):
       
    64         raise NotImplementedError("method must be implemented by subclass")
       
    65 
       
    66     def allchildren(self):
       
    67         "Return a list of all of the direct children of this node"
       
    68         raise NotImplementedError("method must be implemented by subclass")
       
    69     def nextsibling(self):
       
    70         """
       
    71         Return the closest next item of the same type where there are no items
       
    72         of different types between the current item and this closest item.
       
    73         If no such item exists, return None.
       
    74 
       
    75         """
       
    76         raise NotImplementedError("method must be implemented by subclass")
       
    77 
       
    78     def prevsibling(self):
       
    79         """
       
    80         Return the closest previous item of the same type where there are no
       
    81         items of different types between the current item and this closest item.
       
    82         If no such item exists, return None.
       
    83 
       
    84         """
       
    85         raise NotImplementedError("method must be implemented by subclass")
       
    86 
       
    87     def parentitem(self):
       
    88         raise NotImplementedError("method must be implemented by subclass")
       
    89 
       
    90 
       
    91     def nextitem(self, constrainlevel=True, skipfolded=True):
       
    92         """
       
    93         If constrainLevel == True, return the closest next item
       
    94         of the same type where there are no items of different types between
       
    95         the current item and this closest item.
       
    96 
       
    97         If constrainLevel == False, then try to return the next item
       
    98         closest to this item, regardless of item's type (header, hunk, or
       
    99         HunkLine).
       
   100 
       
   101         If skipFolded == True, and the current item is folded, then the child
       
   102         items that are hidden due to folding will be skipped when determining
       
   103         the next item.
       
   104 
       
   105         If it is not possible to get the next item, return None.
       
   106 
       
   107         """
       
   108         try:
       
   109             itemfolded = self.folded
       
   110         except AttributeError:
       
   111             itemfolded = False
       
   112         if constrainlevel:
       
   113             return self.nextsibling()
       
   114         elif skipfolded and itemfolded:
       
   115             nextitem = self.nextsibling()
       
   116             if nextitem is None:
       
   117                 try:
       
   118                     nextitem = self.parentitem().nextsibling()
       
   119                 except AttributeError:
       
   120                     nextitem = None
       
   121             return nextitem
       
   122         else:
       
   123             # try child
       
   124             item = self.firstchild()
       
   125             if item is not None:
       
   126                 return item
       
   127 
       
   128             # else try next sibling
       
   129             item = self.nextsibling()
       
   130             if item is not None:
       
   131                 return item
       
   132 
       
   133             try:
       
   134                 # else try parent's next sibling
       
   135                 item = self.parentitem().nextsibling()
       
   136                 if item is not None:
       
   137                     return item
       
   138 
       
   139                 # else return grandparent's next sibling (or None)
       
   140                 return self.parentitem().parentitem().nextsibling()
       
   141 
       
   142             except AttributeError: # parent and/or grandparent was None
       
   143                 return None
       
   144 
       
   145     def previtem(self, constrainlevel=True, skipfolded=True):
       
   146         """
       
   147         If constrainLevel == True, return the closest previous item
       
   148         of the same type where there are no items of different types between
       
   149         the current item and this closest item.
       
   150 
       
   151         If constrainLevel == False, then try to return the previous item
       
   152         closest to this item, regardless of item's type (header, hunk, or
       
   153         HunkLine).
       
   154 
       
   155         If skipFolded == True, and the current item is folded, then the items
       
   156         that are hidden due to folding will be skipped when determining the
       
   157         next item.
       
   158 
       
   159         If it is not possible to get the previous item, return None.
       
   160 
       
   161         """
       
   162         if constrainlevel:
       
   163             return self.prevsibling()
       
   164         else:
       
   165             # try previous sibling's last child's last child,
       
   166             # else try previous sibling's last child, else try previous sibling
       
   167             prevsibling = self.prevsibling()
       
   168             if prevsibling is not None:
       
   169                 prevsiblinglastchild = prevsibling.lastchild()
       
   170                 if ((prevsiblinglastchild is not None) and
       
   171                     not prevsibling.folded):
       
   172                     prevsiblinglclc = prevsiblinglastchild.lastchild()
       
   173                     if ((prevsiblinglclc is not None) and
       
   174                         not prevsiblinglastchild.folded):
       
   175                         return prevsiblinglclc
       
   176                     else:
       
   177                         return prevsiblinglastchild
       
   178                 else:
       
   179                     return prevsibling
       
   180 
       
   181             # try parent (or None)
       
   182             return self.parentitem()
       
   183 
       
   184 class patch(patchnode, list): # todo: rename patchroot
       
   185     """
       
   186     list of header objects representing the patch.
       
   187 
       
   188     """
       
   189     def __init__(self, headerlist):
       
   190         self.extend(headerlist)
       
   191         # add parent patch object reference to each header
       
   192         for header in self:
       
   193             header.patch = self
       
   194 
       
   195 class uiheader(patchnode):
       
   196     """patch header
       
   197 
       
   198     xxx shoudn't we move this to mercurial/patch.py ?
       
   199     """
       
   200 
       
   201     def __init__(self, header):
       
   202         self.nonuiheader = header
       
   203         # flag to indicate whether to apply this chunk
       
   204         self.applied = True
       
   205         # flag which only affects the status display indicating if a node's
       
   206         # children are partially applied (i.e. some applied, some not).
       
   207         self.partial = False
       
   208 
       
   209         # flag to indicate whether to display as folded/unfolded to user
       
   210         self.folded = True
       
   211 
       
   212         # list of all headers in patch
       
   213         self.patch = None
       
   214 
       
   215         # flag is False if this header was ever unfolded from initial state
       
   216         self.neverunfolded = True
       
   217         self.hunks = [uihunk(h, self) for h in self.hunks]
       
   218 
       
   219 
       
   220     def prettystr(self):
       
   221         x = cStringIO.StringIO()
       
   222         self.pretty(x)
       
   223         return x.getvalue()
       
   224 
       
   225     def nextsibling(self):
       
   226         numheadersinpatch = len(self.patch)
       
   227         indexofthisheader = self.patch.index(self)
       
   228 
       
   229         if indexofthisheader < numheadersinpatch - 1:
       
   230             nextheader = self.patch[indexofthisheader + 1]
       
   231             return nextheader
       
   232         else:
       
   233             return None
       
   234 
       
   235     def prevsibling(self):
       
   236         indexofthisheader = self.patch.index(self)
       
   237         if indexofthisheader > 0:
       
   238             previousheader = self.patch[indexofthisheader - 1]
       
   239             return previousheader
       
   240         else:
       
   241             return None
       
   242 
       
   243     def parentitem(self):
       
   244         """
       
   245         there is no 'real' parent item of a header that can be selected,
       
   246         so return None.
       
   247         """
       
   248         return None
       
   249 
       
   250     def firstchild(self):
       
   251         "return the first child of this item, if one exists.  otherwise None."
       
   252         if len(self.hunks) > 0:
       
   253             return self.hunks[0]
       
   254         else:
       
   255             return None
       
   256 
       
   257     def lastchild(self):
       
   258         "return the last child of this item, if one exists.  otherwise None."
       
   259         if len(self.hunks) > 0:
       
   260             return self.hunks[-1]
       
   261         else:
       
   262             return None
       
   263 
       
   264     def allchildren(self):
       
   265         "return a list of all of the direct children of this node"
       
   266         return self.hunks
       
   267 
       
   268     def __getattr__(self, name):
       
   269         return getattr(self.nonuiheader, name)
       
   270 
       
   271 class uihunkline(patchnode):
       
   272     "represents a changed line in a hunk"
       
   273     def __init__(self, linetext, hunk):
       
   274         self.linetext = linetext
       
   275         self.applied = True
       
   276         # the parent hunk to which this line belongs
       
   277         self.hunk = hunk
       
   278         # folding lines currently is not used/needed, but this flag is needed
       
   279         # in the previtem method.
       
   280         self.folded = False
       
   281 
       
   282     def prettystr(self):
       
   283         return self.linetext
       
   284 
       
   285     def nextsibling(self):
       
   286         numlinesinhunk = len(self.hunk.changedlines)
       
   287         indexofthisline = self.hunk.changedlines.index(self)
       
   288 
       
   289         if (indexofthisline < numlinesinhunk - 1):
       
   290             nextline = self.hunk.changedlines[indexofthisline + 1]
       
   291             return nextline
       
   292         else:
       
   293             return None
       
   294 
       
   295     def prevsibling(self):
       
   296         indexofthisline = self.hunk.changedlines.index(self)
       
   297         if indexofthisline > 0:
       
   298             previousline = self.hunk.changedlines[indexofthisline - 1]
       
   299             return previousline
       
   300         else:
       
   301             return None
       
   302 
       
   303     def parentitem(self):
       
   304         "return the parent to the current item"
       
   305         return self.hunk
       
   306 
       
   307     def firstchild(self):
       
   308         "return the first child of this item, if one exists.  otherwise None."
       
   309         # hunk-lines don't have children
       
   310         return None
       
   311 
       
   312     def lastchild(self):
       
   313         "return the last child of this item, if one exists.  otherwise None."
       
   314         # hunk-lines don't have children
       
   315         return None
       
   316 
       
   317 class uihunk(patchnode):
       
   318     """ui patch hunk, wraps a hunk and keep track of ui behavior """
       
   319     maxcontext = 3
       
   320 
       
   321     def __init__(self, hunk, header):
       
   322         self._hunk = hunk
       
   323         self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
       
   324         self.header = header
       
   325         # used at end for detecting how many removed lines were un-applied
       
   326         self.originalremoved = self.removed
       
   327 
       
   328         # flag to indicate whether to display as folded/unfolded to user
       
   329         self.folded = True
       
   330         # flag to indicate whether to apply this chunk
       
   331         self.applied = True
       
   332         # flag which only affects the status display indicating if a node's
       
   333         # children are partially applied (i.e. some applied, some not).
       
   334         self.partial = False
       
   335 
       
   336     def nextsibling(self):
       
   337         numhunksinheader = len(self.header.hunks)
       
   338         indexofthishunk = self.header.hunks.index(self)
       
   339 
       
   340         if (indexofthishunk < numhunksinheader - 1):
       
   341             nexthunk = self.header.hunks[indexofthishunk + 1]
       
   342             return nexthunk
       
   343         else:
       
   344             return None
       
   345 
       
   346     def prevsibling(self):
       
   347         indexofthishunk = self.header.hunks.index(self)
       
   348         if indexofthishunk > 0:
       
   349             previoushunk = self.header.hunks[indexofthishunk - 1]
       
   350             return previoushunk
       
   351         else:
       
   352             return None
       
   353 
       
   354     def parentitem(self):
       
   355         "return the parent to the current item"
       
   356         return self.header
       
   357 
       
   358     def firstchild(self):
       
   359         "return the first child of this item, if one exists.  otherwise None."
       
   360         if len(self.changedlines) > 0:
       
   361             return self.changedlines[0]
       
   362         else:
       
   363             return None
       
   364 
       
   365     def lastchild(self):
       
   366         "return the last child of this item, if one exists.  otherwise None."
       
   367         if len(self.changedlines) > 0:
       
   368             return self.changedlines[-1]
       
   369         else:
       
   370             return None
       
   371 
       
   372     def allchildren(self):
       
   373         "return a list of all of the direct children of this node"
       
   374         return self.changedlines
       
   375     def countchanges(self):
       
   376         """changedlines -> (n+,n-)"""
       
   377         add = len([l for l in self.changedlines if l.applied
       
   378                    and l.prettystr()[0] == '+'])
       
   379         rem = len([l for l in self.changedlines if l.applied
       
   380                    and l.prettystr()[0] == '-'])
       
   381         return add, rem
       
   382 
       
   383     def getfromtoline(self):
       
   384         # calculate the number of removed lines converted to context lines
       
   385         removedconvertedtocontext = self.originalremoved - self.removed
       
   386 
       
   387         contextlen = (len(self.before) + len(self.after) +
       
   388                       removedconvertedtocontext)
       
   389         if self.after and self.after[-1] == '\\ no newline at end of file\n':
       
   390             contextlen -= 1
       
   391         fromlen = contextlen + self.removed
       
   392         tolen = contextlen + self.added
       
   393 
       
   394         # diffutils manual, section "2.2.2.2 detailed description of unified
       
   395         # format": "an empty hunk is considered to end at the line that
       
   396         # precedes the hunk."
       
   397         #
       
   398         # so, if either of hunks is empty, decrease its line start. --immerrr
       
   399         # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
       
   400         fromline, toline = self.fromline, self.toline
       
   401         if fromline != 0:
       
   402             if fromlen == 0:
       
   403                 fromline -= 1
       
   404             if tolen == 0:
       
   405                 toline -= 1
       
   406 
       
   407         fromtoline = '@@ -%d,%d +%d,%d @@%s\n' % (
       
   408             fromline, fromlen, toline, tolen,
       
   409             self.proc and (' ' + self.proc))
       
   410         return fromtoline
       
   411 
       
   412     def write(self, fp):
       
   413         # updated self.added/removed, which are used by getfromtoline()
       
   414         self.added, self.removed = self.countchanges()
       
   415         fp.write(self.getfromtoline())
       
   416 
       
   417         hunklinelist = []
       
   418         # add the following to the list: (1) all applied lines, and
       
   419         # (2) all unapplied removal lines (convert these to context lines)
       
   420         for changedline in self.changedlines:
       
   421             changedlinestr = changedline.prettystr()
       
   422             if changedline.applied:
       
   423                 hunklinelist.append(changedlinestr)
       
   424             elif changedlinestr[0] == "-":
       
   425                 hunklinelist.append(" " + changedlinestr[1:])
       
   426 
       
   427         fp.write(''.join(self.before + hunklinelist + self.after))
       
   428 
       
   429     pretty = write
       
   430 
       
   431     def prettystr(self):
       
   432         x = cStringIO.StringIO()
       
   433         self.pretty(x)
       
   434         return x.getvalue()
       
   435 
       
   436     def __getattr__(self, name):
       
   437         return getattr(self._hunk, name)
       
   438     def __repr__(self):
       
   439         return '<hunk %r@%d>' % (self.filename(), self.fromline)
       
   440 
       
   441 def filterpatch(ui, chunks, chunk_selector):
       
   442     """interactively filter patch chunks into applied-only chunks"""
       
   443 
       
   444     chunks = list(chunks)
       
   445     # convert chunks list into structure suitable for displaying/modifying
       
   446     # with curses.  create a list of headers only.
       
   447     headers = [c for c in chunks if isinstance(c, patchmod.header)]
       
   448 
       
   449     # if there are no changed files
       
   450     if len(headers) == 0:
       
   451         return []
       
   452     uiheaders = [uiheader(h) for h in headers]
       
   453     # let user choose headers/hunks/lines, and mark their applied flags
       
   454     # accordingly
       
   455     chunk_selector(uiheaders, ui)
       
   456     appliedhunklist = []
       
   457     for hdr in uiheaders:
       
   458         if (hdr.applied and
       
   459             (hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0)):
       
   460             appliedhunklist.append(hdr)
       
   461             fixoffset = 0
       
   462             for hnk in hdr.hunks:
       
   463                 if hnk.applied:
       
   464                     appliedhunklist.append(hnk)
       
   465                     # adjust the 'to'-line offset of the hunk to be correct
       
   466                     # after de-activating some of the other hunks for this file
       
   467                     if fixoffset:
       
   468                         #hnk = copy.copy(hnk) # necessary??
       
   469                         hnk.toline += fixoffset
       
   470                 else:
       
   471                     fixoffset += hnk.removed - hnk.added
       
   472 
       
   473     return appliedhunklist
       
   474 
       
   475 
       
   476 
       
   477 def gethw():
       
   478     """
       
   479     magically get the current height and width of the window (without initscr)
       
   480 
       
   481     this is a rip-off of a rip-off - taken from the bpython code.  it is
       
   482     useful / necessary because otherwise curses.initscr() must be called,
       
   483     which can leave the terminal in a nasty state after exiting.
       
   484 
       
   485     """
       
   486     h, w = struct.unpack(
       
   487         "hhhh", fcntl.ioctl(orig_stdout, termios.TIOCGWINSZ, "\000"*8))[0:2]
       
   488     return h, w
       
   489 
       
   490 
       
   491 def chunkselector(headerlist, ui):
       
   492     """
       
   493     curses interface to get selection of chunks, and mark the applied flags
       
   494     of the chosen chunks.
       
   495 
       
   496     """
       
   497     chunkselector = curseschunkselector(headerlist, ui)
       
   498     curses.wrapper(chunkselector.main)
       
   499 
       
   500 def testdecorator(testfn, f):
       
   501     def u(*args, **kwargs):
       
   502         return f(testfn, *args, **kwargs)
       
   503     return u
       
   504 
       
   505 def testchunkselector(testfn, headerlist, ui):
       
   506     """
       
   507     test interface to get selection of chunks, and mark the applied flags
       
   508     of the chosen chunks.
       
   509 
       
   510     """
       
   511     chunkselector = curseschunkselector(headerlist, ui)
       
   512     if testfn and os.path.exists(testfn):
       
   513         testf = open(testfn)
       
   514         testcommands = map(lambda x: x.rstrip('\n'), testf.readlines())
       
   515         testf.close()
       
   516         while True:
       
   517             if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
       
   518                 break
       
   519 
       
   520 class curseschunkselector(object):
       
   521     def __init__(self, headerlist, ui):
       
   522         # put the headers into a patch object
       
   523         self.headerlist = patch(headerlist)
       
   524 
       
   525         self.ui = ui
       
   526 
       
   527         # list of all chunks
       
   528         self.chunklist = []
       
   529         for h in headerlist:
       
   530             self.chunklist.append(h)
       
   531             self.chunklist.extend(h.hunks)
       
   532 
       
   533         # dictionary mapping (fgcolor, bgcolor) pairs to the
       
   534         # corresponding curses color-pair value.
       
   535         self.colorpairs = {}
       
   536         # maps custom nicknames of color-pairs to curses color-pair values
       
   537         self.colorpairnames = {}
       
   538 
       
   539         # the currently selected header, hunk, or hunk-line
       
   540         self.currentselecteditem = self.headerlist[0]
       
   541 
       
   542         # updated when printing out patch-display -- the 'lines' here are the
       
   543         # line positions *in the pad*, not on the screen.
       
   544         self.selecteditemstartline = 0
       
   545         self.selecteditemendline = None
       
   546 
       
   547         # define indentation levels
       
   548         self.headerindentnumchars = 0
       
   549         self.hunkindentnumchars = 3
       
   550         self.hunklineindentnumchars = 6
       
   551 
       
   552         # the first line of the pad to print to the screen
       
   553         self.firstlineofpadtoprint = 0
       
   554 
       
   555         # keeps track of the number of lines in the pad
       
   556         self.numpadlines = None
       
   557 
       
   558         self.numstatuslines = 2
       
   559 
       
   560         # keep a running count of the number of lines printed to the pad
       
   561         # (used for determining when the selected item begins/ends)
       
   562         self.linesprintedtopadsofar = 0
       
   563 
       
   564         # the first line of the pad which is visible on the screen
       
   565         self.firstlineofpadtoprint = 0
       
   566 
       
   567         # stores optional text for a commit comment provided by the user
       
   568         self.commenttext = ""
       
   569 
       
   570         # if the last 'toggle all' command caused all changes to be applied
       
   571         self.waslasttoggleallapplied = True
       
   572 
       
   573     def uparrowevent(self):
       
   574         """
       
   575         try to select the previous item to the current item that has the
       
   576         most-indented level.  for example, if a hunk is selected, try to select
       
   577         the last hunkline of the hunk prior to the selected hunk.  or, if
       
   578         the first hunkline of a hunk is currently selected, then select the
       
   579         hunk itself.
       
   580 
       
   581         if the currently selected item is already at the top of the screen,
       
   582         scroll the screen down to show the new-selected item.
       
   583 
       
   584         """
       
   585         currentitem = self.currentselecteditem
       
   586 
       
   587         nextitem = currentitem.previtem(constrainlevel=False)
       
   588 
       
   589         if nextitem is None:
       
   590             # if no parent item (i.e. currentitem is the first header), then
       
   591             # no change...
       
   592             nextitem = currentitem
       
   593 
       
   594         self.currentselecteditem = nextitem
       
   595 
       
   596     def uparrowshiftevent(self):
       
   597         """
       
   598         select (if possible) the previous item on the same level as the
       
   599         currently selected item.  otherwise, select (if possible) the
       
   600         parent-item of the currently selected item.
       
   601 
       
   602         if the currently selected item is already at the top of the screen,
       
   603         scroll the screen down to show the new-selected item.
       
   604 
       
   605         """
       
   606         currentitem = self.currentselecteditem
       
   607         nextitem = currentitem.previtem()
       
   608         # if there's no previous item on this level, try choosing the parent
       
   609         if nextitem is None:
       
   610             nextitem = currentitem.parentitem()
       
   611         if nextitem is None:
       
   612             # if no parent item (i.e. currentitem is the first header), then
       
   613             # no change...
       
   614             nextitem = currentitem
       
   615 
       
   616         self.currentselecteditem = nextitem
       
   617 
       
   618     def downarrowevent(self):
       
   619         """
       
   620         try to select the next item to the current item that has the
       
   621         most-indented level.  for example, if a hunk is selected, select
       
   622         the first hunkline of the selected hunk.  or, if the last hunkline of
       
   623         a hunk is currently selected, then select the next hunk, if one exists,
       
   624         or if not, the next header if one exists.
       
   625 
       
   626         if the currently selected item is already at the bottom of the screen,
       
   627         scroll the screen up to show the new-selected item.
       
   628 
       
   629         """
       
   630         #self.startprintline += 1 #debug
       
   631         currentitem = self.currentselecteditem
       
   632 
       
   633         nextitem = currentitem.nextitem(constrainlevel=False)
       
   634         # if there's no next item, keep the selection as-is
       
   635         if nextitem is None:
       
   636             nextitem = currentitem
       
   637 
       
   638         self.currentselecteditem = nextitem
       
   639 
       
   640     def downarrowshiftevent(self):
       
   641         """
       
   642         if the cursor is already at the bottom chunk, scroll the screen up and
       
   643         move the cursor-position to the subsequent chunk.  otherwise, only move
       
   644         the cursor position down one chunk.
       
   645 
       
   646         """
       
   647         # todo: update docstring
       
   648 
       
   649         currentitem = self.currentselecteditem
       
   650         nextitem = currentitem.nextitem()
       
   651         # if there's no previous item on this level, try choosing the parent's
       
   652         # nextitem.
       
   653         if nextitem is None:
       
   654             try:
       
   655                 nextitem = currentitem.parentitem().nextitem()
       
   656             except AttributeError:
       
   657                 # parentitem returned None, so nextitem() can't be called
       
   658                 nextitem = None
       
   659         if nextitem is None:
       
   660             # if no next item on parent-level, then no change...
       
   661             nextitem = currentitem
       
   662 
       
   663         self.currentselecteditem = nextitem
       
   664 
       
   665     def rightarrowevent(self):
       
   666         """
       
   667         select (if possible) the first of this item's child-items.
       
   668 
       
   669         """
       
   670         currentitem = self.currentselecteditem
       
   671         nextitem = currentitem.firstchild()
       
   672 
       
   673         # turn off folding if we want to show a child-item
       
   674         if currentitem.folded:
       
   675             self.togglefolded(currentitem)
       
   676 
       
   677         if nextitem is None:
       
   678             # if no next item on parent-level, then no change...
       
   679             nextitem = currentitem
       
   680 
       
   681         self.currentselecteditem = nextitem
       
   682 
       
   683     def leftarrowevent(self):
       
   684         """
       
   685         if the current item can be folded (i.e. it is an unfolded header or
       
   686         hunk), then fold it.  otherwise try select (if possible) the parent
       
   687         of this item.
       
   688 
       
   689         """
       
   690         currentitem = self.currentselecteditem
       
   691 
       
   692         # try to fold the item
       
   693         if not isinstance(currentitem, uihunkline):
       
   694             if not currentitem.folded:
       
   695                 self.togglefolded(item=currentitem)
       
   696                 return
       
   697 
       
   698         # if it can't be folded, try to select the parent item
       
   699         nextitem = currentitem.parentitem()
       
   700 
       
   701         if nextitem is None:
       
   702             # if no item on parent-level, then no change...
       
   703             nextitem = currentitem
       
   704             if not nextitem.folded:
       
   705                 self.togglefolded(item=nextitem)
       
   706 
       
   707         self.currentselecteditem = nextitem
       
   708 
       
   709     def leftarrowshiftevent(self):
       
   710         """
       
   711         select the header of the current item (or fold current item if the
       
   712         current item is already a header).
       
   713 
       
   714         """
       
   715         currentitem = self.currentselecteditem
       
   716 
       
   717         if isinstance(currentitem, uiheader):
       
   718             if not currentitem.folded:
       
   719                 self.togglefolded(item=currentitem)
       
   720                 return
       
   721 
       
   722         # select the parent item recursively until we're at a header
       
   723         while True:
       
   724             nextitem = currentitem.parentitem()
       
   725             if nextitem is None:
       
   726                 break
       
   727             else:
       
   728                 currentitem = nextitem
       
   729 
       
   730         self.currentselecteditem = currentitem
       
   731 
       
   732     def updatescroll(self):
       
   733         "scroll the screen to fully show the currently-selected"
       
   734         selstart = self.selecteditemstartline
       
   735         selend = self.selecteditemendline
       
   736         #selnumlines = selend - selstart
       
   737         padstart = self.firstlineofpadtoprint
       
   738         padend = padstart + self.yscreensize - self.numstatuslines - 1
       
   739         # 'buffered' pad start/end values which scroll with a certain
       
   740         # top/bottom context margin
       
   741         padstartbuffered = padstart + 3
       
   742         padendbuffered = padend - 3
       
   743 
       
   744         if selend > padendbuffered:
       
   745             self.scrolllines(selend - padendbuffered)
       
   746         elif selstart < padstartbuffered:
       
   747             # negative values scroll in pgup direction
       
   748             self.scrolllines(selstart - padstartbuffered)
       
   749 
       
   750 
       
   751     def scrolllines(self, numlines):
       
   752         "scroll the screen up (down) by numlines when numlines >0 (<0)."
       
   753         self.firstlineofpadtoprint += numlines
       
   754         if self.firstlineofpadtoprint < 0:
       
   755             self.firstlineofpadtoprint = 0
       
   756         if self.firstlineofpadtoprint > self.numpadlines - 1:
       
   757             self.firstlineofpadtoprint = self.numpadlines - 1
       
   758 
       
   759     def toggleapply(self, item=None):
       
   760         """
       
   761         toggle the applied flag of the specified item.  if no item is specified,
       
   762         toggle the flag of the currently selected item.
       
   763 
       
   764         """
       
   765         if item is None:
       
   766             item = self.currentselecteditem
       
   767 
       
   768         item.applied = not item.applied
       
   769 
       
   770         if isinstance(item, uiheader):
       
   771             item.partial = False
       
   772             if item.applied:
       
   773                 if not item.special():
       
   774                     # apply all its hunks
       
   775                     for hnk in item.hunks:
       
   776                         hnk.applied = True
       
   777                         # apply all their hunklines
       
   778                         for hunkline in hnk.changedlines:
       
   779                             hunkline.applied = True
       
   780                 else:
       
   781                     # all children are off (but the header is on)
       
   782                     if len(item.allchildren()) > 0:
       
   783                         item.partial = True
       
   784             else:
       
   785                 # un-apply all its hunks
       
   786                 for hnk in item.hunks:
       
   787                     hnk.applied = False
       
   788                     hnk.partial = False
       
   789                     # un-apply all their hunklines
       
   790                     for hunkline in hnk.changedlines:
       
   791                         hunkline.applied = False
       
   792         elif isinstance(item, uihunk):
       
   793             item.partial = False
       
   794             # apply all it's hunklines
       
   795             for hunkline in item.changedlines:
       
   796                 hunkline.applied = item.applied
       
   797 
       
   798             siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
       
   799             allsiblingsapplied = not (False in siblingappliedstatus)
       
   800             nosiblingsapplied = not (True in siblingappliedstatus)
       
   801 
       
   802             siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
       
   803             somesiblingspartial = (True in siblingspartialstatus)
       
   804 
       
   805             #cases where applied or partial should be removed from header
       
   806 
       
   807             # if no 'sibling' hunks are applied (including this hunk)
       
   808             if nosiblingsapplied:
       
   809                 if not item.header.special():
       
   810                     item.header.applied = False
       
   811                     item.header.partial = False
       
   812             else: # some/all parent siblings are applied
       
   813                 item.header.applied = True
       
   814                 item.header.partial = (somesiblingspartial or
       
   815                                         not allsiblingsapplied)
       
   816 
       
   817         elif isinstance(item, uihunkline):
       
   818             siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
       
   819             allsiblingsapplied = not (False in siblingappliedstatus)
       
   820             nosiblingsapplied = not (True in siblingappliedstatus)
       
   821 
       
   822             # if no 'sibling' lines are applied
       
   823             if nosiblingsapplied:
       
   824                 item.hunk.applied = False
       
   825                 item.hunk.partial = False
       
   826             elif allsiblingsapplied:
       
   827                 item.hunk.applied = True
       
   828                 item.hunk.partial = False
       
   829             else: # some siblings applied
       
   830                 item.hunk.applied = True
       
   831                 item.hunk.partial = True
       
   832 
       
   833             parentsiblingsapplied = [hnk.applied for hnk
       
   834                                      in item.hunk.header.hunks]
       
   835             noparentsiblingsapplied = not (True in parentsiblingsapplied)
       
   836             allparentsiblingsapplied = not (False in parentsiblingsapplied)
       
   837 
       
   838             parentsiblingspartial = [hnk.partial for hnk
       
   839                                      in item.hunk.header.hunks]
       
   840             someparentsiblingspartial = (True in parentsiblingspartial)
       
   841 
       
   842             # if all parent hunks are not applied, un-apply header
       
   843             if noparentsiblingsapplied:
       
   844                 if not item.hunk.header.special():
       
   845                     item.hunk.header.applied = False
       
   846                     item.hunk.header.partial = False
       
   847             # set the applied and partial status of the header if needed
       
   848             else: # some/all parent siblings are applied
       
   849                 item.hunk.header.applied = True
       
   850                 item.hunk.header.partial = (someparentsiblingspartial or
       
   851                                             not allparentsiblingsapplied)
       
   852 
       
   853     def toggleall(self):
       
   854         "toggle the applied flag of all items."
       
   855         if self.waslasttoggleallapplied: # then unapply them this time
       
   856             for item in self.headerlist:
       
   857                 if item.applied:
       
   858                     self.toggleapply(item)
       
   859         else:
       
   860             for item in self.headerlist:
       
   861                 if not item.applied:
       
   862                     self.toggleapply(item)
       
   863         self.waslasttoggleallapplied = not self.waslasttoggleallapplied
       
   864 
       
   865     def togglefolded(self, item=None, foldparent=False):
       
   866         "toggle folded flag of specified item (defaults to currently selected)"
       
   867         if item is None:
       
   868             item = self.currentselecteditem
       
   869         if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
       
   870             if not isinstance(item, uiheader):
       
   871                 # we need to select the parent item in this case
       
   872                 self.currentselecteditem = item = item.parentitem()
       
   873             elif item.neverunfolded:
       
   874                 item.neverunfolded = False
       
   875 
       
   876             # also fold any foldable children of the parent/current item
       
   877             if isinstance(item, uiheader): # the original or 'new' item
       
   878                 for child in item.allchildren():
       
   879                     child.folded = not item.folded
       
   880 
       
   881         if isinstance(item, (uiheader, uihunk)):
       
   882             item.folded = not item.folded
       
   883 
       
   884 
       
   885     def alignstring(self, instr, window):
       
   886         """
       
   887         add whitespace to the end of a string in order to make it fill
       
   888         the screen in the x direction.  the current cursor position is
       
   889         taken into account when making this calculation.  the string can span
       
   890         multiple lines.
       
   891 
       
   892         """
       
   893         y, xstart = window.getyx()
       
   894         width = self.xscreensize
       
   895         # turn tabs into spaces
       
   896         instr = instr.expandtabs(4)
       
   897         try:
       
   898             strlen = len(unicode(encoding.fromlocal(instr), code))
       
   899         except Exception:
       
   900             # if text is not utf8, then assume an 8-bit single-byte encoding.
       
   901             strlen = len(instr)
       
   902 
       
   903         numspaces = (width - ((strlen + xstart) % width) - 1)
       
   904         return instr + " " * numspaces + "\n"
       
   905 
       
   906     def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None,
       
   907         pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False):
       
   908         """
       
   909         print the string, text, with the specified colors and attributes, to
       
   910         the specified curses window object.
       
   911 
       
   912         the foreground and background colors are of the form
       
   913         curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
       
   914         magenta, red, white, yellow].  if pairname is provided, a color
       
   915         pair will be looked up in the self.colorpairnames dictionary.
       
   916 
       
   917         attrlist is a list containing text attributes in the form of
       
   918         curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
       
   919         underline].
       
   920 
       
   921         if align == True, whitespace is added to the printed string such that
       
   922         the string stretches to the right border of the window.
       
   923 
       
   924         if showwhtspc == True, trailing whitespace of a string is highlighted.
       
   925 
       
   926         """
       
   927         # preprocess the text, converting tabs to spaces
       
   928         text = text.expandtabs(4)
       
   929         # strip \n, and convert control characters to ^[char] representation
       
   930         text = re.sub(r'[\x00-\x08\x0a-\x1f]',
       
   931                 lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n'))
       
   932 
       
   933         if pair is not None:
       
   934             colorpair = pair
       
   935         elif pairname is not None:
       
   936             colorpair = self.colorpairnames[pairname]
       
   937         else:
       
   938             if fgcolor is None:
       
   939                 fgcolor = -1
       
   940             if bgcolor is None:
       
   941                 bgcolor = -1
       
   942             if (fgcolor, bgcolor) in self.colorpairs:
       
   943                 colorpair = self.colorpairs[(fgcolor, bgcolor)]
       
   944             else:
       
   945                 colorpair = self.getcolorpair(fgcolor, bgcolor)
       
   946         # add attributes if possible
       
   947         if attrlist is None:
       
   948             attrlist = []
       
   949         if colorpair < 256:
       
   950             # then it is safe to apply all attributes
       
   951             for textattr in attrlist:
       
   952                 colorpair |= textattr
       
   953         else:
       
   954             # just apply a select few (safe?) attributes
       
   955             for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
       
   956                 if textattr in attrlist:
       
   957                     colorpair |= textattr
       
   958 
       
   959         y, xstart = self.chunkpad.getyx()
       
   960         t = "" # variable for counting lines printed
       
   961         # if requested, show trailing whitespace
       
   962         if showwhtspc:
       
   963             origlen = len(text)
       
   964             text = text.rstrip(' \n') # tabs have already been expanded
       
   965             strippedlen = len(text)
       
   966             numtrailingspaces = origlen - strippedlen
       
   967 
       
   968         if towin:
       
   969             window.addstr(text, colorpair)
       
   970         t += text
       
   971 
       
   972         if showwhtspc:
       
   973                 wscolorpair = colorpair | curses.A_REVERSE
       
   974                 if towin:
       
   975                     for i in range(numtrailingspaces):
       
   976                         window.addch(curses.ACS_CKBOARD, wscolorpair)
       
   977                 t += " " * numtrailingspaces
       
   978 
       
   979         if align:
       
   980             if towin:
       
   981                 extrawhitespace = self.alignstring("", window)
       
   982                 window.addstr(extrawhitespace, colorpair)
       
   983             else:
       
   984                 # need to use t, since the x position hasn't incremented
       
   985                 extrawhitespace = self.alignstring(t, window)
       
   986             t += extrawhitespace
       
   987 
       
   988         # is reset to 0 at the beginning of printitem()
       
   989 
       
   990         linesprinted = (xstart + len(t)) / self.xscreensize
       
   991         self.linesprintedtopadsofar += linesprinted
       
   992         return t
       
   993 
       
   994     def updatescreen(self):
       
   995         self.statuswin.erase()
       
   996         self.chunkpad.erase()
       
   997 
       
   998         printstring = self.printstring
       
   999 
       
  1000         # print out the status lines at the top
       
  1001         try:
       
  1002             printstring(self.statuswin,
       
  1003                         "SELECT CHUNKS: (j/k/up/dn/pgup/pgdn) move cursor; "
       
  1004                         "(space/A) toggle hunk/all; (e)dit hunk;",
       
  1005                         pairname="legend")
       
  1006             printstring(self.statuswin,
       
  1007                         " (f)old/unfold; (c)ommit applied; (q)uit; (?) help "
       
  1008                         "| [X]=hunk applied **=folded",
       
  1009                         pairname="legend")
       
  1010         except curses.error:
       
  1011             pass
       
  1012 
       
  1013         # print out the patch in the remaining part of the window
       
  1014         try:
       
  1015             self.printitem()
       
  1016             self.updatescroll()
       
  1017             self.chunkpad.refresh(self.firstlineofpadtoprint, 0,
       
  1018                                   self.numstatuslines, 0,
       
  1019                                   self.yscreensize + 1 - self.numstatuslines,
       
  1020                                   self.xscreensize)
       
  1021         except curses.error:
       
  1022             pass
       
  1023 
       
  1024         # refresh([pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol])
       
  1025         self.statuswin.refresh()
       
  1026 
       
  1027     def getstatusprefixstring(self, item):
       
  1028         """
       
  1029         create a string to prefix a line with which indicates whether 'item'
       
  1030         is applied and/or folded.
       
  1031 
       
  1032         """
       
  1033         # create checkbox string
       
  1034         if item.applied:
       
  1035             if not isinstance(item, uihunkline) and item.partial:
       
  1036                 checkbox = "[~]"
       
  1037             else:
       
  1038                 checkbox = "[x]"
       
  1039         else:
       
  1040             checkbox = "[ ]"
       
  1041 
       
  1042         try:
       
  1043             if item.folded:
       
  1044                 checkbox += "**"
       
  1045                 if isinstance(item, uiheader):
       
  1046                     # one of "m", "a", or "d" (modified, added, deleted)
       
  1047                     filestatus = item.changetype
       
  1048 
       
  1049                     checkbox += filestatus + " "
       
  1050             else:
       
  1051                 checkbox += "  "
       
  1052                 if isinstance(item, uiheader):
       
  1053                     # add two more spaces for headers
       
  1054                     checkbox += "  "
       
  1055         except AttributeError: # not foldable
       
  1056             checkbox += "  "
       
  1057 
       
  1058         return checkbox
       
  1059 
       
  1060     def printheader(self, header, selected=False, towin=True,
       
  1061                     ignorefolding=False):
       
  1062         """
       
  1063         print the header to the pad.  if countlines is True, don't print
       
  1064         anything, but just count the number of lines which would be printed.
       
  1065 
       
  1066         """
       
  1067         outstr = ""
       
  1068         text = header.prettystr()
       
  1069         chunkindex = self.chunklist.index(header)
       
  1070 
       
  1071         if chunkindex != 0 and not header.folded:
       
  1072             # add separating line before headers
       
  1073             outstr += self.printstring(self.chunkpad, '_' * self.xscreensize,
       
  1074                                        towin=towin, align=False)
       
  1075         # select color-pair based on if the header is selected
       
  1076         colorpair = self.getcolorpair(name=selected and "selected" or "normal",
       
  1077                                       attrlist=[curses.A_BOLD])
       
  1078 
       
  1079         # print out each line of the chunk, expanding it to screen width
       
  1080 
       
  1081         # number of characters to indent lines on this level by
       
  1082         indentnumchars = 0
       
  1083         checkbox = self.getstatusprefixstring(header)
       
  1084         if not header.folded or ignorefolding:
       
  1085             textlist = text.split("\n")
       
  1086             linestr = checkbox + textlist[0]
       
  1087         else:
       
  1088             linestr = checkbox + header.filename()
       
  1089         outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
       
  1090                                    towin=towin)
       
  1091         if not header.folded or ignorefolding:
       
  1092             if len(textlist) > 1:
       
  1093                 for line in textlist[1:]:
       
  1094                     linestr = " "*(indentnumchars + len(checkbox)) + line
       
  1095                     outstr += self.printstring(self.chunkpad, linestr,
       
  1096                                                pair=colorpair, towin=towin)
       
  1097 
       
  1098         return outstr
       
  1099 
       
  1100     def printhunklinesbefore(self, hunk, selected=False, towin=True,
       
  1101                              ignorefolding=False):
       
  1102         "includes start/end line indicator"
       
  1103         outstr = ""
       
  1104         # where hunk is in list of siblings
       
  1105         hunkindex = hunk.header.hunks.index(hunk)
       
  1106 
       
  1107         if hunkindex != 0:
       
  1108             # add separating line before headers
       
  1109             outstr += self.printstring(self.chunkpad, ' '*self.xscreensize,
       
  1110                                        towin=towin, align=False)
       
  1111 
       
  1112         colorpair = self.getcolorpair(name=selected and "selected" or "normal",
       
  1113                                       attrlist=[curses.A_BOLD])
       
  1114 
       
  1115         # print out from-to line with checkbox
       
  1116         checkbox = self.getstatusprefixstring(hunk)
       
  1117 
       
  1118         lineprefix = " "*self.hunkindentnumchars + checkbox
       
  1119         frtoline = "   " + hunk.getfromtoline().strip("\n")
       
  1120 
       
  1121 
       
  1122         outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
       
  1123                                    align=False) # add uncolored checkbox/indent
       
  1124         outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair,
       
  1125                                    towin=towin)
       
  1126 
       
  1127         if hunk.folded and not ignorefolding:
       
  1128             # skip remainder of output
       
  1129             return outstr
       
  1130 
       
  1131         # print out lines of the chunk preceeding changed-lines
       
  1132         for line in hunk.before:
       
  1133             linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
       
  1134             outstr += self.printstring(self.chunkpad, linestr, towin=towin)
       
  1135 
       
  1136         return outstr
       
  1137 
       
  1138     def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
       
  1139         outstr = ""
       
  1140         if hunk.folded and not ignorefolding:
       
  1141             return outstr
       
  1142 
       
  1143         # a bit superfluous, but to avoid hard-coding indent amount
       
  1144         checkbox = self.getstatusprefixstring(hunk)
       
  1145         for line in hunk.after:
       
  1146             linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
       
  1147             outstr += self.printstring(self.chunkpad, linestr, towin=towin)
       
  1148 
       
  1149         return outstr
       
  1150 
       
  1151     def printhunkchangedline(self, hunkline, selected=False, towin=True):
       
  1152         outstr = ""
       
  1153         checkbox = self.getstatusprefixstring(hunkline)
       
  1154 
       
  1155         linestr = hunkline.prettystr().strip("\n")
       
  1156 
       
  1157         # select color-pair based on whether line is an addition/removal
       
  1158         if selected:
       
  1159             colorpair = self.getcolorpair(name="selected")
       
  1160         elif linestr.startswith("+"):
       
  1161             colorpair = self.getcolorpair(name="addition")
       
  1162         elif linestr.startswith("-"):
       
  1163             colorpair = self.getcolorpair(name="deletion")
       
  1164         elif linestr.startswith("\\"):
       
  1165             colorpair = self.getcolorpair(name="normal")
       
  1166 
       
  1167         lineprefix = " "*self.hunklineindentnumchars + checkbox
       
  1168         outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
       
  1169                                    align=False) # add uncolored checkbox/indent
       
  1170         outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
       
  1171                                    towin=towin, showwhtspc=True)
       
  1172         return outstr
       
  1173 
       
  1174     def printitem(self, item=None, ignorefolding=False, recursechildren=True,
       
  1175                   towin=True):
       
  1176         """
       
  1177         use __printitem() to print the the specified item.applied.
       
  1178         if item is not specified, then print the entire patch.
       
  1179         (hiding folded elements, etc. -- see __printitem() docstring)
       
  1180         """
       
  1181         if item is None:
       
  1182             item = self.headerlist
       
  1183         if recursechildren:
       
  1184             self.linesprintedtopadsofar = 0
       
  1185 
       
  1186         outstr = []
       
  1187         self.__printitem(item, ignorefolding, recursechildren, outstr,
       
  1188                                   towin=towin)
       
  1189         return ''.join(outstr)
       
  1190 
       
  1191     def outofdisplayedarea(self):
       
  1192         y, _ = self.chunkpad.getyx() # cursor location
       
  1193         # * 2 here works but an optimization would be the max number of
       
  1194         # consecutive non selectable lines
       
  1195         # i.e the max number of context line for any hunk in the patch
       
  1196         miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
       
  1197         maxy = self.firstlineofpadtoprint + self.yscreensize * 2
       
  1198         return y < miny or y > maxy
       
  1199 
       
  1200     def handleselection(self, item, recursechildren):
       
  1201         selected = (item is self.currentselecteditem)
       
  1202         if selected and recursechildren:
       
  1203             # assumes line numbering starting from line 0
       
  1204             self.selecteditemstartline = self.linesprintedtopadsofar
       
  1205             selecteditemlines = self.getnumlinesdisplayed(item,
       
  1206                                                           recursechildren=False)
       
  1207             self.selecteditemendline = (self.selecteditemstartline +
       
  1208                                         selecteditemlines - 1)
       
  1209         return selected
       
  1210 
       
  1211     def __printitem(self, item, ignorefolding, recursechildren, outstr,
       
  1212                     towin=True):
       
  1213         """
       
  1214         recursive method for printing out patch/header/hunk/hunk-line data to
       
  1215         screen.  also returns a string with all of the content of the displayed
       
  1216         patch (not including coloring, etc.).
       
  1217 
       
  1218         if ignorefolding is True, then folded items are printed out.
       
  1219 
       
  1220         if recursechildren is False, then only print the item without its
       
  1221         child items.
       
  1222 
       
  1223         """
       
  1224         if towin and self.outofdisplayedarea():
       
  1225             return
       
  1226 
       
  1227         selected = self.handleselection(item, recursechildren)
       
  1228 
       
  1229         # patch object is a list of headers
       
  1230         if isinstance(item, patch):
       
  1231             if recursechildren:
       
  1232                 for hdr in item:
       
  1233                     self.__printitem(hdr, ignorefolding,
       
  1234                             recursechildren, outstr, towin)
       
  1235         # todo: eliminate all isinstance() calls
       
  1236         if isinstance(item, uiheader):
       
  1237             outstr.append(self.printheader(item, selected, towin=towin,
       
  1238                                        ignorefolding=ignorefolding))
       
  1239             if recursechildren:
       
  1240                 for hnk in item.hunks:
       
  1241                     self.__printitem(hnk, ignorefolding,
       
  1242                             recursechildren, outstr, towin)
       
  1243         elif (isinstance(item, uihunk) and
       
  1244               ((not item.header.folded) or ignorefolding)):
       
  1245             # print the hunk data which comes before the changed-lines
       
  1246             outstr.append(self.printhunklinesbefore(item, selected, towin=towin,
       
  1247                                                 ignorefolding=ignorefolding))
       
  1248             if recursechildren:
       
  1249                 for l in item.changedlines:
       
  1250                     self.__printitem(l, ignorefolding,
       
  1251                             recursechildren, outstr, towin)
       
  1252                 outstr.append(self.printhunklinesafter(item, towin=towin,
       
  1253                                                 ignorefolding=ignorefolding))
       
  1254         elif (isinstance(item, uihunkline) and
       
  1255               ((not item.hunk.folded) or ignorefolding)):
       
  1256             outstr.append(self.printhunkchangedline(item, selected,
       
  1257                 towin=towin))
       
  1258 
       
  1259         return outstr
       
  1260 
       
  1261     def getnumlinesdisplayed(self, item=None, ignorefolding=False,
       
  1262                              recursechildren=True):
       
  1263         """
       
  1264         return the number of lines which would be displayed if the item were
       
  1265         to be printed to the display.  the item will not be printed to the
       
  1266         display (pad).
       
  1267         if no item is given, assume the entire patch.
       
  1268         if ignorefolding is True, folded items will be unfolded when counting
       
  1269         the number of lines.
       
  1270 
       
  1271         """
       
  1272         # temporarily disable printing to windows by printstring
       
  1273         patchdisplaystring = self.printitem(item, ignorefolding,
       
  1274                                             recursechildren, towin=False)
       
  1275         numlines = len(patchdisplaystring) / self.xscreensize
       
  1276         return numlines
       
  1277 
       
  1278     def sigwinchhandler(self, n, frame):
       
  1279         "handle window resizing"
       
  1280         try:
       
  1281             curses.endwin()
       
  1282             self.yscreensize, self.xscreensize = gethw()
       
  1283             self.statuswin.resize(self.numstatuslines, self.xscreensize)
       
  1284             self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
       
  1285             self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
       
  1286             # todo: try to resize commit message window if possible
       
  1287         except curses.error:
       
  1288             pass
       
  1289 
       
  1290     def getcolorpair(self, fgcolor=None, bgcolor=None, name=None,
       
  1291                      attrlist=None):
       
  1292         """
       
  1293         get a curses color pair, adding it to self.colorpairs if it is not
       
  1294         already defined.  an optional string, name, can be passed as a shortcut
       
  1295         for referring to the color-pair.  by default, if no arguments are
       
  1296         specified, the white foreground / black background color-pair is
       
  1297         returned.
       
  1298 
       
  1299         it is expected that this function will be used exclusively for
       
  1300         initializing color pairs, and not curses.init_pair().
       
  1301 
       
  1302         attrlist is used to 'flavor' the returned color-pair.  this information
       
  1303         is not stored in self.colorpairs.  it contains attribute values like
       
  1304         curses.A_BOLD.
       
  1305 
       
  1306         """
       
  1307         if (name is not None) and name in self.colorpairnames:
       
  1308             # then get the associated color pair and return it
       
  1309             colorpair = self.colorpairnames[name]
       
  1310         else:
       
  1311             if fgcolor is None:
       
  1312                 fgcolor = -1
       
  1313             if bgcolor is None:
       
  1314                 bgcolor = -1
       
  1315             if (fgcolor, bgcolor) in self.colorpairs:
       
  1316                 colorpair = self.colorpairs[(fgcolor, bgcolor)]
       
  1317             else:
       
  1318                 pairindex = len(self.colorpairs) + 1
       
  1319                 curses.init_pair(pairindex, fgcolor, bgcolor)
       
  1320                 colorpair = self.colorpairs[(fgcolor, bgcolor)] = (
       
  1321                     curses.color_pair(pairindex))
       
  1322                 if name is not None:
       
  1323                     self.colorpairnames[name] = curses.color_pair(pairindex)
       
  1324 
       
  1325         # add attributes if possible
       
  1326         if attrlist is None:
       
  1327             attrlist = []
       
  1328         if colorpair < 256:
       
  1329             # then it is safe to apply all attributes
       
  1330             for textattr in attrlist:
       
  1331                 colorpair |= textattr
       
  1332         else:
       
  1333             # just apply a select few (safe?) attributes
       
  1334             for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
       
  1335                 if textattrib in attrlist:
       
  1336                     colorpair |= textattrib
       
  1337         return colorpair
       
  1338 
       
  1339     def initcolorpair(self, *args, **kwargs):
       
  1340         "same as getcolorpair."
       
  1341         self.getcolorpair(*args, **kwargs)
       
  1342 
       
  1343     def helpwindow(self):
       
  1344         "print a help window to the screen.  exit after any keypress."
       
  1345         helptext = """            [press any key to return to the patch-display]
       
  1346 
       
  1347 crecord allows you to interactively choose among the changes you have made,
       
  1348 and commit only those changes you select.  after committing the selected
       
  1349 changes, the unselected changes are still present in your working copy, so you
       
  1350 can use crecord multiple times to split large changes into smaller changesets.
       
  1351 the following are valid keystrokes:
       
  1352 
       
  1353                 [space] : (un-)select item ([~]/[x] = partly/fully applied)
       
  1354                       a : (un-)select all items
       
  1355     up/down-arrow [k/j] : go to previous/next unfolded item
       
  1356         pgup/pgdn [k/j] : go to previous/next item of same type
       
  1357  right/left-arrow [l/h] : go to child item / parent item
       
  1358  shift-left-arrow   [h] : go to parent header / fold selected header
       
  1359                       f : fold / unfold item, hiding/revealing its children
       
  1360                       f : fold / unfold parent item and all of its ancestors
       
  1361                       m : edit / resume editing the commit message
       
  1362                       e : edit the currently selected hunk
       
  1363                       a : toggle amend mode (hg rev >= 2.2)
       
  1364                       c : commit selected changes
       
  1365                       r : review/edit and commit selected changes
       
  1366                       q : quit without committing (no changes will be made)
       
  1367                       ? : help (what you're currently reading)"""
       
  1368 
       
  1369         helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
       
  1370         helplines = helptext.split("\n")
       
  1371         helplines = helplines + [" "]*(
       
  1372             self.yscreensize - self.numstatuslines - len(helplines) - 1)
       
  1373         try:
       
  1374             for line in helplines:
       
  1375                 self.printstring(helpwin, line, pairname="legend")
       
  1376         except curses.error:
       
  1377             pass
       
  1378         helpwin.refresh()
       
  1379         try:
       
  1380             helpwin.getkey()
       
  1381         except curses.error:
       
  1382             pass
       
  1383 
       
  1384     def confirmationwindow(self, windowtext):
       
  1385         "display an informational window, then wait for and return a keypress."
       
  1386 
       
  1387         confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
       
  1388         try:
       
  1389             lines = windowtext.split("\n")
       
  1390             for line in lines:
       
  1391                 self.printstring(confirmwin, line, pairname="selected")
       
  1392         except curses.error:
       
  1393             pass
       
  1394         self.stdscr.refresh()
       
  1395         confirmwin.refresh()
       
  1396         try:
       
  1397             response = chr(self.stdscr.getch())
       
  1398         except ValueError:
       
  1399             response = None
       
  1400 
       
  1401         return response
       
  1402 
       
  1403     def confirmcommit(self, review=False):
       
  1404         "ask for 'y' to be pressed to confirm commit. return True if confirmed."
       
  1405         if review:
       
  1406             confirmtext = (
       
  1407 """if you answer yes to the following, the your currently chosen patch chunks
       
  1408 will be loaded into an editor.  you may modify the patch from the editor, and
       
  1409 save the changes if you wish to change the patch.  otherwise, you can just
       
  1410 close the editor without saving to accept the current patch as-is.
       
  1411 
       
  1412 note: don't add/remove lines unless you also modify the range information.
       
  1413       failing to follow this rule will result in the commit aborting.
       
  1414 
       
  1415 are you sure you want to review/edit and commit the selected changes [yn]? """)
       
  1416         else:
       
  1417             confirmtext = (
       
  1418                 "are you sure you want to commit the selected changes [yn]? ")
       
  1419 
       
  1420         response = self.confirmationwindow(confirmtext)
       
  1421         if response is None:
       
  1422             response = "n"
       
  1423         if response.lower().startswith("y"):
       
  1424             return True
       
  1425         else:
       
  1426             return False
       
  1427 
       
  1428     def recenterdisplayedarea(self):
       
  1429         """
       
  1430         once we scrolled with pg up pg down we can be pointing outside of the
       
  1431         display zone. we print the patch with towin=False to compute the
       
  1432         location of the selected item eventhough it is outside of the displayed
       
  1433         zone and then update the scroll.
       
  1434         """
       
  1435         self.printitem(towin=False)
       
  1436         self.updatescroll()
       
  1437 
       
  1438     def toggleedit(self, item=None, test=False):
       
  1439         """
       
  1440             edit the currently chelected chunk
       
  1441         """
       
  1442 
       
  1443         def editpatchwitheditor(self, chunk):
       
  1444             if chunk is None:
       
  1445                 self.ui.write(_('cannot edit patch for whole file'))
       
  1446                 self.ui.write("\n")
       
  1447                 return None
       
  1448             if chunk.header.binary():
       
  1449                 self.ui.write(_('cannot edit patch for binary file'))
       
  1450                 self.ui.write("\n")
       
  1451                 return None
       
  1452             # patch comment based on the git one (based on comment at end of
       
  1453             # http://mercurial.selenic.com/wiki/recordextension)
       
  1454             phelp = '---' + _("""
       
  1455     to remove '-' lines, make them ' ' lines (context).
       
  1456     to remove '+' lines, delete them.
       
  1457     lines starting with # will be removed from the patch.
       
  1458 
       
  1459     if the patch applies cleanly, the edited hunk will immediately be
       
  1460     added to the record list. if it does not apply cleanly, a rejects
       
  1461     file will be generated: you can use that when you try again. if
       
  1462     all lines of the hunk are removed, then the edit is aborted and
       
  1463     the hunk is left unchanged.
       
  1464     """)
       
  1465             (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
       
  1466                     suffix=".diff", text=True)
       
  1467             ncpatchfp = None
       
  1468             try:
       
  1469                 # write the initial patch
       
  1470                 f = os.fdopen(patchfd, "w")
       
  1471                 chunk.header.write(f)
       
  1472                 chunk.write(f)
       
  1473                 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
       
  1474                 f.close()
       
  1475                 # start the editor and wait for it to complete
       
  1476                 editor = self.ui.geteditor()
       
  1477                 self.ui.system("%s \"%s\"" % (editor, patchfn),
       
  1478                           environ={'hguser': self.ui.username()},
       
  1479                           onerr=util.Abort, errprefix=_("edit failed"))
       
  1480                 # remove comment lines
       
  1481                 patchfp = open(patchfn)
       
  1482                 ncpatchfp = cStringIO.StringIO()
       
  1483                 for line in patchfp:
       
  1484                     if not line.startswith('#'):
       
  1485                         ncpatchfp.write(line)
       
  1486                 patchfp.close()
       
  1487                 ncpatchfp.seek(0)
       
  1488                 newpatches = patchmod.parsepatch(ncpatchfp)
       
  1489             finally:
       
  1490                 os.unlink(patchfn)
       
  1491                 del ncpatchfp
       
  1492             return newpatches
       
  1493         if item is None:
       
  1494             item = self.currentselecteditem
       
  1495         if isinstance(item, uiheader):
       
  1496             return
       
  1497         if isinstance(item, uihunkline):
       
  1498             item = item.parentitem()
       
  1499         if not isinstance(item, uihunk):
       
  1500             return
       
  1501 
       
  1502         beforeadded, beforeremoved = item.added, item.removed
       
  1503         newpatches = editpatchwitheditor(self, item)
       
  1504         header = item.header
       
  1505         editedhunkindex = header.hunks.index(item)
       
  1506         hunksbefore = header.hunks[:editedhunkindex]
       
  1507         hunksafter = header.hunks[editedhunkindex + 1:]
       
  1508         newpatchheader = newpatches[0]
       
  1509         newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
       
  1510         newadded = sum([h.added for h in newhunks])
       
  1511         newremoved = sum([h.removed for h in newhunks])
       
  1512         offset = (newadded - beforeadded) - (newremoved - beforeremoved)
       
  1513 
       
  1514         for h in hunksafter:
       
  1515             h.toline += offset
       
  1516         for h in newhunks:
       
  1517             h.folded = False
       
  1518         header.hunks = hunksbefore + newhunks + hunksafter
       
  1519         if self.emptypatch():
       
  1520             header.hunks = hunksbefore + [item] + hunksafter
       
  1521         self.currentselecteditem = header
       
  1522 
       
  1523         if not test:
       
  1524             self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
       
  1525             self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
       
  1526             self.updatescroll()
       
  1527             self.stdscr.refresh()
       
  1528             self.statuswin.refresh()
       
  1529             self.stdscr.keypad(1)
       
  1530 
       
  1531     def emptypatch(self):
       
  1532         item = self.headerlist
       
  1533         if not item:
       
  1534             return True
       
  1535         for header in item:
       
  1536             if header.hunks:
       
  1537                 return False
       
  1538         return True
       
  1539 
       
  1540     def handlekeypressed(self, keypressed, test=False):
       
  1541         if keypressed in ["k", "KEY_UP"]:
       
  1542             self.uparrowevent()
       
  1543         if keypressed in ["k", "KEY_PPAGE"]:
       
  1544             self.uparrowshiftevent()
       
  1545         elif keypressed in ["j", "KEY_DOWN"]:
       
  1546             self.downarrowevent()
       
  1547         elif keypressed in ["j", "KEY_NPAGE"]:
       
  1548             self.downarrowshiftevent()
       
  1549         elif keypressed in ["l", "KEY_RIGHT"]:
       
  1550             self.rightarrowevent()
       
  1551         elif keypressed in ["h", "KEY_LEFT"]:
       
  1552             self.leftarrowevent()
       
  1553         elif keypressed in ["h", "KEY_SLEFT"]:
       
  1554             self.leftarrowshiftevent()
       
  1555         elif keypressed in ["q"]:
       
  1556             raise util.Abort(_('user quit'))
       
  1557         elif keypressed in ["c"]:
       
  1558             if self.confirmcommit():
       
  1559                 return True
       
  1560         elif keypressed in ["r"]:
       
  1561             if self.confirmcommit(review=True):
       
  1562                 return True
       
  1563         elif test and keypressed in ['X']:
       
  1564             return True
       
  1565         elif keypressed in [' '] or (test and keypressed in ["TOGGLE"]):
       
  1566             self.toggleapply()
       
  1567         elif keypressed in ['A']:
       
  1568             self.toggleall()
       
  1569         elif keypressed in ['e']:
       
  1570             self.toggleedit(test=test)
       
  1571         elif keypressed in ["f"]:
       
  1572             self.togglefolded()
       
  1573         elif keypressed in ["f"]:
       
  1574             self.togglefolded(foldparent=True)
       
  1575         elif keypressed in ["?"]:
       
  1576             self.helpwindow()
       
  1577 
       
  1578     def main(self, stdscr):
       
  1579         """
       
  1580         method to be wrapped by curses.wrapper() for selecting chunks.
       
  1581 
       
  1582         """
       
  1583         signal.signal(signal.SIGWINCH, self.sigwinchhandler)
       
  1584         self.stdscr = stdscr
       
  1585         self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
       
  1586 
       
  1587         curses.start_color()
       
  1588         curses.use_default_colors()
       
  1589 
       
  1590         # available colors: black, blue, cyan, green, magenta, white, yellow
       
  1591         # init_pair(color_id, foreground_color, background_color)
       
  1592         self.initcolorpair(None, None, name="normal")
       
  1593         self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA,
       
  1594                            name="selected")
       
  1595         self.initcolorpair(curses.COLOR_RED, None, name="deletion")
       
  1596         self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
       
  1597         self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
       
  1598         # newwin([height, width,] begin_y, begin_x)
       
  1599         self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
       
  1600         self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
       
  1601 
       
  1602         # figure out how much space to allocate for the chunk-pad which is
       
  1603         # used for displaying the patch
       
  1604 
       
  1605         # stupid hack to prevent getnumlinesdisplayed from failing
       
  1606         self.chunkpad = curses.newpad(1, self.xscreensize)
       
  1607 
       
  1608         # add 1 so to account for last line text reaching end of line
       
  1609         self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
       
  1610         self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
       
  1611 
       
  1612         # initialize selecteitemendline (initial start-line is 0)
       
  1613         self.selecteditemendline = self.getnumlinesdisplayed(
       
  1614             self.currentselecteditem, recursechildren=False)
       
  1615 
       
  1616         while True:
       
  1617             self.updatescreen()
       
  1618             try:
       
  1619                 keypressed = self.statuswin.getkey()
       
  1620             except curses.error:
       
  1621                 keypressed = "foobar"
       
  1622             if self.handlekeypressed(keypressed):
       
  1623                 break