##// END OF EJS Templates
revert: fix interactive reverting of end-of-file newline changes...
Rodrigo Damazio Bovendorp -
r45713:8b6a4465 default
parent child Browse files
Show More
@@ -0,0 +1,55 b''
1 #require tic
2
3 Revert interactive tests with the Curses interface
4
5 $ cat <<EOF >> $HGRCPATH
6 > [ui]
7 > interactive = true
8 > interface = curses
9 > [experimental]
10 > crecordtest = testModeCommands
11 > EOF
12
13 TODO: Make a curses version of the other tests from test-revert-interactive.t.
14
15 When a line without EOL is selected during "revert -i"
16
17 $ hg init $TESTTMP/revert-i-curses-eol
18 $ cd $TESTTMP/revert-i-curses-eol
19 $ echo 0 > a
20 $ hg ci -qAm 0
21 $ printf 1 >> a
22 $ hg ci -qAm 1
23 $ cat a
24 0
25 1 (no-eol)
26
27 $ cat <<EOF >testModeCommands
28 > c
29 > EOF
30
31 $ hg revert -ir'.^'
32 reverting a
33 $ cat a
34 0
35
36 When a selected line is reverted to have no EOL
37
38 $ hg init $TESTTMP/revert-i-curses-eol2
39 $ cd $TESTTMP/revert-i-curses-eol2
40 $ printf 0 > a
41 $ hg ci -qAm 0
42 $ echo 0 > a
43 $ hg ci -qAm 1
44 $ cat a
45 0
46
47 $ cat <<EOF >testModeCommands
48 > c
49 > EOF
50
51 $ hg revert -ir'.^'
52 reverting a
53 $ cat a
54 0 (no-eol)
55
@@ -1,2026 +1,2033 b''
1 # stuff related specifically to patch manipulation / parsing
1 # stuff related specifically to patch manipulation / parsing
2 #
2 #
3 # Copyright 2008 Mark Edgington <edgimar@gmail.com>
3 # Copyright 2008 Mark Edgington <edgimar@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 #
7 #
8 # This code is based on the Mark Edgington's crecord extension.
8 # This code is based on the Mark Edgington's crecord extension.
9 # (Itself based on Bryan O'Sullivan's record extension.)
9 # (Itself based on Bryan O'Sullivan's record extension.)
10
10
11 from __future__ import absolute_import
11 from __future__ import absolute_import
12
12
13 import os
13 import os
14 import re
14 import re
15 import signal
15 import signal
16
16
17 from .i18n import _
17 from .i18n import _
18 from .pycompat import (
18 from .pycompat import (
19 getattr,
19 getattr,
20 open,
20 open,
21 )
21 )
22 from . import (
22 from . import (
23 encoding,
23 encoding,
24 error,
24 error,
25 patch as patchmod,
25 patch as patchmod,
26 pycompat,
26 pycompat,
27 scmutil,
27 scmutil,
28 util,
28 util,
29 )
29 )
30 from .utils import stringutil
30 from .utils import stringutil
31
31
32 stringio = util.stringio
32 stringio = util.stringio
33
33
34 # patch comments based on the git one
34 # patch comments based on the git one
35 diffhelptext = _(
35 diffhelptext = _(
36 """# To remove '-' lines, make them ' ' lines (context).
36 """# To remove '-' lines, make them ' ' lines (context).
37 # To remove '+' lines, delete them.
37 # To remove '+' lines, delete them.
38 # Lines starting with # will be removed from the patch.
38 # Lines starting with # will be removed from the patch.
39 """
39 """
40 )
40 )
41
41
42 hunkhelptext = _(
42 hunkhelptext = _(
43 """#
43 """#
44 # If the patch applies cleanly, the edited hunk will immediately be
44 # If the patch applies cleanly, the edited hunk will immediately be
45 # added to the record list. If it does not apply cleanly, a rejects file
45 # added to the record list. If it does not apply cleanly, a rejects file
46 # will be generated. You can use that when you try again. If all lines
46 # will be generated. You can use that when you try again. If all lines
47 # of the hunk are removed, then the edit is aborted and the hunk is left
47 # of the hunk are removed, then the edit is aborted and the hunk is left
48 # unchanged.
48 # unchanged.
49 """
49 """
50 )
50 )
51
51
52 patchhelptext = _(
52 patchhelptext = _(
53 """#
53 """#
54 # If the patch applies cleanly, the edited patch will immediately
54 # If the patch applies cleanly, the edited patch will immediately
55 # be finalised. If it does not apply cleanly, rejects files will be
55 # be finalised. If it does not apply cleanly, rejects files will be
56 # generated. You can use those when you try again.
56 # generated. You can use those when you try again.
57 """
57 """
58 )
58 )
59
59
60 try:
60 try:
61 import curses
61 import curses
62 import curses.ascii
62 import curses.ascii
63
63
64 curses.error
64 curses.error
65 except (ImportError, AttributeError):
65 except (ImportError, AttributeError):
66 curses = False
66 curses = False
67
67
68
68
69 class fallbackerror(error.Abort):
69 class fallbackerror(error.Abort):
70 """Error that indicates the client should try to fallback to text mode."""
70 """Error that indicates the client should try to fallback to text mode."""
71
71
72 # Inherits from error.Abort so that existing behavior is preserved if the
72 # Inherits from error.Abort so that existing behavior is preserved if the
73 # calling code does not know how to fallback.
73 # calling code does not know how to fallback.
74
74
75
75
76 def checkcurses(ui):
76 def checkcurses(ui):
77 """Return True if the user wants to use curses
77 """Return True if the user wants to use curses
78
78
79 This method returns True if curses is found (and that python is built with
79 This method returns True if curses is found (and that python is built with
80 it) and that the user has the correct flag for the ui.
80 it) and that the user has the correct flag for the ui.
81 """
81 """
82 return curses and ui.interface(b"chunkselector") == b"curses"
82 return curses and ui.interface(b"chunkselector") == b"curses"
83
83
84
84
85 class patchnode(object):
85 class patchnode(object):
86 """abstract class for patch graph nodes
86 """abstract class for patch graph nodes
87 (i.e. patchroot, header, hunk, hunkline)
87 (i.e. patchroot, header, hunk, hunkline)
88 """
88 """
89
89
90 def firstchild(self):
90 def firstchild(self):
91 raise NotImplementedError(b"method must be implemented by subclass")
91 raise NotImplementedError(b"method must be implemented by subclass")
92
92
93 def lastchild(self):
93 def lastchild(self):
94 raise NotImplementedError(b"method must be implemented by subclass")
94 raise NotImplementedError(b"method must be implemented by subclass")
95
95
96 def allchildren(self):
96 def allchildren(self):
97 """Return a list of all of the direct children of this node"""
97 """Return a list of all of the direct children of this node"""
98 raise NotImplementedError(b"method must be implemented by subclass")
98 raise NotImplementedError(b"method must be implemented by subclass")
99
99
100 def nextsibling(self):
100 def nextsibling(self):
101 """
101 """
102 Return the closest next item of the same type where there are no items
102 Return the closest next item of the same type where there are no items
103 of different types between the current item and this closest item.
103 of different types between the current item and this closest item.
104 If no such item exists, return None.
104 If no such item exists, return None.
105 """
105 """
106 raise NotImplementedError(b"method must be implemented by subclass")
106 raise NotImplementedError(b"method must be implemented by subclass")
107
107
108 def prevsibling(self):
108 def prevsibling(self):
109 """
109 """
110 Return the closest previous item of the same type where there are no
110 Return the closest previous item of the same type where there are no
111 items of different types between the current item and this closest item.
111 items of different types between the current item and this closest item.
112 If no such item exists, return None.
112 If no such item exists, return None.
113 """
113 """
114 raise NotImplementedError(b"method must be implemented by subclass")
114 raise NotImplementedError(b"method must be implemented by subclass")
115
115
116 def parentitem(self):
116 def parentitem(self):
117 raise NotImplementedError(b"method must be implemented by subclass")
117 raise NotImplementedError(b"method must be implemented by subclass")
118
118
119 def nextitem(self, skipfolded=True):
119 def nextitem(self, skipfolded=True):
120 """
120 """
121 Try to return the next item closest to this item, regardless of item's
121 Try to return the next item closest to this item, regardless of item's
122 type (header, hunk, or hunkline).
122 type (header, hunk, or hunkline).
123
123
124 If skipfolded == True, and the current item is folded, then the child
124 If skipfolded == True, and the current item is folded, then the child
125 items that are hidden due to folding will be skipped when determining
125 items that are hidden due to folding will be skipped when determining
126 the next item.
126 the next item.
127
127
128 If it is not possible to get the next item, return None.
128 If it is not possible to get the next item, return None.
129 """
129 """
130 try:
130 try:
131 itemfolded = self.folded
131 itemfolded = self.folded
132 except AttributeError:
132 except AttributeError:
133 itemfolded = False
133 itemfolded = False
134 if skipfolded and itemfolded:
134 if skipfolded and itemfolded:
135 nextitem = self.nextsibling()
135 nextitem = self.nextsibling()
136 if nextitem is None:
136 if nextitem is None:
137 try:
137 try:
138 nextitem = self.parentitem().nextsibling()
138 nextitem = self.parentitem().nextsibling()
139 except AttributeError:
139 except AttributeError:
140 nextitem = None
140 nextitem = None
141 return nextitem
141 return nextitem
142 else:
142 else:
143 # try child
143 # try child
144 item = self.firstchild()
144 item = self.firstchild()
145 if item is not None:
145 if item is not None:
146 return item
146 return item
147
147
148 # else try next sibling
148 # else try next sibling
149 item = self.nextsibling()
149 item = self.nextsibling()
150 if item is not None:
150 if item is not None:
151 return item
151 return item
152
152
153 try:
153 try:
154 # else try parent's next sibling
154 # else try parent's next sibling
155 item = self.parentitem().nextsibling()
155 item = self.parentitem().nextsibling()
156 if item is not None:
156 if item is not None:
157 return item
157 return item
158
158
159 # else return grandparent's next sibling (or None)
159 # else return grandparent's next sibling (or None)
160 return self.parentitem().parentitem().nextsibling()
160 return self.parentitem().parentitem().nextsibling()
161
161
162 except AttributeError: # parent and/or grandparent was None
162 except AttributeError: # parent and/or grandparent was None
163 return None
163 return None
164
164
165 def previtem(self):
165 def previtem(self):
166 """
166 """
167 Try to return the previous item closest to this item, regardless of
167 Try to return the previous item closest to this item, regardless of
168 item's type (header, hunk, or hunkline).
168 item's type (header, hunk, or hunkline).
169
169
170 If it is not possible to get the previous item, return None.
170 If it is not possible to get the previous item, return None.
171 """
171 """
172 # try previous sibling's last child's last child,
172 # try previous sibling's last child's last child,
173 # else try previous sibling's last child, else try previous sibling
173 # else try previous sibling's last child, else try previous sibling
174 prevsibling = self.prevsibling()
174 prevsibling = self.prevsibling()
175 if prevsibling is not None:
175 if prevsibling is not None:
176 prevsiblinglastchild = prevsibling.lastchild()
176 prevsiblinglastchild = prevsibling.lastchild()
177 if (prevsiblinglastchild is not None) and not prevsibling.folded:
177 if (prevsiblinglastchild is not None) and not prevsibling.folded:
178 prevsiblinglclc = prevsiblinglastchild.lastchild()
178 prevsiblinglclc = prevsiblinglastchild.lastchild()
179 if (
179 if (
180 prevsiblinglclc is not None
180 prevsiblinglclc is not None
181 ) and not prevsiblinglastchild.folded:
181 ) and not prevsiblinglastchild.folded:
182 return prevsiblinglclc
182 return prevsiblinglclc
183 else:
183 else:
184 return prevsiblinglastchild
184 return prevsiblinglastchild
185 else:
185 else:
186 return prevsibling
186 return prevsibling
187
187
188 # try parent (or None)
188 # try parent (or None)
189 return self.parentitem()
189 return self.parentitem()
190
190
191
191
192 class patch(patchnode, list): # todo: rename patchroot
192 class patch(patchnode, list): # todo: rename patchroot
193 """
193 """
194 list of header objects representing the patch.
194 list of header objects representing the patch.
195 """
195 """
196
196
197 def __init__(self, headerlist):
197 def __init__(self, headerlist):
198 self.extend(headerlist)
198 self.extend(headerlist)
199 # add parent patch object reference to each header
199 # add parent patch object reference to each header
200 for header in self:
200 for header in self:
201 header.patch = self
201 header.patch = self
202
202
203
203
204 class uiheader(patchnode):
204 class uiheader(patchnode):
205 """patch header
205 """patch header
206
206
207 xxx shouldn't we move this to mercurial/patch.py ?
207 xxx shouldn't we move this to mercurial/patch.py ?
208 """
208 """
209
209
210 def __init__(self, header):
210 def __init__(self, header):
211 self.nonuiheader = header
211 self.nonuiheader = header
212 # flag to indicate whether to apply this chunk
212 # flag to indicate whether to apply this chunk
213 self.applied = True
213 self.applied = True
214 # flag which only affects the status display indicating if a node's
214 # flag which only affects the status display indicating if a node's
215 # children are partially applied (i.e. some applied, some not).
215 # children are partially applied (i.e. some applied, some not).
216 self.partial = False
216 self.partial = False
217
217
218 # flag to indicate whether to display as folded/unfolded to user
218 # flag to indicate whether to display as folded/unfolded to user
219 self.folded = True
219 self.folded = True
220
220
221 # list of all headers in patch
221 # list of all headers in patch
222 self.patch = None
222 self.patch = None
223
223
224 # flag is False if this header was ever unfolded from initial state
224 # flag is False if this header was ever unfolded from initial state
225 self.neverunfolded = True
225 self.neverunfolded = True
226 self.hunks = [uihunk(h, self) for h in self.hunks]
226 self.hunks = [uihunk(h, self) for h in self.hunks]
227
227
228 def prettystr(self):
228 def prettystr(self):
229 x = stringio()
229 x = stringio()
230 self.pretty(x)
230 self.pretty(x)
231 return x.getvalue()
231 return x.getvalue()
232
232
233 def nextsibling(self):
233 def nextsibling(self):
234 numheadersinpatch = len(self.patch)
234 numheadersinpatch = len(self.patch)
235 indexofthisheader = self.patch.index(self)
235 indexofthisheader = self.patch.index(self)
236
236
237 if indexofthisheader < numheadersinpatch - 1:
237 if indexofthisheader < numheadersinpatch - 1:
238 nextheader = self.patch[indexofthisheader + 1]
238 nextheader = self.patch[indexofthisheader + 1]
239 return nextheader
239 return nextheader
240 else:
240 else:
241 return None
241 return None
242
242
243 def prevsibling(self):
243 def prevsibling(self):
244 indexofthisheader = self.patch.index(self)
244 indexofthisheader = self.patch.index(self)
245 if indexofthisheader > 0:
245 if indexofthisheader > 0:
246 previousheader = self.patch[indexofthisheader - 1]
246 previousheader = self.patch[indexofthisheader - 1]
247 return previousheader
247 return previousheader
248 else:
248 else:
249 return None
249 return None
250
250
251 def parentitem(self):
251 def parentitem(self):
252 """
252 """
253 there is no 'real' parent item of a header that can be selected,
253 there is no 'real' parent item of a header that can be selected,
254 so return None.
254 so return None.
255 """
255 """
256 return None
256 return None
257
257
258 def firstchild(self):
258 def firstchild(self):
259 """return the first child of this item, if one exists. otherwise
259 """return the first child of this item, if one exists. otherwise
260 None."""
260 None."""
261 if len(self.hunks) > 0:
261 if len(self.hunks) > 0:
262 return self.hunks[0]
262 return self.hunks[0]
263 else:
263 else:
264 return None
264 return None
265
265
266 def lastchild(self):
266 def lastchild(self):
267 """return the last child of this item, if one exists. otherwise
267 """return the last child of this item, if one exists. otherwise
268 None."""
268 None."""
269 if len(self.hunks) > 0:
269 if len(self.hunks) > 0:
270 return self.hunks[-1]
270 return self.hunks[-1]
271 else:
271 else:
272 return None
272 return None
273
273
274 def allchildren(self):
274 def allchildren(self):
275 """return a list of all of the direct children of this node"""
275 """return a list of all of the direct children of this node"""
276 return self.hunks
276 return self.hunks
277
277
278 def __getattr__(self, name):
278 def __getattr__(self, name):
279 return getattr(self.nonuiheader, name)
279 return getattr(self.nonuiheader, name)
280
280
281
281
282 class uihunkline(patchnode):
282 class uihunkline(patchnode):
283 """represents a changed line in a hunk"""
283 """represents a changed line in a hunk"""
284
284
285 def __init__(self, linetext, hunk):
285 def __init__(self, linetext, hunk):
286 self.linetext = linetext
286 self.linetext = linetext
287 self.applied = True
287 self.applied = True
288 # the parent hunk to which this line belongs
288 # the parent hunk to which this line belongs
289 self.hunk = hunk
289 self.hunk = hunk
290 # folding lines currently is not used/needed, but this flag is needed
290 # folding lines currently is not used/needed, but this flag is needed
291 # in the previtem method.
291 # in the previtem method.
292 self.folded = False
292 self.folded = False
293
293
294 def prettystr(self):
294 def prettystr(self):
295 return self.linetext
295 return self.linetext
296
296
297 def nextsibling(self):
297 def nextsibling(self):
298 numlinesinhunk = len(self.hunk.changedlines)
298 numlinesinhunk = len(self.hunk.changedlines)
299 indexofthisline = self.hunk.changedlines.index(self)
299 indexofthisline = self.hunk.changedlines.index(self)
300
300
301 if indexofthisline < numlinesinhunk - 1:
301 if indexofthisline < numlinesinhunk - 1:
302 nextline = self.hunk.changedlines[indexofthisline + 1]
302 nextline = self.hunk.changedlines[indexofthisline + 1]
303 return nextline
303 return nextline
304 else:
304 else:
305 return None
305 return None
306
306
307 def prevsibling(self):
307 def prevsibling(self):
308 indexofthisline = self.hunk.changedlines.index(self)
308 indexofthisline = self.hunk.changedlines.index(self)
309 if indexofthisline > 0:
309 if indexofthisline > 0:
310 previousline = self.hunk.changedlines[indexofthisline - 1]
310 previousline = self.hunk.changedlines[indexofthisline - 1]
311 return previousline
311 return previousline
312 else:
312 else:
313 return None
313 return None
314
314
315 def parentitem(self):
315 def parentitem(self):
316 """return the parent to the current item"""
316 """return the parent to the current item"""
317 return self.hunk
317 return self.hunk
318
318
319 def firstchild(self):
319 def firstchild(self):
320 """return the first child of this item, if one exists. otherwise
320 """return the first child of this item, if one exists. otherwise
321 None."""
321 None."""
322 # hunk-lines don't have children
322 # hunk-lines don't have children
323 return None
323 return None
324
324
325 def lastchild(self):
325 def lastchild(self):
326 """return the last child of this item, if one exists. otherwise
326 """return the last child of this item, if one exists. otherwise
327 None."""
327 None."""
328 # hunk-lines don't have children
328 # hunk-lines don't have children
329 return None
329 return None
330
330
331
331
332 class uihunk(patchnode):
332 class uihunk(patchnode):
333 """ui patch hunk, wraps a hunk and keep track of ui behavior """
333 """ui patch hunk, wraps a hunk and keep track of ui behavior """
334
334
335 maxcontext = 3
335 maxcontext = 3
336
336
337 def __init__(self, hunk, header):
337 def __init__(self, hunk, header):
338 self._hunk = hunk
338 self._hunk = hunk
339 self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
339 self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
340 self.header = header
340 self.header = header
341 # used at end for detecting how many removed lines were un-applied
341 # used at end for detecting how many removed lines were un-applied
342 self.originalremoved = self.removed
342 self.originalremoved = self.removed
343
343
344 # flag to indicate whether to display as folded/unfolded to user
344 # flag to indicate whether to display as folded/unfolded to user
345 self.folded = True
345 self.folded = True
346 # flag to indicate whether to apply this chunk
346 # flag to indicate whether to apply this chunk
347 self.applied = True
347 self.applied = True
348 # flag which only affects the status display indicating if a node's
348 # flag which only affects the status display indicating if a node's
349 # children are partially applied (i.e. some applied, some not).
349 # children are partially applied (i.e. some applied, some not).
350 self.partial = False
350 self.partial = False
351
351
352 def nextsibling(self):
352 def nextsibling(self):
353 numhunksinheader = len(self.header.hunks)
353 numhunksinheader = len(self.header.hunks)
354 indexofthishunk = self.header.hunks.index(self)
354 indexofthishunk = self.header.hunks.index(self)
355
355
356 if indexofthishunk < numhunksinheader - 1:
356 if indexofthishunk < numhunksinheader - 1:
357 nexthunk = self.header.hunks[indexofthishunk + 1]
357 nexthunk = self.header.hunks[indexofthishunk + 1]
358 return nexthunk
358 return nexthunk
359 else:
359 else:
360 return None
360 return None
361
361
362 def prevsibling(self):
362 def prevsibling(self):
363 indexofthishunk = self.header.hunks.index(self)
363 indexofthishunk = self.header.hunks.index(self)
364 if indexofthishunk > 0:
364 if indexofthishunk > 0:
365 previoushunk = self.header.hunks[indexofthishunk - 1]
365 previoushunk = self.header.hunks[indexofthishunk - 1]
366 return previoushunk
366 return previoushunk
367 else:
367 else:
368 return None
368 return None
369
369
370 def parentitem(self):
370 def parentitem(self):
371 """return the parent to the current item"""
371 """return the parent to the current item"""
372 return self.header
372 return self.header
373
373
374 def firstchild(self):
374 def firstchild(self):
375 """return the first child of this item, if one exists. otherwise
375 """return the first child of this item, if one exists. otherwise
376 None."""
376 None."""
377 if len(self.changedlines) > 0:
377 if len(self.changedlines) > 0:
378 return self.changedlines[0]
378 return self.changedlines[0]
379 else:
379 else:
380 return None
380 return None
381
381
382 def lastchild(self):
382 def lastchild(self):
383 """return the last child of this item, if one exists. otherwise
383 """return the last child of this item, if one exists. otherwise
384 None."""
384 None."""
385 if len(self.changedlines) > 0:
385 if len(self.changedlines) > 0:
386 return self.changedlines[-1]
386 return self.changedlines[-1]
387 else:
387 else:
388 return None
388 return None
389
389
390 def allchildren(self):
390 def allchildren(self):
391 """return a list of all of the direct children of this node"""
391 """return a list of all of the direct children of this node"""
392 return self.changedlines
392 return self.changedlines
393
393
394 def countchanges(self):
394 def countchanges(self):
395 """changedlines -> (n+,n-)"""
395 """changedlines -> (n+,n-)"""
396 add = len(
396 add = len(
397 [
397 [
398 l
398 l
399 for l in self.changedlines
399 for l in self.changedlines
400 if l.applied and l.prettystr().startswith(b'+')
400 if l.applied and l.prettystr().startswith(b'+')
401 ]
401 ]
402 )
402 )
403 rem = len(
403 rem = len(
404 [
404 [
405 l
405 l
406 for l in self.changedlines
406 for l in self.changedlines
407 if l.applied and l.prettystr().startswith(b'-')
407 if l.applied and l.prettystr().startswith(b'-')
408 ]
408 ]
409 )
409 )
410 return add, rem
410 return add, rem
411
411
412 def getfromtoline(self):
412 def getfromtoline(self):
413 # calculate the number of removed lines converted to context lines
413 # calculate the number of removed lines converted to context lines
414 removedconvertedtocontext = self.originalremoved - self.removed
414 removedconvertedtocontext = self.originalremoved - self.removed
415
415
416 contextlen = (
416 contextlen = (
417 len(self.before) + len(self.after) + removedconvertedtocontext
417 len(self.before) + len(self.after) + removedconvertedtocontext
418 )
418 )
419 if self.after and self.after[-1] == b'\\ No newline at end of file\n':
419 if self.after and self.after[-1] == b'\\ No newline at end of file\n':
420 contextlen -= 1
420 contextlen -= 1
421 fromlen = contextlen + self.removed
421 fromlen = contextlen + self.removed
422 tolen = contextlen + self.added
422 tolen = contextlen + self.added
423
423
424 # diffutils manual, section "2.2.2.2 detailed description of unified
424 # diffutils manual, section "2.2.2.2 detailed description of unified
425 # format": "an empty hunk is considered to end at the line that
425 # format": "an empty hunk is considered to end at the line that
426 # precedes the hunk."
426 # precedes the hunk."
427 #
427 #
428 # so, if either of hunks is empty, decrease its line start. --immerrr
428 # so, if either of hunks is empty, decrease its line start. --immerrr
429 # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
429 # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
430 fromline, toline = self.fromline, self.toline
430 fromline, toline = self.fromline, self.toline
431 if fromline != 0:
431 if fromline != 0:
432 if fromlen == 0:
432 if fromlen == 0:
433 fromline -= 1
433 fromline -= 1
434 if tolen == 0 and toline > 0:
434 if tolen == 0 and toline > 0:
435 toline -= 1
435 toline -= 1
436
436
437 fromtoline = b'@@ -%d,%d +%d,%d @@%s\n' % (
437 fromtoline = b'@@ -%d,%d +%d,%d @@%s\n' % (
438 fromline,
438 fromline,
439 fromlen,
439 fromlen,
440 toline,
440 toline,
441 tolen,
441 tolen,
442 self.proc and (b' ' + self.proc),
442 self.proc and (b' ' + self.proc),
443 )
443 )
444 return fromtoline
444 return fromtoline
445
445
446 def write(self, fp):
446 def write(self, fp):
447 # updated self.added/removed, which are used by getfromtoline()
447 # updated self.added/removed, which are used by getfromtoline()
448 self.added, self.removed = self.countchanges()
448 self.added, self.removed = self.countchanges()
449 fp.write(self.getfromtoline())
449 fp.write(self.getfromtoline())
450
450
451 hunklinelist = []
451 hunklinelist = []
452 # add the following to the list: (1) all applied lines, and
452 # add the following to the list: (1) all applied lines, and
453 # (2) all unapplied removal lines (convert these to context lines)
453 # (2) all unapplied removal lines (convert these to context lines)
454 for changedline in self.changedlines:
454 for changedline in self.changedlines:
455 changedlinestr = changedline.prettystr()
455 changedlinestr = changedline.prettystr()
456 if changedline.applied:
456 if changedline.applied:
457 hunklinelist.append(changedlinestr)
457 hunklinelist.append(changedlinestr)
458 elif changedlinestr.startswith(b"-"):
458 elif changedlinestr.startswith(b"-"):
459 hunklinelist.append(b" " + changedlinestr[1:])
459 hunklinelist.append(b" " + changedlinestr[1:])
460
460
461 fp.write(b''.join(self.before + hunklinelist + self.after))
461 fp.write(b''.join(self.before + hunklinelist + self.after))
462
462
463 pretty = write
463 pretty = write
464
464
465 def prettystr(self):
465 def prettystr(self):
466 x = stringio()
466 x = stringio()
467 self.pretty(x)
467 self.pretty(x)
468 return x.getvalue()
468 return x.getvalue()
469
469
470 def reversehunk(self):
470 def reversehunk(self):
471 """return a recordhunk which is the reverse of the hunk
471 """return a recordhunk which is the reverse of the hunk
472
472
473 Assuming the displayed patch is diff(A, B) result. The returned hunk is
473 Assuming the displayed patch is diff(A, B) result. The returned hunk is
474 intended to be applied to B, instead of A.
474 intended to be applied to B, instead of A.
475
475
476 For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and
476 For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and
477 the user made the following selection:
477 the user made the following selection:
478
478
479 0
479 0
480 [x] -1 [x]: selected
480 [x] -1 [x]: selected
481 [ ] -2 [ ]: not selected
481 [ ] -2 [ ]: not selected
482 [x] +3
482 [x] +3
483 [ ] +4
483 [ ] +4
484 [x] +5
484 [x] +5
485 6
485 6
486
486
487 This function returns a hunk like:
487 This function returns a hunk like:
488
488
489 0
489 0
490 -3
490 -3
491 -4
491 -4
492 -5
492 -5
493 +1
493 +1
494 +4
494 +4
495 6
495 6
496
496
497 Note "4" was first deleted then added. That's because "4" exists in B
497 Note "4" was first deleted then added. That's because "4" exists in B
498 side and "-4" must exist between "-3" and "-5" to make the patch
498 side and "-4" must exist between "-3" and "-5" to make the patch
499 applicable to B.
499 applicable to B.
500 """
500 """
501 dels = []
501 dels = []
502 adds = []
502 adds = []
503 noeol = False
503 for line in self.changedlines:
504 for line in self.changedlines:
504 text = line.linetext
505 text = line.linetext
506 if line.linetext == b'\\ No newline at end of file\n':
507 noeol = True
508 break
505 if line.applied:
509 if line.applied:
506 if text.startswith(b'+'):
510 if text.startswith(b'+'):
507 dels.append(text[1:])
511 dels.append(text[1:])
508 elif text.startswith(b'-'):
512 elif text.startswith(b'-'):
509 adds.append(text[1:])
513 adds.append(text[1:])
510 elif text.startswith(b'+'):
514 elif text.startswith(b'+'):
511 dels.append(text[1:])
515 dels.append(text[1:])
512 adds.append(text[1:])
516 adds.append(text[1:])
513 hunk = [b'-%s' % l for l in dels] + [b'+%s' % l for l in adds]
517 hunk = [b'-%s' % l for l in dels] + [b'+%s' % l for l in adds]
518 if noeol and hunk:
519 # Remove the newline from the end of the hunk.
520 hunk[-1] = hunk[-1][:-1]
514 h = self._hunk
521 h = self._hunk
515 return patchmod.recordhunk(
522 return patchmod.recordhunk(
516 h.header, h.toline, h.fromline, h.proc, h.before, hunk, h.after
523 h.header, h.toline, h.fromline, h.proc, h.before, hunk, h.after
517 )
524 )
518
525
519 def __getattr__(self, name):
526 def __getattr__(self, name):
520 return getattr(self._hunk, name)
527 return getattr(self._hunk, name)
521
528
522 def __repr__(self):
529 def __repr__(self):
523 return '<hunk %r@%d>' % (self.filename(), self.fromline)
530 return '<hunk %r@%d>' % (self.filename(), self.fromline)
524
531
525
532
526 def filterpatch(ui, chunks, chunkselector, operation=None):
533 def filterpatch(ui, chunks, chunkselector, operation=None):
527 """interactively filter patch chunks into applied-only chunks"""
534 """interactively filter patch chunks into applied-only chunks"""
528 chunks = list(chunks)
535 chunks = list(chunks)
529 # convert chunks list into structure suitable for displaying/modifying
536 # convert chunks list into structure suitable for displaying/modifying
530 # with curses. create a list of headers only.
537 # with curses. create a list of headers only.
531 headers = [c for c in chunks if isinstance(c, patchmod.header)]
538 headers = [c for c in chunks if isinstance(c, patchmod.header)]
532
539
533 # if there are no changed files
540 # if there are no changed files
534 if len(headers) == 0:
541 if len(headers) == 0:
535 return [], {}
542 return [], {}
536 uiheaders = [uiheader(h) for h in headers]
543 uiheaders = [uiheader(h) for h in headers]
537 # let user choose headers/hunks/lines, and mark their applied flags
544 # let user choose headers/hunks/lines, and mark their applied flags
538 # accordingly
545 # accordingly
539 ret = chunkselector(ui, uiheaders, operation=operation)
546 ret = chunkselector(ui, uiheaders, operation=operation)
540 appliedhunklist = []
547 appliedhunklist = []
541 for hdr in uiheaders:
548 for hdr in uiheaders:
542 if hdr.applied and (
549 if hdr.applied and (
543 hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0
550 hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0
544 ):
551 ):
545 appliedhunklist.append(hdr)
552 appliedhunklist.append(hdr)
546 fixoffset = 0
553 fixoffset = 0
547 for hnk in hdr.hunks:
554 for hnk in hdr.hunks:
548 if hnk.applied:
555 if hnk.applied:
549 appliedhunklist.append(hnk)
556 appliedhunklist.append(hnk)
550 # adjust the 'to'-line offset of the hunk to be correct
557 # adjust the 'to'-line offset of the hunk to be correct
551 # after de-activating some of the other hunks for this file
558 # after de-activating some of the other hunks for this file
552 if fixoffset:
559 if fixoffset:
553 # hnk = copy.copy(hnk) # necessary??
560 # hnk = copy.copy(hnk) # necessary??
554 hnk.toline += fixoffset
561 hnk.toline += fixoffset
555 else:
562 else:
556 fixoffset += hnk.removed - hnk.added
563 fixoffset += hnk.removed - hnk.added
557
564
558 return (appliedhunklist, ret)
565 return (appliedhunklist, ret)
559
566
560
567
561 def chunkselector(ui, headerlist, operation=None):
568 def chunkselector(ui, headerlist, operation=None):
562 """
569 """
563 curses interface to get selection of chunks, and mark the applied flags
570 curses interface to get selection of chunks, and mark the applied flags
564 of the chosen chunks.
571 of the chosen chunks.
565 """
572 """
566 ui.write(_(b'starting interactive selection\n'))
573 ui.write(_(b'starting interactive selection\n'))
567 chunkselector = curseschunkselector(headerlist, ui, operation)
574 chunkselector = curseschunkselector(headerlist, ui, operation)
568 origsigtstp = sentinel = object()
575 origsigtstp = sentinel = object()
569 if util.safehasattr(signal, b'SIGTSTP'):
576 if util.safehasattr(signal, b'SIGTSTP'):
570 origsigtstp = signal.getsignal(signal.SIGTSTP)
577 origsigtstp = signal.getsignal(signal.SIGTSTP)
571 try:
578 try:
572 with util.with_lc_ctype():
579 with util.with_lc_ctype():
573 curses.wrapper(chunkselector.main)
580 curses.wrapper(chunkselector.main)
574 if chunkselector.initexc is not None:
581 if chunkselector.initexc is not None:
575 raise chunkselector.initexc
582 raise chunkselector.initexc
576 # ncurses does not restore signal handler for SIGTSTP
583 # ncurses does not restore signal handler for SIGTSTP
577 finally:
584 finally:
578 if origsigtstp is not sentinel:
585 if origsigtstp is not sentinel:
579 signal.signal(signal.SIGTSTP, origsigtstp)
586 signal.signal(signal.SIGTSTP, origsigtstp)
580 return chunkselector.opts
587 return chunkselector.opts
581
588
582
589
583 def testdecorator(testfn, f):
590 def testdecorator(testfn, f):
584 def u(*args, **kwargs):
591 def u(*args, **kwargs):
585 return f(testfn, *args, **kwargs)
592 return f(testfn, *args, **kwargs)
586
593
587 return u
594 return u
588
595
589
596
590 def testchunkselector(testfn, ui, headerlist, operation=None):
597 def testchunkselector(testfn, ui, headerlist, operation=None):
591 """
598 """
592 test interface to get selection of chunks, and mark the applied flags
599 test interface to get selection of chunks, and mark the applied flags
593 of the chosen chunks.
600 of the chosen chunks.
594 """
601 """
595 chunkselector = curseschunkselector(headerlist, ui, operation)
602 chunkselector = curseschunkselector(headerlist, ui, operation)
596
603
597 class dummystdscr(object):
604 class dummystdscr(object):
598 def clear(self):
605 def clear(self):
599 pass
606 pass
600
607
601 def refresh(self):
608 def refresh(self):
602 pass
609 pass
603
610
604 chunkselector.stdscr = dummystdscr()
611 chunkselector.stdscr = dummystdscr()
605 if testfn and os.path.exists(testfn):
612 if testfn and os.path.exists(testfn):
606 testf = open(testfn, 'r')
613 testf = open(testfn, 'r')
607 testcommands = [x.rstrip('\n') for x in testf.readlines()]
614 testcommands = [x.rstrip('\n') for x in testf.readlines()]
608 testf.close()
615 testf.close()
609 while True:
616 while True:
610 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
617 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
611 break
618 break
612 return chunkselector.opts
619 return chunkselector.opts
613
620
614
621
615 _headermessages = { # {operation: text}
622 _headermessages = { # {operation: text}
616 b'apply': _(b'Select hunks to apply'),
623 b'apply': _(b'Select hunks to apply'),
617 b'discard': _(b'Select hunks to discard'),
624 b'discard': _(b'Select hunks to discard'),
618 b'keep': _(b'Select hunks to keep'),
625 b'keep': _(b'Select hunks to keep'),
619 None: _(b'Select hunks to record'),
626 None: _(b'Select hunks to record'),
620 }
627 }
621
628
622
629
623 class curseschunkselector(object):
630 class curseschunkselector(object):
624 def __init__(self, headerlist, ui, operation=None):
631 def __init__(self, headerlist, ui, operation=None):
625 # put the headers into a patch object
632 # put the headers into a patch object
626 self.headerlist = patch(headerlist)
633 self.headerlist = patch(headerlist)
627
634
628 self.ui = ui
635 self.ui = ui
629 self.opts = {}
636 self.opts = {}
630
637
631 self.errorstr = None
638 self.errorstr = None
632 # list of all chunks
639 # list of all chunks
633 self.chunklist = []
640 self.chunklist = []
634 for h in headerlist:
641 for h in headerlist:
635 self.chunklist.append(h)
642 self.chunklist.append(h)
636 self.chunklist.extend(h.hunks)
643 self.chunklist.extend(h.hunks)
637
644
638 # dictionary mapping (fgcolor, bgcolor) pairs to the
645 # dictionary mapping (fgcolor, bgcolor) pairs to the
639 # corresponding curses color-pair value.
646 # corresponding curses color-pair value.
640 self.colorpairs = {}
647 self.colorpairs = {}
641 # maps custom nicknames of color-pairs to curses color-pair values
648 # maps custom nicknames of color-pairs to curses color-pair values
642 self.colorpairnames = {}
649 self.colorpairnames = {}
643
650
644 # Honor color setting of ui section. Keep colored setup as
651 # Honor color setting of ui section. Keep colored setup as
645 # long as not explicitly set to a falsy value - especially,
652 # long as not explicitly set to a falsy value - especially,
646 # when not set at all. This is to stay most compatible with
653 # when not set at all. This is to stay most compatible with
647 # previous (color only) behaviour.
654 # previous (color only) behaviour.
648 uicolor = stringutil.parsebool(self.ui.config(b'ui', b'color'))
655 uicolor = stringutil.parsebool(self.ui.config(b'ui', b'color'))
649 self.usecolor = uicolor is not False
656 self.usecolor = uicolor is not False
650
657
651 # the currently selected header, hunk, or hunk-line
658 # the currently selected header, hunk, or hunk-line
652 self.currentselecteditem = self.headerlist[0]
659 self.currentselecteditem = self.headerlist[0]
653 self.lastapplieditem = None
660 self.lastapplieditem = None
654
661
655 # updated when printing out patch-display -- the 'lines' here are the
662 # updated when printing out patch-display -- the 'lines' here are the
656 # line positions *in the pad*, not on the screen.
663 # line positions *in the pad*, not on the screen.
657 self.selecteditemstartline = 0
664 self.selecteditemstartline = 0
658 self.selecteditemendline = None
665 self.selecteditemendline = None
659
666
660 # define indentation levels
667 # define indentation levels
661 self.headerindentnumchars = 0
668 self.headerindentnumchars = 0
662 self.hunkindentnumchars = 3
669 self.hunkindentnumchars = 3
663 self.hunklineindentnumchars = 6
670 self.hunklineindentnumchars = 6
664
671
665 # the first line of the pad to print to the screen
672 # the first line of the pad to print to the screen
666 self.firstlineofpadtoprint = 0
673 self.firstlineofpadtoprint = 0
667
674
668 # keeps track of the number of lines in the pad
675 # keeps track of the number of lines in the pad
669 self.numpadlines = None
676 self.numpadlines = None
670
677
671 self.numstatuslines = 1
678 self.numstatuslines = 1
672
679
673 # keep a running count of the number of lines printed to the pad
680 # keep a running count of the number of lines printed to the pad
674 # (used for determining when the selected item begins/ends)
681 # (used for determining when the selected item begins/ends)
675 self.linesprintedtopadsofar = 0
682 self.linesprintedtopadsofar = 0
676
683
677 # stores optional text for a commit comment provided by the user
684 # stores optional text for a commit comment provided by the user
678 self.commenttext = b""
685 self.commenttext = b""
679
686
680 # if the last 'toggle all' command caused all changes to be applied
687 # if the last 'toggle all' command caused all changes to be applied
681 self.waslasttoggleallapplied = True
688 self.waslasttoggleallapplied = True
682
689
683 # affects some ui text
690 # affects some ui text
684 if operation not in _headermessages:
691 if operation not in _headermessages:
685 raise error.ProgrammingError(
692 raise error.ProgrammingError(
686 b'unexpected operation: %s' % operation
693 b'unexpected operation: %s' % operation
687 )
694 )
688 self.operation = operation
695 self.operation = operation
689
696
690 def uparrowevent(self):
697 def uparrowevent(self):
691 """
698 """
692 try to select the previous item to the current item that has the
699 try to select the previous item to the current item that has the
693 most-indented level. for example, if a hunk is selected, try to select
700 most-indented level. for example, if a hunk is selected, try to select
694 the last hunkline of the hunk prior to the selected hunk. or, if
701 the last hunkline of the hunk prior to the selected hunk. or, if
695 the first hunkline of a hunk is currently selected, then select the
702 the first hunkline of a hunk is currently selected, then select the
696 hunk itself.
703 hunk itself.
697 """
704 """
698 currentitem = self.currentselecteditem
705 currentitem = self.currentselecteditem
699
706
700 nextitem = currentitem.previtem()
707 nextitem = currentitem.previtem()
701
708
702 if nextitem is None:
709 if nextitem is None:
703 # if no parent item (i.e. currentitem is the first header), then
710 # if no parent item (i.e. currentitem is the first header), then
704 # no change...
711 # no change...
705 nextitem = currentitem
712 nextitem = currentitem
706
713
707 self.currentselecteditem = nextitem
714 self.currentselecteditem = nextitem
708
715
709 def uparrowshiftevent(self):
716 def uparrowshiftevent(self):
710 """
717 """
711 select (if possible) the previous item on the same level as the
718 select (if possible) the previous item on the same level as the
712 currently selected item. otherwise, select (if possible) the
719 currently selected item. otherwise, select (if possible) the
713 parent-item of the currently selected item.
720 parent-item of the currently selected item.
714 """
721 """
715 currentitem = self.currentselecteditem
722 currentitem = self.currentselecteditem
716 nextitem = currentitem.prevsibling()
723 nextitem = currentitem.prevsibling()
717 # if there's no previous sibling, try choosing the parent
724 # if there's no previous sibling, try choosing the parent
718 if nextitem is None:
725 if nextitem is None:
719 nextitem = currentitem.parentitem()
726 nextitem = currentitem.parentitem()
720 if nextitem is None:
727 if nextitem is None:
721 # if no parent item (i.e. currentitem is the first header), then
728 # if no parent item (i.e. currentitem is the first header), then
722 # no change...
729 # no change...
723 nextitem = currentitem
730 nextitem = currentitem
724
731
725 self.currentselecteditem = nextitem
732 self.currentselecteditem = nextitem
726 self.recenterdisplayedarea()
733 self.recenterdisplayedarea()
727
734
728 def downarrowevent(self):
735 def downarrowevent(self):
729 """
736 """
730 try to select the next item to the current item that has the
737 try to select the next item to the current item that has the
731 most-indented level. for example, if a hunk is selected, select
738 most-indented level. for example, if a hunk is selected, select
732 the first hunkline of the selected hunk. or, if the last hunkline of
739 the first hunkline of the selected hunk. or, if the last hunkline of
733 a hunk is currently selected, then select the next hunk, if one exists,
740 a hunk is currently selected, then select the next hunk, if one exists,
734 or if not, the next header if one exists.
741 or if not, the next header if one exists.
735 """
742 """
736 # self.startprintline += 1 #debug
743 # self.startprintline += 1 #debug
737 currentitem = self.currentselecteditem
744 currentitem = self.currentselecteditem
738
745
739 nextitem = currentitem.nextitem()
746 nextitem = currentitem.nextitem()
740 # if there's no next item, keep the selection as-is
747 # if there's no next item, keep the selection as-is
741 if nextitem is None:
748 if nextitem is None:
742 nextitem = currentitem
749 nextitem = currentitem
743
750
744 self.currentselecteditem = nextitem
751 self.currentselecteditem = nextitem
745
752
746 def downarrowshiftevent(self):
753 def downarrowshiftevent(self):
747 """
754 """
748 select (if possible) the next item on the same level as the currently
755 select (if possible) the next item on the same level as the currently
749 selected item. otherwise, select (if possible) the next item on the
756 selected item. otherwise, select (if possible) the next item on the
750 same level as the parent item of the currently selected item.
757 same level as the parent item of the currently selected item.
751 """
758 """
752 currentitem = self.currentselecteditem
759 currentitem = self.currentselecteditem
753 nextitem = currentitem.nextsibling()
760 nextitem = currentitem.nextsibling()
754 # if there's no next sibling, try choosing the parent's nextsibling
761 # if there's no next sibling, try choosing the parent's nextsibling
755 if nextitem is None:
762 if nextitem is None:
756 try:
763 try:
757 nextitem = currentitem.parentitem().nextsibling()
764 nextitem = currentitem.parentitem().nextsibling()
758 except AttributeError:
765 except AttributeError:
759 # parentitem returned None, so nextsibling() can't be called
766 # parentitem returned None, so nextsibling() can't be called
760 nextitem = None
767 nextitem = None
761 if nextitem is None:
768 if nextitem is None:
762 # if parent has no next sibling, then no change...
769 # if parent has no next sibling, then no change...
763 nextitem = currentitem
770 nextitem = currentitem
764
771
765 self.currentselecteditem = nextitem
772 self.currentselecteditem = nextitem
766 self.recenterdisplayedarea()
773 self.recenterdisplayedarea()
767
774
768 def nextsametype(self, test=False):
775 def nextsametype(self, test=False):
769 currentitem = self.currentselecteditem
776 currentitem = self.currentselecteditem
770 sametype = lambda item: isinstance(item, type(currentitem))
777 sametype = lambda item: isinstance(item, type(currentitem))
771 nextitem = currentitem.nextitem()
778 nextitem = currentitem.nextitem()
772
779
773 while nextitem is not None and not sametype(nextitem):
780 while nextitem is not None and not sametype(nextitem):
774 nextitem = nextitem.nextitem()
781 nextitem = nextitem.nextitem()
775
782
776 if nextitem is None:
783 if nextitem is None:
777 nextitem = currentitem
784 nextitem = currentitem
778 else:
785 else:
779 parent = nextitem.parentitem()
786 parent = nextitem.parentitem()
780 if parent is not None and parent.folded:
787 if parent is not None and parent.folded:
781 self.togglefolded(parent)
788 self.togglefolded(parent)
782
789
783 self.currentselecteditem = nextitem
790 self.currentselecteditem = nextitem
784 if not test:
791 if not test:
785 self.recenterdisplayedarea()
792 self.recenterdisplayedarea()
786
793
787 def rightarrowevent(self):
794 def rightarrowevent(self):
788 """
795 """
789 select (if possible) the first of this item's child-items.
796 select (if possible) the first of this item's child-items.
790 """
797 """
791 currentitem = self.currentselecteditem
798 currentitem = self.currentselecteditem
792 nextitem = currentitem.firstchild()
799 nextitem = currentitem.firstchild()
793
800
794 # turn off folding if we want to show a child-item
801 # turn off folding if we want to show a child-item
795 if currentitem.folded:
802 if currentitem.folded:
796 self.togglefolded(currentitem)
803 self.togglefolded(currentitem)
797
804
798 if nextitem is None:
805 if nextitem is None:
799 # if no next item on parent-level, then no change...
806 # if no next item on parent-level, then no change...
800 nextitem = currentitem
807 nextitem = currentitem
801
808
802 self.currentselecteditem = nextitem
809 self.currentselecteditem = nextitem
803
810
804 def leftarrowevent(self):
811 def leftarrowevent(self):
805 """
812 """
806 if the current item can be folded (i.e. it is an unfolded header or
813 if the current item can be folded (i.e. it is an unfolded header or
807 hunk), then fold it. otherwise try select (if possible) the parent
814 hunk), then fold it. otherwise try select (if possible) the parent
808 of this item.
815 of this item.
809 """
816 """
810 currentitem = self.currentselecteditem
817 currentitem = self.currentselecteditem
811
818
812 # try to fold the item
819 # try to fold the item
813 if not isinstance(currentitem, uihunkline):
820 if not isinstance(currentitem, uihunkline):
814 if not currentitem.folded:
821 if not currentitem.folded:
815 self.togglefolded(item=currentitem)
822 self.togglefolded(item=currentitem)
816 return
823 return
817
824
818 # if it can't be folded, try to select the parent item
825 # if it can't be folded, try to select the parent item
819 nextitem = currentitem.parentitem()
826 nextitem = currentitem.parentitem()
820
827
821 if nextitem is None:
828 if nextitem is None:
822 # if no item on parent-level, then no change...
829 # if no item on parent-level, then no change...
823 nextitem = currentitem
830 nextitem = currentitem
824 if not nextitem.folded:
831 if not nextitem.folded:
825 self.togglefolded(item=nextitem)
832 self.togglefolded(item=nextitem)
826
833
827 self.currentselecteditem = nextitem
834 self.currentselecteditem = nextitem
828
835
829 def leftarrowshiftevent(self):
836 def leftarrowshiftevent(self):
830 """
837 """
831 select the header of the current item (or fold current item if the
838 select the header of the current item (or fold current item if the
832 current item is already a header).
839 current item is already a header).
833 """
840 """
834 currentitem = self.currentselecteditem
841 currentitem = self.currentselecteditem
835
842
836 if isinstance(currentitem, uiheader):
843 if isinstance(currentitem, uiheader):
837 if not currentitem.folded:
844 if not currentitem.folded:
838 self.togglefolded(item=currentitem)
845 self.togglefolded(item=currentitem)
839 return
846 return
840
847
841 # select the parent item recursively until we're at a header
848 # select the parent item recursively until we're at a header
842 while True:
849 while True:
843 nextitem = currentitem.parentitem()
850 nextitem = currentitem.parentitem()
844 if nextitem is None:
851 if nextitem is None:
845 break
852 break
846 else:
853 else:
847 currentitem = nextitem
854 currentitem = nextitem
848
855
849 self.currentselecteditem = currentitem
856 self.currentselecteditem = currentitem
850
857
851 def updatescroll(self):
858 def updatescroll(self):
852 """scroll the screen to fully show the currently-selected"""
859 """scroll the screen to fully show the currently-selected"""
853 selstart = self.selecteditemstartline
860 selstart = self.selecteditemstartline
854 selend = self.selecteditemendline
861 selend = self.selecteditemendline
855
862
856 padstart = self.firstlineofpadtoprint
863 padstart = self.firstlineofpadtoprint
857 padend = padstart + self.yscreensize - self.numstatuslines - 1
864 padend = padstart + self.yscreensize - self.numstatuslines - 1
858 # 'buffered' pad start/end values which scroll with a certain
865 # 'buffered' pad start/end values which scroll with a certain
859 # top/bottom context margin
866 # top/bottom context margin
860 padstartbuffered = padstart + 3
867 padstartbuffered = padstart + 3
861 padendbuffered = padend - 3
868 padendbuffered = padend - 3
862
869
863 if selend > padendbuffered:
870 if selend > padendbuffered:
864 self.scrolllines(selend - padendbuffered)
871 self.scrolllines(selend - padendbuffered)
865 elif selstart < padstartbuffered:
872 elif selstart < padstartbuffered:
866 # negative values scroll in pgup direction
873 # negative values scroll in pgup direction
867 self.scrolllines(selstart - padstartbuffered)
874 self.scrolllines(selstart - padstartbuffered)
868
875
869 def scrolllines(self, numlines):
876 def scrolllines(self, numlines):
870 """scroll the screen up (down) by numlines when numlines >0 (<0)."""
877 """scroll the screen up (down) by numlines when numlines >0 (<0)."""
871 self.firstlineofpadtoprint += numlines
878 self.firstlineofpadtoprint += numlines
872 if self.firstlineofpadtoprint < 0:
879 if self.firstlineofpadtoprint < 0:
873 self.firstlineofpadtoprint = 0
880 self.firstlineofpadtoprint = 0
874 if self.firstlineofpadtoprint > self.numpadlines - 1:
881 if self.firstlineofpadtoprint > self.numpadlines - 1:
875 self.firstlineofpadtoprint = self.numpadlines - 1
882 self.firstlineofpadtoprint = self.numpadlines - 1
876
883
877 def toggleapply(self, item=None):
884 def toggleapply(self, item=None):
878 """
885 """
879 toggle the applied flag of the specified item. if no item is specified,
886 toggle the applied flag of the specified item. if no item is specified,
880 toggle the flag of the currently selected item.
887 toggle the flag of the currently selected item.
881 """
888 """
882 if item is None:
889 if item is None:
883 item = self.currentselecteditem
890 item = self.currentselecteditem
884 # Only set this when NOT using 'toggleall'
891 # Only set this when NOT using 'toggleall'
885 self.lastapplieditem = item
892 self.lastapplieditem = item
886
893
887 item.applied = not item.applied
894 item.applied = not item.applied
888
895
889 if isinstance(item, uiheader):
896 if isinstance(item, uiheader):
890 item.partial = False
897 item.partial = False
891 if item.applied:
898 if item.applied:
892 # apply all its hunks
899 # apply all its hunks
893 for hnk in item.hunks:
900 for hnk in item.hunks:
894 hnk.applied = True
901 hnk.applied = True
895 # apply all their hunklines
902 # apply all their hunklines
896 for hunkline in hnk.changedlines:
903 for hunkline in hnk.changedlines:
897 hunkline.applied = True
904 hunkline.applied = True
898 else:
905 else:
899 # un-apply all its hunks
906 # un-apply all its hunks
900 for hnk in item.hunks:
907 for hnk in item.hunks:
901 hnk.applied = False
908 hnk.applied = False
902 hnk.partial = False
909 hnk.partial = False
903 # un-apply all their hunklines
910 # un-apply all their hunklines
904 for hunkline in hnk.changedlines:
911 for hunkline in hnk.changedlines:
905 hunkline.applied = False
912 hunkline.applied = False
906 elif isinstance(item, uihunk):
913 elif isinstance(item, uihunk):
907 item.partial = False
914 item.partial = False
908 # apply all it's hunklines
915 # apply all it's hunklines
909 for hunkline in item.changedlines:
916 for hunkline in item.changedlines:
910 hunkline.applied = item.applied
917 hunkline.applied = item.applied
911
918
912 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
919 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
913 allsiblingsapplied = not (False in siblingappliedstatus)
920 allsiblingsapplied = not (False in siblingappliedstatus)
914 nosiblingsapplied = not (True in siblingappliedstatus)
921 nosiblingsapplied = not (True in siblingappliedstatus)
915
922
916 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
923 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
917 somesiblingspartial = True in siblingspartialstatus
924 somesiblingspartial = True in siblingspartialstatus
918
925
919 # cases where applied or partial should be removed from header
926 # cases where applied or partial should be removed from header
920
927
921 # if no 'sibling' hunks are applied (including this hunk)
928 # if no 'sibling' hunks are applied (including this hunk)
922 if nosiblingsapplied:
929 if nosiblingsapplied:
923 if not item.header.special():
930 if not item.header.special():
924 item.header.applied = False
931 item.header.applied = False
925 item.header.partial = False
932 item.header.partial = False
926 else: # some/all parent siblings are applied
933 else: # some/all parent siblings are applied
927 item.header.applied = True
934 item.header.applied = True
928 item.header.partial = (
935 item.header.partial = (
929 somesiblingspartial or not allsiblingsapplied
936 somesiblingspartial or not allsiblingsapplied
930 )
937 )
931
938
932 elif isinstance(item, uihunkline):
939 elif isinstance(item, uihunkline):
933 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
940 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
934 allsiblingsapplied = not (False in siblingappliedstatus)
941 allsiblingsapplied = not (False in siblingappliedstatus)
935 nosiblingsapplied = not (True in siblingappliedstatus)
942 nosiblingsapplied = not (True in siblingappliedstatus)
936
943
937 # if no 'sibling' lines are applied
944 # if no 'sibling' lines are applied
938 if nosiblingsapplied:
945 if nosiblingsapplied:
939 item.hunk.applied = False
946 item.hunk.applied = False
940 item.hunk.partial = False
947 item.hunk.partial = False
941 elif allsiblingsapplied:
948 elif allsiblingsapplied:
942 item.hunk.applied = True
949 item.hunk.applied = True
943 item.hunk.partial = False
950 item.hunk.partial = False
944 else: # some siblings applied
951 else: # some siblings applied
945 item.hunk.applied = True
952 item.hunk.applied = True
946 item.hunk.partial = True
953 item.hunk.partial = True
947
954
948 parentsiblingsapplied = [
955 parentsiblingsapplied = [
949 hnk.applied for hnk in item.hunk.header.hunks
956 hnk.applied for hnk in item.hunk.header.hunks
950 ]
957 ]
951 noparentsiblingsapplied = not (True in parentsiblingsapplied)
958 noparentsiblingsapplied = not (True in parentsiblingsapplied)
952 allparentsiblingsapplied = not (False in parentsiblingsapplied)
959 allparentsiblingsapplied = not (False in parentsiblingsapplied)
953
960
954 parentsiblingspartial = [
961 parentsiblingspartial = [
955 hnk.partial for hnk in item.hunk.header.hunks
962 hnk.partial for hnk in item.hunk.header.hunks
956 ]
963 ]
957 someparentsiblingspartial = True in parentsiblingspartial
964 someparentsiblingspartial = True in parentsiblingspartial
958
965
959 # if all parent hunks are not applied, un-apply header
966 # if all parent hunks are not applied, un-apply header
960 if noparentsiblingsapplied:
967 if noparentsiblingsapplied:
961 if not item.hunk.header.special():
968 if not item.hunk.header.special():
962 item.hunk.header.applied = False
969 item.hunk.header.applied = False
963 item.hunk.header.partial = False
970 item.hunk.header.partial = False
964 # set the applied and partial status of the header if needed
971 # set the applied and partial status of the header if needed
965 else: # some/all parent siblings are applied
972 else: # some/all parent siblings are applied
966 item.hunk.header.applied = True
973 item.hunk.header.applied = True
967 item.hunk.header.partial = (
974 item.hunk.header.partial = (
968 someparentsiblingspartial or not allparentsiblingsapplied
975 someparentsiblingspartial or not allparentsiblingsapplied
969 )
976 )
970
977
971 def toggleall(self):
978 def toggleall(self):
972 """toggle the applied flag of all items."""
979 """toggle the applied flag of all items."""
973 if self.waslasttoggleallapplied: # then unapply them this time
980 if self.waslasttoggleallapplied: # then unapply them this time
974 for item in self.headerlist:
981 for item in self.headerlist:
975 if item.applied:
982 if item.applied:
976 self.toggleapply(item)
983 self.toggleapply(item)
977 else:
984 else:
978 for item in self.headerlist:
985 for item in self.headerlist:
979 if not item.applied:
986 if not item.applied:
980 self.toggleapply(item)
987 self.toggleapply(item)
981 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
988 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
982
989
983 def flipselections(self):
990 def flipselections(self):
984 """
991 """
985 Flip all selections. Every selected line is unselected and vice
992 Flip all selections. Every selected line is unselected and vice
986 versa.
993 versa.
987 """
994 """
988 for header in self.headerlist:
995 for header in self.headerlist:
989 for hunk in header.allchildren():
996 for hunk in header.allchildren():
990 for line in hunk.allchildren():
997 for line in hunk.allchildren():
991 self.toggleapply(line)
998 self.toggleapply(line)
992
999
993 def toggleallbetween(self):
1000 def toggleallbetween(self):
994 """toggle applied on or off for all items in range [lastapplied,
1001 """toggle applied on or off for all items in range [lastapplied,
995 current]. """
1002 current]. """
996 if (
1003 if (
997 not self.lastapplieditem
1004 not self.lastapplieditem
998 or self.currentselecteditem == self.lastapplieditem
1005 or self.currentselecteditem == self.lastapplieditem
999 ):
1006 ):
1000 # Treat this like a normal 'x'/' '
1007 # Treat this like a normal 'x'/' '
1001 self.toggleapply()
1008 self.toggleapply()
1002 return
1009 return
1003
1010
1004 startitem = self.lastapplieditem
1011 startitem = self.lastapplieditem
1005 enditem = self.currentselecteditem
1012 enditem = self.currentselecteditem
1006 # Verify that enditem is "after" startitem, otherwise swap them.
1013 # Verify that enditem is "after" startitem, otherwise swap them.
1007 for direction in [b'forward', b'reverse']:
1014 for direction in [b'forward', b'reverse']:
1008 nextitem = startitem.nextitem()
1015 nextitem = startitem.nextitem()
1009 while nextitem and nextitem != enditem:
1016 while nextitem and nextitem != enditem:
1010 nextitem = nextitem.nextitem()
1017 nextitem = nextitem.nextitem()
1011 if nextitem:
1018 if nextitem:
1012 break
1019 break
1013 # Looks like we went the wrong direction :)
1020 # Looks like we went the wrong direction :)
1014 startitem, enditem = enditem, startitem
1021 startitem, enditem = enditem, startitem
1015
1022
1016 if not nextitem:
1023 if not nextitem:
1017 # We didn't find a path going either forward or backward? Don't know
1024 # We didn't find a path going either forward or backward? Don't know
1018 # how this can happen, let's not crash though.
1025 # how this can happen, let's not crash though.
1019 return
1026 return
1020
1027
1021 nextitem = startitem
1028 nextitem = startitem
1022 # Switch all items to be the opposite state of the currently selected
1029 # Switch all items to be the opposite state of the currently selected
1023 # item. Specifically:
1030 # item. Specifically:
1024 # [ ] startitem
1031 # [ ] startitem
1025 # [x] middleitem
1032 # [x] middleitem
1026 # [ ] enditem <-- currently selected
1033 # [ ] enditem <-- currently selected
1027 # This will turn all three on, since the currently selected item is off.
1034 # This will turn all three on, since the currently selected item is off.
1028 # This does *not* invert each item (i.e. middleitem stays marked/on)
1035 # This does *not* invert each item (i.e. middleitem stays marked/on)
1029 desiredstate = not self.currentselecteditem.applied
1036 desiredstate = not self.currentselecteditem.applied
1030 while nextitem != enditem.nextitem():
1037 while nextitem != enditem.nextitem():
1031 if nextitem.applied != desiredstate:
1038 if nextitem.applied != desiredstate:
1032 self.toggleapply(item=nextitem)
1039 self.toggleapply(item=nextitem)
1033 nextitem = nextitem.nextitem()
1040 nextitem = nextitem.nextitem()
1034
1041
1035 def togglefolded(self, item=None, foldparent=False):
1042 def togglefolded(self, item=None, foldparent=False):
1036 """toggle folded flag of specified item (defaults to currently
1043 """toggle folded flag of specified item (defaults to currently
1037 selected)"""
1044 selected)"""
1038 if item is None:
1045 if item is None:
1039 item = self.currentselecteditem
1046 item = self.currentselecteditem
1040 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
1047 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
1041 if not isinstance(item, uiheader):
1048 if not isinstance(item, uiheader):
1042 # we need to select the parent item in this case
1049 # we need to select the parent item in this case
1043 self.currentselecteditem = item = item.parentitem()
1050 self.currentselecteditem = item = item.parentitem()
1044 elif item.neverunfolded:
1051 elif item.neverunfolded:
1045 item.neverunfolded = False
1052 item.neverunfolded = False
1046
1053
1047 # also fold any foldable children of the parent/current item
1054 # also fold any foldable children of the parent/current item
1048 if isinstance(item, uiheader): # the original or 'new' item
1055 if isinstance(item, uiheader): # the original or 'new' item
1049 for child in item.allchildren():
1056 for child in item.allchildren():
1050 child.folded = not item.folded
1057 child.folded = not item.folded
1051
1058
1052 if isinstance(item, (uiheader, uihunk)):
1059 if isinstance(item, (uiheader, uihunk)):
1053 item.folded = not item.folded
1060 item.folded = not item.folded
1054
1061
1055 def alignstring(self, instr, window):
1062 def alignstring(self, instr, window):
1056 """
1063 """
1057 add whitespace to the end of a string in order to make it fill
1064 add whitespace to the end of a string in order to make it fill
1058 the screen in the x direction. the current cursor position is
1065 the screen in the x direction. the current cursor position is
1059 taken into account when making this calculation. the string can span
1066 taken into account when making this calculation. the string can span
1060 multiple lines.
1067 multiple lines.
1061 """
1068 """
1062 y, xstart = window.getyx()
1069 y, xstart = window.getyx()
1063 width = self.xscreensize
1070 width = self.xscreensize
1064 # turn tabs into spaces
1071 # turn tabs into spaces
1065 instr = instr.expandtabs(4)
1072 instr = instr.expandtabs(4)
1066 strwidth = encoding.colwidth(instr)
1073 strwidth = encoding.colwidth(instr)
1067 numspaces = width - ((strwidth + xstart) % width)
1074 numspaces = width - ((strwidth + xstart) % width)
1068 return instr + b" " * numspaces
1075 return instr + b" " * numspaces
1069
1076
1070 def printstring(
1077 def printstring(
1071 self,
1078 self,
1072 window,
1079 window,
1073 text,
1080 text,
1074 fgcolor=None,
1081 fgcolor=None,
1075 bgcolor=None,
1082 bgcolor=None,
1076 pair=None,
1083 pair=None,
1077 pairname=None,
1084 pairname=None,
1078 attrlist=None,
1085 attrlist=None,
1079 towin=True,
1086 towin=True,
1080 align=True,
1087 align=True,
1081 showwhtspc=False,
1088 showwhtspc=False,
1082 ):
1089 ):
1083 """
1090 """
1084 print the string, text, with the specified colors and attributes, to
1091 print the string, text, with the specified colors and attributes, to
1085 the specified curses window object.
1092 the specified curses window object.
1086
1093
1087 the foreground and background colors are of the form
1094 the foreground and background colors are of the form
1088 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
1095 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
1089 magenta, red, white, yellow]. if pairname is provided, a color
1096 magenta, red, white, yellow]. if pairname is provided, a color
1090 pair will be looked up in the self.colorpairnames dictionary.
1097 pair will be looked up in the self.colorpairnames dictionary.
1091
1098
1092 attrlist is a list containing text attributes in the form of
1099 attrlist is a list containing text attributes in the form of
1093 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
1100 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
1094 underline].
1101 underline].
1095
1102
1096 if align == True, whitespace is added to the printed string such that
1103 if align == True, whitespace is added to the printed string such that
1097 the string stretches to the right border of the window.
1104 the string stretches to the right border of the window.
1098
1105
1099 if showwhtspc == True, trailing whitespace of a string is highlighted.
1106 if showwhtspc == True, trailing whitespace of a string is highlighted.
1100 """
1107 """
1101 # preprocess the text, converting tabs to spaces
1108 # preprocess the text, converting tabs to spaces
1102 text = text.expandtabs(4)
1109 text = text.expandtabs(4)
1103 # strip \n, and convert control characters to ^[char] representation
1110 # strip \n, and convert control characters to ^[char] representation
1104 text = re.sub(
1111 text = re.sub(
1105 br'[\x00-\x08\x0a-\x1f]',
1112 br'[\x00-\x08\x0a-\x1f]',
1106 lambda m: b'^' + pycompat.sysbytes(chr(ord(m.group()) + 64)),
1113 lambda m: b'^' + pycompat.sysbytes(chr(ord(m.group()) + 64)),
1107 text.strip(b'\n'),
1114 text.strip(b'\n'),
1108 )
1115 )
1109
1116
1110 if pair is not None:
1117 if pair is not None:
1111 colorpair = pair
1118 colorpair = pair
1112 elif pairname is not None:
1119 elif pairname is not None:
1113 colorpair = self.colorpairnames[pairname]
1120 colorpair = self.colorpairnames[pairname]
1114 else:
1121 else:
1115 if fgcolor is None:
1122 if fgcolor is None:
1116 fgcolor = -1
1123 fgcolor = -1
1117 if bgcolor is None:
1124 if bgcolor is None:
1118 bgcolor = -1
1125 bgcolor = -1
1119 if (fgcolor, bgcolor) in self.colorpairs:
1126 if (fgcolor, bgcolor) in self.colorpairs:
1120 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1127 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1121 else:
1128 else:
1122 colorpair = self.getcolorpair(fgcolor, bgcolor)
1129 colorpair = self.getcolorpair(fgcolor, bgcolor)
1123 # add attributes if possible
1130 # add attributes if possible
1124 if attrlist is None:
1131 if attrlist is None:
1125 attrlist = []
1132 attrlist = []
1126 if colorpair < 256:
1133 if colorpair < 256:
1127 # then it is safe to apply all attributes
1134 # then it is safe to apply all attributes
1128 for textattr in attrlist:
1135 for textattr in attrlist:
1129 colorpair |= textattr
1136 colorpair |= textattr
1130 else:
1137 else:
1131 # just apply a select few (safe?) attributes
1138 # just apply a select few (safe?) attributes
1132 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
1139 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
1133 if textattr in attrlist:
1140 if textattr in attrlist:
1134 colorpair |= textattr
1141 colorpair |= textattr
1135
1142
1136 y, xstart = self.chunkpad.getyx()
1143 y, xstart = self.chunkpad.getyx()
1137 t = b"" # variable for counting lines printed
1144 t = b"" # variable for counting lines printed
1138 # if requested, show trailing whitespace
1145 # if requested, show trailing whitespace
1139 if showwhtspc:
1146 if showwhtspc:
1140 origlen = len(text)
1147 origlen = len(text)
1141 text = text.rstrip(b' \n') # tabs have already been expanded
1148 text = text.rstrip(b' \n') # tabs have already been expanded
1142 strippedlen = len(text)
1149 strippedlen = len(text)
1143 numtrailingspaces = origlen - strippedlen
1150 numtrailingspaces = origlen - strippedlen
1144
1151
1145 if towin:
1152 if towin:
1146 window.addstr(text, colorpair)
1153 window.addstr(text, colorpair)
1147 t += text
1154 t += text
1148
1155
1149 if showwhtspc:
1156 if showwhtspc:
1150 wscolorpair = colorpair | curses.A_REVERSE
1157 wscolorpair = colorpair | curses.A_REVERSE
1151 if towin:
1158 if towin:
1152 for i in range(numtrailingspaces):
1159 for i in range(numtrailingspaces):
1153 window.addch(curses.ACS_CKBOARD, wscolorpair)
1160 window.addch(curses.ACS_CKBOARD, wscolorpair)
1154 t += b" " * numtrailingspaces
1161 t += b" " * numtrailingspaces
1155
1162
1156 if align:
1163 if align:
1157 if towin:
1164 if towin:
1158 extrawhitespace = self.alignstring(b"", window)
1165 extrawhitespace = self.alignstring(b"", window)
1159 window.addstr(extrawhitespace, colorpair)
1166 window.addstr(extrawhitespace, colorpair)
1160 else:
1167 else:
1161 # need to use t, since the x position hasn't incremented
1168 # need to use t, since the x position hasn't incremented
1162 extrawhitespace = self.alignstring(t, window)
1169 extrawhitespace = self.alignstring(t, window)
1163 t += extrawhitespace
1170 t += extrawhitespace
1164
1171
1165 # is reset to 0 at the beginning of printitem()
1172 # is reset to 0 at the beginning of printitem()
1166
1173
1167 linesprinted = (xstart + len(t)) // self.xscreensize
1174 linesprinted = (xstart + len(t)) // self.xscreensize
1168 self.linesprintedtopadsofar += linesprinted
1175 self.linesprintedtopadsofar += linesprinted
1169 return t
1176 return t
1170
1177
1171 def _getstatuslinesegments(self):
1178 def _getstatuslinesegments(self):
1172 """-> [str]. return segments"""
1179 """-> [str]. return segments"""
1173 selected = self.currentselecteditem.applied
1180 selected = self.currentselecteditem.applied
1174 spaceselect = _(b'space/enter: select')
1181 spaceselect = _(b'space/enter: select')
1175 spacedeselect = _(b'space/enter: deselect')
1182 spacedeselect = _(b'space/enter: deselect')
1176 # Format the selected label into a place as long as the longer of the
1183 # Format the selected label into a place as long as the longer of the
1177 # two possible labels. This may vary by language.
1184 # two possible labels. This may vary by language.
1178 spacelen = max(len(spaceselect), len(spacedeselect))
1185 spacelen = max(len(spaceselect), len(spacedeselect))
1179 selectedlabel = b'%-*s' % (
1186 selectedlabel = b'%-*s' % (
1180 spacelen,
1187 spacelen,
1181 spacedeselect if selected else spaceselect,
1188 spacedeselect if selected else spaceselect,
1182 )
1189 )
1183 segments = [
1190 segments = [
1184 _headermessages[self.operation],
1191 _headermessages[self.operation],
1185 b'-',
1192 b'-',
1186 _(b'[x]=selected **=collapsed'),
1193 _(b'[x]=selected **=collapsed'),
1187 _(b'c: confirm'),
1194 _(b'c: confirm'),
1188 _(b'q: abort'),
1195 _(b'q: abort'),
1189 _(b'arrow keys: move/expand/collapse'),
1196 _(b'arrow keys: move/expand/collapse'),
1190 selectedlabel,
1197 selectedlabel,
1191 _(b'?: help'),
1198 _(b'?: help'),
1192 ]
1199 ]
1193 return segments
1200 return segments
1194
1201
1195 def _getstatuslines(self):
1202 def _getstatuslines(self):
1196 """() -> [str]. return short help used in the top status window"""
1203 """() -> [str]. return short help used in the top status window"""
1197 if self.errorstr is not None:
1204 if self.errorstr is not None:
1198 lines = [self.errorstr, _(b'Press any key to continue')]
1205 lines = [self.errorstr, _(b'Press any key to continue')]
1199 else:
1206 else:
1200 # wrap segments to lines
1207 # wrap segments to lines
1201 segments = self._getstatuslinesegments()
1208 segments = self._getstatuslinesegments()
1202 width = self.xscreensize
1209 width = self.xscreensize
1203 lines = []
1210 lines = []
1204 lastwidth = width
1211 lastwidth = width
1205 for s in segments:
1212 for s in segments:
1206 w = encoding.colwidth(s)
1213 w = encoding.colwidth(s)
1207 sep = b' ' * (1 + (s and s[0] not in b'-['))
1214 sep = b' ' * (1 + (s and s[0] not in b'-['))
1208 if lastwidth + w + len(sep) >= width:
1215 if lastwidth + w + len(sep) >= width:
1209 lines.append(s)
1216 lines.append(s)
1210 lastwidth = w
1217 lastwidth = w
1211 else:
1218 else:
1212 lines[-1] += sep + s
1219 lines[-1] += sep + s
1213 lastwidth += w + len(sep)
1220 lastwidth += w + len(sep)
1214 if len(lines) != self.numstatuslines:
1221 if len(lines) != self.numstatuslines:
1215 self.numstatuslines = len(lines)
1222 self.numstatuslines = len(lines)
1216 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1223 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1217 return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines]
1224 return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines]
1218
1225
1219 def updatescreen(self):
1226 def updatescreen(self):
1220 self.statuswin.erase()
1227 self.statuswin.erase()
1221 self.chunkpad.erase()
1228 self.chunkpad.erase()
1222
1229
1223 printstring = self.printstring
1230 printstring = self.printstring
1224
1231
1225 # print out the status lines at the top
1232 # print out the status lines at the top
1226 try:
1233 try:
1227 for line in self._getstatuslines():
1234 for line in self._getstatuslines():
1228 printstring(self.statuswin, line, pairname=b"legend")
1235 printstring(self.statuswin, line, pairname=b"legend")
1229 self.statuswin.refresh()
1236 self.statuswin.refresh()
1230 except curses.error:
1237 except curses.error:
1231 pass
1238 pass
1232 if self.errorstr is not None:
1239 if self.errorstr is not None:
1233 return
1240 return
1234
1241
1235 # print out the patch in the remaining part of the window
1242 # print out the patch in the remaining part of the window
1236 try:
1243 try:
1237 self.printitem()
1244 self.printitem()
1238 self.updatescroll()
1245 self.updatescroll()
1239 self.chunkpad.refresh(
1246 self.chunkpad.refresh(
1240 self.firstlineofpadtoprint,
1247 self.firstlineofpadtoprint,
1241 0,
1248 0,
1242 self.numstatuslines,
1249 self.numstatuslines,
1243 0,
1250 0,
1244 self.yscreensize - self.numstatuslines,
1251 self.yscreensize - self.numstatuslines,
1245 self.xscreensize,
1252 self.xscreensize,
1246 )
1253 )
1247 except curses.error:
1254 except curses.error:
1248 pass
1255 pass
1249
1256
1250 def getstatusprefixstring(self, item):
1257 def getstatusprefixstring(self, item):
1251 """
1258 """
1252 create a string to prefix a line with which indicates whether 'item'
1259 create a string to prefix a line with which indicates whether 'item'
1253 is applied and/or folded.
1260 is applied and/or folded.
1254 """
1261 """
1255
1262
1256 # create checkbox string
1263 # create checkbox string
1257 if item.applied:
1264 if item.applied:
1258 if not isinstance(item, uihunkline) and item.partial:
1265 if not isinstance(item, uihunkline) and item.partial:
1259 checkbox = b"[~]"
1266 checkbox = b"[~]"
1260 else:
1267 else:
1261 checkbox = b"[x]"
1268 checkbox = b"[x]"
1262 else:
1269 else:
1263 checkbox = b"[ ]"
1270 checkbox = b"[ ]"
1264
1271
1265 try:
1272 try:
1266 if item.folded:
1273 if item.folded:
1267 checkbox += b"**"
1274 checkbox += b"**"
1268 if isinstance(item, uiheader):
1275 if isinstance(item, uiheader):
1269 # one of "m", "a", or "d" (modified, added, deleted)
1276 # one of "m", "a", or "d" (modified, added, deleted)
1270 filestatus = item.changetype
1277 filestatus = item.changetype
1271
1278
1272 checkbox += filestatus + b" "
1279 checkbox += filestatus + b" "
1273 else:
1280 else:
1274 checkbox += b" "
1281 checkbox += b" "
1275 if isinstance(item, uiheader):
1282 if isinstance(item, uiheader):
1276 # add two more spaces for headers
1283 # add two more spaces for headers
1277 checkbox += b" "
1284 checkbox += b" "
1278 except AttributeError: # not foldable
1285 except AttributeError: # not foldable
1279 checkbox += b" "
1286 checkbox += b" "
1280
1287
1281 return checkbox
1288 return checkbox
1282
1289
1283 def printheader(
1290 def printheader(
1284 self, header, selected=False, towin=True, ignorefolding=False
1291 self, header, selected=False, towin=True, ignorefolding=False
1285 ):
1292 ):
1286 """
1293 """
1287 print the header to the pad. if countlines is True, don't print
1294 print the header to the pad. if countlines is True, don't print
1288 anything, but just count the number of lines which would be printed.
1295 anything, but just count the number of lines which would be printed.
1289 """
1296 """
1290
1297
1291 outstr = b""
1298 outstr = b""
1292 text = header.prettystr()
1299 text = header.prettystr()
1293 chunkindex = self.chunklist.index(header)
1300 chunkindex = self.chunklist.index(header)
1294
1301
1295 if chunkindex != 0 and not header.folded:
1302 if chunkindex != 0 and not header.folded:
1296 # add separating line before headers
1303 # add separating line before headers
1297 outstr += self.printstring(
1304 outstr += self.printstring(
1298 self.chunkpad, b'_' * self.xscreensize, towin=towin, align=False
1305 self.chunkpad, b'_' * self.xscreensize, towin=towin, align=False
1299 )
1306 )
1300 # select color-pair based on if the header is selected
1307 # select color-pair based on if the header is selected
1301 colorpair = self.getcolorpair(
1308 colorpair = self.getcolorpair(
1302 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1309 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1303 )
1310 )
1304
1311
1305 # print out each line of the chunk, expanding it to screen width
1312 # print out each line of the chunk, expanding it to screen width
1306
1313
1307 # number of characters to indent lines on this level by
1314 # number of characters to indent lines on this level by
1308 indentnumchars = 0
1315 indentnumchars = 0
1309 checkbox = self.getstatusprefixstring(header)
1316 checkbox = self.getstatusprefixstring(header)
1310 if not header.folded or ignorefolding:
1317 if not header.folded or ignorefolding:
1311 textlist = text.split(b"\n")
1318 textlist = text.split(b"\n")
1312 linestr = checkbox + textlist[0]
1319 linestr = checkbox + textlist[0]
1313 else:
1320 else:
1314 linestr = checkbox + header.filename()
1321 linestr = checkbox + header.filename()
1315 outstr += self.printstring(
1322 outstr += self.printstring(
1316 self.chunkpad, linestr, pair=colorpair, towin=towin
1323 self.chunkpad, linestr, pair=colorpair, towin=towin
1317 )
1324 )
1318 if not header.folded or ignorefolding:
1325 if not header.folded or ignorefolding:
1319 if len(textlist) > 1:
1326 if len(textlist) > 1:
1320 for line in textlist[1:]:
1327 for line in textlist[1:]:
1321 linestr = b" " * (indentnumchars + len(checkbox)) + line
1328 linestr = b" " * (indentnumchars + len(checkbox)) + line
1322 outstr += self.printstring(
1329 outstr += self.printstring(
1323 self.chunkpad, linestr, pair=colorpair, towin=towin
1330 self.chunkpad, linestr, pair=colorpair, towin=towin
1324 )
1331 )
1325
1332
1326 return outstr
1333 return outstr
1327
1334
1328 def printhunklinesbefore(
1335 def printhunklinesbefore(
1329 self, hunk, selected=False, towin=True, ignorefolding=False
1336 self, hunk, selected=False, towin=True, ignorefolding=False
1330 ):
1337 ):
1331 """includes start/end line indicator"""
1338 """includes start/end line indicator"""
1332 outstr = b""
1339 outstr = b""
1333 # where hunk is in list of siblings
1340 # where hunk is in list of siblings
1334 hunkindex = hunk.header.hunks.index(hunk)
1341 hunkindex = hunk.header.hunks.index(hunk)
1335
1342
1336 if hunkindex != 0:
1343 if hunkindex != 0:
1337 # add separating line before headers
1344 # add separating line before headers
1338 outstr += self.printstring(
1345 outstr += self.printstring(
1339 self.chunkpad, b' ' * self.xscreensize, towin=towin, align=False
1346 self.chunkpad, b' ' * self.xscreensize, towin=towin, align=False
1340 )
1347 )
1341
1348
1342 colorpair = self.getcolorpair(
1349 colorpair = self.getcolorpair(
1343 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1350 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1344 )
1351 )
1345
1352
1346 # print out from-to line with checkbox
1353 # print out from-to line with checkbox
1347 checkbox = self.getstatusprefixstring(hunk)
1354 checkbox = self.getstatusprefixstring(hunk)
1348
1355
1349 lineprefix = b" " * self.hunkindentnumchars + checkbox
1356 lineprefix = b" " * self.hunkindentnumchars + checkbox
1350 frtoline = b" " + hunk.getfromtoline().strip(b"\n")
1357 frtoline = b" " + hunk.getfromtoline().strip(b"\n")
1351
1358
1352 outstr += self.printstring(
1359 outstr += self.printstring(
1353 self.chunkpad, lineprefix, towin=towin, align=False
1360 self.chunkpad, lineprefix, towin=towin, align=False
1354 ) # add uncolored checkbox/indent
1361 ) # add uncolored checkbox/indent
1355 outstr += self.printstring(
1362 outstr += self.printstring(
1356 self.chunkpad, frtoline, pair=colorpair, towin=towin
1363 self.chunkpad, frtoline, pair=colorpair, towin=towin
1357 )
1364 )
1358
1365
1359 if hunk.folded and not ignorefolding:
1366 if hunk.folded and not ignorefolding:
1360 # skip remainder of output
1367 # skip remainder of output
1361 return outstr
1368 return outstr
1362
1369
1363 # print out lines of the chunk preceeding changed-lines
1370 # print out lines of the chunk preceeding changed-lines
1364 for line in hunk.before:
1371 for line in hunk.before:
1365 linestr = (
1372 linestr = (
1366 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1373 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1367 )
1374 )
1368 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1375 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1369
1376
1370 return outstr
1377 return outstr
1371
1378
1372 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1379 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1373 outstr = b""
1380 outstr = b""
1374 if hunk.folded and not ignorefolding:
1381 if hunk.folded and not ignorefolding:
1375 return outstr
1382 return outstr
1376
1383
1377 # a bit superfluous, but to avoid hard-coding indent amount
1384 # a bit superfluous, but to avoid hard-coding indent amount
1378 checkbox = self.getstatusprefixstring(hunk)
1385 checkbox = self.getstatusprefixstring(hunk)
1379 for line in hunk.after:
1386 for line in hunk.after:
1380 linestr = (
1387 linestr = (
1381 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1388 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1382 )
1389 )
1383 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1390 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1384
1391
1385 return outstr
1392 return outstr
1386
1393
1387 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1394 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1388 outstr = b""
1395 outstr = b""
1389 checkbox = self.getstatusprefixstring(hunkline)
1396 checkbox = self.getstatusprefixstring(hunkline)
1390
1397
1391 linestr = hunkline.prettystr().strip(b"\n")
1398 linestr = hunkline.prettystr().strip(b"\n")
1392
1399
1393 # select color-pair based on whether line is an addition/removal
1400 # select color-pair based on whether line is an addition/removal
1394 if selected:
1401 if selected:
1395 colorpair = self.getcolorpair(name=b"selected")
1402 colorpair = self.getcolorpair(name=b"selected")
1396 elif linestr.startswith(b"+"):
1403 elif linestr.startswith(b"+"):
1397 colorpair = self.getcolorpair(name=b"addition")
1404 colorpair = self.getcolorpair(name=b"addition")
1398 elif linestr.startswith(b"-"):
1405 elif linestr.startswith(b"-"):
1399 colorpair = self.getcolorpair(name=b"deletion")
1406 colorpair = self.getcolorpair(name=b"deletion")
1400 elif linestr.startswith(b"\\"):
1407 elif linestr.startswith(b"\\"):
1401 colorpair = self.getcolorpair(name=b"normal")
1408 colorpair = self.getcolorpair(name=b"normal")
1402
1409
1403 lineprefix = b" " * self.hunklineindentnumchars + checkbox
1410 lineprefix = b" " * self.hunklineindentnumchars + checkbox
1404 outstr += self.printstring(
1411 outstr += self.printstring(
1405 self.chunkpad, lineprefix, towin=towin, align=False
1412 self.chunkpad, lineprefix, towin=towin, align=False
1406 ) # add uncolored checkbox/indent
1413 ) # add uncolored checkbox/indent
1407 outstr += self.printstring(
1414 outstr += self.printstring(
1408 self.chunkpad, linestr, pair=colorpair, towin=towin, showwhtspc=True
1415 self.chunkpad, linestr, pair=colorpair, towin=towin, showwhtspc=True
1409 )
1416 )
1410 return outstr
1417 return outstr
1411
1418
1412 def printitem(
1419 def printitem(
1413 self, item=None, ignorefolding=False, recursechildren=True, towin=True
1420 self, item=None, ignorefolding=False, recursechildren=True, towin=True
1414 ):
1421 ):
1415 """
1422 """
1416 use __printitem() to print the the specified item.applied.
1423 use __printitem() to print the the specified item.applied.
1417 if item is not specified, then print the entire patch.
1424 if item is not specified, then print the entire patch.
1418 (hiding folded elements, etc. -- see __printitem() docstring)
1425 (hiding folded elements, etc. -- see __printitem() docstring)
1419 """
1426 """
1420
1427
1421 if item is None:
1428 if item is None:
1422 item = self.headerlist
1429 item = self.headerlist
1423 if recursechildren:
1430 if recursechildren:
1424 self.linesprintedtopadsofar = 0
1431 self.linesprintedtopadsofar = 0
1425
1432
1426 outstr = []
1433 outstr = []
1427 self.__printitem(
1434 self.__printitem(
1428 item, ignorefolding, recursechildren, outstr, towin=towin
1435 item, ignorefolding, recursechildren, outstr, towin=towin
1429 )
1436 )
1430 return b''.join(outstr)
1437 return b''.join(outstr)
1431
1438
1432 def outofdisplayedarea(self):
1439 def outofdisplayedarea(self):
1433 y, _ = self.chunkpad.getyx() # cursor location
1440 y, _ = self.chunkpad.getyx() # cursor location
1434 # * 2 here works but an optimization would be the max number of
1441 # * 2 here works but an optimization would be the max number of
1435 # consecutive non selectable lines
1442 # consecutive non selectable lines
1436 # i.e the max number of context line for any hunk in the patch
1443 # i.e the max number of context line for any hunk in the patch
1437 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1444 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1438 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1445 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1439 return y < miny or y > maxy
1446 return y < miny or y > maxy
1440
1447
1441 def handleselection(self, item, recursechildren):
1448 def handleselection(self, item, recursechildren):
1442 selected = item is self.currentselecteditem
1449 selected = item is self.currentselecteditem
1443 if selected and recursechildren:
1450 if selected and recursechildren:
1444 # assumes line numbering starting from line 0
1451 # assumes line numbering starting from line 0
1445 self.selecteditemstartline = self.linesprintedtopadsofar
1452 self.selecteditemstartline = self.linesprintedtopadsofar
1446 selecteditemlines = self.getnumlinesdisplayed(
1453 selecteditemlines = self.getnumlinesdisplayed(
1447 item, recursechildren=False
1454 item, recursechildren=False
1448 )
1455 )
1449 self.selecteditemendline = (
1456 self.selecteditemendline = (
1450 self.selecteditemstartline + selecteditemlines - 1
1457 self.selecteditemstartline + selecteditemlines - 1
1451 )
1458 )
1452 return selected
1459 return selected
1453
1460
1454 def __printitem(
1461 def __printitem(
1455 self, item, ignorefolding, recursechildren, outstr, towin=True
1462 self, item, ignorefolding, recursechildren, outstr, towin=True
1456 ):
1463 ):
1457 """
1464 """
1458 recursive method for printing out patch/header/hunk/hunk-line data to
1465 recursive method for printing out patch/header/hunk/hunk-line data to
1459 screen. also returns a string with all of the content of the displayed
1466 screen. also returns a string with all of the content of the displayed
1460 patch (not including coloring, etc.).
1467 patch (not including coloring, etc.).
1461
1468
1462 if ignorefolding is True, then folded items are printed out.
1469 if ignorefolding is True, then folded items are printed out.
1463
1470
1464 if recursechildren is False, then only print the item without its
1471 if recursechildren is False, then only print the item without its
1465 child items.
1472 child items.
1466 """
1473 """
1467
1474
1468 if towin and self.outofdisplayedarea():
1475 if towin and self.outofdisplayedarea():
1469 return
1476 return
1470
1477
1471 selected = self.handleselection(item, recursechildren)
1478 selected = self.handleselection(item, recursechildren)
1472
1479
1473 # patch object is a list of headers
1480 # patch object is a list of headers
1474 if isinstance(item, patch):
1481 if isinstance(item, patch):
1475 if recursechildren:
1482 if recursechildren:
1476 for hdr in item:
1483 for hdr in item:
1477 self.__printitem(
1484 self.__printitem(
1478 hdr, ignorefolding, recursechildren, outstr, towin
1485 hdr, ignorefolding, recursechildren, outstr, towin
1479 )
1486 )
1480 # todo: eliminate all isinstance() calls
1487 # todo: eliminate all isinstance() calls
1481 if isinstance(item, uiheader):
1488 if isinstance(item, uiheader):
1482 outstr.append(
1489 outstr.append(
1483 self.printheader(
1490 self.printheader(
1484 item, selected, towin=towin, ignorefolding=ignorefolding
1491 item, selected, towin=towin, ignorefolding=ignorefolding
1485 )
1492 )
1486 )
1493 )
1487 if recursechildren:
1494 if recursechildren:
1488 for hnk in item.hunks:
1495 for hnk in item.hunks:
1489 self.__printitem(
1496 self.__printitem(
1490 hnk, ignorefolding, recursechildren, outstr, towin
1497 hnk, ignorefolding, recursechildren, outstr, towin
1491 )
1498 )
1492 elif isinstance(item, uihunk) and (
1499 elif isinstance(item, uihunk) and (
1493 (not item.header.folded) or ignorefolding
1500 (not item.header.folded) or ignorefolding
1494 ):
1501 ):
1495 # print the hunk data which comes before the changed-lines
1502 # print the hunk data which comes before the changed-lines
1496 outstr.append(
1503 outstr.append(
1497 self.printhunklinesbefore(
1504 self.printhunklinesbefore(
1498 item, selected, towin=towin, ignorefolding=ignorefolding
1505 item, selected, towin=towin, ignorefolding=ignorefolding
1499 )
1506 )
1500 )
1507 )
1501 if recursechildren:
1508 if recursechildren:
1502 for l in item.changedlines:
1509 for l in item.changedlines:
1503 self.__printitem(
1510 self.__printitem(
1504 l, ignorefolding, recursechildren, outstr, towin
1511 l, ignorefolding, recursechildren, outstr, towin
1505 )
1512 )
1506 outstr.append(
1513 outstr.append(
1507 self.printhunklinesafter(
1514 self.printhunklinesafter(
1508 item, towin=towin, ignorefolding=ignorefolding
1515 item, towin=towin, ignorefolding=ignorefolding
1509 )
1516 )
1510 )
1517 )
1511 elif isinstance(item, uihunkline) and (
1518 elif isinstance(item, uihunkline) and (
1512 (not item.hunk.folded) or ignorefolding
1519 (not item.hunk.folded) or ignorefolding
1513 ):
1520 ):
1514 outstr.append(
1521 outstr.append(
1515 self.printhunkchangedline(item, selected, towin=towin)
1522 self.printhunkchangedline(item, selected, towin=towin)
1516 )
1523 )
1517
1524
1518 return outstr
1525 return outstr
1519
1526
1520 def getnumlinesdisplayed(
1527 def getnumlinesdisplayed(
1521 self, item=None, ignorefolding=False, recursechildren=True
1528 self, item=None, ignorefolding=False, recursechildren=True
1522 ):
1529 ):
1523 """
1530 """
1524 return the number of lines which would be displayed if the item were
1531 return the number of lines which would be displayed if the item were
1525 to be printed to the display. the item will not be printed to the
1532 to be printed to the display. the item will not be printed to the
1526 display (pad).
1533 display (pad).
1527 if no item is given, assume the entire patch.
1534 if no item is given, assume the entire patch.
1528 if ignorefolding is True, folded items will be unfolded when counting
1535 if ignorefolding is True, folded items will be unfolded when counting
1529 the number of lines.
1536 the number of lines.
1530 """
1537 """
1531
1538
1532 # temporarily disable printing to windows by printstring
1539 # temporarily disable printing to windows by printstring
1533 patchdisplaystring = self.printitem(
1540 patchdisplaystring = self.printitem(
1534 item, ignorefolding, recursechildren, towin=False
1541 item, ignorefolding, recursechildren, towin=False
1535 )
1542 )
1536 numlines = len(patchdisplaystring) // self.xscreensize
1543 numlines = len(patchdisplaystring) // self.xscreensize
1537 return numlines
1544 return numlines
1538
1545
1539 def sigwinchhandler(self, n, frame):
1546 def sigwinchhandler(self, n, frame):
1540 """handle window resizing"""
1547 """handle window resizing"""
1541 try:
1548 try:
1542 curses.endwin()
1549 curses.endwin()
1543 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1550 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1544 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1551 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1545 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1552 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1546 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1553 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1547 except curses.error:
1554 except curses.error:
1548 pass
1555 pass
1549
1556
1550 def getcolorpair(
1557 def getcolorpair(
1551 self, fgcolor=None, bgcolor=None, name=None, attrlist=None
1558 self, fgcolor=None, bgcolor=None, name=None, attrlist=None
1552 ):
1559 ):
1553 """
1560 """
1554 get a curses color pair, adding it to self.colorpairs if it is not
1561 get a curses color pair, adding it to self.colorpairs if it is not
1555 already defined. an optional string, name, can be passed as a shortcut
1562 already defined. an optional string, name, can be passed as a shortcut
1556 for referring to the color-pair. by default, if no arguments are
1563 for referring to the color-pair. by default, if no arguments are
1557 specified, the white foreground / black background color-pair is
1564 specified, the white foreground / black background color-pair is
1558 returned.
1565 returned.
1559
1566
1560 it is expected that this function will be used exclusively for
1567 it is expected that this function will be used exclusively for
1561 initializing color pairs, and not curses.init_pair().
1568 initializing color pairs, and not curses.init_pair().
1562
1569
1563 attrlist is used to 'flavor' the returned color-pair. this information
1570 attrlist is used to 'flavor' the returned color-pair. this information
1564 is not stored in self.colorpairs. it contains attribute values like
1571 is not stored in self.colorpairs. it contains attribute values like
1565 curses.A_BOLD.
1572 curses.A_BOLD.
1566 """
1573 """
1567
1574
1568 if (name is not None) and name in self.colorpairnames:
1575 if (name is not None) and name in self.colorpairnames:
1569 # then get the associated color pair and return it
1576 # then get the associated color pair and return it
1570 colorpair = self.colorpairnames[name]
1577 colorpair = self.colorpairnames[name]
1571 else:
1578 else:
1572 if fgcolor is None:
1579 if fgcolor is None:
1573 fgcolor = -1
1580 fgcolor = -1
1574 if bgcolor is None:
1581 if bgcolor is None:
1575 bgcolor = -1
1582 bgcolor = -1
1576 if (fgcolor, bgcolor) in self.colorpairs:
1583 if (fgcolor, bgcolor) in self.colorpairs:
1577 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1584 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1578 else:
1585 else:
1579 pairindex = len(self.colorpairs) + 1
1586 pairindex = len(self.colorpairs) + 1
1580 if self.usecolor:
1587 if self.usecolor:
1581 curses.init_pair(pairindex, fgcolor, bgcolor)
1588 curses.init_pair(pairindex, fgcolor, bgcolor)
1582 colorpair = self.colorpairs[
1589 colorpair = self.colorpairs[
1583 (fgcolor, bgcolor)
1590 (fgcolor, bgcolor)
1584 ] = curses.color_pair(pairindex)
1591 ] = curses.color_pair(pairindex)
1585 if name is not None:
1592 if name is not None:
1586 self.colorpairnames[name] = curses.color_pair(pairindex)
1593 self.colorpairnames[name] = curses.color_pair(pairindex)
1587 else:
1594 else:
1588 cval = 0
1595 cval = 0
1589 if name is not None:
1596 if name is not None:
1590 if name == b'selected':
1597 if name == b'selected':
1591 cval = curses.A_REVERSE
1598 cval = curses.A_REVERSE
1592 self.colorpairnames[name] = cval
1599 self.colorpairnames[name] = cval
1593 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1600 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1594
1601
1595 # add attributes if possible
1602 # add attributes if possible
1596 if attrlist is None:
1603 if attrlist is None:
1597 attrlist = []
1604 attrlist = []
1598 if colorpair < 256:
1605 if colorpair < 256:
1599 # then it is safe to apply all attributes
1606 # then it is safe to apply all attributes
1600 for textattr in attrlist:
1607 for textattr in attrlist:
1601 colorpair |= textattr
1608 colorpair |= textattr
1602 else:
1609 else:
1603 # just apply a select few (safe?) attributes
1610 # just apply a select few (safe?) attributes
1604 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1611 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1605 if textattrib in attrlist:
1612 if textattrib in attrlist:
1606 colorpair |= textattrib
1613 colorpair |= textattrib
1607 return colorpair
1614 return colorpair
1608
1615
1609 def initcolorpair(self, *args, **kwargs):
1616 def initcolorpair(self, *args, **kwargs):
1610 """same as getcolorpair."""
1617 """same as getcolorpair."""
1611 self.getcolorpair(*args, **kwargs)
1618 self.getcolorpair(*args, **kwargs)
1612
1619
1613 def helpwindow(self):
1620 def helpwindow(self):
1614 """print a help window to the screen. exit after any keypress."""
1621 """print a help window to the screen. exit after any keypress."""
1615 helptext = _(
1622 helptext = _(
1616 """ [press any key to return to the patch-display]
1623 """ [press any key to return to the patch-display]
1617
1624
1618 The curses hunk selector allows you to interactively choose among the
1625 The curses hunk selector allows you to interactively choose among the
1619 changes you have made, and confirm only those changes you select for
1626 changes you have made, and confirm only those changes you select for
1620 further processing by the command you are running (such as commit,
1627 further processing by the command you are running (such as commit,
1621 shelve, or revert). After confirming the selected changes, the
1628 shelve, or revert). After confirming the selected changes, the
1622 unselected changes are still present in your working copy, so you can
1629 unselected changes are still present in your working copy, so you can
1623 use the hunk selector multiple times to split large changes into
1630 use the hunk selector multiple times to split large changes into
1624 smaller changesets. the following are valid keystrokes:
1631 smaller changesets. the following are valid keystrokes:
1625
1632
1626 x [space] : (un-)select item ([~]/[x] = partly/fully applied)
1633 x [space] : (un-)select item ([~]/[x] = partly/fully applied)
1627 [enter] : (un-)select item and go to next item of same type
1634 [enter] : (un-)select item and go to next item of same type
1628 A : (un-)select all items
1635 A : (un-)select all items
1629 X : (un-)select all items between current and most-recent
1636 X : (un-)select all items between current and most-recent
1630 up/down-arrow [k/j] : go to previous/next unfolded item
1637 up/down-arrow [k/j] : go to previous/next unfolded item
1631 pgup/pgdn [K/J] : go to previous/next item of same type
1638 pgup/pgdn [K/J] : go to previous/next item of same type
1632 right/left-arrow [l/h] : go to child item / parent item
1639 right/left-arrow [l/h] : go to child item / parent item
1633 shift-left-arrow [H] : go to parent header / fold selected header
1640 shift-left-arrow [H] : go to parent header / fold selected header
1634 g : go to the top
1641 g : go to the top
1635 G : go to the bottom
1642 G : go to the bottom
1636 f : fold / unfold item, hiding/revealing its children
1643 f : fold / unfold item, hiding/revealing its children
1637 F : fold / unfold parent item and all of its ancestors
1644 F : fold / unfold parent item and all of its ancestors
1638 ctrl-l : scroll the selected line to the top of the screen
1645 ctrl-l : scroll the selected line to the top of the screen
1639 m : edit / resume editing the commit message
1646 m : edit / resume editing the commit message
1640 e : edit the currently selected hunk
1647 e : edit the currently selected hunk
1641 a : toggle all selections
1648 a : toggle all selections
1642 c : confirm selected changes
1649 c : confirm selected changes
1643 r : review/edit and confirm selected changes
1650 r : review/edit and confirm selected changes
1644 q : quit without confirming (no changes will be made)
1651 q : quit without confirming (no changes will be made)
1645 ? : help (what you're currently reading)"""
1652 ? : help (what you're currently reading)"""
1646 )
1653 )
1647
1654
1648 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1655 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1649 helplines = helptext.split(b"\n")
1656 helplines = helptext.split(b"\n")
1650 helplines = helplines + [b" "] * (
1657 helplines = helplines + [b" "] * (
1651 self.yscreensize - self.numstatuslines - len(helplines) - 1
1658 self.yscreensize - self.numstatuslines - len(helplines) - 1
1652 )
1659 )
1653 try:
1660 try:
1654 for line in helplines:
1661 for line in helplines:
1655 self.printstring(helpwin, line, pairname=b"legend")
1662 self.printstring(helpwin, line, pairname=b"legend")
1656 except curses.error:
1663 except curses.error:
1657 pass
1664 pass
1658 helpwin.refresh()
1665 helpwin.refresh()
1659 try:
1666 try:
1660 with self.ui.timeblockedsection(b'crecord'):
1667 with self.ui.timeblockedsection(b'crecord'):
1661 helpwin.getkey()
1668 helpwin.getkey()
1662 except curses.error:
1669 except curses.error:
1663 pass
1670 pass
1664
1671
1665 def commitMessageWindow(self):
1672 def commitMessageWindow(self):
1666 """Create a temporary commit message editing window on the screen."""
1673 """Create a temporary commit message editing window on the screen."""
1667
1674
1668 curses.raw()
1675 curses.raw()
1669 curses.def_prog_mode()
1676 curses.def_prog_mode()
1670 curses.endwin()
1677 curses.endwin()
1671 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1678 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1672 curses.cbreak()
1679 curses.cbreak()
1673 self.stdscr.refresh()
1680 self.stdscr.refresh()
1674 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1681 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1675
1682
1676 def handlefirstlineevent(self):
1683 def handlefirstlineevent(self):
1677 """
1684 """
1678 Handle 'g' to navigate to the top most file in the ncurses window.
1685 Handle 'g' to navigate to the top most file in the ncurses window.
1679 """
1686 """
1680 self.currentselecteditem = self.headerlist[0]
1687 self.currentselecteditem = self.headerlist[0]
1681 currentitem = self.currentselecteditem
1688 currentitem = self.currentselecteditem
1682 # select the parent item recursively until we're at a header
1689 # select the parent item recursively until we're at a header
1683 while True:
1690 while True:
1684 nextitem = currentitem.parentitem()
1691 nextitem = currentitem.parentitem()
1685 if nextitem is None:
1692 if nextitem is None:
1686 break
1693 break
1687 else:
1694 else:
1688 currentitem = nextitem
1695 currentitem = nextitem
1689
1696
1690 self.currentselecteditem = currentitem
1697 self.currentselecteditem = currentitem
1691
1698
1692 def handlelastlineevent(self):
1699 def handlelastlineevent(self):
1693 """
1700 """
1694 Handle 'G' to navigate to the bottom most file/hunk/line depending
1701 Handle 'G' to navigate to the bottom most file/hunk/line depending
1695 on the whether the fold is active or not.
1702 on the whether the fold is active or not.
1696
1703
1697 If the bottom most file is folded, it navigates to that file and
1704 If the bottom most file is folded, it navigates to that file and
1698 stops there. If the bottom most file is unfolded, it navigates to
1705 stops there. If the bottom most file is unfolded, it navigates to
1699 the bottom most hunk in that file and stops there. If the bottom most
1706 the bottom most hunk in that file and stops there. If the bottom most
1700 hunk is unfolded, it navigates to the bottom most line in that hunk.
1707 hunk is unfolded, it navigates to the bottom most line in that hunk.
1701 """
1708 """
1702 currentitem = self.currentselecteditem
1709 currentitem = self.currentselecteditem
1703 nextitem = currentitem.nextitem()
1710 nextitem = currentitem.nextitem()
1704 # select the child item recursively until we're at a footer
1711 # select the child item recursively until we're at a footer
1705 while nextitem is not None:
1712 while nextitem is not None:
1706 nextitem = currentitem.nextitem()
1713 nextitem = currentitem.nextitem()
1707 if nextitem is None:
1714 if nextitem is None:
1708 break
1715 break
1709 else:
1716 else:
1710 currentitem = nextitem
1717 currentitem = nextitem
1711
1718
1712 self.currentselecteditem = currentitem
1719 self.currentselecteditem = currentitem
1713 self.recenterdisplayedarea()
1720 self.recenterdisplayedarea()
1714
1721
1715 def confirmationwindow(self, windowtext):
1722 def confirmationwindow(self, windowtext):
1716 """display an informational window, then wait for and return a
1723 """display an informational window, then wait for and return a
1717 keypress."""
1724 keypress."""
1718
1725
1719 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1726 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1720 try:
1727 try:
1721 lines = windowtext.split(b"\n")
1728 lines = windowtext.split(b"\n")
1722 for line in lines:
1729 for line in lines:
1723 self.printstring(confirmwin, line, pairname=b"selected")
1730 self.printstring(confirmwin, line, pairname=b"selected")
1724 except curses.error:
1731 except curses.error:
1725 pass
1732 pass
1726 self.stdscr.refresh()
1733 self.stdscr.refresh()
1727 confirmwin.refresh()
1734 confirmwin.refresh()
1728 try:
1735 try:
1729 with self.ui.timeblockedsection(b'crecord'):
1736 with self.ui.timeblockedsection(b'crecord'):
1730 response = chr(self.stdscr.getch())
1737 response = chr(self.stdscr.getch())
1731 except ValueError:
1738 except ValueError:
1732 response = None
1739 response = None
1733
1740
1734 return response
1741 return response
1735
1742
1736 def reviewcommit(self):
1743 def reviewcommit(self):
1737 """ask for 'y' to be pressed to confirm selected. return True if
1744 """ask for 'y' to be pressed to confirm selected. return True if
1738 confirmed."""
1745 confirmed."""
1739 confirmtext = _(
1746 confirmtext = _(
1740 """If you answer yes to the following, your currently chosen patch chunks
1747 """If you answer yes to the following, your currently chosen patch chunks
1741 will be loaded into an editor. To modify the patch, make the changes in your
1748 will be loaded into an editor. To modify the patch, make the changes in your
1742 editor and save. To accept the current patch as-is, close the editor without
1749 editor and save. To accept the current patch as-is, close the editor without
1743 saving.
1750 saving.
1744
1751
1745 note: don't add/remove lines unless you also modify the range information.
1752 note: don't add/remove lines unless you also modify the range information.
1746 failing to follow this rule will result in the commit aborting.
1753 failing to follow this rule will result in the commit aborting.
1747
1754
1748 are you sure you want to review/edit and confirm the selected changes [yn]?
1755 are you sure you want to review/edit and confirm the selected changes [yn]?
1749 """
1756 """
1750 )
1757 )
1751 with self.ui.timeblockedsection(b'crecord'):
1758 with self.ui.timeblockedsection(b'crecord'):
1752 response = self.confirmationwindow(confirmtext)
1759 response = self.confirmationwindow(confirmtext)
1753 if response is None:
1760 if response is None:
1754 response = "n"
1761 response = "n"
1755 if response.lower().startswith("y"):
1762 if response.lower().startswith("y"):
1756 return True
1763 return True
1757 else:
1764 else:
1758 return False
1765 return False
1759
1766
1760 def recenterdisplayedarea(self):
1767 def recenterdisplayedarea(self):
1761 """
1768 """
1762 once we scrolled with pg up pg down we can be pointing outside of the
1769 once we scrolled with pg up pg down we can be pointing outside of the
1763 display zone. we print the patch with towin=False to compute the
1770 display zone. we print the patch with towin=False to compute the
1764 location of the selected item even though it is outside of the displayed
1771 location of the selected item even though it is outside of the displayed
1765 zone and then update the scroll.
1772 zone and then update the scroll.
1766 """
1773 """
1767 self.printitem(towin=False)
1774 self.printitem(towin=False)
1768 self.updatescroll()
1775 self.updatescroll()
1769
1776
1770 def toggleedit(self, item=None, test=False):
1777 def toggleedit(self, item=None, test=False):
1771 """
1778 """
1772 edit the currently selected chunk
1779 edit the currently selected chunk
1773 """
1780 """
1774
1781
1775 def updateui(self):
1782 def updateui(self):
1776 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1783 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1777 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1784 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1778 self.updatescroll()
1785 self.updatescroll()
1779 self.stdscr.refresh()
1786 self.stdscr.refresh()
1780 self.statuswin.refresh()
1787 self.statuswin.refresh()
1781 self.stdscr.keypad(1)
1788 self.stdscr.keypad(1)
1782
1789
1783 def editpatchwitheditor(self, chunk):
1790 def editpatchwitheditor(self, chunk):
1784 if chunk is None:
1791 if chunk is None:
1785 self.ui.write(_(b'cannot edit patch for whole file'))
1792 self.ui.write(_(b'cannot edit patch for whole file'))
1786 self.ui.write(b"\n")
1793 self.ui.write(b"\n")
1787 return None
1794 return None
1788 if chunk.header.binary():
1795 if chunk.header.binary():
1789 self.ui.write(_(b'cannot edit patch for binary file'))
1796 self.ui.write(_(b'cannot edit patch for binary file'))
1790 self.ui.write(b"\n")
1797 self.ui.write(b"\n")
1791 return None
1798 return None
1792
1799
1793 # write the initial patch
1800 # write the initial patch
1794 patch = stringio()
1801 patch = stringio()
1795 patch.write(diffhelptext + hunkhelptext)
1802 patch.write(diffhelptext + hunkhelptext)
1796 chunk.header.write(patch)
1803 chunk.header.write(patch)
1797 chunk.write(patch)
1804 chunk.write(patch)
1798
1805
1799 # start the editor and wait for it to complete
1806 # start the editor and wait for it to complete
1800 try:
1807 try:
1801 patch = self.ui.edit(patch.getvalue(), b"", action=b"diff")
1808 patch = self.ui.edit(patch.getvalue(), b"", action=b"diff")
1802 except error.Abort as exc:
1809 except error.Abort as exc:
1803 self.errorstr = stringutil.forcebytestr(exc)
1810 self.errorstr = stringutil.forcebytestr(exc)
1804 return None
1811 return None
1805 finally:
1812 finally:
1806 self.stdscr.clear()
1813 self.stdscr.clear()
1807 self.stdscr.refresh()
1814 self.stdscr.refresh()
1808
1815
1809 # remove comment lines
1816 # remove comment lines
1810 patch = [
1817 patch = [
1811 line + b'\n'
1818 line + b'\n'
1812 for line in patch.splitlines()
1819 for line in patch.splitlines()
1813 if not line.startswith(b'#')
1820 if not line.startswith(b'#')
1814 ]
1821 ]
1815 return patchmod.parsepatch(patch)
1822 return patchmod.parsepatch(patch)
1816
1823
1817 if item is None:
1824 if item is None:
1818 item = self.currentselecteditem
1825 item = self.currentselecteditem
1819 if isinstance(item, uiheader):
1826 if isinstance(item, uiheader):
1820 return
1827 return
1821 if isinstance(item, uihunkline):
1828 if isinstance(item, uihunkline):
1822 item = item.parentitem()
1829 item = item.parentitem()
1823 if not isinstance(item, uihunk):
1830 if not isinstance(item, uihunk):
1824 return
1831 return
1825
1832
1826 # To go back to that hunk or its replacement at the end of the edit
1833 # To go back to that hunk or its replacement at the end of the edit
1827 itemindex = item.parentitem().hunks.index(item)
1834 itemindex = item.parentitem().hunks.index(item)
1828
1835
1829 beforeadded, beforeremoved = item.added, item.removed
1836 beforeadded, beforeremoved = item.added, item.removed
1830 newpatches = editpatchwitheditor(self, item)
1837 newpatches = editpatchwitheditor(self, item)
1831 if newpatches is None:
1838 if newpatches is None:
1832 if not test:
1839 if not test:
1833 updateui(self)
1840 updateui(self)
1834 return
1841 return
1835 header = item.header
1842 header = item.header
1836 editedhunkindex = header.hunks.index(item)
1843 editedhunkindex = header.hunks.index(item)
1837 hunksbefore = header.hunks[:editedhunkindex]
1844 hunksbefore = header.hunks[:editedhunkindex]
1838 hunksafter = header.hunks[editedhunkindex + 1 :]
1845 hunksafter = header.hunks[editedhunkindex + 1 :]
1839 newpatchheader = newpatches[0]
1846 newpatchheader = newpatches[0]
1840 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1847 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1841 newadded = sum([h.added for h in newhunks])
1848 newadded = sum([h.added for h in newhunks])
1842 newremoved = sum([h.removed for h in newhunks])
1849 newremoved = sum([h.removed for h in newhunks])
1843 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1850 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1844
1851
1845 for h in hunksafter:
1852 for h in hunksafter:
1846 h.toline += offset
1853 h.toline += offset
1847 for h in newhunks:
1854 for h in newhunks:
1848 h.folded = False
1855 h.folded = False
1849 header.hunks = hunksbefore + newhunks + hunksafter
1856 header.hunks = hunksbefore + newhunks + hunksafter
1850 if self.emptypatch():
1857 if self.emptypatch():
1851 header.hunks = hunksbefore + [item] + hunksafter
1858 header.hunks = hunksbefore + [item] + hunksafter
1852 self.currentselecteditem = header
1859 self.currentselecteditem = header
1853 if len(header.hunks) > itemindex:
1860 if len(header.hunks) > itemindex:
1854 self.currentselecteditem = header.hunks[itemindex]
1861 self.currentselecteditem = header.hunks[itemindex]
1855
1862
1856 if not test:
1863 if not test:
1857 updateui(self)
1864 updateui(self)
1858
1865
1859 def emptypatch(self):
1866 def emptypatch(self):
1860 item = self.headerlist
1867 item = self.headerlist
1861 if not item:
1868 if not item:
1862 return True
1869 return True
1863 for header in item:
1870 for header in item:
1864 if header.hunks:
1871 if header.hunks:
1865 return False
1872 return False
1866 return True
1873 return True
1867
1874
1868 def handlekeypressed(self, keypressed, test=False):
1875 def handlekeypressed(self, keypressed, test=False):
1869 """
1876 """
1870 Perform actions based on pressed keys.
1877 Perform actions based on pressed keys.
1871
1878
1872 Return true to exit the main loop.
1879 Return true to exit the main loop.
1873 """
1880 """
1874 if keypressed in ["k", "KEY_UP"]:
1881 if keypressed in ["k", "KEY_UP"]:
1875 self.uparrowevent()
1882 self.uparrowevent()
1876 elif keypressed in ["K", "KEY_PPAGE"]:
1883 elif keypressed in ["K", "KEY_PPAGE"]:
1877 self.uparrowshiftevent()
1884 self.uparrowshiftevent()
1878 elif keypressed in ["j", "KEY_DOWN"]:
1885 elif keypressed in ["j", "KEY_DOWN"]:
1879 self.downarrowevent()
1886 self.downarrowevent()
1880 elif keypressed in ["J", "KEY_NPAGE"]:
1887 elif keypressed in ["J", "KEY_NPAGE"]:
1881 self.downarrowshiftevent()
1888 self.downarrowshiftevent()
1882 elif keypressed in ["l", "KEY_RIGHT"]:
1889 elif keypressed in ["l", "KEY_RIGHT"]:
1883 self.rightarrowevent()
1890 self.rightarrowevent()
1884 elif keypressed in ["h", "KEY_LEFT"]:
1891 elif keypressed in ["h", "KEY_LEFT"]:
1885 self.leftarrowevent()
1892 self.leftarrowevent()
1886 elif keypressed in ["H", "KEY_SLEFT"]:
1893 elif keypressed in ["H", "KEY_SLEFT"]:
1887 self.leftarrowshiftevent()
1894 self.leftarrowshiftevent()
1888 elif keypressed in ["q"]:
1895 elif keypressed in ["q"]:
1889 raise error.Abort(_(b'user quit'))
1896 raise error.Abort(_(b'user quit'))
1890 elif keypressed in ['a']:
1897 elif keypressed in ['a']:
1891 self.flipselections()
1898 self.flipselections()
1892 elif keypressed in ["c"]:
1899 elif keypressed in ["c"]:
1893 return True
1900 return True
1894 elif keypressed in ["r"]:
1901 elif keypressed in ["r"]:
1895 if self.reviewcommit():
1902 if self.reviewcommit():
1896 self.opts[b'review'] = True
1903 self.opts[b'review'] = True
1897 return True
1904 return True
1898 elif test and keypressed in ["R"]:
1905 elif test and keypressed in ["R"]:
1899 self.opts[b'review'] = True
1906 self.opts[b'review'] = True
1900 return True
1907 return True
1901 elif keypressed in [" ", "x"]:
1908 elif keypressed in [" ", "x"]:
1902 self.toggleapply()
1909 self.toggleapply()
1903 elif keypressed in ["\n", "KEY_ENTER"]:
1910 elif keypressed in ["\n", "KEY_ENTER"]:
1904 self.toggleapply()
1911 self.toggleapply()
1905 self.nextsametype(test=test)
1912 self.nextsametype(test=test)
1906 elif keypressed in ["X"]:
1913 elif keypressed in ["X"]:
1907 self.toggleallbetween()
1914 self.toggleallbetween()
1908 elif keypressed in ["A"]:
1915 elif keypressed in ["A"]:
1909 self.toggleall()
1916 self.toggleall()
1910 elif keypressed in ["e"]:
1917 elif keypressed in ["e"]:
1911 self.toggleedit(test=test)
1918 self.toggleedit(test=test)
1912 elif keypressed in ["f"]:
1919 elif keypressed in ["f"]:
1913 self.togglefolded()
1920 self.togglefolded()
1914 elif keypressed in ["F"]:
1921 elif keypressed in ["F"]:
1915 self.togglefolded(foldparent=True)
1922 self.togglefolded(foldparent=True)
1916 elif keypressed in ["m"]:
1923 elif keypressed in ["m"]:
1917 self.commitMessageWindow()
1924 self.commitMessageWindow()
1918 elif keypressed in ["g", "KEY_HOME"]:
1925 elif keypressed in ["g", "KEY_HOME"]:
1919 self.handlefirstlineevent()
1926 self.handlefirstlineevent()
1920 elif keypressed in ["G", "KEY_END"]:
1927 elif keypressed in ["G", "KEY_END"]:
1921 self.handlelastlineevent()
1928 self.handlelastlineevent()
1922 elif keypressed in ["?"]:
1929 elif keypressed in ["?"]:
1923 self.helpwindow()
1930 self.helpwindow()
1924 self.stdscr.clear()
1931 self.stdscr.clear()
1925 self.stdscr.refresh()
1932 self.stdscr.refresh()
1926 elif keypressed in [curses.ascii.ctrl("L")]:
1933 elif keypressed in [curses.ascii.ctrl("L")]:
1927 # scroll the current line to the top of the screen, and redraw
1934 # scroll the current line to the top of the screen, and redraw
1928 # everything
1935 # everything
1929 self.scrolllines(self.selecteditemstartline)
1936 self.scrolllines(self.selecteditemstartline)
1930 self.stdscr.clear()
1937 self.stdscr.clear()
1931 self.stdscr.refresh()
1938 self.stdscr.refresh()
1932
1939
1933 def main(self, stdscr):
1940 def main(self, stdscr):
1934 """
1941 """
1935 method to be wrapped by curses.wrapper() for selecting chunks.
1942 method to be wrapped by curses.wrapper() for selecting chunks.
1936 """
1943 """
1937
1944
1938 origsigwinch = sentinel = object()
1945 origsigwinch = sentinel = object()
1939 if util.safehasattr(signal, b'SIGWINCH'):
1946 if util.safehasattr(signal, b'SIGWINCH'):
1940 origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1947 origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1941 try:
1948 try:
1942 return self._main(stdscr)
1949 return self._main(stdscr)
1943 finally:
1950 finally:
1944 if origsigwinch is not sentinel:
1951 if origsigwinch is not sentinel:
1945 signal.signal(signal.SIGWINCH, origsigwinch)
1952 signal.signal(signal.SIGWINCH, origsigwinch)
1946
1953
1947 def _main(self, stdscr):
1954 def _main(self, stdscr):
1948 self.stdscr = stdscr
1955 self.stdscr = stdscr
1949 # error during initialization, cannot be printed in the curses
1956 # error during initialization, cannot be printed in the curses
1950 # interface, it should be printed by the calling code
1957 # interface, it should be printed by the calling code
1951 self.initexc = None
1958 self.initexc = None
1952 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1959 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1953
1960
1954 curses.start_color()
1961 curses.start_color()
1955 try:
1962 try:
1956 curses.use_default_colors()
1963 curses.use_default_colors()
1957 except curses.error:
1964 except curses.error:
1958 self.usecolor = False
1965 self.usecolor = False
1959
1966
1960 # In some situations we may have some cruft left on the "alternate
1967 # In some situations we may have some cruft left on the "alternate
1961 # screen" from another program (or previous iterations of ourself), and
1968 # screen" from another program (or previous iterations of ourself), and
1962 # we won't clear it if the scroll region is small enough to comfortably
1969 # we won't clear it if the scroll region is small enough to comfortably
1963 # fit on the terminal.
1970 # fit on the terminal.
1964 self.stdscr.clear()
1971 self.stdscr.clear()
1965
1972
1966 # don't display the cursor
1973 # don't display the cursor
1967 try:
1974 try:
1968 curses.curs_set(0)
1975 curses.curs_set(0)
1969 except curses.error:
1976 except curses.error:
1970 pass
1977 pass
1971
1978
1972 # available colors: black, blue, cyan, green, magenta, white, yellow
1979 # available colors: black, blue, cyan, green, magenta, white, yellow
1973 # init_pair(color_id, foreground_color, background_color)
1980 # init_pair(color_id, foreground_color, background_color)
1974 self.initcolorpair(None, None, name=b"normal")
1981 self.initcolorpair(None, None, name=b"normal")
1975 self.initcolorpair(
1982 self.initcolorpair(
1976 curses.COLOR_WHITE, curses.COLOR_MAGENTA, name=b"selected"
1983 curses.COLOR_WHITE, curses.COLOR_MAGENTA, name=b"selected"
1977 )
1984 )
1978 self.initcolorpair(curses.COLOR_RED, None, name=b"deletion")
1985 self.initcolorpair(curses.COLOR_RED, None, name=b"deletion")
1979 self.initcolorpair(curses.COLOR_GREEN, None, name=b"addition")
1986 self.initcolorpair(curses.COLOR_GREEN, None, name=b"addition")
1980 self.initcolorpair(
1987 self.initcolorpair(
1981 curses.COLOR_WHITE, curses.COLOR_BLUE, name=b"legend"
1988 curses.COLOR_WHITE, curses.COLOR_BLUE, name=b"legend"
1982 )
1989 )
1983 # newwin([height, width,] begin_y, begin_x)
1990 # newwin([height, width,] begin_y, begin_x)
1984 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1991 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1985 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1992 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1986
1993
1987 # figure out how much space to allocate for the chunk-pad which is
1994 # figure out how much space to allocate for the chunk-pad which is
1988 # used for displaying the patch
1995 # used for displaying the patch
1989
1996
1990 # stupid hack to prevent getnumlinesdisplayed from failing
1997 # stupid hack to prevent getnumlinesdisplayed from failing
1991 self.chunkpad = curses.newpad(1, self.xscreensize)
1998 self.chunkpad = curses.newpad(1, self.xscreensize)
1992
1999
1993 # add 1 so to account for last line text reaching end of line
2000 # add 1 so to account for last line text reaching end of line
1994 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
2001 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1995
2002
1996 try:
2003 try:
1997 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
2004 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1998 except curses.error:
2005 except curses.error:
1999 self.initexc = fallbackerror(
2006 self.initexc = fallbackerror(
2000 _(b'this diff is too large to be displayed')
2007 _(b'this diff is too large to be displayed')
2001 )
2008 )
2002 return
2009 return
2003 # initialize selecteditemendline (initial start-line is 0)
2010 # initialize selecteditemendline (initial start-line is 0)
2004 self.selecteditemendline = self.getnumlinesdisplayed(
2011 self.selecteditemendline = self.getnumlinesdisplayed(
2005 self.currentselecteditem, recursechildren=False
2012 self.currentselecteditem, recursechildren=False
2006 )
2013 )
2007
2014
2008 while True:
2015 while True:
2009 self.updatescreen()
2016 self.updatescreen()
2010 try:
2017 try:
2011 with self.ui.timeblockedsection(b'crecord'):
2018 with self.ui.timeblockedsection(b'crecord'):
2012 keypressed = self.statuswin.getkey()
2019 keypressed = self.statuswin.getkey()
2013 if self.errorstr is not None:
2020 if self.errorstr is not None:
2014 self.errorstr = None
2021 self.errorstr = None
2015 continue
2022 continue
2016 except curses.error:
2023 except curses.error:
2017 keypressed = b"foobar"
2024 keypressed = b"foobar"
2018 if self.handlekeypressed(keypressed):
2025 if self.handlekeypressed(keypressed):
2019 break
2026 break
2020
2027
2021 if self.commenttext != b"":
2028 if self.commenttext != b"":
2022 whitespaceremoved = re.sub(
2029 whitespaceremoved = re.sub(
2023 br"(?m)^\s.*(\n|$)", b"", self.commenttext
2030 br"(?m)^\s.*(\n|$)", b"", self.commenttext
2024 )
2031 )
2025 if whitespaceremoved != b"":
2032 if whitespaceremoved != b"":
2026 self.opts[b'message'] = self.commenttext
2033 self.opts[b'message'] = self.commenttext
General Comments 0
You need to be logged in to leave comments. Login now