crecord.py
2034 lines
| 70.9 KiB
| text/x-python
|
PythonLexer
/ mercurial / crecord.py
Laurent Charignon
|
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.) | ||||
Gregory Szorc
|
r25940 | from __future__ import absolute_import | ||
Laurent Charignon
|
r24310 | |||
Gregory Szorc
|
r25940 | import os | ||
import re | ||||
import signal | ||||
from .i18n import _ | ||||
Gregory Szorc
|
r43359 | from .pycompat import ( | ||
getattr, | ||||
open, | ||||
) | ||||
Gregory Szorc
|
r25940 | from . import ( | ||
Rodrigo Damazio Bovendorp
|
r45714 | diffhelper, | ||
Gregory Szorc
|
r25940 | encoding, | ||
Pierre-Yves David
|
r26587 | error, | ||
Gregory Szorc
|
r25940 | patch as patchmod, | ||
Kyle Lippincott
|
r44609 | pycompat, | ||
Yuya Nishihara
|
r30315 | scmutil, | ||
Laurent Charignon
|
r27156 | util, | ||
Gregory Szorc
|
r25940 | ) | ||
Augie Fackler
|
r43346 | from .utils import stringutil | ||
timeless
|
r28861 | stringio = util.stringio | ||
Matt Mackall
|
r24314 | |||
Jordi Gutiérrez Hermoso
|
r28637 | # patch comments based on the git one | ||
Augie Fackler
|
r43346 | diffhelptext = _( | ||
"""# To remove '-' lines, make them ' ' lines (context). | ||||
Jordi Gutiérrez Hermoso
|
r28637 | # To remove '+' lines, delete them. | ||
# Lines starting with # will be removed from the patch. | ||||
Augie Fackler
|
r43346 | """ | ||
) | ||||
Jordi Gutiérrez Hermoso
|
r28637 | |||
Augie Fackler
|
r43346 | hunkhelptext = _( | ||
"""# | ||||
Jordi Gutiérrez Hermoso
|
r28637 | # 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. | ||||
Augie Fackler
|
r43346 | """ | ||
) | ||||
Jordi Gutiérrez Hermoso
|
r28638 | |||
Augie Fackler
|
r43346 | patchhelptext = _( | ||
"""# | ||||
Jordi Gutiérrez Hermoso
|
r28638 | # If the patch applies cleanly, the edited patch will immediately | ||
# be finalised. If it does not apply cleanly, rejects files will be | ||||
# generated. You can use those when you try again. | ||||
Augie Fackler
|
r43346 | """ | ||
) | ||||
Jordi Gutiérrez Hermoso
|
r28638 | |||
Sean Farley
|
r27528 | try: | ||
Matt Harbison
|
r24909 | import curses | ||
Denis Laxalde
|
r43796 | import curses.ascii | ||
Augie Fackler
|
r43346 | |||
Sean Farley
|
r27528 | curses.error | ||
Pulkit Goyal
|
r44736 | except (ImportError, AttributeError): | ||
Manuel Jacob
|
r45544 | curses = False | ||
Laurent Charignon
|
r24310 | |||
Augie Fackler
|
r43346 | |||
Kyle Lippincott
|
r38058 | class fallbackerror(error.Abort): | ||
"""Error that indicates the client should try to fallback to text mode.""" | ||||
Augie Fackler
|
r43346 | |||
Kyle Lippincott
|
r38058 | # Inherits from error.Abort so that existing behavior is preserved if the | ||
# calling code does not know how to fallback. | ||||
Augie Fackler
|
r43346 | |||
Sean Farley
|
r27529 | def checkcurses(ui): | ||
"""Return True if the user wants to use curses | ||||
This method returns True if curses is found (and that python is built with | ||||
it) and that the user has the correct flag for the ui. | ||||
""" | ||||
Augie Fackler
|
r43347 | return curses and ui.interface(b"chunkselector") == b"curses" | ||
Sean Farley
|
r27529 | |||
Augie Fackler
|
r43346 | |||
Laurent Charignon
|
r24310 | class patchnode(object): | ||
"""abstract class for patch graph nodes | ||||
(i.e. patchroot, header, hunk, hunkline) | ||||
""" | ||||
def firstchild(self): | ||||
Augie Fackler
|
r43347 | raise NotImplementedError(b"method must be implemented by subclass") | ||
Laurent Charignon
|
r24310 | |||
def lastchild(self): | ||||
Augie Fackler
|
r43347 | raise NotImplementedError(b"method must be implemented by subclass") | ||
Laurent Charignon
|
r24310 | |||
def allchildren(self): | ||||
Matt Harbison
|
r44226 | """Return a list of all of the direct children of this node""" | ||
Augie Fackler
|
r43347 | raise NotImplementedError(b"method must be implemented by subclass") | ||
r29076 | ||||
Laurent Charignon
|
r24310 | 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. | ||||
""" | ||||
Augie Fackler
|
r43347 | raise NotImplementedError(b"method must be implemented by subclass") | ||
Laurent Charignon
|
r24310 | |||
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. | ||||
""" | ||||
Augie Fackler
|
r43347 | raise NotImplementedError(b"method must be implemented by subclass") | ||
Laurent Charignon
|
r24310 | |||
def parentitem(self): | ||||
Augie Fackler
|
r43347 | raise NotImplementedError(b"method must be implemented by subclass") | ||
Laurent Charignon
|
r24310 | |||
r29130 | def nextitem(self, skipfolded=True): | |||
Laurent Charignon
|
r24310 | """ | ||
r29130 | Try to return the next item closest to this item, regardless of item's | |||
type (header, hunk, or hunkline). | ||||
Laurent Charignon
|
r24310 | |||
r29130 | If skipfolded == True, and the current item is folded, then the child | |||
Laurent Charignon
|
r24310 | 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 | ||||
r29130 | if skipfolded and itemfolded: | |||
Laurent Charignon
|
r24310 | 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() | ||||
Augie Fackler
|
r43346 | except AttributeError: # parent and/or grandparent was None | ||
Laurent Charignon
|
r24310 | return None | ||
r29130 | def previtem(self): | |||
Laurent Charignon
|
r24310 | """ | ||
r29130 | Try to return the previous item closest to this item, regardless of | |||
item's type (header, hunk, or hunkline). | ||||
Laurent Charignon
|
r24310 | |||
If it is not possible to get the previous item, return None. | ||||
""" | ||||
r29130 | # 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() | ||||
Augie Fackler
|
r43346 | if (prevsiblinglastchild is not None) and not prevsibling.folded: | ||
r29130 | prevsiblinglclc = prevsiblinglastchild.lastchild() | |||
Augie Fackler
|
r43346 | if ( | ||
prevsiblinglclc is not None | ||||
) and not prevsiblinglastchild.folded: | ||||
r29130 | return prevsiblinglclc | |||
Laurent Charignon
|
r24310 | else: | ||
r29130 | return prevsiblinglastchild | |||
else: | ||||
return prevsibling | ||||
Laurent Charignon
|
r24310 | |||
r29130 | # try parent (or None) | |||
return self.parentitem() | ||||
Laurent Charignon
|
r24310 | |||
Augie Fackler
|
r43346 | |||
class patch(patchnode, list): # todo: rename patchroot | ||||
Laurent Charignon
|
r24310 | """ | ||
list of header objects representing the patch. | ||||
""" | ||||
Augie Fackler
|
r43346 | |||
Laurent Charignon
|
r24310 | def __init__(self, headerlist): | ||
self.extend(headerlist) | ||||
# add parent patch object reference to each header | ||||
for header in self: | ||||
header.patch = self | ||||
Augie Fackler
|
r43346 | |||
Laurent Charignon
|
r24310 | class uiheader(patchnode): | ||
"""patch header | ||||
Mads Kiilerich
|
r26781 | xxx shouldn't we move this to mercurial/patch.py ? | ||
Laurent Charignon
|
r24310 | """ | ||
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): | ||||
timeless
|
r28861 | x = stringio() | ||
Laurent Charignon
|
r24310 | 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): | ||||
Matt Harbison
|
r44226 | """return the first child of this item, if one exists. otherwise | ||
None.""" | ||||
Laurent Charignon
|
r24310 | if len(self.hunks) > 0: | ||
return self.hunks[0] | ||||
else: | ||||
return None | ||||
def lastchild(self): | ||||
Matt Harbison
|
r44226 | """return the last child of this item, if one exists. otherwise | ||
None.""" | ||||
Laurent Charignon
|
r24310 | if len(self.hunks) > 0: | ||
return self.hunks[-1] | ||||
else: | ||||
return None | ||||
def allchildren(self): | ||||
Matt Harbison
|
r44226 | """return a list of all of the direct children of this node""" | ||
Laurent Charignon
|
r24310 | return self.hunks | ||
def __getattr__(self, name): | ||||
return getattr(self.nonuiheader, name) | ||||
Augie Fackler
|
r43346 | |||
Laurent Charignon
|
r24310 | class uihunkline(patchnode): | ||
Matt Harbison
|
r44226 | """represents a changed line in a hunk""" | ||
Augie Fackler
|
r43346 | |||
Laurent Charignon
|
r24310 | 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) | ||||
Augie Fackler
|
r43346 | if indexofthisline < numlinesinhunk - 1: | ||
Laurent Charignon
|
r24310 | 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): | ||||
Matt Harbison
|
r44226 | """return the parent to the current item""" | ||
Laurent Charignon
|
r24310 | return self.hunk | ||
def firstchild(self): | ||||
Matt Harbison
|
r44226 | """return the first child of this item, if one exists. otherwise | ||
None.""" | ||||
Laurent Charignon
|
r24310 | # hunk-lines don't have children | ||
return None | ||||
def lastchild(self): | ||||
Matt Harbison
|
r44226 | """return the last child of this item, if one exists. otherwise | ||
None.""" | ||||
Laurent Charignon
|
r24310 | # hunk-lines don't have children | ||
return None | ||||
Augie Fackler
|
r43346 | |||
Laurent Charignon
|
r24310 | class uihunk(patchnode): | ||
"""ui patch hunk, wraps a hunk and keep track of ui behavior """ | ||||
Augie Fackler
|
r43346 | |||
Laurent Charignon
|
r24310 | 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) | ||||
Augie Fackler
|
r43346 | if indexofthishunk < numhunksinheader - 1: | ||
Laurent Charignon
|
r24310 | 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): | ||||
Matt Harbison
|
r44226 | """return the parent to the current item""" | ||
Laurent Charignon
|
r24310 | return self.header | ||
def firstchild(self): | ||||
Matt Harbison
|
r44226 | """return the first child of this item, if one exists. otherwise | ||
None.""" | ||||
Laurent Charignon
|
r24310 | if len(self.changedlines) > 0: | ||
return self.changedlines[0] | ||||
else: | ||||
return None | ||||
def lastchild(self): | ||||
Matt Harbison
|
r44226 | """return the last child of this item, if one exists. otherwise | ||
None.""" | ||||
Laurent Charignon
|
r24310 | if len(self.changedlines) > 0: | ||
return self.changedlines[-1] | ||||
else: | ||||
return None | ||||
def allchildren(self): | ||||
Matt Harbison
|
r44226 | """return a list of all of the direct children of this node""" | ||
Laurent Charignon
|
r24310 | return self.changedlines | ||
r29076 | ||||
Laurent Charignon
|
r24310 | def countchanges(self): | ||
"""changedlines -> (n+,n-)""" | ||||
Augie Fackler
|
r43346 | add = len( | ||
[ | ||||
l | ||||
for l in self.changedlines | ||||
Augie Fackler
|
r43347 | if l.applied and l.prettystr().startswith(b'+') | ||
Augie Fackler
|
r43346 | ] | ||
) | ||||
rem = len( | ||||
[ | ||||
l | ||||
for l in self.changedlines | ||||
Augie Fackler
|
r43347 | if l.applied and l.prettystr().startswith(b'-') | ||
Augie Fackler
|
r43346 | ] | ||
) | ||||
Laurent Charignon
|
r24310 | return add, rem | ||
def getfromtoline(self): | ||||
# calculate the number of removed lines converted to context lines | ||||
removedconvertedtocontext = self.originalremoved - self.removed | ||||
Augie Fackler
|
r43346 | contextlen = ( | ||
len(self.before) + len(self.after) + removedconvertedtocontext | ||||
) | ||||
Rodrigo Damazio Bovendorp
|
r45714 | if self.after and self.after[-1] == diffhelper.MISSING_NEWLINE_MARKER: | ||
Laurent Charignon
|
r24310 | 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 | ||||
Jun Wu
|
r38330 | if tolen == 0 and toline > 0: | ||
Laurent Charignon
|
r24310 | toline -= 1 | ||
Augie Fackler
|
r43347 | fromtoline = b'@@ -%d,%d +%d,%d @@%s\n' % ( | ||
Augie Fackler
|
r43346 | fromline, | ||
fromlen, | ||||
toline, | ||||
tolen, | ||||
Augie Fackler
|
r43347 | self.proc and (b' ' + self.proc), | ||
Augie Fackler
|
r43346 | ) | ||
Laurent Charignon
|
r24310 | 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) | ||||
Augie Fackler
|
r43347 | elif changedlinestr.startswith(b"-"): | ||
hunklinelist.append(b" " + changedlinestr[1:]) | ||||
Laurent Charignon
|
r24310 | |||
Augie Fackler
|
r43347 | fp.write(b''.join(self.before + hunklinelist + self.after)) | ||
Laurent Charignon
|
r24310 | |||
pretty = write | ||||
def prettystr(self): | ||||
timeless
|
r28861 | x = stringio() | ||
Laurent Charignon
|
r24310 | self.pretty(x) | ||
return x.getvalue() | ||||
Jun Wu
|
r32979 | def reversehunk(self): | ||
"""return a recordhunk which is the reverse of the hunk | ||||
Assuming the displayed patch is diff(A, B) result. The returned hunk is | ||||
intended to be applied to B, instead of A. | ||||
For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and | ||||
the user made the following selection: | ||||
0 | ||||
[x] -1 [x]: selected | ||||
[ ] -2 [ ]: not selected | ||||
[x] +3 | ||||
[ ] +4 | ||||
[x] +5 | ||||
6 | ||||
This function returns a hunk like: | ||||
0 | ||||
-3 | ||||
-4 | ||||
-5 | ||||
+1 | ||||
+4 | ||||
6 | ||||
Note "4" was first deleted then added. That's because "4" exists in B | ||||
side and "-4" must exist between "-3" and "-5" to make the patch | ||||
applicable to B. | ||||
""" | ||||
dels = [] | ||||
adds = [] | ||||
Rodrigo Damazio Bovendorp
|
r45713 | noeol = False | ||
Jun Wu
|
r32979 | for line in self.changedlines: | ||
text = line.linetext | ||||
Rodrigo Damazio Bovendorp
|
r45714 | if line.linetext == diffhelper.MISSING_NEWLINE_MARKER: | ||
Rodrigo Damazio Bovendorp
|
r45713 | noeol = True | ||
break | ||||
Jun Wu
|
r32979 | if line.applied: | ||
Augie Fackler
|
r43347 | if text.startswith(b'+'): | ||
Jun Wu
|
r32979 | dels.append(text[1:]) | ||
Augie Fackler
|
r43347 | elif text.startswith(b'-'): | ||
Jun Wu
|
r32979 | adds.append(text[1:]) | ||
Augie Fackler
|
r43347 | elif text.startswith(b'+'): | ||
Jun Wu
|
r32979 | dels.append(text[1:]) | ||
adds.append(text[1:]) | ||||
Augie Fackler
|
r43347 | hunk = [b'-%s' % l for l in dels] + [b'+%s' % l for l in adds] | ||
Rodrigo Damazio Bovendorp
|
r45713 | if noeol and hunk: | ||
# Remove the newline from the end of the hunk. | ||||
hunk[-1] = hunk[-1][:-1] | ||||
Jun Wu
|
r32979 | h = self._hunk | ||
Augie Fackler
|
r43346 | return patchmod.recordhunk( | ||
h.header, h.toline, h.fromline, h.proc, h.before, hunk, h.after | ||||
) | ||||
Jun Wu
|
r32979 | |||
Laurent Charignon
|
r24310 | def __getattr__(self, name): | ||
return getattr(self._hunk, name) | ||||
r29076 | ||||
Laurent Charignon
|
r24310 | def __repr__(self): | ||
Augie Fackler
|
r43906 | return '<hunk %r@%d>' % (self.filename(), self.fromline) | ||
Laurent Charignon
|
r24310 | |||
Augie Fackler
|
r43346 | |||
Jun Wu
|
r30533 | def filterpatch(ui, chunks, chunkselector, operation=None): | ||
Laurent Charignon
|
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: | ||||
Laurent Charignon
|
r27321 | return [], {} | ||
Laurent Charignon
|
r24310 | uiheaders = [uiheader(h) for h in headers] | ||
# let user choose headers/hunks/lines, and mark their applied flags | ||||
# accordingly | ||||
Jun Wu
|
r30533 | ret = chunkselector(ui, uiheaders, operation=operation) | ||
Laurent Charignon
|
r24310 | appliedhunklist = [] | ||
for hdr in uiheaders: | ||||
Augie Fackler
|
r43346 | if hdr.applied and ( | ||
hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0 | ||||
): | ||||
Laurent Charignon
|
r24310 | 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: | ||||
Augie Fackler
|
r43346 | # hnk = copy.copy(hnk) # necessary?? | ||
Laurent Charignon
|
r24310 | hnk.toline += fixoffset | ||
else: | ||||
fixoffset += hnk.removed - hnk.added | ||||
Laurent Charignon
|
r27155 | return (appliedhunklist, ret) | ||
Laurent Charignon
|
r24310 | |||
Augie Fackler
|
r43346 | |||
Jun Wu
|
r30533 | def chunkselector(ui, headerlist, operation=None): | ||
Laurent Charignon
|
r24310 | """ | ||
curses interface to get selection of chunks, and mark the applied flags | ||||
of the chosen chunks. | ||||
""" | ||||
Augie Fackler
|
r43347 | ui.write(_(b'starting interactive selection\n')) | ||
Jun Wu
|
r30533 | chunkselector = curseschunkselector(headerlist, ui, operation) | ||
Pierre-Yves David
|
r31933 | origsigtstp = sentinel = object() | ||
Augie Fackler
|
r43347 | if util.safehasattr(signal, b'SIGTSTP'): | ||
Pierre-Yves David
|
r31933 | origsigtstp = signal.getsignal(signal.SIGTSTP) | ||
Pierre-Yves David
|
r31932 | try: | ||
Manuel Jacob
|
r45550 | with util.with_lc_ctype(): | ||
curses.wrapper(chunkselector.main) | ||||
Kyle Lippincott
|
r38058 | if chunkselector.initexc is not None: | ||
raise chunkselector.initexc | ||||
Pierre-Yves David
|
r31932 | # ncurses does not restore signal handler for SIGTSTP | ||
finally: | ||||
Pierre-Yves David
|
r31933 | if origsigtstp is not sentinel: | ||
signal.signal(signal.SIGTSTP, origsigtstp) | ||||
Laurent Charignon
|
r27155 | return chunkselector.opts | ||
Laurent Charignon
|
r24310 | |||
Augie Fackler
|
r43346 | |||
Laurent Charignon
|
r24310 | def testdecorator(testfn, f): | ||
def u(*args, **kwargs): | ||||
return f(testfn, *args, **kwargs) | ||||
Augie Fackler
|
r43346 | |||
Laurent Charignon
|
r24310 | return u | ||
Augie Fackler
|
r43346 | |||
Jun Wu
|
r30533 | def testchunkselector(testfn, ui, headerlist, operation=None): | ||
Laurent Charignon
|
r24310 | """ | ||
test interface to get selection of chunks, and mark the applied flags | ||||
of the chosen chunks. | ||||
""" | ||||
Jun Wu
|
r30533 | chunkselector = curseschunkselector(headerlist, ui, operation) | ||
Kyle Lippincott
|
r42158 | |||
class dummystdscr(object): | ||||
def clear(self): | ||||
pass | ||||
Augie Fackler
|
r43346 | |||
Kyle Lippincott
|
r42158 | def refresh(self): | ||
pass | ||||
chunkselector.stdscr = dummystdscr() | ||||
Laurent Charignon
|
r24310 | if testfn and os.path.exists(testfn): | ||
Denis Laxalde
|
r43795 | testf = open(testfn, 'r') | ||
testcommands = [x.rstrip('\n') for x in testf.readlines()] | ||||
Laurent Charignon
|
r24310 | testf.close() | ||
while True: | ||||
if chunkselector.handlekeypressed(testcommands.pop(0), test=True): | ||||
break | ||||
Laurent Charignon
|
r27155 | return chunkselector.opts | ||
Laurent Charignon
|
r24310 | |||
Augie Fackler
|
r43346 | |||
_headermessages = { # {operation: text} | ||||
Augie Fackler
|
r43347 | b'apply': _(b'Select hunks to apply'), | ||
b'discard': _(b'Select hunks to discard'), | ||||
b'keep': _(b'Select hunks to keep'), | ||||
None: _(b'Select hunks to record'), | ||||
Jun Wu
|
r30548 | } | ||
Augie Fackler
|
r43346 | |||
Laurent Charignon
|
r24310 | class curseschunkselector(object): | ||
Jun Wu
|
r30533 | def __init__(self, headerlist, ui, operation=None): | ||
Laurent Charignon
|
r24310 | # put the headers into a patch object | ||
self.headerlist = patch(headerlist) | ||||
self.ui = ui | ||||
Laurent Charignon
|
r27155 | self.opts = {} | ||
Laurent Charignon
|
r24310 | |||
Laurent Charignon
|
r25556 | self.errorstr = None | ||
Laurent Charignon
|
r24310 | # 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 = {} | ||||
Elmar Bartel
|
r35545 | # Honor color setting of ui section. Keep colored setup as | ||
# long as not explicitly set to a falsy value - especially, | ||||
# when not set at all. This is to stay most compatible with | ||||
# previous (color only) behaviour. | ||||
Augie Fackler
|
r43347 | uicolor = stringutil.parsebool(self.ui.config(b'ui', b'color')) | ||
Elmar Bartel
|
r35545 | self.usecolor = uicolor is not False | ||
Laurent Charignon
|
r24310 | # the currently selected header, hunk, or hunk-line | ||
self.currentselecteditem = self.headerlist[0] | ||||
Kyle Lippincott
|
r42773 | self.lastapplieditem = None | ||
Laurent Charignon
|
r24310 | |||
# 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 | ||||
Jun Wu
|
r30545 | self.numstatuslines = 1 | ||
Laurent Charignon
|
r24310 | |||
# keep a running count of the number of lines printed to the pad | ||||
# (used for determining when the selected item begins/ends) | ||||
self.linesprintedtopadsofar = 0 | ||||
# stores optional text for a commit comment provided by the user | ||||
Augie Fackler
|
r43347 | self.commenttext = b"" | ||
Laurent Charignon
|
r24310 | |||
# if the last 'toggle all' command caused all changes to be applied | ||||
self.waslasttoggleallapplied = True | ||||
Jun Wu
|
r30533 | # affects some ui text | ||
Jun Wu
|
r30548 | if operation not in _headermessages: | ||
Augie Fackler
|
r43347 | raise error.ProgrammingError( | ||
b'unexpected operation: %s' % operation | ||||
) | ||||
Jun Wu
|
r30533 | self.operation = operation | ||
Laurent Charignon
|
r24310 | def uparrowevent(self): | ||
""" | ||||
try to select the previous item to the current item that has the | ||||
most-indented level. for example, if a hunk is selected, try to select | ||||
the last hunkline of the hunk prior to the selected hunk. or, if | ||||
the first hunkline of a hunk is currently selected, then select the | ||||
hunk itself. | ||||
""" | ||||
currentitem = self.currentselecteditem | ||||
r29130 | nextitem = currentitem.previtem() | |||
Laurent Charignon
|
r24310 | |||
if nextitem is None: | ||||
# if no parent item (i.e. currentitem is the first header), then | ||||
# no change... | ||||
nextitem = currentitem | ||||
self.currentselecteditem = nextitem | ||||
def uparrowshiftevent(self): | ||||
""" | ||||
select (if possible) the previous item on the same level as the | ||||
currently selected item. otherwise, select (if possible) the | ||||
parent-item of the currently selected item. | ||||
""" | ||||
currentitem = self.currentselecteditem | ||||
r29130 | nextitem = currentitem.prevsibling() | |||
# if there's no previous sibling, try choosing the parent | ||||
Laurent Charignon
|
r24310 | 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 | ||||
Matti Hamalainen
|
r38432 | self.recenterdisplayedarea() | ||
Laurent Charignon
|
r24310 | |||
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. | ||||
""" | ||||
Augie Fackler
|
r43346 | # self.startprintline += 1 #debug | ||
Laurent Charignon
|
r24310 | currentitem = self.currentselecteditem | ||
r29130 | nextitem = currentitem.nextitem() | |||
Laurent Charignon
|
r24310 | # if there's no next item, keep the selection as-is | ||
if nextitem is None: | ||||
nextitem = currentitem | ||||
self.currentselecteditem = nextitem | ||||
def downarrowshiftevent(self): | ||||
""" | ||||
r29081 | select (if possible) the next item on the same level as the currently | |||
selected item. otherwise, select (if possible) the next item on the | ||||
same level as the parent item of the currently selected item. | ||||
Laurent Charignon
|
r24310 | """ | ||
currentitem = self.currentselecteditem | ||||
r29130 | nextitem = currentitem.nextsibling() | |||
# if there's no next sibling, try choosing the parent's nextsibling | ||||
Laurent Charignon
|
r24310 | if nextitem is None: | ||
try: | ||||
r29130 | nextitem = currentitem.parentitem().nextsibling() | |||
Laurent Charignon
|
r24310 | except AttributeError: | ||
r29130 | # parentitem returned None, so nextsibling() can't be called | |||
Laurent Charignon
|
r24310 | nextitem = None | ||
if nextitem is None: | ||||
r29130 | # if parent has no next sibling, then no change... | |||
Laurent Charignon
|
r24310 | nextitem = currentitem | ||
self.currentselecteditem = nextitem | ||||
Matti Hamalainen
|
r38432 | self.recenterdisplayedarea() | ||
Laurent Charignon
|
r24310 | |||
Kyle Lippincott
|
r42772 | def nextsametype(self, test=False): | ||
r40289 | currentitem = self.currentselecteditem | |||
sametype = lambda item: isinstance(item, type(currentitem)) | ||||
nextitem = currentitem.nextitem() | ||||
while nextitem is not None and not sametype(nextitem): | ||||
nextitem = nextitem.nextitem() | ||||
if nextitem is None: | ||||
nextitem = currentitem | ||||
else: | ||||
parent = nextitem.parentitem() | ||||
r40473 | if parent is not None and parent.folded: | |||
r40289 | self.togglefolded(parent) | |||
self.currentselecteditem = nextitem | ||||
Kyle Lippincott
|
r42772 | if not test: | ||
self.recenterdisplayedarea() | ||||
r40289 | ||||
Laurent Charignon
|
r24310 | 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): | ||||
Matt Harbison
|
r44226 | """scroll the screen to fully show the currently-selected""" | ||
Laurent Charignon
|
r24310 | selstart = self.selecteditemstartline | ||
selend = self.selecteditemendline | ||||
Nathan Goldbaum
|
r29942 | |||
Laurent Charignon
|
r24310 | 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): | ||||
Matt Harbison
|
r44226 | """scroll the screen up (down) by numlines when numlines >0 (<0).""" | ||
Laurent Charignon
|
r24310 | 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 | ||||
Kyle Lippincott
|
r42773 | # Only set this when NOT using 'toggleall' | ||
self.lastapplieditem = item | ||||
Laurent Charignon
|
r24310 | |||
item.applied = not item.applied | ||||
if isinstance(item, uiheader): | ||||
item.partial = False | ||||
if item.applied: | ||||
Laurent Charignon
|
r24492 | # apply all its hunks | ||
for hnk in item.hunks: | ||||
hnk.applied = True | ||||
# apply all their hunklines | ||||
for hunkline in hnk.changedlines: | ||||
hunkline.applied = True | ||||
Laurent Charignon
|
r24310 | 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] | ||||
Augie Fackler
|
r43346 | somesiblingspartial = True in siblingspartialstatus | ||
Laurent Charignon
|
r24310 | |||
Augie Fackler
|
r43346 | # cases where applied or partial should be removed from header | ||
Laurent Charignon
|
r24310 | |||
# if no 'sibling' hunks are applied (including this hunk) | ||||
if nosiblingsapplied: | ||||
if not item.header.special(): | ||||
item.header.applied = False | ||||
item.header.partial = False | ||||
Augie Fackler
|
r43346 | else: # some/all parent siblings are applied | ||
Laurent Charignon
|
r24310 | item.header.applied = True | ||
Augie Fackler
|
r43346 | item.header.partial = ( | ||
somesiblingspartial or not allsiblingsapplied | ||||
) | ||||
Laurent Charignon
|
r24310 | |||
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 | ||||
Augie Fackler
|
r43346 | else: # some siblings applied | ||
Laurent Charignon
|
r24310 | item.hunk.applied = True | ||
item.hunk.partial = True | ||||
Augie Fackler
|
r43346 | parentsiblingsapplied = [ | ||
hnk.applied for hnk in item.hunk.header.hunks | ||||
] | ||||
Laurent Charignon
|
r24310 | noparentsiblingsapplied = not (True in parentsiblingsapplied) | ||
allparentsiblingsapplied = not (False in parentsiblingsapplied) | ||||
Augie Fackler
|
r43346 | parentsiblingspartial = [ | ||
hnk.partial for hnk in item.hunk.header.hunks | ||||
] | ||||
someparentsiblingspartial = True in parentsiblingspartial | ||||
Laurent Charignon
|
r24310 | |||
# 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 | ||||
Augie Fackler
|
r43346 | else: # some/all parent siblings are applied | ||
Laurent Charignon
|
r24310 | item.hunk.header.applied = True | ||
Augie Fackler
|
r43346 | item.hunk.header.partial = ( | ||
someparentsiblingspartial or not allparentsiblingsapplied | ||||
) | ||||
Laurent Charignon
|
r24310 | |||
def toggleall(self): | ||||
Matt Harbison
|
r44226 | """toggle the applied flag of all items.""" | ||
Augie Fackler
|
r43346 | if self.waslasttoggleallapplied: # then unapply them this time | ||
Laurent Charignon
|
r24310 | 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 | ||||
Jordi Gutiérrez Hermoso
|
r44339 | def flipselections(self): | ||
""" | ||||
Flip all selections. Every selected line is unselected and vice | ||||
versa. | ||||
""" | ||||
for header in self.headerlist: | ||||
for hunk in header.allchildren(): | ||||
for line in hunk.allchildren(): | ||||
self.toggleapply(line) | ||||
Kyle Lippincott
|
r42773 | def toggleallbetween(self): | ||
Matt Harbison
|
r44226 | """toggle applied on or off for all items in range [lastapplied, | ||
current]. """ | ||||
Augie Fackler
|
r43346 | if ( | ||
not self.lastapplieditem | ||||
or self.currentselecteditem == self.lastapplieditem | ||||
): | ||||
Kyle Lippincott
|
r42773 | # Treat this like a normal 'x'/' ' | ||
self.toggleapply() | ||||
return | ||||
startitem = self.lastapplieditem | ||||
enditem = self.currentselecteditem | ||||
# Verify that enditem is "after" startitem, otherwise swap them. | ||||
Augie Fackler
|
r43347 | for direction in [b'forward', b'reverse']: | ||
Kyle Lippincott
|
r42773 | nextitem = startitem.nextitem() | ||
while nextitem and nextitem != enditem: | ||||
nextitem = nextitem.nextitem() | ||||
if nextitem: | ||||
break | ||||
# Looks like we went the wrong direction :) | ||||
startitem, enditem = enditem, startitem | ||||
if not nextitem: | ||||
# We didn't find a path going either forward or backward? Don't know | ||||
# how this can happen, let's not crash though. | ||||
return | ||||
nextitem = startitem | ||||
# Switch all items to be the opposite state of the currently selected | ||||
# item. Specifically: | ||||
# [ ] startitem | ||||
# [x] middleitem | ||||
# [ ] enditem <-- currently selected | ||||
# This will turn all three on, since the currently selected item is off. | ||||
# This does *not* invert each item (i.e. middleitem stays marked/on) | ||||
desiredstate = not self.currentselecteditem.applied | ||||
while nextitem != enditem.nextitem(): | ||||
if nextitem.applied != desiredstate: | ||||
self.toggleapply(item=nextitem) | ||||
nextitem = nextitem.nextitem() | ||||
Laurent Charignon
|
r24310 | def togglefolded(self, item=None, foldparent=False): | ||
Matt Harbison
|
r44226 | """toggle folded flag of specified item (defaults to currently | ||
selected)""" | ||||
Laurent Charignon
|
r24310 | 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 | ||||
Augie Fackler
|
r43346 | if isinstance(item, uiheader): # the original or 'new' item | ||
Laurent Charignon
|
r24310 | 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) | ||||
Matt Mackall
|
r24351 | strwidth = encoding.colwidth(instr) | ||
Augie Fackler
|
r43346 | numspaces = width - ((strwidth + xstart) % width) | ||
Augie Fackler
|
r43347 | return instr + b" " * numspaces | ||
Laurent Charignon
|
r24310 | |||
Augie Fackler
|
r43346 | def printstring( | ||
self, | ||||
window, | ||||
text, | ||||
fgcolor=None, | ||||
bgcolor=None, | ||||
pair=None, | ||||
pairname=None, | ||||
attrlist=None, | ||||
towin=True, | ||||
align=True, | ||||
showwhtspc=False, | ||||
): | ||||
Laurent Charignon
|
r24310 | """ | ||
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 | ||||
Augie Fackler
|
r43346 | text = re.sub( | ||
br'[\x00-\x08\x0a-\x1f]', | ||||
Kyle Lippincott
|
r44609 | lambda m: b'^' + pycompat.sysbytes(chr(ord(m.group()) + 64)), | ||
Augie Fackler
|
r43347 | text.strip(b'\n'), | ||
Augie Fackler
|
r43346 | ) | ||
Laurent Charignon
|
r24310 | |||
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() | ||||
Augie Fackler
|
r43347 | t = b"" # variable for counting lines printed | ||
Laurent Charignon
|
r24310 | # if requested, show trailing whitespace | ||
if showwhtspc: | ||||
origlen = len(text) | ||||
Augie Fackler
|
r43347 | text = text.rstrip(b' \n') # tabs have already been expanded | ||
Laurent Charignon
|
r24310 | strippedlen = len(text) | ||
numtrailingspaces = origlen - strippedlen | ||||
if towin: | ||||
window.addstr(text, colorpair) | ||||
t += text | ||||
if showwhtspc: | ||||
Augie Fackler
|
r43346 | wscolorpair = colorpair | curses.A_REVERSE | ||
if towin: | ||||
for i in range(numtrailingspaces): | ||||
window.addch(curses.ACS_CKBOARD, wscolorpair) | ||||
Augie Fackler
|
r43347 | t += b" " * numtrailingspaces | ||
Laurent Charignon
|
r24310 | |||
if align: | ||||
if towin: | ||||
Augie Fackler
|
r43347 | extrawhitespace = self.alignstring(b"", window) | ||
Laurent Charignon
|
r24310 | 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() | ||||
Denis Laxalde
|
r43429 | linesprinted = (xstart + len(t)) // self.xscreensize | ||
Laurent Charignon
|
r24310 | self.linesprintedtopadsofar += linesprinted | ||
return t | ||||
Jun Wu
|
r30546 | def _getstatuslinesegments(self): | ||
"""-> [str]. return segments""" | ||||
Jun Wu
|
r30547 | selected = self.currentselecteditem.applied | ||
Augie Fackler
|
r43347 | spaceselect = _(b'space/enter: select') | ||
spacedeselect = _(b'space/enter: deselect') | ||||
Filip Filmar
|
r33812 | # Format the selected label into a place as long as the longer of the | ||
# two possible labels. This may vary by language. | ||||
spacelen = max(len(spaceselect), len(spacedeselect)) | ||||
Augie Fackler
|
r43347 | selectedlabel = b'%-*s' % ( | ||
Augie Fackler
|
r43346 | spacelen, | ||
spacedeselect if selected else spaceselect, | ||||
) | ||||
Jun Wu
|
r30546 | segments = [ | ||
Jun Wu
|
r30548 | _headermessages[self.operation], | ||
Augie Fackler
|
r43347 | b'-', | ||
_(b'[x]=selected **=collapsed'), | ||||
_(b'c: confirm'), | ||||
_(b'q: abort'), | ||||
_(b'arrow keys: move/expand/collapse'), | ||||
Filip Filmar
|
r33812 | selectedlabel, | ||
Augie Fackler
|
r43347 | _(b'?: help'), | ||
Jun Wu
|
r30546 | ] | ||
return segments | ||||
Jun Wu
|
r30544 | def _getstatuslines(self): | ||
"""() -> [str]. return short help used in the top status window""" | ||||
if self.errorstr is not None: | ||||
Augie Fackler
|
r43347 | lines = [self.errorstr, _(b'Press any key to continue')] | ||
Jun Wu
|
r30544 | else: | ||
Jun Wu
|
r30546 | # wrap segments to lines | ||
segments = self._getstatuslinesegments() | ||||
width = self.xscreensize | ||||
lines = [] | ||||
lastwidth = width | ||||
for s in segments: | ||||
w = encoding.colwidth(s) | ||||
Augie Fackler
|
r43347 | sep = b' ' * (1 + (s and s[0] not in b'-[')) | ||
Jun Wu
|
r30546 | if lastwidth + w + len(sep) >= width: | ||
lines.append(s) | ||||
lastwidth = w | ||||
else: | ||||
lines[-1] += sep + s | ||||
lastwidth += w + len(sep) | ||||
Jun Wu
|
r30545 | if len(lines) != self.numstatuslines: | ||
self.numstatuslines = len(lines) | ||||
self.statuswin.resize(self.numstatuslines, self.xscreensize) | ||||
Yuya Nishihara
|
r37102 | return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines] | ||
Jun Wu
|
r30544 | |||
Laurent Charignon
|
r24310 | def updatescreen(self): | ||
self.statuswin.erase() | ||||
self.chunkpad.erase() | ||||
printstring = self.printstring | ||||
# print out the status lines at the top | ||||
try: | ||||
Jun Wu
|
r30544 | for line in self._getstatuslines(): | ||
Augie Fackler
|
r43347 | printstring(self.statuswin, line, pairname=b"legend") | ||
Jun Wu
|
r30544 | self.statuswin.refresh() | ||
Laurent Charignon
|
r24310 | except curses.error: | ||
pass | ||||
Jun Wu
|
r30544 | if self.errorstr is not None: | ||
return | ||||
Laurent Charignon
|
r24310 | |||
# print out the patch in the remaining part of the window | ||||
try: | ||||
self.printitem() | ||||
self.updatescroll() | ||||
Augie Fackler
|
r43346 | self.chunkpad.refresh( | ||
self.firstlineofpadtoprint, | ||||
0, | ||||
self.numstatuslines, | ||||
0, | ||||
self.yscreensize - self.numstatuslines, | ||||
self.xscreensize, | ||||
) | ||||
Laurent Charignon
|
r24310 | except curses.error: | ||
pass | ||||
def getstatusprefixstring(self, item): | ||||
""" | ||||
create a string to prefix a line with which indicates whether 'item' | ||||
is applied and/or folded. | ||||
Ryan McElroy
|
r28579 | """ | ||
Laurent Charignon
|
r24310 | |||
# create checkbox string | ||||
if item.applied: | ||||
if not isinstance(item, uihunkline) and item.partial: | ||||
Augie Fackler
|
r43347 | checkbox = b"[~]" | ||
Laurent Charignon
|
r24310 | else: | ||
Augie Fackler
|
r43347 | checkbox = b"[x]" | ||
Laurent Charignon
|
r24310 | else: | ||
Augie Fackler
|
r43347 | checkbox = b"[ ]" | ||
Laurent Charignon
|
r24310 | |||
try: | ||||
if item.folded: | ||||
Augie Fackler
|
r43347 | checkbox += b"**" | ||
Laurent Charignon
|
r24310 | if isinstance(item, uiheader): | ||
# one of "m", "a", or "d" (modified, added, deleted) | ||||
filestatus = item.changetype | ||||
Augie Fackler
|
r43347 | checkbox += filestatus + b" " | ||
Laurent Charignon
|
r24310 | else: | ||
Augie Fackler
|
r43347 | checkbox += b" " | ||
Laurent Charignon
|
r24310 | if isinstance(item, uiheader): | ||
# add two more spaces for headers | ||||
Augie Fackler
|
r43347 | checkbox += b" " | ||
Augie Fackler
|
r43346 | except AttributeError: # not foldable | ||
Augie Fackler
|
r43347 | checkbox += b" " | ||
Laurent Charignon
|
r24310 | |||
return checkbox | ||||
Augie Fackler
|
r43346 | def printheader( | ||
self, header, selected=False, towin=True, ignorefolding=False | ||||
): | ||||
Laurent Charignon
|
r24310 | """ | ||
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. | ||||
Ryan McElroy
|
r28579 | """ | ||
Laurent Charignon
|
r24310 | |||
Augie Fackler
|
r43347 | outstr = b"" | ||
Laurent Charignon
|
r24310 | text = header.prettystr() | ||
chunkindex = self.chunklist.index(header) | ||||
if chunkindex != 0 and not header.folded: | ||||
# add separating line before headers | ||||
Augie Fackler
|
r43346 | outstr += self.printstring( | ||
Augie Fackler
|
r43347 | self.chunkpad, b'_' * self.xscreensize, towin=towin, align=False | ||
Augie Fackler
|
r43346 | ) | ||
Laurent Charignon
|
r24310 | # select color-pair based on if the header is selected | ||
Augie Fackler
|
r43346 | colorpair = self.getcolorpair( | ||
Augie Fackler
|
r43347 | name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD] | ||
Augie Fackler
|
r43346 | ) | ||
Laurent Charignon
|
r24310 | |||
# 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: | ||||
Augie Fackler
|
r43347 | textlist = text.split(b"\n") | ||
Laurent Charignon
|
r24310 | linestr = checkbox + textlist[0] | ||
else: | ||||
linestr = checkbox + header.filename() | ||||
Augie Fackler
|
r43346 | outstr += self.printstring( | ||
self.chunkpad, linestr, pair=colorpair, towin=towin | ||||
) | ||||
Laurent Charignon
|
r24310 | if not header.folded or ignorefolding: | ||
if len(textlist) > 1: | ||||
for line in textlist[1:]: | ||||
Augie Fackler
|
r43347 | linestr = b" " * (indentnumchars + len(checkbox)) + line | ||
Augie Fackler
|
r43346 | outstr += self.printstring( | ||
self.chunkpad, linestr, pair=colorpair, towin=towin | ||||
) | ||||
Laurent Charignon
|
r24310 | |||
return outstr | ||||
Augie Fackler
|
r43346 | def printhunklinesbefore( | ||
self, hunk, selected=False, towin=True, ignorefolding=False | ||||
): | ||||
Matt Harbison
|
r44226 | """includes start/end line indicator""" | ||
Augie Fackler
|
r43347 | outstr = b"" | ||
Laurent Charignon
|
r24310 | # where hunk is in list of siblings | ||
hunkindex = hunk.header.hunks.index(hunk) | ||||
if hunkindex != 0: | ||||
# add separating line before headers | ||||
Augie Fackler
|
r43346 | outstr += self.printstring( | ||
Augie Fackler
|
r43347 | self.chunkpad, b' ' * self.xscreensize, towin=towin, align=False | ||
Augie Fackler
|
r43346 | ) | ||
Laurent Charignon
|
r24310 | |||
Augie Fackler
|
r43346 | colorpair = self.getcolorpair( | ||
Augie Fackler
|
r43347 | name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD] | ||
Augie Fackler
|
r43346 | ) | ||
Laurent Charignon
|
r24310 | |||
# print out from-to line with checkbox | ||||
checkbox = self.getstatusprefixstring(hunk) | ||||
Augie Fackler
|
r43347 | lineprefix = b" " * self.hunkindentnumchars + checkbox | ||
frtoline = b" " + hunk.getfromtoline().strip(b"\n") | ||||
Laurent Charignon
|
r24310 | |||
Augie Fackler
|
r43346 | outstr += self.printstring( | ||
self.chunkpad, lineprefix, towin=towin, align=False | ||||
) # add uncolored checkbox/indent | ||||
outstr += self.printstring( | ||||
self.chunkpad, frtoline, pair=colorpair, towin=towin | ||||
) | ||||
Laurent Charignon
|
r24310 | |||
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: | ||||
Augie Fackler
|
r43347 | linestr = ( | ||
b" " * (self.hunklineindentnumchars + len(checkbox)) + line | ||||
) | ||||
Laurent Charignon
|
r24310 | outstr += self.printstring(self.chunkpad, linestr, towin=towin) | ||
return outstr | ||||
def printhunklinesafter(self, hunk, towin=True, ignorefolding=False): | ||||
Augie Fackler
|
r43347 | outstr = b"" | ||
Laurent Charignon
|
r24310 | 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: | ||||
Augie Fackler
|
r43347 | linestr = ( | ||
b" " * (self.hunklineindentnumchars + len(checkbox)) + line | ||||
) | ||||
Laurent Charignon
|
r24310 | outstr += self.printstring(self.chunkpad, linestr, towin=towin) | ||
return outstr | ||||
def printhunkchangedline(self, hunkline, selected=False, towin=True): | ||||
Augie Fackler
|
r43347 | outstr = b"" | ||
Laurent Charignon
|
r24310 | checkbox = self.getstatusprefixstring(hunkline) | ||
Augie Fackler
|
r43347 | linestr = hunkline.prettystr().strip(b"\n") | ||
Laurent Charignon
|
r24310 | |||
# select color-pair based on whether line is an addition/removal | ||||
if selected: | ||||
Augie Fackler
|
r43347 | colorpair = self.getcolorpair(name=b"selected") | ||
elif linestr.startswith(b"+"): | ||||
colorpair = self.getcolorpair(name=b"addition") | ||||
elif linestr.startswith(b"-"): | ||||
colorpair = self.getcolorpair(name=b"deletion") | ||||
elif linestr.startswith(b"\\"): | ||||
colorpair = self.getcolorpair(name=b"normal") | ||||
Laurent Charignon
|
r24310 | |||
Augie Fackler
|
r43347 | lineprefix = b" " * self.hunklineindentnumchars + checkbox | ||
Augie Fackler
|
r43346 | 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 | ||||
) | ||||
Laurent Charignon
|
r24310 | return outstr | ||
Augie Fackler
|
r43346 | def printitem( | ||
self, item=None, ignorefolding=False, recursechildren=True, towin=True | ||||
): | ||||
Laurent Charignon
|
r24310 | """ | ||
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) | ||||
""" | ||||
Ryan McElroy
|
r28579 | |||
Laurent Charignon
|
r24310 | if item is None: | ||
item = self.headerlist | ||||
if recursechildren: | ||||
self.linesprintedtopadsofar = 0 | ||||
outstr = [] | ||||
Augie Fackler
|
r43346 | self.__printitem( | ||
item, ignorefolding, recursechildren, outstr, towin=towin | ||||
) | ||||
Augie Fackler
|
r43347 | return b''.join(outstr) | ||
Laurent Charignon
|
r24310 | |||
def outofdisplayedarea(self): | ||||
Augie Fackler
|
r43346 | y, _ = self.chunkpad.getyx() # cursor location | ||
Laurent Charignon
|
r24310 | # * 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): | ||||
Augie Fackler
|
r43346 | selected = item is self.currentselecteditem | ||
Laurent Charignon
|
r24310 | if selected and recursechildren: | ||
# assumes line numbering starting from line 0 | ||||
self.selecteditemstartline = self.linesprintedtopadsofar | ||||
Augie Fackler
|
r43346 | selecteditemlines = self.getnumlinesdisplayed( | ||
item, recursechildren=False | ||||
) | ||||
self.selecteditemendline = ( | ||||
self.selecteditemstartline + selecteditemlines - 1 | ||||
) | ||||
Laurent Charignon
|
r24310 | return selected | ||
Augie Fackler
|
r43346 | def __printitem( | ||
self, item, ignorefolding, recursechildren, outstr, towin=True | ||||
): | ||||
Laurent Charignon
|
r24310 | """ | ||
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. | ||||
Ryan McElroy
|
r28579 | """ | ||
Laurent Charignon
|
r24310 | |||
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: | ||||
Augie Fackler
|
r43346 | self.__printitem( | ||
hdr, ignorefolding, recursechildren, outstr, towin | ||||
) | ||||
Laurent Charignon
|
r24310 | # todo: eliminate all isinstance() calls | ||
if isinstance(item, uiheader): | ||||
Augie Fackler
|
r43346 | outstr.append( | ||
self.printheader( | ||||
item, selected, towin=towin, ignorefolding=ignorefolding | ||||
) | ||||
) | ||||
Laurent Charignon
|
r24310 | if recursechildren: | ||
for hnk in item.hunks: | ||||
Augie Fackler
|
r43346 | self.__printitem( | ||
hnk, ignorefolding, recursechildren, outstr, towin | ||||
) | ||||
elif isinstance(item, uihunk) and ( | ||||
(not item.header.folded) or ignorefolding | ||||
): | ||||
Laurent Charignon
|
r24310 | # print the hunk data which comes before the changed-lines | ||
Augie Fackler
|
r43346 | outstr.append( | ||
self.printhunklinesbefore( | ||||
item, selected, towin=towin, ignorefolding=ignorefolding | ||||
) | ||||
) | ||||
Laurent Charignon
|
r24310 | if recursechildren: | ||
for l in item.changedlines: | ||||
Augie Fackler
|
r43346 | 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) | ||||
) | ||||
Laurent Charignon
|
r24310 | |||
return outstr | ||||
Augie Fackler
|
r43346 | def getnumlinesdisplayed( | ||
self, item=None, ignorefolding=False, recursechildren=True | ||||
): | ||||
Laurent Charignon
|
r24310 | """ | ||
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. | ||||
Ryan McElroy
|
r28579 | """ | ||
Laurent Charignon
|
r24310 | |||
# temporarily disable printing to windows by printstring | ||||
Augie Fackler
|
r43346 | patchdisplaystring = self.printitem( | ||
item, ignorefolding, recursechildren, towin=False | ||||
) | ||||
Pulkit Goyal
|
r36324 | numlines = len(patchdisplaystring) // self.xscreensize | ||
Laurent Charignon
|
r24310 | return numlines | ||
def sigwinchhandler(self, n, frame): | ||||
Matt Harbison
|
r44226 | """handle window resizing""" | ||
Laurent Charignon
|
r24310 | try: | ||
curses.endwin() | ||||
Yuya Nishihara
|
r30315 | self.xscreensize, self.yscreensize = scmutil.termsize(self.ui) | ||
Laurent Charignon
|
r24310 | self.statuswin.resize(self.numstatuslines, self.xscreensize) | ||
self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1 | ||||
self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize) | ||||
except curses.error: | ||||
pass | ||||
Augie Fackler
|
r43346 | def getcolorpair( | ||
self, fgcolor=None, bgcolor=None, name=None, attrlist=None | ||||
): | ||||
Laurent Charignon
|
r24310 | """ | ||
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. | ||||
Ryan McElroy
|
r28579 | """ | ||
Laurent Charignon
|
r24310 | |||
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 | ||||
Elmar Bartel
|
r35545 | if self.usecolor: | ||
curses.init_pair(pairindex, fgcolor, bgcolor) | ||||
Augie Fackler
|
r43346 | colorpair = self.colorpairs[ | ||
(fgcolor, bgcolor) | ||||
] = curses.color_pair(pairindex) | ||||
Elmar Bartel
|
r35545 | if name is not None: | ||
self.colorpairnames[name] = curses.color_pair(pairindex) | ||||
else: | ||||
cval = 0 | ||||
if name is not None: | ||||
Augie Fackler
|
r43347 | if name == b'selected': | ||
Elmar Bartel
|
r35545 | cval = curses.A_REVERSE | ||
self.colorpairnames[name] = cval | ||||
colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval | ||||
Laurent Charignon
|
r24310 | |||
# 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): | ||||
Matt Harbison
|
r44226 | """same as getcolorpair.""" | ||
Laurent Charignon
|
r24310 | self.getcolorpair(*args, **kwargs) | ||
def helpwindow(self): | ||||
Matt Harbison
|
r44226 | """print a help window to the screen. exit after any keypress.""" | ||
Jun Wu
|
r30535 | helptext = _( | ||
""" [press any key to return to the patch-display] | ||||
Laurent Charignon
|
r24310 | |||
Jordi Gutiérrez Hermoso
|
r44337 | The curses hunk selector allows you to interactively choose among the | ||
changes you have made, and confirm only those changes you select for | ||||
further processing by the command you are running (such as commit, | ||||
shelve, or revert). After confirming the selected changes, the | ||||
unselected changes are still present in your working copy, so you can | ||||
use the hunk selector multiple times to split large changes into | ||||
smaller changesets. the following are valid keystrokes: | ||||
Laurent Charignon
|
r24310 | |||
Kyle Lippincott
|
r42770 | x [space] : (un-)select item ([~]/[x] = partly/fully applied) | ||
r40289 | [enter] : (un-)select item and go to next item of same type | |||
Laurent Charignon
|
r27936 | A : (un-)select all items | ||
Kyle Lippincott
|
r42773 | X : (un-)select all items between current and most-recent | ||
Laurent Charignon
|
r24310 | up/down-arrow [k/j] : go to previous/next unfolded item | ||
Laurent Charignon
|
r25460 | pgup/pgdn [K/J] : go to previous/next item of same type | ||
Laurent Charignon
|
r24310 | right/left-arrow [l/h] : go to child item / parent item | ||
Laurent Charignon
|
r25460 | shift-left-arrow [H] : go to parent header / fold selected header | ||
Arun Chandrasekaran
|
r42240 | g : go to the top | ||
G : go to the bottom | ||||
Laurent Charignon
|
r24310 | f : fold / unfold item, hiding/revealing its children | ||
Laurent Charignon
|
r25447 | F : fold / unfold parent item and all of its ancestors | ||
Nathan Goldbaum
|
r29957 | ctrl-l : scroll the selected line to the top of the screen | ||
Laurent Charignon
|
r24310 | m : edit / resume editing the commit message | ||
e : edit the currently selected hunk | ||||
Jordi Gutiérrez Hermoso
|
r44339 | a : toggle all selections | ||
Laurent Charignon
|
r24840 | c : confirm selected changes | ||
r : review/edit and confirm selected changes | ||||
q : quit without confirming (no changes will be made) | ||||
Augie Fackler
|
r43346 | ? : help (what you're currently reading)""" | ||
) | ||||
Laurent Charignon
|
r24310 | |||
helpwin = curses.newwin(self.yscreensize, 0, 0, 0) | ||||
Augie Fackler
|
r43347 | helplines = helptext.split(b"\n") | ||
helplines = helplines + [b" "] * ( | ||||
Augie Fackler
|
r43346 | self.yscreensize - self.numstatuslines - len(helplines) - 1 | ||
) | ||||
Laurent Charignon
|
r24310 | try: | ||
for line in helplines: | ||||
Augie Fackler
|
r43347 | self.printstring(helpwin, line, pairname=b"legend") | ||
Laurent Charignon
|
r24310 | except curses.error: | ||
pass | ||||
helpwin.refresh() | ||||
try: | ||||
Augie Fackler
|
r43347 | with self.ui.timeblockedsection(b'crecord'): | ||
Simon Farnsworth
|
r30981 | helpwin.getkey() | ||
Laurent Charignon
|
r24310 | except curses.error: | ||
pass | ||||
Peter Vitt
|
r33975 | def commitMessageWindow(self): | ||
Matt Harbison
|
r44226 | """Create a temporary commit message editing window on the screen.""" | ||
Peter Vitt
|
r33975 | |||
curses.raw() | ||||
curses.def_prog_mode() | ||||
curses.endwin() | ||||
self.commenttext = self.ui.edit(self.commenttext, self.ui.username()) | ||||
curses.cbreak() | ||||
self.stdscr.refresh() | ||||
Augie Fackler
|
r43346 | self.stdscr.keypad(1) # allow arrow-keys to continue to function | ||
Peter Vitt
|
r33975 | |||
Arun Chandrasekaran
|
r42240 | def handlefirstlineevent(self): | ||
""" | ||||
Handle 'g' to navigate to the top most file in the ncurses window. | ||||
""" | ||||
self.currentselecteditem = self.headerlist[0] | ||||
currentitem = self.currentselecteditem | ||||
# 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 handlelastlineevent(self): | ||||
""" | ||||
Handle 'G' to navigate to the bottom most file/hunk/line depending | ||||
on the whether the fold is active or not. | ||||
If the bottom most file is folded, it navigates to that file and | ||||
stops there. If the bottom most file is unfolded, it navigates to | ||||
the bottom most hunk in that file and stops there. If the bottom most | ||||
hunk is unfolded, it navigates to the bottom most line in that hunk. | ||||
""" | ||||
currentitem = self.currentselecteditem | ||||
nextitem = currentitem.nextitem() | ||||
# select the child item recursively until we're at a footer | ||||
while nextitem is not None: | ||||
nextitem = currentitem.nextitem() | ||||
if nextitem is None: | ||||
break | ||||
else: | ||||
currentitem = nextitem | ||||
self.currentselecteditem = currentitem | ||||
self.recenterdisplayedarea() | ||||
Laurent Charignon
|
r24310 | def confirmationwindow(self, windowtext): | ||
Matt Harbison
|
r44226 | """display an informational window, then wait for and return a | ||
keypress.""" | ||||
Laurent Charignon
|
r24310 | |||
confirmwin = curses.newwin(self.yscreensize, 0, 0, 0) | ||||
try: | ||||
Augie Fackler
|
r43347 | lines = windowtext.split(b"\n") | ||
Laurent Charignon
|
r24310 | for line in lines: | ||
Augie Fackler
|
r43347 | self.printstring(confirmwin, line, pairname=b"selected") | ||
Laurent Charignon
|
r24310 | except curses.error: | ||
pass | ||||
self.stdscr.refresh() | ||||
confirmwin.refresh() | ||||
try: | ||||
Augie Fackler
|
r43347 | with self.ui.timeblockedsection(b'crecord'): | ||
Simon Farnsworth
|
r30981 | response = chr(self.stdscr.getch()) | ||
Laurent Charignon
|
r24310 | except ValueError: | ||
response = None | ||||
return response | ||||
Pierre-Yves David
|
r28926 | def reviewcommit(self): | ||
Laurent Charignon
|
r24840 | """ask for 'y' to be pressed to confirm selected. return True if | ||
confirmed.""" | ||||
Jun Wu
|
r30535 | confirmtext = _( | ||
Augie Fackler
|
r43346 | """If you answer yes to the following, your currently chosen patch chunks | ||
Kyle Lippincott
|
r42105 | will be loaded into an editor. To modify the patch, make the changes in your | ||
editor and save. To accept the current patch as-is, close the editor without | ||||
saving. | ||||
Laurent Charignon
|
r24310 | |||
note: don't add/remove lines unless you also modify the range information. | ||||
failing to follow this rule will result in the commit aborting. | ||||
Laurent Charignon
|
r24840 | are you sure you want to review/edit and confirm the selected changes [yn]? | ||
Augie Fackler
|
r43346 | """ | ||
) | ||||
Augie Fackler
|
r43347 | with self.ui.timeblockedsection(b'crecord'): | ||
Simon Farnsworth
|
r30981 | response = self.confirmationwindow(confirmtext) | ||
Laurent Charignon
|
r24310 | if response is None: | ||
Denis Laxalde
|
r43794 | response = "n" | ||
if response.lower().startswith("y"): | ||||
Laurent Charignon
|
r24310 | 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 | ||||
Mads Kiilerich
|
r26781 | location of the selected item even though it is outside of the displayed | ||
Laurent Charignon
|
r24310 | zone and then update the scroll. | ||
""" | ||||
self.printitem(towin=False) | ||||
self.updatescroll() | ||||
def toggleedit(self, item=None, test=False): | ||||
""" | ||||
Ryan McElroy
|
r28580 | edit the currently selected chunk | ||
Laurent Charignon
|
r24310 | """ | ||
Augie Fackler
|
r43346 | |||
Laurent Charignon
|
r25555 | def updateui(self): | ||
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) | ||||
Laurent Charignon
|
r24310 | |||
def editpatchwitheditor(self, chunk): | ||||
if chunk is None: | ||||
Augie Fackler
|
r43347 | self.ui.write(_(b'cannot edit patch for whole file')) | ||
self.ui.write(b"\n") | ||||
Laurent Charignon
|
r24310 | return None | ||
if chunk.header.binary(): | ||||
Augie Fackler
|
r43347 | self.ui.write(_(b'cannot edit patch for binary file')) | ||
self.ui.write(b"\n") | ||||
Laurent Charignon
|
r24310 | return None | ||
Jordi Gutiérrez Hermoso
|
r28636 | # write the initial patch | ||
timeless
|
r28861 | patch = stringio() | ||
Jordi Gutiérrez Hermoso
|
r28637 | patch.write(diffhelptext + hunkhelptext) | ||
Jordi Gutiérrez Hermoso
|
r28636 | chunk.header.write(patch) | ||
chunk.write(patch) | ||||
# start the editor and wait for it to complete | ||||
Laurent Charignon
|
r24310 | try: | ||
Augie Fackler
|
r43347 | patch = self.ui.edit(patch.getvalue(), b"", action=b"diff") | ||
Jordi Gutiérrez Hermoso
|
r28636 | except error.Abort as exc: | ||
Martin von Zweigbergk
|
r46274 | self.errorstr = exc.message | ||
Jordi Gutiérrez Hermoso
|
r28636 | return None | ||
Kyle Lippincott
|
r42158 | finally: | ||
self.stdscr.clear() | ||||
self.stdscr.refresh() | ||||
Jordi Gutiérrez Hermoso
|
r28636 | |||
# remove comment lines | ||||
Augie Fackler
|
r43346 | patch = [ | ||
Augie Fackler
|
r43347 | line + b'\n' | ||
Augie Fackler
|
r43346 | for line in patch.splitlines() | ||
Augie Fackler
|
r43347 | if not line.startswith(b'#') | ||
Augie Fackler
|
r43346 | ] | ||
Jordi Gutiérrez Hermoso
|
r28636 | return patchmod.parsepatch(patch) | ||
Laurent Charignon
|
r24310 | 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 | ||||
Laurent Charignon
|
r27914 | # To go back to that hunk or its replacement at the end of the edit | ||
itemindex = item.parentitem().hunks.index(item) | ||||
Laurent Charignon
|
r24310 | beforeadded, beforeremoved = item.added, item.removed | ||
newpatches = editpatchwitheditor(self, item) | ||||
Laurent Charignon
|
r25557 | if newpatches is None: | ||
if not test: | ||||
updateui(self) | ||||
return | ||||
Laurent Charignon
|
r24310 | header = item.header | ||
editedhunkindex = header.hunks.index(item) | ||||
hunksbefore = header.hunks[:editedhunkindex] | ||||
Augie Fackler
|
r43346 | hunksafter = header.hunks[editedhunkindex + 1 :] | ||
Laurent Charignon
|
r24310 | 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 | ||||
Laurent Charignon
|
r27914 | if len(header.hunks) > itemindex: | ||
self.currentselecteditem = header.hunks[itemindex] | ||||
Laurent Charignon
|
r24310 | |||
if not test: | ||||
Laurent Charignon
|
r25555 | updateui(self) | ||
Laurent Charignon
|
r24310 | |||
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): | ||||
Ryan McElroy
|
r28581 | """ | ||
Perform actions based on pressed keys. | ||||
Return true to exit the main loop. | ||||
""" | ||||
Denis Laxalde
|
r43795 | if keypressed in ["k", "KEY_UP"]: | ||
Laurent Charignon
|
r24310 | self.uparrowevent() | ||
Denis Laxalde
|
r43795 | elif keypressed in ["K", "KEY_PPAGE"]: | ||
Laurent Charignon
|
r24310 | self.uparrowshiftevent() | ||
Denis Laxalde
|
r43795 | elif keypressed in ["j", "KEY_DOWN"]: | ||
Laurent Charignon
|
r24310 | self.downarrowevent() | ||
Denis Laxalde
|
r43795 | elif keypressed in ["J", "KEY_NPAGE"]: | ||
Laurent Charignon
|
r24310 | self.downarrowshiftevent() | ||
Denis Laxalde
|
r43795 | elif keypressed in ["l", "KEY_RIGHT"]: | ||
Laurent Charignon
|
r24310 | self.rightarrowevent() | ||
Denis Laxalde
|
r43795 | elif keypressed in ["h", "KEY_LEFT"]: | ||
Laurent Charignon
|
r24310 | self.leftarrowevent() | ||
Denis Laxalde
|
r43795 | elif keypressed in ["H", "KEY_SLEFT"]: | ||
Laurent Charignon
|
r24310 | self.leftarrowshiftevent() | ||
Denis Laxalde
|
r43795 | elif keypressed in ["q"]: | ||
Augie Fackler
|
r43347 | raise error.Abort(_(b'user quit')) | ||
Denis Laxalde
|
r43795 | elif keypressed in ['a']: | ||
Jordi Gutiérrez Hermoso
|
r44339 | self.flipselections() | ||
Denis Laxalde
|
r43795 | elif keypressed in ["c"]: | ||
Pierre-Yves David
|
r28925 | return True | ||
Denis Laxalde
|
r43795 | elif keypressed in ["r"]: | ||
Pierre-Yves David
|
r28926 | if self.reviewcommit(): | ||
Augie Fackler
|
r43347 | self.opts[b'review'] = True | ||
Laurent Charignon
|
r24310 | return True | ||
Denis Laxalde
|
r43795 | elif test and keypressed in ["R"]: | ||
Augie Fackler
|
r43347 | self.opts[b'review'] = True | ||
Laurent Charignon
|
r24310 | return True | ||
Denis Laxalde
|
r43795 | elif keypressed in [" ", "x"]: | ||
Laurent Charignon
|
r24310 | self.toggleapply() | ||
Denis Laxalde
|
r43795 | elif keypressed in ["\n", "KEY_ENTER"]: | ||
r40289 | self.toggleapply() | |||
Kyle Lippincott
|
r42772 | self.nextsametype(test=test) | ||
Denis Laxalde
|
r43795 | elif keypressed in ["X"]: | ||
Kyle Lippincott
|
r42773 | self.toggleallbetween() | ||
Denis Laxalde
|
r43795 | elif keypressed in ["A"]: | ||
Laurent Charignon
|
r24310 | self.toggleall() | ||
Denis Laxalde
|
r43795 | elif keypressed in ["e"]: | ||
Laurent Charignon
|
r24310 | self.toggleedit(test=test) | ||
Denis Laxalde
|
r43795 | elif keypressed in ["f"]: | ||
Laurent Charignon
|
r24310 | self.togglefolded() | ||
Denis Laxalde
|
r43795 | elif keypressed in ["F"]: | ||
Laurent Charignon
|
r24310 | self.togglefolded(foldparent=True) | ||
Denis Laxalde
|
r43795 | elif keypressed in ["m"]: | ||
Peter Vitt
|
r33975 | self.commitMessageWindow() | ||
Denis Laxalde
|
r43795 | elif keypressed in ["g", "KEY_HOME"]: | ||
Arun Chandrasekaran
|
r42240 | self.handlefirstlineevent() | ||
Denis Laxalde
|
r43795 | elif keypressed in ["G", "KEY_END"]: | ||
Arun Chandrasekaran
|
r42240 | self.handlelastlineevent() | ||
Denis Laxalde
|
r43795 | elif keypressed in ["?"]: | ||
Laurent Charignon
|
r24310 | self.helpwindow() | ||
Laurent Charignon
|
r25419 | self.stdscr.clear() | ||
self.stdscr.refresh() | ||||
Denis Laxalde
|
r43796 | elif keypressed in [curses.ascii.ctrl("L")]: | ||
Kyle Lippincott
|
r42159 | # scroll the current line to the top of the screen, and redraw | ||
# everything | ||||
Nathan Goldbaum
|
r29957 | self.scrolllines(self.selecteditemstartline) | ||
Kyle Lippincott
|
r42159 | self.stdscr.clear() | ||
self.stdscr.refresh() | ||||
Laurent Charignon
|
r24310 | |||
def main(self, stdscr): | ||||
""" | ||||
method to be wrapped by curses.wrapper() for selecting chunks. | ||||
Ryan McElroy
|
r28579 | """ | ||
Laurent Charignon
|
r24310 | |||
Pierre-Yves David
|
r31931 | origsigwinch = sentinel = object() | ||
Augie Fackler
|
r43347 | if util.safehasattr(signal, b'SIGWINCH'): | ||
Augie Fackler
|
r43346 | origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler) | ||
Pierre-Yves David
|
r31930 | try: | ||
return self._main(stdscr) | ||||
finally: | ||||
Pierre-Yves David
|
r31931 | if origsigwinch is not sentinel: | ||
signal.signal(signal.SIGWINCH, origsigwinch) | ||||
Pierre-Yves David
|
r31929 | |||
def _main(self, stdscr): | ||||
Laurent Charignon
|
r24310 | self.stdscr = stdscr | ||
Laurent Charignon
|
r25820 | # error during initialization, cannot be printed in the curses | ||
# interface, it should be printed by the calling code | ||||
Kyle Lippincott
|
r38058 | self.initexc = None | ||
Laurent Charignon
|
r24310 | self.yscreensize, self.xscreensize = self.stdscr.getmaxyx() | ||
curses.start_color() | ||||
Elmar Bartel
|
r35546 | try: | ||
curses.use_default_colors() | ||||
except curses.error: | ||||
self.usecolor = False | ||||
Laurent Charignon
|
r24310 | |||
Kyle Lippincott
|
r42160 | # In some situations we may have some cruft left on the "alternate | ||
# screen" from another program (or previous iterations of ourself), and | ||||
# we won't clear it if the scroll region is small enough to comfortably | ||||
# fit on the terminal. | ||||
self.stdscr.clear() | ||||
Alexander Kobjolke
|
r42192 | # don't display the cursor | ||
try: | ||||
curses.curs_set(0) | ||||
except curses.error: | ||||
pass | ||||
Laurent Charignon
|
r24310 | # available colors: black, blue, cyan, green, magenta, white, yellow | ||
# init_pair(color_id, foreground_color, background_color) | ||||
Augie Fackler
|
r43347 | self.initcolorpair(None, None, name=b"normal") | ||
Augie Fackler
|
r43346 | self.initcolorpair( | ||
Augie Fackler
|
r43347 | curses.COLOR_WHITE, curses.COLOR_MAGENTA, name=b"selected" | ||
Augie Fackler
|
r43346 | ) | ||
Augie Fackler
|
r43347 | self.initcolorpair(curses.COLOR_RED, None, name=b"deletion") | ||
self.initcolorpair(curses.COLOR_GREEN, None, name=b"addition") | ||||
self.initcolorpair( | ||||
curses.COLOR_WHITE, curses.COLOR_BLUE, name=b"legend" | ||||
) | ||||
Laurent Charignon
|
r24310 | # newwin([height, width,] begin_y, begin_x) | ||
self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0) | ||||
Augie Fackler
|
r43346 | self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences | ||
Laurent Charignon
|
r24310 | |||
# 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 | ||||
Laurent Charignon
|
r25821 | try: | ||
self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize) | ||||
except curses.error: | ||||
Kyle Lippincott
|
r38058 | self.initexc = fallbackerror( | ||
Augie Fackler
|
r43347 | _(b'this diff is too large to be displayed') | ||
Augie Fackler
|
r43346 | ) | ||
Laurent Charignon
|
r25821 | return | ||
Mads Kiilerich
|
r30332 | # initialize selecteditemendline (initial start-line is 0) | ||
Laurent Charignon
|
r24310 | self.selecteditemendline = self.getnumlinesdisplayed( | ||
Augie Fackler
|
r43346 | self.currentselecteditem, recursechildren=False | ||
) | ||||
Laurent Charignon
|
r24310 | |||
while True: | ||||
self.updatescreen() | ||||
try: | ||||
Augie Fackler
|
r43347 | with self.ui.timeblockedsection(b'crecord'): | ||
Simon Farnsworth
|
r30981 | keypressed = self.statuswin.getkey() | ||
Laurent Charignon
|
r25556 | if self.errorstr is not None: | ||
self.errorstr = None | ||||
continue | ||||
Laurent Charignon
|
r24310 | except curses.error: | ||
Augie Fackler
|
r43347 | keypressed = b"foobar" | ||
Laurent Charignon
|
r24310 | if self.handlekeypressed(keypressed): | ||
break | ||||
Peter Vitt
|
r33975 | |||
Augie Fackler
|
r43347 | if self.commenttext != b"": | ||
Augie Fackler
|
r43346 | whitespaceremoved = re.sub( | ||
br"(?m)^\s.*(\n|$)", b"", self.commenttext | ||||
) | ||||
Augie Fackler
|
r43347 | if whitespaceremoved != b"": | ||
self.opts[b'message'] = self.commenttext | ||||