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