crecord.py
1596 lines
| 59.4 KiB
| text/x-python
|
PythonLexer
/ mercurial / crecord.py
|
r24310 | # stuff related specifically to patch manipulation / parsing | ||
# | ||||
# Copyright 2008 Mark Edgington <edgimar@gmail.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
# | ||||
# This code is based on the Mark Edgington's crecord extension. | ||||
# (Itself based on Bryan O'Sullivan's record extension.) | ||||
|
r24313 | from i18n import _ | ||
import patch as patchmod | ||||
import util, encoding | ||||
|
r24310 | |||
|
r24314 | import os, re, sys, fcntl, struct, termios, signal, tempfile, locale, cStringIO | ||
|
r24310 | # This is required for ncurses to display non-ASCII characters in default user | ||
# locale encoding correctly. --immerrr | ||||
locale.setlocale(locale.LC_ALL, '') | ||||
|
r24317 | |||
|
r24310 | # os.name is one of: 'posix', 'nt', 'dos', 'os2', 'mac', or 'ce' | ||
if os.name == 'posix': | ||||
import curses | ||||
else: | ||||
# I have no idea if wcurses works with crecord... | ||||
import wcurses as curses | ||||
try: | ||||
curses | ||||
except NameError: | ||||
raise util.Abort( | ||||
_('the python curses/wcurses module is not available/installed')) | ||||
|
r24316 | _origstdout = sys.__stdout__ # used by gethw() | ||
|
r24310 | |||
class patchnode(object): | ||||
"""abstract class for patch graph nodes | ||||
(i.e. patchroot, header, hunk, hunkline) | ||||
""" | ||||
def firstchild(self): | ||||
raise NotImplementedError("method must be implemented by subclass") | ||||
def lastchild(self): | ||||
raise NotImplementedError("method must be implemented by subclass") | ||||
def allchildren(self): | ||||
"Return a list of all of the direct children of this node" | ||||
raise NotImplementedError("method must be implemented by subclass") | ||||
def nextsibling(self): | ||||
""" | ||||
Return the closest next item of the same type where there are no items | ||||
of different types between the current item and this closest item. | ||||
If no such item exists, return None. | ||||
""" | ||||
raise NotImplementedError("method must be implemented by subclass") | ||||
def prevsibling(self): | ||||
""" | ||||
Return the closest previous item of the same type where there are no | ||||
items of different types between the current item and this closest item. | ||||
If no such item exists, return None. | ||||
""" | ||||
raise NotImplementedError("method must be implemented by subclass") | ||||
def parentitem(self): | ||||
raise NotImplementedError("method must be implemented by subclass") | ||||
def nextitem(self, constrainlevel=True, skipfolded=True): | ||||
""" | ||||
If constrainLevel == True, return the closest next item | ||||
of the same type where there are no items of different types between | ||||
the current item and this closest item. | ||||
If constrainLevel == False, then try to return the next item | ||||
closest to this item, regardless of item's type (header, hunk, or | ||||
HunkLine). | ||||
If skipFolded == True, and the current item is folded, then the child | ||||
items that are hidden due to folding will be skipped when determining | ||||
the next item. | ||||
If it is not possible to get the next item, return None. | ||||
""" | ||||
try: | ||||
itemfolded = self.folded | ||||
except AttributeError: | ||||
itemfolded = False | ||||
if constrainlevel: | ||||
return self.nextsibling() | ||||
elif skipfolded and itemfolded: | ||||
nextitem = self.nextsibling() | ||||
if nextitem is None: | ||||
try: | ||||
nextitem = self.parentitem().nextsibling() | ||||
except AttributeError: | ||||
nextitem = None | ||||
return nextitem | ||||
else: | ||||
# try child | ||||
item = self.firstchild() | ||||
if item is not None: | ||||
return item | ||||
# else try next sibling | ||||
item = self.nextsibling() | ||||
if item is not None: | ||||
return item | ||||
try: | ||||
# else try parent's next sibling | ||||
item = self.parentitem().nextsibling() | ||||
if item is not None: | ||||
return item | ||||
# else return grandparent's next sibling (or None) | ||||
return self.parentitem().parentitem().nextsibling() | ||||
except AttributeError: # parent and/or grandparent was None | ||||
return None | ||||
def previtem(self, constrainlevel=True, skipfolded=True): | ||||
""" | ||||
If constrainLevel == True, return the closest previous item | ||||
of the same type where there are no items of different types between | ||||
the current item and this closest item. | ||||
If constrainLevel == False, then try to return the previous item | ||||
closest to this item, regardless of item's type (header, hunk, or | ||||
HunkLine). | ||||
If skipFolded == True, and the current item is folded, then the items | ||||
that are hidden due to folding will be skipped when determining the | ||||
next item. | ||||
If it is not possible to get the previous item, return None. | ||||
""" | ||||
if constrainlevel: | ||||
return self.prevsibling() | ||||
else: | ||||
# try previous sibling's last child's last child, | ||||
# else try previous sibling's last child, else try previous sibling | ||||
prevsibling = self.prevsibling() | ||||
if prevsibling is not None: | ||||
prevsiblinglastchild = prevsibling.lastchild() | ||||
if ((prevsiblinglastchild is not None) and | ||||
not prevsibling.folded): | ||||
prevsiblinglclc = prevsiblinglastchild.lastchild() | ||||
if ((prevsiblinglclc is not None) and | ||||
not prevsiblinglastchild.folded): | ||||
return prevsiblinglclc | ||||
else: | ||||
return prevsiblinglastchild | ||||
else: | ||||
return prevsibling | ||||
# try parent (or None) | ||||
return self.parentitem() | ||||
class patch(patchnode, list): # todo: rename patchroot | ||||
""" | ||||
list of header objects representing the patch. | ||||
""" | ||||
def __init__(self, headerlist): | ||||
self.extend(headerlist) | ||||
# add parent patch object reference to each header | ||||
for header in self: | ||||
header.patch = self | ||||
class uiheader(patchnode): | ||||
"""patch header | ||||
xxx shoudn't we move this to mercurial/patch.py ? | ||||
""" | ||||
def __init__(self, header): | ||||
self.nonuiheader = header | ||||
# flag to indicate whether to apply this chunk | ||||
self.applied = True | ||||
# flag which only affects the status display indicating if a node's | ||||
# children are partially applied (i.e. some applied, some not). | ||||
self.partial = False | ||||
# flag to indicate whether to display as folded/unfolded to user | ||||
self.folded = True | ||||
# list of all headers in patch | ||||
self.patch = None | ||||
# flag is False if this header was ever unfolded from initial state | ||||
self.neverunfolded = True | ||||
self.hunks = [uihunk(h, self) for h in self.hunks] | ||||
def prettystr(self): | ||||
x = cStringIO.StringIO() | ||||
self.pretty(x) | ||||
return x.getvalue() | ||||
def nextsibling(self): | ||||
numheadersinpatch = len(self.patch) | ||||
indexofthisheader = self.patch.index(self) | ||||
if indexofthisheader < numheadersinpatch - 1: | ||||
nextheader = self.patch[indexofthisheader + 1] | ||||
return nextheader | ||||
else: | ||||
return None | ||||
def prevsibling(self): | ||||
indexofthisheader = self.patch.index(self) | ||||
if indexofthisheader > 0: | ||||
previousheader = self.patch[indexofthisheader - 1] | ||||
return previousheader | ||||
else: | ||||
return None | ||||
def parentitem(self): | ||||
""" | ||||
there is no 'real' parent item of a header that can be selected, | ||||
so return None. | ||||
""" | ||||
return None | ||||
def firstchild(self): | ||||
"return the first child of this item, if one exists. otherwise None." | ||||
if len(self.hunks) > 0: | ||||
return self.hunks[0] | ||||
else: | ||||
return None | ||||
def lastchild(self): | ||||
"return the last child of this item, if one exists. otherwise None." | ||||
if len(self.hunks) > 0: | ||||
return self.hunks[-1] | ||||
else: | ||||
return None | ||||
def allchildren(self): | ||||
"return a list of all of the direct children of this node" | ||||
return self.hunks | ||||
def __getattr__(self, name): | ||||
return getattr(self.nonuiheader, name) | ||||
class uihunkline(patchnode): | ||||
"represents a changed line in a hunk" | ||||
def __init__(self, linetext, hunk): | ||||
self.linetext = linetext | ||||
self.applied = True | ||||
# the parent hunk to which this line belongs | ||||
self.hunk = hunk | ||||
# folding lines currently is not used/needed, but this flag is needed | ||||
# in the previtem method. | ||||
self.folded = False | ||||
def prettystr(self): | ||||
return self.linetext | ||||
def nextsibling(self): | ||||
numlinesinhunk = len(self.hunk.changedlines) | ||||
indexofthisline = self.hunk.changedlines.index(self) | ||||
if (indexofthisline < numlinesinhunk - 1): | ||||
nextline = self.hunk.changedlines[indexofthisline + 1] | ||||
return nextline | ||||
else: | ||||
return None | ||||
def prevsibling(self): | ||||
indexofthisline = self.hunk.changedlines.index(self) | ||||
if indexofthisline > 0: | ||||
previousline = self.hunk.changedlines[indexofthisline - 1] | ||||
return previousline | ||||
else: | ||||
return None | ||||
def parentitem(self): | ||||
"return the parent to the current item" | ||||
return self.hunk | ||||
def firstchild(self): | ||||
"return the first child of this item, if one exists. otherwise None." | ||||
# hunk-lines don't have children | ||||
return None | ||||
def lastchild(self): | ||||
"return the last child of this item, if one exists. otherwise None." | ||||
# hunk-lines don't have children | ||||
return None | ||||
class uihunk(patchnode): | ||||
"""ui patch hunk, wraps a hunk and keep track of ui behavior """ | ||||
maxcontext = 3 | ||||
def __init__(self, hunk, header): | ||||
self._hunk = hunk | ||||
self.changedlines = [uihunkline(line, self) for line in hunk.hunk] | ||||
self.header = header | ||||
# used at end for detecting how many removed lines were un-applied | ||||
self.originalremoved = self.removed | ||||
# flag to indicate whether to display as folded/unfolded to user | ||||
self.folded = True | ||||
# flag to indicate whether to apply this chunk | ||||
self.applied = True | ||||
# flag which only affects the status display indicating if a node's | ||||
# children are partially applied (i.e. some applied, some not). | ||||
self.partial = False | ||||
def nextsibling(self): | ||||
numhunksinheader = len(self.header.hunks) | ||||
indexofthishunk = self.header.hunks.index(self) | ||||
if (indexofthishunk < numhunksinheader - 1): | ||||
nexthunk = self.header.hunks[indexofthishunk + 1] | ||||
return nexthunk | ||||
else: | ||||
return None | ||||
def prevsibling(self): | ||||
indexofthishunk = self.header.hunks.index(self) | ||||
if indexofthishunk > 0: | ||||
previoushunk = self.header.hunks[indexofthishunk - 1] | ||||
return previoushunk | ||||
else: | ||||
return None | ||||
def parentitem(self): | ||||
"return the parent to the current item" | ||||
return self.header | ||||
def firstchild(self): | ||||
"return the first child of this item, if one exists. otherwise None." | ||||
if len(self.changedlines) > 0: | ||||
return self.changedlines[0] | ||||
else: | ||||
return None | ||||
def lastchild(self): | ||||
"return the last child of this item, if one exists. otherwise None." | ||||
if len(self.changedlines) > 0: | ||||
return self.changedlines[-1] | ||||
else: | ||||
return None | ||||
def allchildren(self): | ||||
"return a list of all of the direct children of this node" | ||||
return self.changedlines | ||||
def countchanges(self): | ||||
"""changedlines -> (n+,n-)""" | ||||
add = len([l for l in self.changedlines if l.applied | ||||
and l.prettystr()[0] == '+']) | ||||
rem = len([l for l in self.changedlines if l.applied | ||||
and l.prettystr()[0] == '-']) | ||||
return add, rem | ||||
def getfromtoline(self): | ||||
# calculate the number of removed lines converted to context lines | ||||
removedconvertedtocontext = self.originalremoved - self.removed | ||||
contextlen = (len(self.before) + len(self.after) + | ||||
removedconvertedtocontext) | ||||
if self.after and self.after[-1] == '\\ no newline at end of file\n': | ||||
contextlen -= 1 | ||||
fromlen = contextlen + self.removed | ||||
tolen = contextlen + self.added | ||||
# diffutils manual, section "2.2.2.2 detailed description of unified | ||||
# format": "an empty hunk is considered to end at the line that | ||||
# precedes the hunk." | ||||
# | ||||
# so, if either of hunks is empty, decrease its line start. --immerrr | ||||
# but only do this if fromline > 0, to avoid having, e.g fromline=-1. | ||||
fromline, toline = self.fromline, self.toline | ||||
if fromline != 0: | ||||
if fromlen == 0: | ||||
fromline -= 1 | ||||
if tolen == 0: | ||||
toline -= 1 | ||||
fromtoline = '@@ -%d,%d +%d,%d @@%s\n' % ( | ||||
fromline, fromlen, toline, tolen, | ||||
self.proc and (' ' + self.proc)) | ||||
return fromtoline | ||||
def write(self, fp): | ||||
# updated self.added/removed, which are used by getfromtoline() | ||||
self.added, self.removed = self.countchanges() | ||||
fp.write(self.getfromtoline()) | ||||
hunklinelist = [] | ||||
# add the following to the list: (1) all applied lines, and | ||||
# (2) all unapplied removal lines (convert these to context lines) | ||||
for changedline in self.changedlines: | ||||
changedlinestr = changedline.prettystr() | ||||
if changedline.applied: | ||||
hunklinelist.append(changedlinestr) | ||||
elif changedlinestr[0] == "-": | ||||
hunklinelist.append(" " + changedlinestr[1:]) | ||||
fp.write(''.join(self.before + hunklinelist + self.after)) | ||||
pretty = write | ||||
def prettystr(self): | ||||
x = cStringIO.StringIO() | ||||
self.pretty(x) | ||||
return x.getvalue() | ||||
def __getattr__(self, name): | ||||
return getattr(self._hunk, name) | ||||
def __repr__(self): | ||||
return '<hunk %r@%d>' % (self.filename(), self.fromline) | ||||
|
r24318 | def filterpatch(ui, chunks, chunkselector): | ||
|
r24310 | """interactively filter patch chunks into applied-only chunks""" | ||
chunks = list(chunks) | ||||
# convert chunks list into structure suitable for displaying/modifying | ||||
# with curses. create a list of headers only. | ||||
headers = [c for c in chunks if isinstance(c, patchmod.header)] | ||||
# if there are no changed files | ||||
if len(headers) == 0: | ||||
return [] | ||||
uiheaders = [uiheader(h) for h in headers] | ||||
# let user choose headers/hunks/lines, and mark their applied flags | ||||
# accordingly | ||||
|
r24342 | chunkselector(ui, uiheaders) | ||
|
r24310 | appliedhunklist = [] | ||
for hdr in uiheaders: | ||||
if (hdr.applied and | ||||
(hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0)): | ||||
appliedhunklist.append(hdr) | ||||
fixoffset = 0 | ||||
for hnk in hdr.hunks: | ||||
if hnk.applied: | ||||
appliedhunklist.append(hnk) | ||||
# adjust the 'to'-line offset of the hunk to be correct | ||||
# after de-activating some of the other hunks for this file | ||||
if fixoffset: | ||||
#hnk = copy.copy(hnk) # necessary?? | ||||
hnk.toline += fixoffset | ||||
else: | ||||
fixoffset += hnk.removed - hnk.added | ||||
return appliedhunklist | ||||
def gethw(): | ||||
""" | ||||
magically get the current height and width of the window (without initscr) | ||||
this is a rip-off of a rip-off - taken from the bpython code. it is | ||||
useful / necessary because otherwise curses.initscr() must be called, | ||||
which can leave the terminal in a nasty state after exiting. | ||||
""" | ||||
h, w = struct.unpack( | ||||
|
r24316 | "hhhh", fcntl.ioctl(_origstdout, termios.TIOCGWINSZ, "\000"*8))[0:2] | ||
|
r24310 | return h, w | ||
|
r24342 | def chunkselector(ui, headerlist): | ||
|
r24310 | """ | ||
curses interface to get selection of chunks, and mark the applied flags | ||||
of the chosen chunks. | ||||
""" | ||||
chunkselector = curseschunkselector(headerlist, ui) | ||||
curses.wrapper(chunkselector.main) | ||||
def testdecorator(testfn, f): | ||||
def u(*args, **kwargs): | ||||
return f(testfn, *args, **kwargs) | ||||
return u | ||||
|
r24342 | def testchunkselector(testfn, ui, headerlist): | ||
|
r24310 | """ | ||
test interface to get selection of chunks, and mark the applied flags | ||||
of the chosen chunks. | ||||
""" | ||||
chunkselector = curseschunkselector(headerlist, ui) | ||||
if testfn and os.path.exists(testfn): | ||||
testf = open(testfn) | ||||
testcommands = map(lambda x: x.rstrip('\n'), testf.readlines()) | ||||
testf.close() | ||||
while True: | ||||
if chunkselector.handlekeypressed(testcommands.pop(0), test=True): | ||||
break | ||||
class curseschunkselector(object): | ||||
def __init__(self, headerlist, ui): | ||||
# put the headers into a patch object | ||||
self.headerlist = patch(headerlist) | ||||
self.ui = ui | ||||
# list of all chunks | ||||
self.chunklist = [] | ||||
for h in headerlist: | ||||
self.chunklist.append(h) | ||||
self.chunklist.extend(h.hunks) | ||||
# dictionary mapping (fgcolor, bgcolor) pairs to the | ||||
# corresponding curses color-pair value. | ||||
self.colorpairs = {} | ||||
# maps custom nicknames of color-pairs to curses color-pair values | ||||
self.colorpairnames = {} | ||||
# the currently selected header, hunk, or hunk-line | ||||
self.currentselecteditem = self.headerlist[0] | ||||
# updated when printing out patch-display -- the 'lines' here are the | ||||
# line positions *in the pad*, not on the screen. | ||||
self.selecteditemstartline = 0 | ||||
self.selecteditemendline = None | ||||
# define indentation levels | ||||
self.headerindentnumchars = 0 | ||||
self.hunkindentnumchars = 3 | ||||
self.hunklineindentnumchars = 6 | ||||
# the first line of the pad to print to the screen | ||||
self.firstlineofpadtoprint = 0 | ||||
# keeps track of the number of lines in the pad | ||||
self.numpadlines = None | ||||
self.numstatuslines = 2 | ||||
# keep a running count of the number of lines printed to the pad | ||||
# (used for determining when the selected item begins/ends) | ||||
self.linesprintedtopadsofar = 0 | ||||
# the first line of the pad which is visible on the screen | ||||
self.firstlineofpadtoprint = 0 | ||||
# stores optional text for a commit comment provided by the user | ||||
self.commenttext = "" | ||||
# if the last 'toggle all' command caused all changes to be applied | ||||
self.waslasttoggleallapplied = True | ||||
def uparrowevent(self): | ||||
""" | ||||
try to select the previous item to the current item that has the | ||||
most-indented level. for example, if a hunk is selected, try to select | ||||
the last hunkline of the hunk prior to the selected hunk. or, if | ||||
the first hunkline of a hunk is currently selected, then select the | ||||
hunk itself. | ||||
if the currently selected item is already at the top of the screen, | ||||
scroll the screen down to show the new-selected item. | ||||
""" | ||||
currentitem = self.currentselecteditem | ||||
nextitem = currentitem.previtem(constrainlevel=False) | ||||
if nextitem is None: | ||||
# if no parent item (i.e. currentitem is the first header), then | ||||
# no change... | ||||
nextitem = currentitem | ||||
self.currentselecteditem = nextitem | ||||
def uparrowshiftevent(self): | ||||
""" | ||||
select (if possible) the previous item on the same level as the | ||||
currently selected item. otherwise, select (if possible) the | ||||
parent-item of the currently selected item. | ||||
if the currently selected item is already at the top of the screen, | ||||
scroll the screen down to show the new-selected item. | ||||
""" | ||||
currentitem = self.currentselecteditem | ||||
nextitem = currentitem.previtem() | ||||
# if there's no previous item on this level, try choosing the parent | ||||
if nextitem is None: | ||||
nextitem = currentitem.parentitem() | ||||
if nextitem is None: | ||||
# if no parent item (i.e. currentitem is the first header), then | ||||
# no change... | ||||
nextitem = currentitem | ||||
self.currentselecteditem = nextitem | ||||
def downarrowevent(self): | ||||
""" | ||||
try to select the next item to the current item that has the | ||||
most-indented level. for example, if a hunk is selected, select | ||||
the first hunkline of the selected hunk. or, if the last hunkline of | ||||
a hunk is currently selected, then select the next hunk, if one exists, | ||||
or if not, the next header if one exists. | ||||
if the currently selected item is already at the bottom of the screen, | ||||
scroll the screen up to show the new-selected item. | ||||
""" | ||||
#self.startprintline += 1 #debug | ||||
currentitem = self.currentselecteditem | ||||
nextitem = currentitem.nextitem(constrainlevel=False) | ||||
# if there's no next item, keep the selection as-is | ||||
if nextitem is None: | ||||
nextitem = currentitem | ||||
self.currentselecteditem = nextitem | ||||
def downarrowshiftevent(self): | ||||
""" | ||||
if the cursor is already at the bottom chunk, scroll the screen up and | ||||
move the cursor-position to the subsequent chunk. otherwise, only move | ||||
the cursor position down one chunk. | ||||
""" | ||||
# todo: update docstring | ||||
currentitem = self.currentselecteditem | ||||
nextitem = currentitem.nextitem() | ||||
# if there's no previous item on this level, try choosing the parent's | ||||
# nextitem. | ||||
if nextitem is None: | ||||
try: | ||||
nextitem = currentitem.parentitem().nextitem() | ||||
except AttributeError: | ||||
# parentitem returned None, so nextitem() can't be called | ||||
nextitem = None | ||||
if nextitem is None: | ||||
# if no next item on parent-level, then no change... | ||||
nextitem = currentitem | ||||
self.currentselecteditem = nextitem | ||||
def rightarrowevent(self): | ||||
""" | ||||
select (if possible) the first of this item's child-items. | ||||
""" | ||||
currentitem = self.currentselecteditem | ||||
nextitem = currentitem.firstchild() | ||||
# turn off folding if we want to show a child-item | ||||
if currentitem.folded: | ||||
self.togglefolded(currentitem) | ||||
if nextitem is None: | ||||
# if no next item on parent-level, then no change... | ||||
nextitem = currentitem | ||||
self.currentselecteditem = nextitem | ||||
def leftarrowevent(self): | ||||
""" | ||||
if the current item can be folded (i.e. it is an unfolded header or | ||||
hunk), then fold it. otherwise try select (if possible) the parent | ||||
of this item. | ||||
""" | ||||
currentitem = self.currentselecteditem | ||||
# try to fold the item | ||||
if not isinstance(currentitem, uihunkline): | ||||
if not currentitem.folded: | ||||
self.togglefolded(item=currentitem) | ||||
return | ||||
# if it can't be folded, try to select the parent item | ||||
nextitem = currentitem.parentitem() | ||||
if nextitem is None: | ||||
# if no item on parent-level, then no change... | ||||
nextitem = currentitem | ||||
if not nextitem.folded: | ||||
self.togglefolded(item=nextitem) | ||||
self.currentselecteditem = nextitem | ||||
def leftarrowshiftevent(self): | ||||
""" | ||||
select the header of the current item (or fold current item if the | ||||
current item is already a header). | ||||
""" | ||||
currentitem = self.currentselecteditem | ||||
if isinstance(currentitem, uiheader): | ||||
if not currentitem.folded: | ||||
self.togglefolded(item=currentitem) | ||||
return | ||||
# select the parent item recursively until we're at a header | ||||
while True: | ||||
nextitem = currentitem.parentitem() | ||||
if nextitem is None: | ||||
break | ||||
else: | ||||
currentitem = nextitem | ||||
self.currentselecteditem = currentitem | ||||
def updatescroll(self): | ||||
"scroll the screen to fully show the currently-selected" | ||||
selstart = self.selecteditemstartline | ||||
selend = self.selecteditemendline | ||||
#selnumlines = selend - selstart | ||||
padstart = self.firstlineofpadtoprint | ||||
padend = padstart + self.yscreensize - self.numstatuslines - 1 | ||||
# 'buffered' pad start/end values which scroll with a certain | ||||
# top/bottom context margin | ||||
padstartbuffered = padstart + 3 | ||||
padendbuffered = padend - 3 | ||||
if selend > padendbuffered: | ||||
self.scrolllines(selend - padendbuffered) | ||||
elif selstart < padstartbuffered: | ||||
# negative values scroll in pgup direction | ||||
self.scrolllines(selstart - padstartbuffered) | ||||
def scrolllines(self, numlines): | ||||
"scroll the screen up (down) by numlines when numlines >0 (<0)." | ||||
self.firstlineofpadtoprint += numlines | ||||
if self.firstlineofpadtoprint < 0: | ||||
self.firstlineofpadtoprint = 0 | ||||
if self.firstlineofpadtoprint > self.numpadlines - 1: | ||||
self.firstlineofpadtoprint = self.numpadlines - 1 | ||||
def toggleapply(self, item=None): | ||||
""" | ||||
toggle the applied flag of the specified item. if no item is specified, | ||||
toggle the flag of the currently selected item. | ||||
""" | ||||
if item is None: | ||||
item = self.currentselecteditem | ||||
item.applied = not item.applied | ||||
if isinstance(item, uiheader): | ||||
item.partial = False | ||||
if item.applied: | ||||
if not item.special(): | ||||
# apply all its hunks | ||||
for hnk in item.hunks: | ||||
hnk.applied = True | ||||
# apply all their hunklines | ||||
for hunkline in hnk.changedlines: | ||||
hunkline.applied = True | ||||
else: | ||||
# all children are off (but the header is on) | ||||
if len(item.allchildren()) > 0: | ||||
item.partial = True | ||||
else: | ||||
# un-apply all its hunks | ||||
for hnk in item.hunks: | ||||
hnk.applied = False | ||||
hnk.partial = False | ||||
# un-apply all their hunklines | ||||
for hunkline in hnk.changedlines: | ||||
hunkline.applied = False | ||||
elif isinstance(item, uihunk): | ||||
item.partial = False | ||||
# apply all it's hunklines | ||||
for hunkline in item.changedlines: | ||||
hunkline.applied = item.applied | ||||
siblingappliedstatus = [hnk.applied for hnk in item.header.hunks] | ||||
allsiblingsapplied = not (False in siblingappliedstatus) | ||||
nosiblingsapplied = not (True in siblingappliedstatus) | ||||
siblingspartialstatus = [hnk.partial for hnk in item.header.hunks] | ||||
somesiblingspartial = (True in siblingspartialstatus) | ||||
#cases where applied or partial should be removed from header | ||||
# if no 'sibling' hunks are applied (including this hunk) | ||||
if nosiblingsapplied: | ||||
if not item.header.special(): | ||||
item.header.applied = False | ||||
item.header.partial = False | ||||
else: # some/all parent siblings are applied | ||||
item.header.applied = True | ||||
item.header.partial = (somesiblingspartial or | ||||
not allsiblingsapplied) | ||||
elif isinstance(item, uihunkline): | ||||
siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines] | ||||
allsiblingsapplied = not (False in siblingappliedstatus) | ||||
nosiblingsapplied = not (True in siblingappliedstatus) | ||||
# if no 'sibling' lines are applied | ||||
if nosiblingsapplied: | ||||
item.hunk.applied = False | ||||
item.hunk.partial = False | ||||
elif allsiblingsapplied: | ||||
item.hunk.applied = True | ||||
item.hunk.partial = False | ||||
else: # some siblings applied | ||||
item.hunk.applied = True | ||||
item.hunk.partial = True | ||||
parentsiblingsapplied = [hnk.applied for hnk | ||||
in item.hunk.header.hunks] | ||||
noparentsiblingsapplied = not (True in parentsiblingsapplied) | ||||
allparentsiblingsapplied = not (False in parentsiblingsapplied) | ||||
parentsiblingspartial = [hnk.partial for hnk | ||||
in item.hunk.header.hunks] | ||||
someparentsiblingspartial = (True in parentsiblingspartial) | ||||
# if all parent hunks are not applied, un-apply header | ||||
if noparentsiblingsapplied: | ||||
if not item.hunk.header.special(): | ||||
item.hunk.header.applied = False | ||||
item.hunk.header.partial = False | ||||
# set the applied and partial status of the header if needed | ||||
else: # some/all parent siblings are applied | ||||
item.hunk.header.applied = True | ||||
item.hunk.header.partial = (someparentsiblingspartial or | ||||
not allparentsiblingsapplied) | ||||
def toggleall(self): | ||||
"toggle the applied flag of all items." | ||||
if self.waslasttoggleallapplied: # then unapply them this time | ||||
for item in self.headerlist: | ||||
if item.applied: | ||||
self.toggleapply(item) | ||||
else: | ||||
for item in self.headerlist: | ||||
if not item.applied: | ||||
self.toggleapply(item) | ||||
self.waslasttoggleallapplied = not self.waslasttoggleallapplied | ||||
def togglefolded(self, item=None, foldparent=False): | ||||
"toggle folded flag of specified item (defaults to currently selected)" | ||||
if item is None: | ||||
item = self.currentselecteditem | ||||
if foldparent or (isinstance(item, uiheader) and item.neverunfolded): | ||||
if not isinstance(item, uiheader): | ||||
# we need to select the parent item in this case | ||||
self.currentselecteditem = item = item.parentitem() | ||||
elif item.neverunfolded: | ||||
item.neverunfolded = False | ||||
# also fold any foldable children of the parent/current item | ||||
if isinstance(item, uiheader): # the original or 'new' item | ||||
for child in item.allchildren(): | ||||
child.folded = not item.folded | ||||
if isinstance(item, (uiheader, uihunk)): | ||||
item.folded = not item.folded | ||||
def alignstring(self, instr, window): | ||||
""" | ||||
add whitespace to the end of a string in order to make it fill | ||||
the screen in the x direction. the current cursor position is | ||||
taken into account when making this calculation. the string can span | ||||
multiple lines. | ||||
""" | ||||
y, xstart = window.getyx() | ||||
width = self.xscreensize | ||||
# turn tabs into spaces | ||||
instr = instr.expandtabs(4) | ||||
|
r24315 | strwidth = encoding.ucolwidth(instr) | ||
numspaces = (width - ((strwidth + xstart) % width) - 1) | ||||
|
r24310 | return instr + " " * numspaces + "\n" | ||
def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None, | ||||
pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False): | ||||
""" | ||||
print the string, text, with the specified colors and attributes, to | ||||
the specified curses window object. | ||||
the foreground and background colors are of the form | ||||
curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green, | ||||
magenta, red, white, yellow]. if pairname is provided, a color | ||||
pair will be looked up in the self.colorpairnames dictionary. | ||||
attrlist is a list containing text attributes in the form of | ||||
curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout, | ||||
underline]. | ||||
if align == True, whitespace is added to the printed string such that | ||||
the string stretches to the right border of the window. | ||||
if showwhtspc == True, trailing whitespace of a string is highlighted. | ||||
""" | ||||
# preprocess the text, converting tabs to spaces | ||||
text = text.expandtabs(4) | ||||
# strip \n, and convert control characters to ^[char] representation | ||||
text = re.sub(r'[\x00-\x08\x0a-\x1f]', | ||||
lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n')) | ||||
if pair is not None: | ||||
colorpair = pair | ||||
elif pairname is not None: | ||||
colorpair = self.colorpairnames[pairname] | ||||
else: | ||||
if fgcolor is None: | ||||
fgcolor = -1 | ||||
if bgcolor is None: | ||||
bgcolor = -1 | ||||
if (fgcolor, bgcolor) in self.colorpairs: | ||||
colorpair = self.colorpairs[(fgcolor, bgcolor)] | ||||
else: | ||||
colorpair = self.getcolorpair(fgcolor, bgcolor) | ||||
# add attributes if possible | ||||
if attrlist is None: | ||||
attrlist = [] | ||||
if colorpair < 256: | ||||
# then it is safe to apply all attributes | ||||
for textattr in attrlist: | ||||
colorpair |= textattr | ||||
else: | ||||
# just apply a select few (safe?) attributes | ||||
for textattr in (curses.A_UNDERLINE, curses.A_BOLD): | ||||
if textattr in attrlist: | ||||
colorpair |= textattr | ||||
y, xstart = self.chunkpad.getyx() | ||||
t = "" # variable for counting lines printed | ||||
# if requested, show trailing whitespace | ||||
if showwhtspc: | ||||
origlen = len(text) | ||||
text = text.rstrip(' \n') # tabs have already been expanded | ||||
strippedlen = len(text) | ||||
numtrailingspaces = origlen - strippedlen | ||||
if towin: | ||||
window.addstr(text, colorpair) | ||||
t += text | ||||
if showwhtspc: | ||||
wscolorpair = colorpair | curses.A_REVERSE | ||||
if towin: | ||||
for i in range(numtrailingspaces): | ||||
window.addch(curses.ACS_CKBOARD, wscolorpair) | ||||
t += " " * numtrailingspaces | ||||
if align: | ||||
if towin: | ||||
extrawhitespace = self.alignstring("", window) | ||||
window.addstr(extrawhitespace, colorpair) | ||||
else: | ||||
# need to use t, since the x position hasn't incremented | ||||
extrawhitespace = self.alignstring(t, window) | ||||
t += extrawhitespace | ||||
# is reset to 0 at the beginning of printitem() | ||||
linesprinted = (xstart + len(t)) / self.xscreensize | ||||
self.linesprintedtopadsofar += linesprinted | ||||
return t | ||||
def updatescreen(self): | ||||
self.statuswin.erase() | ||||
self.chunkpad.erase() | ||||
printstring = self.printstring | ||||
# print out the status lines at the top | ||||
try: | ||||
printstring(self.statuswin, | ||||
"SELECT CHUNKS: (j/k/up/dn/pgup/pgdn) move cursor; " | ||||
"(space/A) toggle hunk/all; (e)dit hunk;", | ||||
pairname="legend") | ||||
printstring(self.statuswin, | ||||
" (f)old/unfold; (c)ommit applied; (q)uit; (?) help " | ||||
"| [X]=hunk applied **=folded", | ||||
pairname="legend") | ||||
except curses.error: | ||||
pass | ||||
# print out the patch in the remaining part of the window | ||||
try: | ||||
self.printitem() | ||||
self.updatescroll() | ||||
self.chunkpad.refresh(self.firstlineofpadtoprint, 0, | ||||
self.numstatuslines, 0, | ||||
self.yscreensize + 1 - self.numstatuslines, | ||||
self.xscreensize) | ||||
except curses.error: | ||||
pass | ||||
# refresh([pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol]) | ||||
self.statuswin.refresh() | ||||
def getstatusprefixstring(self, item): | ||||
""" | ||||
create a string to prefix a line with which indicates whether 'item' | ||||
is applied and/or folded. | ||||
""" | ||||
# create checkbox string | ||||
if item.applied: | ||||
if not isinstance(item, uihunkline) and item.partial: | ||||
checkbox = "[~]" | ||||
else: | ||||
checkbox = "[x]" | ||||
else: | ||||
checkbox = "[ ]" | ||||
try: | ||||
if item.folded: | ||||
checkbox += "**" | ||||
if isinstance(item, uiheader): | ||||
# one of "m", "a", or "d" (modified, added, deleted) | ||||
filestatus = item.changetype | ||||
checkbox += filestatus + " " | ||||
else: | ||||
checkbox += " " | ||||
if isinstance(item, uiheader): | ||||
# add two more spaces for headers | ||||
checkbox += " " | ||||
except AttributeError: # not foldable | ||||
checkbox += " " | ||||
return checkbox | ||||
def printheader(self, header, selected=False, towin=True, | ||||
ignorefolding=False): | ||||
""" | ||||
print the header to the pad. if countlines is True, don't print | ||||
anything, but just count the number of lines which would be printed. | ||||
""" | ||||
outstr = "" | ||||
text = header.prettystr() | ||||
chunkindex = self.chunklist.index(header) | ||||
if chunkindex != 0 and not header.folded: | ||||
# add separating line before headers | ||||
outstr += self.printstring(self.chunkpad, '_' * self.xscreensize, | ||||
towin=towin, align=False) | ||||
# select color-pair based on if the header is selected | ||||
colorpair = self.getcolorpair(name=selected and "selected" or "normal", | ||||
attrlist=[curses.A_BOLD]) | ||||
# print out each line of the chunk, expanding it to screen width | ||||
# number of characters to indent lines on this level by | ||||
indentnumchars = 0 | ||||
checkbox = self.getstatusprefixstring(header) | ||||
if not header.folded or ignorefolding: | ||||
textlist = text.split("\n") | ||||
linestr = checkbox + textlist[0] | ||||
else: | ||||
linestr = checkbox + header.filename() | ||||
outstr += self.printstring(self.chunkpad, linestr, pair=colorpair, | ||||
towin=towin) | ||||
if not header.folded or ignorefolding: | ||||
if len(textlist) > 1: | ||||
for line in textlist[1:]: | ||||
linestr = " "*(indentnumchars + len(checkbox)) + line | ||||
outstr += self.printstring(self.chunkpad, linestr, | ||||
pair=colorpair, towin=towin) | ||||
return outstr | ||||
def printhunklinesbefore(self, hunk, selected=False, towin=True, | ||||
ignorefolding=False): | ||||
"includes start/end line indicator" | ||||
outstr = "" | ||||
# where hunk is in list of siblings | ||||
hunkindex = hunk.header.hunks.index(hunk) | ||||
if hunkindex != 0: | ||||
# add separating line before headers | ||||
outstr += self.printstring(self.chunkpad, ' '*self.xscreensize, | ||||
towin=towin, align=False) | ||||
colorpair = self.getcolorpair(name=selected and "selected" or "normal", | ||||
attrlist=[curses.A_BOLD]) | ||||
# print out from-to line with checkbox | ||||
checkbox = self.getstatusprefixstring(hunk) | ||||
lineprefix = " "*self.hunkindentnumchars + checkbox | ||||
frtoline = " " + hunk.getfromtoline().strip("\n") | ||||
outstr += self.printstring(self.chunkpad, lineprefix, towin=towin, | ||||
align=False) # add uncolored checkbox/indent | ||||
outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair, | ||||
towin=towin) | ||||
if hunk.folded and not ignorefolding: | ||||
# skip remainder of output | ||||
return outstr | ||||
# print out lines of the chunk preceeding changed-lines | ||||
for line in hunk.before: | ||||
linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line | ||||
outstr += self.printstring(self.chunkpad, linestr, towin=towin) | ||||
return outstr | ||||
def printhunklinesafter(self, hunk, towin=True, ignorefolding=False): | ||||
outstr = "" | ||||
if hunk.folded and not ignorefolding: | ||||
return outstr | ||||
# a bit superfluous, but to avoid hard-coding indent amount | ||||
checkbox = self.getstatusprefixstring(hunk) | ||||
for line in hunk.after: | ||||
linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line | ||||
outstr += self.printstring(self.chunkpad, linestr, towin=towin) | ||||
return outstr | ||||
def printhunkchangedline(self, hunkline, selected=False, towin=True): | ||||
outstr = "" | ||||
checkbox = self.getstatusprefixstring(hunkline) | ||||
linestr = hunkline.prettystr().strip("\n") | ||||
# select color-pair based on whether line is an addition/removal | ||||
if selected: | ||||
colorpair = self.getcolorpair(name="selected") | ||||
elif linestr.startswith("+"): | ||||
colorpair = self.getcolorpair(name="addition") | ||||
elif linestr.startswith("-"): | ||||
colorpair = self.getcolorpair(name="deletion") | ||||
elif linestr.startswith("\\"): | ||||
colorpair = self.getcolorpair(name="normal") | ||||
lineprefix = " "*self.hunklineindentnumchars + checkbox | ||||
outstr += self.printstring(self.chunkpad, lineprefix, towin=towin, | ||||
align=False) # add uncolored checkbox/indent | ||||
outstr += self.printstring(self.chunkpad, linestr, pair=colorpair, | ||||
towin=towin, showwhtspc=True) | ||||
return outstr | ||||
def printitem(self, item=None, ignorefolding=False, recursechildren=True, | ||||
towin=True): | ||||
""" | ||||
use __printitem() to print the the specified item.applied. | ||||
if item is not specified, then print the entire patch. | ||||
(hiding folded elements, etc. -- see __printitem() docstring) | ||||
""" | ||||
if item is None: | ||||
item = self.headerlist | ||||
if recursechildren: | ||||
self.linesprintedtopadsofar = 0 | ||||
outstr = [] | ||||
self.__printitem(item, ignorefolding, recursechildren, outstr, | ||||
towin=towin) | ||||
return ''.join(outstr) | ||||
def outofdisplayedarea(self): | ||||
y, _ = self.chunkpad.getyx() # cursor location | ||||
# * 2 here works but an optimization would be the max number of | ||||
# consecutive non selectable lines | ||||
# i.e the max number of context line for any hunk in the patch | ||||
miny = min(0, self.firstlineofpadtoprint - self.yscreensize) | ||||
maxy = self.firstlineofpadtoprint + self.yscreensize * 2 | ||||
return y < miny or y > maxy | ||||
def handleselection(self, item, recursechildren): | ||||
selected = (item is self.currentselecteditem) | ||||
if selected and recursechildren: | ||||
# assumes line numbering starting from line 0 | ||||
self.selecteditemstartline = self.linesprintedtopadsofar | ||||
selecteditemlines = self.getnumlinesdisplayed(item, | ||||
recursechildren=False) | ||||
self.selecteditemendline = (self.selecteditemstartline + | ||||
selecteditemlines - 1) | ||||
return selected | ||||
def __printitem(self, item, ignorefolding, recursechildren, outstr, | ||||
towin=True): | ||||
""" | ||||
recursive method for printing out patch/header/hunk/hunk-line data to | ||||
screen. also returns a string with all of the content of the displayed | ||||
patch (not including coloring, etc.). | ||||
if ignorefolding is True, then folded items are printed out. | ||||
if recursechildren is False, then only print the item without its | ||||
child items. | ||||
""" | ||||
if towin and self.outofdisplayedarea(): | ||||
return | ||||
selected = self.handleselection(item, recursechildren) | ||||
# patch object is a list of headers | ||||
if isinstance(item, patch): | ||||
if recursechildren: | ||||
for hdr in item: | ||||
self.__printitem(hdr, ignorefolding, | ||||
recursechildren, outstr, towin) | ||||
# todo: eliminate all isinstance() calls | ||||
if isinstance(item, uiheader): | ||||
outstr.append(self.printheader(item, selected, towin=towin, | ||||
ignorefolding=ignorefolding)) | ||||
if recursechildren: | ||||
for hnk in item.hunks: | ||||
self.__printitem(hnk, ignorefolding, | ||||
recursechildren, outstr, towin) | ||||
elif (isinstance(item, uihunk) and | ||||
((not item.header.folded) or ignorefolding)): | ||||
# print the hunk data which comes before the changed-lines | ||||
outstr.append(self.printhunklinesbefore(item, selected, towin=towin, | ||||
ignorefolding=ignorefolding)) | ||||
if recursechildren: | ||||
for l in item.changedlines: | ||||
self.__printitem(l, ignorefolding, | ||||
recursechildren, outstr, towin) | ||||
outstr.append(self.printhunklinesafter(item, towin=towin, | ||||
ignorefolding=ignorefolding)) | ||||
elif (isinstance(item, uihunkline) and | ||||
((not item.hunk.folded) or ignorefolding)): | ||||
outstr.append(self.printhunkchangedline(item, selected, | ||||
towin=towin)) | ||||
return outstr | ||||
def getnumlinesdisplayed(self, item=None, ignorefolding=False, | ||||
recursechildren=True): | ||||
""" | ||||
return the number of lines which would be displayed if the item were | ||||
to be printed to the display. the item will not be printed to the | ||||
display (pad). | ||||
if no item is given, assume the entire patch. | ||||
if ignorefolding is True, folded items will be unfolded when counting | ||||
the number of lines. | ||||
""" | ||||
# temporarily disable printing to windows by printstring | ||||
patchdisplaystring = self.printitem(item, ignorefolding, | ||||
recursechildren, towin=False) | ||||
numlines = len(patchdisplaystring) / self.xscreensize | ||||
return numlines | ||||
def sigwinchhandler(self, n, frame): | ||||
"handle window resizing" | ||||
try: | ||||
curses.endwin() | ||||
self.yscreensize, self.xscreensize = gethw() | ||||
self.statuswin.resize(self.numstatuslines, self.xscreensize) | ||||
self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1 | ||||
self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize) | ||||
# todo: try to resize commit message window if possible | ||||
except curses.error: | ||||
pass | ||||
def getcolorpair(self, fgcolor=None, bgcolor=None, name=None, | ||||
attrlist=None): | ||||
""" | ||||
get a curses color pair, adding it to self.colorpairs if it is not | ||||
already defined. an optional string, name, can be passed as a shortcut | ||||
for referring to the color-pair. by default, if no arguments are | ||||
specified, the white foreground / black background color-pair is | ||||
returned. | ||||
it is expected that this function will be used exclusively for | ||||
initializing color pairs, and not curses.init_pair(). | ||||
attrlist is used to 'flavor' the returned color-pair. this information | ||||
is not stored in self.colorpairs. it contains attribute values like | ||||
curses.A_BOLD. | ||||
""" | ||||
if (name is not None) and name in self.colorpairnames: | ||||
# then get the associated color pair and return it | ||||
colorpair = self.colorpairnames[name] | ||||
else: | ||||
if fgcolor is None: | ||||
fgcolor = -1 | ||||
if bgcolor is None: | ||||
bgcolor = -1 | ||||
if (fgcolor, bgcolor) in self.colorpairs: | ||||
colorpair = self.colorpairs[(fgcolor, bgcolor)] | ||||
else: | ||||
pairindex = len(self.colorpairs) + 1 | ||||
curses.init_pair(pairindex, fgcolor, bgcolor) | ||||
colorpair = self.colorpairs[(fgcolor, bgcolor)] = ( | ||||
curses.color_pair(pairindex)) | ||||
if name is not None: | ||||
self.colorpairnames[name] = curses.color_pair(pairindex) | ||||
# add attributes if possible | ||||
if attrlist is None: | ||||
attrlist = [] | ||||
if colorpair < 256: | ||||
# then it is safe to apply all attributes | ||||
for textattr in attrlist: | ||||
colorpair |= textattr | ||||
else: | ||||
# just apply a select few (safe?) attributes | ||||
for textattrib in (curses.A_UNDERLINE, curses.A_BOLD): | ||||
if textattrib in attrlist: | ||||
colorpair |= textattrib | ||||
return colorpair | ||||
def initcolorpair(self, *args, **kwargs): | ||||
"same as getcolorpair." | ||||
self.getcolorpair(*args, **kwargs) | ||||
def helpwindow(self): | ||||
"print a help window to the screen. exit after any keypress." | ||||
helptext = """ [press any key to return to the patch-display] | ||||
crecord allows you to interactively choose among the changes you have made, | ||||
and commit only those changes you select. after committing the selected | ||||
changes, the unselected changes are still present in your working copy, so you | ||||
can use crecord multiple times to split large changes into smaller changesets. | ||||
the following are valid keystrokes: | ||||
[space] : (un-)select item ([~]/[x] = partly/fully applied) | ||||
a : (un-)select all items | ||||
up/down-arrow [k/j] : go to previous/next unfolded item | ||||
pgup/pgdn [k/j] : go to previous/next item of same type | ||||
right/left-arrow [l/h] : go to child item / parent item | ||||
shift-left-arrow [h] : go to parent header / fold selected header | ||||
f : fold / unfold item, hiding/revealing its children | ||||
f : fold / unfold parent item and all of its ancestors | ||||
m : edit / resume editing the commit message | ||||
e : edit the currently selected hunk | ||||
a : toggle amend mode (hg rev >= 2.2) | ||||
c : commit selected changes | ||||
r : review/edit and commit selected changes | ||||
q : quit without committing (no changes will be made) | ||||
? : help (what you're currently reading)""" | ||||
helpwin = curses.newwin(self.yscreensize, 0, 0, 0) | ||||
helplines = helptext.split("\n") | ||||
helplines = helplines + [" "]*( | ||||
self.yscreensize - self.numstatuslines - len(helplines) - 1) | ||||
try: | ||||
for line in helplines: | ||||
self.printstring(helpwin, line, pairname="legend") | ||||
except curses.error: | ||||
pass | ||||
helpwin.refresh() | ||||
try: | ||||
helpwin.getkey() | ||||
except curses.error: | ||||
pass | ||||
def confirmationwindow(self, windowtext): | ||||
"display an informational window, then wait for and return a keypress." | ||||
confirmwin = curses.newwin(self.yscreensize, 0, 0, 0) | ||||
try: | ||||
lines = windowtext.split("\n") | ||||
for line in lines: | ||||
self.printstring(confirmwin, line, pairname="selected") | ||||
except curses.error: | ||||
pass | ||||
self.stdscr.refresh() | ||||
confirmwin.refresh() | ||||
try: | ||||
response = chr(self.stdscr.getch()) | ||||
except ValueError: | ||||
response = None | ||||
return response | ||||
def confirmcommit(self, review=False): | ||||
"ask for 'y' to be pressed to confirm commit. return True if confirmed." | ||||
if review: | ||||
confirmtext = ( | ||||
"""if you answer yes to the following, the your currently chosen patch chunks | ||||
will be loaded into an editor. you may modify the patch from the editor, and | ||||
save the changes if you wish to change the patch. otherwise, you can just | ||||
close the editor without saving to accept the current patch as-is. | ||||
note: don't add/remove lines unless you also modify the range information. | ||||
failing to follow this rule will result in the commit aborting. | ||||
are you sure you want to review/edit and commit the selected changes [yn]? """) | ||||
else: | ||||
confirmtext = ( | ||||
"are you sure you want to commit the selected changes [yn]? ") | ||||
response = self.confirmationwindow(confirmtext) | ||||
if response is None: | ||||
response = "n" | ||||
if response.lower().startswith("y"): | ||||
return True | ||||
else: | ||||
return False | ||||
def recenterdisplayedarea(self): | ||||
""" | ||||
once we scrolled with pg up pg down we can be pointing outside of the | ||||
display zone. we print the patch with towin=False to compute the | ||||
location of the selected item eventhough it is outside of the displayed | ||||
zone and then update the scroll. | ||||
""" | ||||
self.printitem(towin=False) | ||||
self.updatescroll() | ||||
def toggleedit(self, item=None, test=False): | ||||
""" | ||||
edit the currently chelected chunk | ||||
""" | ||||
def editpatchwitheditor(self, chunk): | ||||
if chunk is None: | ||||
self.ui.write(_('cannot edit patch for whole file')) | ||||
self.ui.write("\n") | ||||
return None | ||||
if chunk.header.binary(): | ||||
self.ui.write(_('cannot edit patch for binary file')) | ||||
self.ui.write("\n") | ||||
return None | ||||
# patch comment based on the git one (based on comment at end of | ||||
# http://mercurial.selenic.com/wiki/recordextension) | ||||
phelp = '---' + _(""" | ||||
to remove '-' lines, make them ' ' lines (context). | ||||
to remove '+' lines, delete them. | ||||
lines starting with # will be removed from the patch. | ||||
if the patch applies cleanly, the edited hunk will immediately be | ||||
added to the record list. if it does not apply cleanly, a rejects | ||||
file will be generated: you can use that when you try again. if | ||||
all lines of the hunk are removed, then the edit is aborted and | ||||
the hunk is left unchanged. | ||||
""") | ||||
(patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-", | ||||
suffix=".diff", text=True) | ||||
ncpatchfp = None | ||||
try: | ||||
# write the initial patch | ||||
f = os.fdopen(patchfd, "w") | ||||
chunk.header.write(f) | ||||
chunk.write(f) | ||||
f.write('\n'.join(['# ' + i for i in phelp.splitlines()])) | ||||
f.close() | ||||
# start the editor and wait for it to complete | ||||
editor = self.ui.geteditor() | ||||
self.ui.system("%s \"%s\"" % (editor, patchfn), | ||||
environ={'hguser': self.ui.username()}, | ||||
onerr=util.Abort, errprefix=_("edit failed")) | ||||
# remove comment lines | ||||
patchfp = open(patchfn) | ||||
ncpatchfp = cStringIO.StringIO() | ||||
for line in patchfp: | ||||
if not line.startswith('#'): | ||||
ncpatchfp.write(line) | ||||
patchfp.close() | ||||
ncpatchfp.seek(0) | ||||
newpatches = patchmod.parsepatch(ncpatchfp) | ||||
finally: | ||||
os.unlink(patchfn) | ||||
del ncpatchfp | ||||
return newpatches | ||||
if item is None: | ||||
item = self.currentselecteditem | ||||
if isinstance(item, uiheader): | ||||
return | ||||
if isinstance(item, uihunkline): | ||||
item = item.parentitem() | ||||
if not isinstance(item, uihunk): | ||||
return | ||||
beforeadded, beforeremoved = item.added, item.removed | ||||
newpatches = editpatchwitheditor(self, item) | ||||
header = item.header | ||||
editedhunkindex = header.hunks.index(item) | ||||
hunksbefore = header.hunks[:editedhunkindex] | ||||
hunksafter = header.hunks[editedhunkindex + 1:] | ||||
newpatchheader = newpatches[0] | ||||
newhunks = [uihunk(h, header) for h in newpatchheader.hunks] | ||||
newadded = sum([h.added for h in newhunks]) | ||||
newremoved = sum([h.removed for h in newhunks]) | ||||
offset = (newadded - beforeadded) - (newremoved - beforeremoved) | ||||
for h in hunksafter: | ||||
h.toline += offset | ||||
for h in newhunks: | ||||
h.folded = False | ||||
header.hunks = hunksbefore + newhunks + hunksafter | ||||
if self.emptypatch(): | ||||
header.hunks = hunksbefore + [item] + hunksafter | ||||
self.currentselecteditem = header | ||||
if not test: | ||||
self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1 | ||||
self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize) | ||||
self.updatescroll() | ||||
self.stdscr.refresh() | ||||
self.statuswin.refresh() | ||||
self.stdscr.keypad(1) | ||||
def emptypatch(self): | ||||
item = self.headerlist | ||||
if not item: | ||||
return True | ||||
for header in item: | ||||
if header.hunks: | ||||
return False | ||||
return True | ||||
def handlekeypressed(self, keypressed, test=False): | ||||
if keypressed in ["k", "KEY_UP"]: | ||||
self.uparrowevent() | ||||
if keypressed in ["k", "KEY_PPAGE"]: | ||||
self.uparrowshiftevent() | ||||
elif keypressed in ["j", "KEY_DOWN"]: | ||||
self.downarrowevent() | ||||
elif keypressed in ["j", "KEY_NPAGE"]: | ||||
self.downarrowshiftevent() | ||||
elif keypressed in ["l", "KEY_RIGHT"]: | ||||
self.rightarrowevent() | ||||
elif keypressed in ["h", "KEY_LEFT"]: | ||||
self.leftarrowevent() | ||||
elif keypressed in ["h", "KEY_SLEFT"]: | ||||
self.leftarrowshiftevent() | ||||
elif keypressed in ["q"]: | ||||
raise util.Abort(_('user quit')) | ||||
elif keypressed in ["c"]: | ||||
if self.confirmcommit(): | ||||
return True | ||||
elif keypressed in ["r"]: | ||||
if self.confirmcommit(review=True): | ||||
return True | ||||
elif test and keypressed in ['X']: | ||||
return True | ||||
elif keypressed in [' '] or (test and keypressed in ["TOGGLE"]): | ||||
self.toggleapply() | ||||
elif keypressed in ['A']: | ||||
self.toggleall() | ||||
elif keypressed in ['e']: | ||||
self.toggleedit(test=test) | ||||
elif keypressed in ["f"]: | ||||
self.togglefolded() | ||||
elif keypressed in ["f"]: | ||||
self.togglefolded(foldparent=True) | ||||
elif keypressed in ["?"]: | ||||
self.helpwindow() | ||||
def main(self, stdscr): | ||||
""" | ||||
method to be wrapped by curses.wrapper() for selecting chunks. | ||||
""" | ||||
signal.signal(signal.SIGWINCH, self.sigwinchhandler) | ||||
self.stdscr = stdscr | ||||
self.yscreensize, self.xscreensize = self.stdscr.getmaxyx() | ||||
curses.start_color() | ||||
curses.use_default_colors() | ||||
# available colors: black, blue, cyan, green, magenta, white, yellow | ||||
# init_pair(color_id, foreground_color, background_color) | ||||
self.initcolorpair(None, None, name="normal") | ||||
self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA, | ||||
name="selected") | ||||
self.initcolorpair(curses.COLOR_RED, None, name="deletion") | ||||
self.initcolorpair(curses.COLOR_GREEN, None, name="addition") | ||||
self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend") | ||||
# newwin([height, width,] begin_y, begin_x) | ||||
self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0) | ||||
self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences | ||||
# figure out how much space to allocate for the chunk-pad which is | ||||
# used for displaying the patch | ||||
# stupid hack to prevent getnumlinesdisplayed from failing | ||||
self.chunkpad = curses.newpad(1, self.xscreensize) | ||||
# add 1 so to account for last line text reaching end of line | ||||
self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1 | ||||
self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize) | ||||
# initialize selecteitemendline (initial start-line is 0) | ||||
self.selecteditemendline = self.getnumlinesdisplayed( | ||||
self.currentselecteditem, recursechildren=False) | ||||
while True: | ||||
self.updatescreen() | ||||
try: | ||||
keypressed = self.statuswin.getkey() | ||||
except curses.error: | ||||
keypressed = "foobar" | ||||
if self.handlekeypressed(keypressed): | ||||
break | ||||