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