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