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