##// END OF EJS Templates
crecord: render chunkpad on Windows (issue6427)...
Barret Rennie -
r46415:91c41ea1 stable
parent child Browse files
Show More
@@ -1,2034 +1,2034 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 """# To remove '-' lines, make them ' ' lines (context).
37 """# To remove '-' lines, make them ' ' lines (context).
38 # To remove '+' lines, delete them.
38 # To remove '+' lines, delete them.
39 # Lines starting with # will be removed from the patch.
39 # Lines starting with # will be removed from the patch.
40 """
40 """
41 )
41 )
42
42
43 hunkhelptext = _(
43 hunkhelptext = _(
44 """#
44 """#
45 # If the patch applies cleanly, the edited hunk will immediately be
45 # If the patch applies cleanly, the edited hunk will immediately be
46 # added to the record list. If it does not apply cleanly, a rejects file
46 # added to the record list. If it does not apply cleanly, a rejects file
47 # will be generated. You can use that when you try again. If all lines
47 # will be generated. You can use that when you try again. If all lines
48 # of the hunk are removed, then the edit is aborted and the hunk is left
48 # of the hunk are removed, then the edit is aborted and the hunk is left
49 # unchanged.
49 # unchanged.
50 """
50 """
51 )
51 )
52
52
53 patchhelptext = _(
53 patchhelptext = _(
54 """#
54 """#
55 # If the patch applies cleanly, the edited patch will immediately
55 # If the patch applies cleanly, the edited patch will immediately
56 # be finalised. If it does not apply cleanly, rejects files will be
56 # be finalised. If it does not apply cleanly, rejects files will be
57 # generated. You can use those when you try again.
57 # generated. You can use those when you try again.
58 """
58 """
59 )
59 )
60
60
61 try:
61 try:
62 import curses
62 import curses
63 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 = False
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, 'r')
614 testf = open(testfn, 'r')
615 testcommands = [x.rstrip('\n') for x in testf.readlines()]
615 testcommands = [x.rstrip('\n') for x in testf.readlines()]
616 testf.close()
616 testf.close()
617 while True:
617 while True:
618 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
618 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
619 break
619 break
620 return chunkselector.opts
620 return chunkselector.opts
621
621
622
622
623 _headermessages = { # {operation: text}
623 _headermessages = { # {operation: text}
624 b'apply': _(b'Select hunks to apply'),
624 b'apply': _(b'Select hunks to apply'),
625 b'discard': _(b'Select hunks to discard'),
625 b'discard': _(b'Select hunks to discard'),
626 b'keep': _(b'Select hunks to keep'),
626 b'keep': _(b'Select hunks to keep'),
627 None: _(b'Select hunks to record'),
627 None: _(b'Select hunks to record'),
628 }
628 }
629
629
630
630
631 class curseschunkselector(object):
631 class curseschunkselector(object):
632 def __init__(self, headerlist, ui, operation=None):
632 def __init__(self, headerlist, ui, operation=None):
633 # put the headers into a patch object
633 # put the headers into a patch object
634 self.headerlist = patch(headerlist)
634 self.headerlist = patch(headerlist)
635
635
636 self.ui = ui
636 self.ui = ui
637 self.opts = {}
637 self.opts = {}
638
638
639 self.errorstr = None
639 self.errorstr = None
640 # list of all chunks
640 # list of all chunks
641 self.chunklist = []
641 self.chunklist = []
642 for h in headerlist:
642 for h in headerlist:
643 self.chunklist.append(h)
643 self.chunklist.append(h)
644 self.chunklist.extend(h.hunks)
644 self.chunklist.extend(h.hunks)
645
645
646 # dictionary mapping (fgcolor, bgcolor) pairs to the
646 # dictionary mapping (fgcolor, bgcolor) pairs to the
647 # corresponding curses color-pair value.
647 # corresponding curses color-pair value.
648 self.colorpairs = {}
648 self.colorpairs = {}
649 # maps custom nicknames of color-pairs to curses color-pair values
649 # maps custom nicknames of color-pairs to curses color-pair values
650 self.colorpairnames = {}
650 self.colorpairnames = {}
651
651
652 # Honor color setting of ui section. Keep colored setup as
652 # Honor color setting of ui section. Keep colored setup as
653 # long as not explicitly set to a falsy value - especially,
653 # long as not explicitly set to a falsy value - especially,
654 # when not set at all. This is to stay most compatible with
654 # when not set at all. This is to stay most compatible with
655 # previous (color only) behaviour.
655 # previous (color only) behaviour.
656 uicolor = stringutil.parsebool(self.ui.config(b'ui', b'color'))
656 uicolor = stringutil.parsebool(self.ui.config(b'ui', b'color'))
657 self.usecolor = uicolor is not False
657 self.usecolor = uicolor is not False
658
658
659 # the currently selected header, hunk, or hunk-line
659 # the currently selected header, hunk, or hunk-line
660 self.currentselecteditem = self.headerlist[0]
660 self.currentselecteditem = self.headerlist[0]
661 self.lastapplieditem = None
661 self.lastapplieditem = None
662
662
663 # updated when printing out patch-display -- the 'lines' here are the
663 # updated when printing out patch-display -- the 'lines' here are the
664 # line positions *in the pad*, not on the screen.
664 # line positions *in the pad*, not on the screen.
665 self.selecteditemstartline = 0
665 self.selecteditemstartline = 0
666 self.selecteditemendline = None
666 self.selecteditemendline = None
667
667
668 # define indentation levels
668 # define indentation levels
669 self.headerindentnumchars = 0
669 self.headerindentnumchars = 0
670 self.hunkindentnumchars = 3
670 self.hunkindentnumchars = 3
671 self.hunklineindentnumchars = 6
671 self.hunklineindentnumchars = 6
672
672
673 # the first line of the pad to print to the screen
673 # the first line of the pad to print to the screen
674 self.firstlineofpadtoprint = 0
674 self.firstlineofpadtoprint = 0
675
675
676 # keeps track of the number of lines in the pad
676 # keeps track of the number of lines in the pad
677 self.numpadlines = None
677 self.numpadlines = None
678
678
679 self.numstatuslines = 1
679 self.numstatuslines = 1
680
680
681 # keep a running count of the number of lines printed to the pad
681 # keep a running count of the number of lines printed to the pad
682 # (used for determining when the selected item begins/ends)
682 # (used for determining when the selected item begins/ends)
683 self.linesprintedtopadsofar = 0
683 self.linesprintedtopadsofar = 0
684
684
685 # stores optional text for a commit comment provided by the user
685 # stores optional text for a commit comment provided by the user
686 self.commenttext = b""
686 self.commenttext = b""
687
687
688 # if the last 'toggle all' command caused all changes to be applied
688 # if the last 'toggle all' command caused all changes to be applied
689 self.waslasttoggleallapplied = True
689 self.waslasttoggleallapplied = True
690
690
691 # affects some ui text
691 # affects some ui text
692 if operation not in _headermessages:
692 if operation not in _headermessages:
693 raise error.ProgrammingError(
693 raise error.ProgrammingError(
694 b'unexpected operation: %s' % operation
694 b'unexpected operation: %s' % operation
695 )
695 )
696 self.operation = operation
696 self.operation = operation
697
697
698 def uparrowevent(self):
698 def uparrowevent(self):
699 """
699 """
700 try to select the previous item to the current item that has the
700 try to select the previous item to the current item that has the
701 most-indented level. for example, if a hunk is selected, try to select
701 most-indented level. for example, if a hunk is selected, try to select
702 the last hunkline of the hunk prior to the selected hunk. or, if
702 the last hunkline of the hunk prior to the selected hunk. or, if
703 the first hunkline of a hunk is currently selected, then select the
703 the first hunkline of a hunk is currently selected, then select the
704 hunk itself.
704 hunk itself.
705 """
705 """
706 currentitem = self.currentselecteditem
706 currentitem = self.currentselecteditem
707
707
708 nextitem = currentitem.previtem()
708 nextitem = currentitem.previtem()
709
709
710 if nextitem is None:
710 if nextitem is None:
711 # if no parent item (i.e. currentitem is the first header), then
711 # if no parent item (i.e. currentitem is the first header), then
712 # no change...
712 # no change...
713 nextitem = currentitem
713 nextitem = currentitem
714
714
715 self.currentselecteditem = nextitem
715 self.currentselecteditem = nextitem
716
716
717 def uparrowshiftevent(self):
717 def uparrowshiftevent(self):
718 """
718 """
719 select (if possible) the previous item on the same level as the
719 select (if possible) the previous item on the same level as the
720 currently selected item. otherwise, select (if possible) the
720 currently selected item. otherwise, select (if possible) the
721 parent-item of the currently selected item.
721 parent-item of the currently selected item.
722 """
722 """
723 currentitem = self.currentselecteditem
723 currentitem = self.currentselecteditem
724 nextitem = currentitem.prevsibling()
724 nextitem = currentitem.prevsibling()
725 # if there's no previous sibling, try choosing the parent
725 # if there's no previous sibling, try choosing the parent
726 if nextitem is None:
726 if nextitem is None:
727 nextitem = currentitem.parentitem()
727 nextitem = currentitem.parentitem()
728 if nextitem is None:
728 if nextitem is None:
729 # if no parent item (i.e. currentitem is the first header), then
729 # if no parent item (i.e. currentitem is the first header), then
730 # no change...
730 # no change...
731 nextitem = currentitem
731 nextitem = currentitem
732
732
733 self.currentselecteditem = nextitem
733 self.currentselecteditem = nextitem
734 self.recenterdisplayedarea()
734 self.recenterdisplayedarea()
735
735
736 def downarrowevent(self):
736 def downarrowevent(self):
737 """
737 """
738 try to select the next item to the current item that has the
738 try to select the next item to the current item that has the
739 most-indented level. for example, if a hunk is selected, select
739 most-indented level. for example, if a hunk is selected, select
740 the first hunkline of the selected hunk. or, if the last hunkline of
740 the first hunkline of the selected hunk. or, if the last hunkline of
741 a hunk is currently selected, then select the next hunk, if one exists,
741 a hunk is currently selected, then select the next hunk, if one exists,
742 or if not, the next header if one exists.
742 or if not, the next header if one exists.
743 """
743 """
744 # self.startprintline += 1 #debug
744 # self.startprintline += 1 #debug
745 currentitem = self.currentselecteditem
745 currentitem = self.currentselecteditem
746
746
747 nextitem = currentitem.nextitem()
747 nextitem = currentitem.nextitem()
748 # if there's no next item, keep the selection as-is
748 # if there's no next item, keep the selection as-is
749 if nextitem is None:
749 if nextitem is None:
750 nextitem = currentitem
750 nextitem = currentitem
751
751
752 self.currentselecteditem = nextitem
752 self.currentselecteditem = nextitem
753
753
754 def downarrowshiftevent(self):
754 def downarrowshiftevent(self):
755 """
755 """
756 select (if possible) the next item on the same level as the currently
756 select (if possible) the next item on the same level as the currently
757 selected item. otherwise, select (if possible) the next item on the
757 selected item. otherwise, select (if possible) the next item on the
758 same level as the parent item of the currently selected item.
758 same level as the parent item of the currently selected item.
759 """
759 """
760 currentitem = self.currentselecteditem
760 currentitem = self.currentselecteditem
761 nextitem = currentitem.nextsibling()
761 nextitem = currentitem.nextsibling()
762 # if there's no next sibling, try choosing the parent's nextsibling
762 # if there's no next sibling, try choosing the parent's nextsibling
763 if nextitem is None:
763 if nextitem is None:
764 try:
764 try:
765 nextitem = currentitem.parentitem().nextsibling()
765 nextitem = currentitem.parentitem().nextsibling()
766 except AttributeError:
766 except AttributeError:
767 # parentitem returned None, so nextsibling() can't be called
767 # parentitem returned None, so nextsibling() can't be called
768 nextitem = None
768 nextitem = None
769 if nextitem is None:
769 if nextitem is None:
770 # if parent has no next sibling, then no change...
770 # if parent has no next sibling, then no change...
771 nextitem = currentitem
771 nextitem = currentitem
772
772
773 self.currentselecteditem = nextitem
773 self.currentselecteditem = nextitem
774 self.recenterdisplayedarea()
774 self.recenterdisplayedarea()
775
775
776 def nextsametype(self, test=False):
776 def nextsametype(self, test=False):
777 currentitem = self.currentselecteditem
777 currentitem = self.currentselecteditem
778 sametype = lambda item: isinstance(item, type(currentitem))
778 sametype = lambda item: isinstance(item, type(currentitem))
779 nextitem = currentitem.nextitem()
779 nextitem = currentitem.nextitem()
780
780
781 while nextitem is not None and not sametype(nextitem):
781 while nextitem is not None and not sametype(nextitem):
782 nextitem = nextitem.nextitem()
782 nextitem = nextitem.nextitem()
783
783
784 if nextitem is None:
784 if nextitem is None:
785 nextitem = currentitem
785 nextitem = currentitem
786 else:
786 else:
787 parent = nextitem.parentitem()
787 parent = nextitem.parentitem()
788 if parent is not None and parent.folded:
788 if parent is not None and parent.folded:
789 self.togglefolded(parent)
789 self.togglefolded(parent)
790
790
791 self.currentselecteditem = nextitem
791 self.currentselecteditem = nextitem
792 if not test:
792 if not test:
793 self.recenterdisplayedarea()
793 self.recenterdisplayedarea()
794
794
795 def rightarrowevent(self):
795 def rightarrowevent(self):
796 """
796 """
797 select (if possible) the first of this item's child-items.
797 select (if possible) the first of this item's child-items.
798 """
798 """
799 currentitem = self.currentselecteditem
799 currentitem = self.currentselecteditem
800 nextitem = currentitem.firstchild()
800 nextitem = currentitem.firstchild()
801
801
802 # turn off folding if we want to show a child-item
802 # turn off folding if we want to show a child-item
803 if currentitem.folded:
803 if currentitem.folded:
804 self.togglefolded(currentitem)
804 self.togglefolded(currentitem)
805
805
806 if nextitem is None:
806 if nextitem is None:
807 # if no next item on parent-level, then no change...
807 # if no next item on parent-level, then no change...
808 nextitem = currentitem
808 nextitem = currentitem
809
809
810 self.currentselecteditem = nextitem
810 self.currentselecteditem = nextitem
811
811
812 def leftarrowevent(self):
812 def leftarrowevent(self):
813 """
813 """
814 if the current item can be folded (i.e. it is an unfolded header or
814 if the current item can be folded (i.e. it is an unfolded header or
815 hunk), then fold it. otherwise try select (if possible) the parent
815 hunk), then fold it. otherwise try select (if possible) the parent
816 of this item.
816 of this item.
817 """
817 """
818 currentitem = self.currentselecteditem
818 currentitem = self.currentselecteditem
819
819
820 # try to fold the item
820 # try to fold the item
821 if not isinstance(currentitem, uihunkline):
821 if not isinstance(currentitem, uihunkline):
822 if not currentitem.folded:
822 if not currentitem.folded:
823 self.togglefolded(item=currentitem)
823 self.togglefolded(item=currentitem)
824 return
824 return
825
825
826 # if it can't be folded, try to select the parent item
826 # if it can't be folded, try to select the parent item
827 nextitem = currentitem.parentitem()
827 nextitem = currentitem.parentitem()
828
828
829 if nextitem is None:
829 if nextitem is None:
830 # if no item on parent-level, then no change...
830 # if no item on parent-level, then no change...
831 nextitem = currentitem
831 nextitem = currentitem
832 if not nextitem.folded:
832 if not nextitem.folded:
833 self.togglefolded(item=nextitem)
833 self.togglefolded(item=nextitem)
834
834
835 self.currentselecteditem = nextitem
835 self.currentselecteditem = nextitem
836
836
837 def leftarrowshiftevent(self):
837 def leftarrowshiftevent(self):
838 """
838 """
839 select the header of the current item (or fold current item if the
839 select the header of the current item (or fold current item if the
840 current item is already a header).
840 current item is already a header).
841 """
841 """
842 currentitem = self.currentselecteditem
842 currentitem = self.currentselecteditem
843
843
844 if isinstance(currentitem, uiheader):
844 if isinstance(currentitem, uiheader):
845 if not currentitem.folded:
845 if not currentitem.folded:
846 self.togglefolded(item=currentitem)
846 self.togglefolded(item=currentitem)
847 return
847 return
848
848
849 # select the parent item recursively until we're at a header
849 # select the parent item recursively until we're at a header
850 while True:
850 while True:
851 nextitem = currentitem.parentitem()
851 nextitem = currentitem.parentitem()
852 if nextitem is None:
852 if nextitem is None:
853 break
853 break
854 else:
854 else:
855 currentitem = nextitem
855 currentitem = nextitem
856
856
857 self.currentselecteditem = currentitem
857 self.currentselecteditem = currentitem
858
858
859 def updatescroll(self):
859 def updatescroll(self):
860 """scroll the screen to fully show the currently-selected"""
860 """scroll the screen to fully show the currently-selected"""
861 selstart = self.selecteditemstartline
861 selstart = self.selecteditemstartline
862 selend = self.selecteditemendline
862 selend = self.selecteditemendline
863
863
864 padstart = self.firstlineofpadtoprint
864 padstart = self.firstlineofpadtoprint
865 padend = padstart + self.yscreensize - self.numstatuslines - 1
865 padend = padstart + self.yscreensize - self.numstatuslines - 1
866 # 'buffered' pad start/end values which scroll with a certain
866 # 'buffered' pad start/end values which scroll with a certain
867 # top/bottom context margin
867 # top/bottom context margin
868 padstartbuffered = padstart + 3
868 padstartbuffered = padstart + 3
869 padendbuffered = padend - 3
869 padendbuffered = padend - 3
870
870
871 if selend > padendbuffered:
871 if selend > padendbuffered:
872 self.scrolllines(selend - padendbuffered)
872 self.scrolllines(selend - padendbuffered)
873 elif selstart < padstartbuffered:
873 elif selstart < padstartbuffered:
874 # negative values scroll in pgup direction
874 # negative values scroll in pgup direction
875 self.scrolllines(selstart - padstartbuffered)
875 self.scrolllines(selstart - padstartbuffered)
876
876
877 def scrolllines(self, numlines):
877 def scrolllines(self, numlines):
878 """scroll the screen up (down) by numlines when numlines >0 (<0)."""
878 """scroll the screen up (down) by numlines when numlines >0 (<0)."""
879 self.firstlineofpadtoprint += numlines
879 self.firstlineofpadtoprint += numlines
880 if self.firstlineofpadtoprint < 0:
880 if self.firstlineofpadtoprint < 0:
881 self.firstlineofpadtoprint = 0
881 self.firstlineofpadtoprint = 0
882 if self.firstlineofpadtoprint > self.numpadlines - 1:
882 if self.firstlineofpadtoprint > self.numpadlines - 1:
883 self.firstlineofpadtoprint = self.numpadlines - 1
883 self.firstlineofpadtoprint = self.numpadlines - 1
884
884
885 def toggleapply(self, item=None):
885 def toggleapply(self, item=None):
886 """
886 """
887 toggle the applied flag of the specified item. if no item is specified,
887 toggle the applied flag of the specified item. if no item is specified,
888 toggle the flag of the currently selected item.
888 toggle the flag of the currently selected item.
889 """
889 """
890 if item is None:
890 if item is None:
891 item = self.currentselecteditem
891 item = self.currentselecteditem
892 # Only set this when NOT using 'toggleall'
892 # Only set this when NOT using 'toggleall'
893 self.lastapplieditem = item
893 self.lastapplieditem = item
894
894
895 item.applied = not item.applied
895 item.applied = not item.applied
896
896
897 if isinstance(item, uiheader):
897 if isinstance(item, uiheader):
898 item.partial = False
898 item.partial = False
899 if item.applied:
899 if item.applied:
900 # apply all its hunks
900 # apply all its hunks
901 for hnk in item.hunks:
901 for hnk in item.hunks:
902 hnk.applied = True
902 hnk.applied = True
903 # apply all their hunklines
903 # apply all their hunklines
904 for hunkline in hnk.changedlines:
904 for hunkline in hnk.changedlines:
905 hunkline.applied = True
905 hunkline.applied = True
906 else:
906 else:
907 # un-apply all its hunks
907 # un-apply all its hunks
908 for hnk in item.hunks:
908 for hnk in item.hunks:
909 hnk.applied = False
909 hnk.applied = False
910 hnk.partial = False
910 hnk.partial = False
911 # un-apply all their hunklines
911 # un-apply all their hunklines
912 for hunkline in hnk.changedlines:
912 for hunkline in hnk.changedlines:
913 hunkline.applied = False
913 hunkline.applied = False
914 elif isinstance(item, uihunk):
914 elif isinstance(item, uihunk):
915 item.partial = False
915 item.partial = False
916 # apply all it's hunklines
916 # apply all it's hunklines
917 for hunkline in item.changedlines:
917 for hunkline in item.changedlines:
918 hunkline.applied = item.applied
918 hunkline.applied = item.applied
919
919
920 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
920 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
921 allsiblingsapplied = not (False in siblingappliedstatus)
921 allsiblingsapplied = not (False in siblingappliedstatus)
922 nosiblingsapplied = not (True in siblingappliedstatus)
922 nosiblingsapplied = not (True in siblingappliedstatus)
923
923
924 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
924 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
925 somesiblingspartial = True in siblingspartialstatus
925 somesiblingspartial = True in siblingspartialstatus
926
926
927 # cases where applied or partial should be removed from header
927 # cases where applied or partial should be removed from header
928
928
929 # if no 'sibling' hunks are applied (including this hunk)
929 # if no 'sibling' hunks are applied (including this hunk)
930 if nosiblingsapplied:
930 if nosiblingsapplied:
931 if not item.header.special():
931 if not item.header.special():
932 item.header.applied = False
932 item.header.applied = False
933 item.header.partial = False
933 item.header.partial = False
934 else: # some/all parent siblings are applied
934 else: # some/all parent siblings are applied
935 item.header.applied = True
935 item.header.applied = True
936 item.header.partial = (
936 item.header.partial = (
937 somesiblingspartial or not allsiblingsapplied
937 somesiblingspartial or not allsiblingsapplied
938 )
938 )
939
939
940 elif isinstance(item, uihunkline):
940 elif isinstance(item, uihunkline):
941 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
941 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
942 allsiblingsapplied = not (False in siblingappliedstatus)
942 allsiblingsapplied = not (False in siblingappliedstatus)
943 nosiblingsapplied = not (True in siblingappliedstatus)
943 nosiblingsapplied = not (True in siblingappliedstatus)
944
944
945 # if no 'sibling' lines are applied
945 # if no 'sibling' lines are applied
946 if nosiblingsapplied:
946 if nosiblingsapplied:
947 item.hunk.applied = False
947 item.hunk.applied = False
948 item.hunk.partial = False
948 item.hunk.partial = False
949 elif allsiblingsapplied:
949 elif allsiblingsapplied:
950 item.hunk.applied = True
950 item.hunk.applied = True
951 item.hunk.partial = False
951 item.hunk.partial = False
952 else: # some siblings applied
952 else: # some siblings applied
953 item.hunk.applied = True
953 item.hunk.applied = True
954 item.hunk.partial = True
954 item.hunk.partial = True
955
955
956 parentsiblingsapplied = [
956 parentsiblingsapplied = [
957 hnk.applied for hnk in item.hunk.header.hunks
957 hnk.applied for hnk in item.hunk.header.hunks
958 ]
958 ]
959 noparentsiblingsapplied = not (True in parentsiblingsapplied)
959 noparentsiblingsapplied = not (True in parentsiblingsapplied)
960 allparentsiblingsapplied = not (False in parentsiblingsapplied)
960 allparentsiblingsapplied = not (False in parentsiblingsapplied)
961
961
962 parentsiblingspartial = [
962 parentsiblingspartial = [
963 hnk.partial for hnk in item.hunk.header.hunks
963 hnk.partial for hnk in item.hunk.header.hunks
964 ]
964 ]
965 someparentsiblingspartial = True in parentsiblingspartial
965 someparentsiblingspartial = True in parentsiblingspartial
966
966
967 # if all parent hunks are not applied, un-apply header
967 # if all parent hunks are not applied, un-apply header
968 if noparentsiblingsapplied:
968 if noparentsiblingsapplied:
969 if not item.hunk.header.special():
969 if not item.hunk.header.special():
970 item.hunk.header.applied = False
970 item.hunk.header.applied = False
971 item.hunk.header.partial = False
971 item.hunk.header.partial = False
972 # set the applied and partial status of the header if needed
972 # set the applied and partial status of the header if needed
973 else: # some/all parent siblings are applied
973 else: # some/all parent siblings are applied
974 item.hunk.header.applied = True
974 item.hunk.header.applied = True
975 item.hunk.header.partial = (
975 item.hunk.header.partial = (
976 someparentsiblingspartial or not allparentsiblingsapplied
976 someparentsiblingspartial or not allparentsiblingsapplied
977 )
977 )
978
978
979 def toggleall(self):
979 def toggleall(self):
980 """toggle the applied flag of all items."""
980 """toggle the applied flag of all items."""
981 if self.waslasttoggleallapplied: # then unapply them this time
981 if self.waslasttoggleallapplied: # then unapply them this time
982 for item in self.headerlist:
982 for item in self.headerlist:
983 if item.applied:
983 if item.applied:
984 self.toggleapply(item)
984 self.toggleapply(item)
985 else:
985 else:
986 for item in self.headerlist:
986 for item in self.headerlist:
987 if not item.applied:
987 if not item.applied:
988 self.toggleapply(item)
988 self.toggleapply(item)
989 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
989 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
990
990
991 def flipselections(self):
991 def flipselections(self):
992 """
992 """
993 Flip all selections. Every selected line is unselected and vice
993 Flip all selections. Every selected line is unselected and vice
994 versa.
994 versa.
995 """
995 """
996 for header in self.headerlist:
996 for header in self.headerlist:
997 for hunk in header.allchildren():
997 for hunk in header.allchildren():
998 for line in hunk.allchildren():
998 for line in hunk.allchildren():
999 self.toggleapply(line)
999 self.toggleapply(line)
1000
1000
1001 def toggleallbetween(self):
1001 def toggleallbetween(self):
1002 """toggle applied on or off for all items in range [lastapplied,
1002 """toggle applied on or off for all items in range [lastapplied,
1003 current]. """
1003 current]. """
1004 if (
1004 if (
1005 not self.lastapplieditem
1005 not self.lastapplieditem
1006 or self.currentselecteditem == self.lastapplieditem
1006 or self.currentselecteditem == self.lastapplieditem
1007 ):
1007 ):
1008 # Treat this like a normal 'x'/' '
1008 # Treat this like a normal 'x'/' '
1009 self.toggleapply()
1009 self.toggleapply()
1010 return
1010 return
1011
1011
1012 startitem = self.lastapplieditem
1012 startitem = self.lastapplieditem
1013 enditem = self.currentselecteditem
1013 enditem = self.currentselecteditem
1014 # Verify that enditem is "after" startitem, otherwise swap them.
1014 # Verify that enditem is "after" startitem, otherwise swap them.
1015 for direction in [b'forward', b'reverse']:
1015 for direction in [b'forward', b'reverse']:
1016 nextitem = startitem.nextitem()
1016 nextitem = startitem.nextitem()
1017 while nextitem and nextitem != enditem:
1017 while nextitem and nextitem != enditem:
1018 nextitem = nextitem.nextitem()
1018 nextitem = nextitem.nextitem()
1019 if nextitem:
1019 if nextitem:
1020 break
1020 break
1021 # Looks like we went the wrong direction :)
1021 # Looks like we went the wrong direction :)
1022 startitem, enditem = enditem, startitem
1022 startitem, enditem = enditem, startitem
1023
1023
1024 if not nextitem:
1024 if not nextitem:
1025 # We didn't find a path going either forward or backward? Don't know
1025 # We didn't find a path going either forward or backward? Don't know
1026 # how this can happen, let's not crash though.
1026 # how this can happen, let's not crash though.
1027 return
1027 return
1028
1028
1029 nextitem = startitem
1029 nextitem = startitem
1030 # Switch all items to be the opposite state of the currently selected
1030 # Switch all items to be the opposite state of the currently selected
1031 # item. Specifically:
1031 # item. Specifically:
1032 # [ ] startitem
1032 # [ ] startitem
1033 # [x] middleitem
1033 # [x] middleitem
1034 # [ ] enditem <-- currently selected
1034 # [ ] enditem <-- currently selected
1035 # This will turn all three on, since the currently selected item is off.
1035 # This will turn all three on, since the currently selected item is off.
1036 # This does *not* invert each item (i.e. middleitem stays marked/on)
1036 # This does *not* invert each item (i.e. middleitem stays marked/on)
1037 desiredstate = not self.currentselecteditem.applied
1037 desiredstate = not self.currentselecteditem.applied
1038 while nextitem != enditem.nextitem():
1038 while nextitem != enditem.nextitem():
1039 if nextitem.applied != desiredstate:
1039 if nextitem.applied != desiredstate:
1040 self.toggleapply(item=nextitem)
1040 self.toggleapply(item=nextitem)
1041 nextitem = nextitem.nextitem()
1041 nextitem = nextitem.nextitem()
1042
1042
1043 def togglefolded(self, item=None, foldparent=False):
1043 def togglefolded(self, item=None, foldparent=False):
1044 """toggle folded flag of specified item (defaults to currently
1044 """toggle folded flag of specified item (defaults to currently
1045 selected)"""
1045 selected)"""
1046 if item is None:
1046 if item is None:
1047 item = self.currentselecteditem
1047 item = self.currentselecteditem
1048 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
1048 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
1049 if not isinstance(item, uiheader):
1049 if not isinstance(item, uiheader):
1050 # we need to select the parent item in this case
1050 # we need to select the parent item in this case
1051 self.currentselecteditem = item = item.parentitem()
1051 self.currentselecteditem = item = item.parentitem()
1052 elif item.neverunfolded:
1052 elif item.neverunfolded:
1053 item.neverunfolded = False
1053 item.neverunfolded = False
1054
1054
1055 # also fold any foldable children of the parent/current item
1055 # also fold any foldable children of the parent/current item
1056 if isinstance(item, uiheader): # the original or 'new' item
1056 if isinstance(item, uiheader): # the original or 'new' item
1057 for child in item.allchildren():
1057 for child in item.allchildren():
1058 child.folded = not item.folded
1058 child.folded = not item.folded
1059
1059
1060 if isinstance(item, (uiheader, uihunk)):
1060 if isinstance(item, (uiheader, uihunk)):
1061 item.folded = not item.folded
1061 item.folded = not item.folded
1062
1062
1063 def alignstring(self, instr, window):
1063 def alignstring(self, instr, window):
1064 """
1064 """
1065 add whitespace to the end of a string in order to make it fill
1065 add whitespace to the end of a string in order to make it fill
1066 the screen in the x direction. the current cursor position is
1066 the screen in the x direction. the current cursor position is
1067 taken into account when making this calculation. the string can span
1067 taken into account when making this calculation. the string can span
1068 multiple lines.
1068 multiple lines.
1069 """
1069 """
1070 y, xstart = window.getyx()
1070 y, xstart = window.getyx()
1071 width = self.xscreensize
1071 width = self.xscreensize
1072 # turn tabs into spaces
1072 # turn tabs into spaces
1073 instr = instr.expandtabs(4)
1073 instr = instr.expandtabs(4)
1074 strwidth = encoding.colwidth(instr)
1074 strwidth = encoding.colwidth(instr)
1075 numspaces = width - ((strwidth + xstart) % width)
1075 numspaces = width - ((strwidth + xstart) % width)
1076 return instr + b" " * numspaces
1076 return instr + b" " * numspaces
1077
1077
1078 def printstring(
1078 def printstring(
1079 self,
1079 self,
1080 window,
1080 window,
1081 text,
1081 text,
1082 fgcolor=None,
1082 fgcolor=None,
1083 bgcolor=None,
1083 bgcolor=None,
1084 pair=None,
1084 pair=None,
1085 pairname=None,
1085 pairname=None,
1086 attrlist=None,
1086 attrlist=None,
1087 towin=True,
1087 towin=True,
1088 align=True,
1088 align=True,
1089 showwhtspc=False,
1089 showwhtspc=False,
1090 ):
1090 ):
1091 """
1091 """
1092 print the string, text, with the specified colors and attributes, to
1092 print the string, text, with the specified colors and attributes, to
1093 the specified curses window object.
1093 the specified curses window object.
1094
1094
1095 the foreground and background colors are of the form
1095 the foreground and background colors are of the form
1096 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
1096 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
1097 magenta, red, white, yellow]. if pairname is provided, a color
1097 magenta, red, white, yellow]. if pairname is provided, a color
1098 pair will be looked up in the self.colorpairnames dictionary.
1098 pair will be looked up in the self.colorpairnames dictionary.
1099
1099
1100 attrlist is a list containing text attributes in the form of
1100 attrlist is a list containing text attributes in the form of
1101 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
1101 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
1102 underline].
1102 underline].
1103
1103
1104 if align == True, whitespace is added to the printed string such that
1104 if align == True, whitespace is added to the printed string such that
1105 the string stretches to the right border of the window.
1105 the string stretches to the right border of the window.
1106
1106
1107 if showwhtspc == True, trailing whitespace of a string is highlighted.
1107 if showwhtspc == True, trailing whitespace of a string is highlighted.
1108 """
1108 """
1109 # preprocess the text, converting tabs to spaces
1109 # preprocess the text, converting tabs to spaces
1110 text = text.expandtabs(4)
1110 text = text.expandtabs(4)
1111 # strip \n, and convert control characters to ^[char] representation
1111 # strip \n, and convert control characters to ^[char] representation
1112 text = re.sub(
1112 text = re.sub(
1113 br'[\x00-\x08\x0a-\x1f]',
1113 br'[\x00-\x08\x0a-\x1f]',
1114 lambda m: b'^' + pycompat.sysbytes(chr(ord(m.group()) + 64)),
1114 lambda m: b'^' + pycompat.sysbytes(chr(ord(m.group()) + 64)),
1115 text.strip(b'\n'),
1115 text.strip(b'\n'),
1116 )
1116 )
1117
1117
1118 if pair is not None:
1118 if pair is not None:
1119 colorpair = pair
1119 colorpair = pair
1120 elif pairname is not None:
1120 elif pairname is not None:
1121 colorpair = self.colorpairnames[pairname]
1121 colorpair = self.colorpairnames[pairname]
1122 else:
1122 else:
1123 if fgcolor is None:
1123 if fgcolor is None:
1124 fgcolor = -1
1124 fgcolor = -1
1125 if bgcolor is None:
1125 if bgcolor is None:
1126 bgcolor = -1
1126 bgcolor = -1
1127 if (fgcolor, bgcolor) in self.colorpairs:
1127 if (fgcolor, bgcolor) in self.colorpairs:
1128 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1128 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1129 else:
1129 else:
1130 colorpair = self.getcolorpair(fgcolor, bgcolor)
1130 colorpair = self.getcolorpair(fgcolor, bgcolor)
1131 # add attributes if possible
1131 # add attributes if possible
1132 if attrlist is None:
1132 if attrlist is None:
1133 attrlist = []
1133 attrlist = []
1134 if colorpair < 256:
1134 if colorpair < 256:
1135 # then it is safe to apply all attributes
1135 # then it is safe to apply all attributes
1136 for textattr in attrlist:
1136 for textattr in attrlist:
1137 colorpair |= textattr
1137 colorpair |= textattr
1138 else:
1138 else:
1139 # just apply a select few (safe?) attributes
1139 # just apply a select few (safe?) attributes
1140 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
1140 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
1141 if textattr in attrlist:
1141 if textattr in attrlist:
1142 colorpair |= textattr
1142 colorpair |= textattr
1143
1143
1144 y, xstart = self.chunkpad.getyx()
1144 y, xstart = self.chunkpad.getyx()
1145 t = b"" # variable for counting lines printed
1145 t = b"" # variable for counting lines printed
1146 # if requested, show trailing whitespace
1146 # if requested, show trailing whitespace
1147 if showwhtspc:
1147 if showwhtspc:
1148 origlen = len(text)
1148 origlen = len(text)
1149 text = text.rstrip(b' \n') # tabs have already been expanded
1149 text = text.rstrip(b' \n') # tabs have already been expanded
1150 strippedlen = len(text)
1150 strippedlen = len(text)
1151 numtrailingspaces = origlen - strippedlen
1151 numtrailingspaces = origlen - strippedlen
1152
1152
1153 if towin:
1153 if towin:
1154 window.addstr(text, colorpair)
1154 window.addstr(text, colorpair)
1155 t += text
1155 t += text
1156
1156
1157 if showwhtspc:
1157 if showwhtspc:
1158 wscolorpair = colorpair | curses.A_REVERSE
1158 wscolorpair = colorpair | curses.A_REVERSE
1159 if towin:
1159 if towin:
1160 for i in range(numtrailingspaces):
1160 for i in range(numtrailingspaces):
1161 window.addch(curses.ACS_CKBOARD, wscolorpair)
1161 window.addch(curses.ACS_CKBOARD, wscolorpair)
1162 t += b" " * numtrailingspaces
1162 t += b" " * numtrailingspaces
1163
1163
1164 if align:
1164 if align:
1165 if towin:
1165 if towin:
1166 extrawhitespace = self.alignstring(b"", window)
1166 extrawhitespace = self.alignstring(b"", window)
1167 window.addstr(extrawhitespace, colorpair)
1167 window.addstr(extrawhitespace, colorpair)
1168 else:
1168 else:
1169 # need to use t, since the x position hasn't incremented
1169 # need to use t, since the x position hasn't incremented
1170 extrawhitespace = self.alignstring(t, window)
1170 extrawhitespace = self.alignstring(t, window)
1171 t += extrawhitespace
1171 t += extrawhitespace
1172
1172
1173 # is reset to 0 at the beginning of printitem()
1173 # is reset to 0 at the beginning of printitem()
1174
1174
1175 linesprinted = (xstart + len(t)) // self.xscreensize
1175 linesprinted = (xstart + len(t)) // self.xscreensize
1176 self.linesprintedtopadsofar += linesprinted
1176 self.linesprintedtopadsofar += linesprinted
1177 return t
1177 return t
1178
1178
1179 def _getstatuslinesegments(self):
1179 def _getstatuslinesegments(self):
1180 """-> [str]. return segments"""
1180 """-> [str]. return segments"""
1181 selected = self.currentselecteditem.applied
1181 selected = self.currentselecteditem.applied
1182 spaceselect = _(b'space/enter: select')
1182 spaceselect = _(b'space/enter: select')
1183 spacedeselect = _(b'space/enter: deselect')
1183 spacedeselect = _(b'space/enter: deselect')
1184 # Format the selected label into a place as long as the longer of the
1184 # Format the selected label into a place as long as the longer of the
1185 # two possible labels. This may vary by language.
1185 # two possible labels. This may vary by language.
1186 spacelen = max(len(spaceselect), len(spacedeselect))
1186 spacelen = max(len(spaceselect), len(spacedeselect))
1187 selectedlabel = b'%-*s' % (
1187 selectedlabel = b'%-*s' % (
1188 spacelen,
1188 spacelen,
1189 spacedeselect if selected else spaceselect,
1189 spacedeselect if selected else spaceselect,
1190 )
1190 )
1191 segments = [
1191 segments = [
1192 _headermessages[self.operation],
1192 _headermessages[self.operation],
1193 b'-',
1193 b'-',
1194 _(b'[x]=selected **=collapsed'),
1194 _(b'[x]=selected **=collapsed'),
1195 _(b'c: confirm'),
1195 _(b'c: confirm'),
1196 _(b'q: abort'),
1196 _(b'q: abort'),
1197 _(b'arrow keys: move/expand/collapse'),
1197 _(b'arrow keys: move/expand/collapse'),
1198 selectedlabel,
1198 selectedlabel,
1199 _(b'?: help'),
1199 _(b'?: help'),
1200 ]
1200 ]
1201 return segments
1201 return segments
1202
1202
1203 def _getstatuslines(self):
1203 def _getstatuslines(self):
1204 """() -> [str]. return short help used in the top status window"""
1204 """() -> [str]. return short help used in the top status window"""
1205 if self.errorstr is not None:
1205 if self.errorstr is not None:
1206 lines = [self.errorstr, _(b'Press any key to continue')]
1206 lines = [self.errorstr, _(b'Press any key to continue')]
1207 else:
1207 else:
1208 # wrap segments to lines
1208 # wrap segments to lines
1209 segments = self._getstatuslinesegments()
1209 segments = self._getstatuslinesegments()
1210 width = self.xscreensize
1210 width = self.xscreensize
1211 lines = []
1211 lines = []
1212 lastwidth = width
1212 lastwidth = width
1213 for s in segments:
1213 for s in segments:
1214 w = encoding.colwidth(s)
1214 w = encoding.colwidth(s)
1215 sep = b' ' * (1 + (s and s[0] not in b'-['))
1215 sep = b' ' * (1 + (s and s[0] not in b'-['))
1216 if lastwidth + w + len(sep) >= width:
1216 if lastwidth + w + len(sep) >= width:
1217 lines.append(s)
1217 lines.append(s)
1218 lastwidth = w
1218 lastwidth = w
1219 else:
1219 else:
1220 lines[-1] += sep + s
1220 lines[-1] += sep + s
1221 lastwidth += w + len(sep)
1221 lastwidth += w + len(sep)
1222 if len(lines) != self.numstatuslines:
1222 if len(lines) != self.numstatuslines:
1223 self.numstatuslines = len(lines)
1223 self.numstatuslines = len(lines)
1224 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1224 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1225 return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines]
1225 return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines]
1226
1226
1227 def updatescreen(self):
1227 def updatescreen(self):
1228 self.statuswin.erase()
1228 self.statuswin.erase()
1229 self.chunkpad.erase()
1229 self.chunkpad.erase()
1230
1230
1231 printstring = self.printstring
1231 printstring = self.printstring
1232
1232
1233 # print out the status lines at the top
1233 # print out the status lines at the top
1234 try:
1234 try:
1235 for line in self._getstatuslines():
1235 for line in self._getstatuslines():
1236 printstring(self.statuswin, line, pairname=b"legend")
1236 printstring(self.statuswin, line, pairname=b"legend")
1237 self.statuswin.refresh()
1237 self.statuswin.refresh()
1238 except curses.error:
1238 except curses.error:
1239 pass
1239 pass
1240 if self.errorstr is not None:
1240 if self.errorstr is not None:
1241 return
1241 return
1242
1242
1243 # print out the patch in the remaining part of the window
1243 # print out the patch in the remaining part of the window
1244 try:
1244 try:
1245 self.printitem()
1245 self.printitem()
1246 self.updatescroll()
1246 self.updatescroll()
1247 self.chunkpad.refresh(
1247 self.chunkpad.refresh(
1248 self.firstlineofpadtoprint,
1248 self.firstlineofpadtoprint,
1249 0,
1249 0,
1250 self.numstatuslines,
1250 self.numstatuslines,
1251 0,
1251 0,
1252 self.yscreensize - self.numstatuslines,
1252 self.yscreensize - self.numstatuslines,
1253 self.xscreensize,
1253 self.xscreensize - 1,
1254 )
1254 )
1255 except curses.error:
1255 except curses.error:
1256 pass
1256 pass
1257
1257
1258 def getstatusprefixstring(self, item):
1258 def getstatusprefixstring(self, item):
1259 """
1259 """
1260 create a string to prefix a line with which indicates whether 'item'
1260 create a string to prefix a line with which indicates whether 'item'
1261 is applied and/or folded.
1261 is applied and/or folded.
1262 """
1262 """
1263
1263
1264 # create checkbox string
1264 # create checkbox string
1265 if item.applied:
1265 if item.applied:
1266 if not isinstance(item, uihunkline) and item.partial:
1266 if not isinstance(item, uihunkline) and item.partial:
1267 checkbox = b"[~]"
1267 checkbox = b"[~]"
1268 else:
1268 else:
1269 checkbox = b"[x]"
1269 checkbox = b"[x]"
1270 else:
1270 else:
1271 checkbox = b"[ ]"
1271 checkbox = b"[ ]"
1272
1272
1273 try:
1273 try:
1274 if item.folded:
1274 if item.folded:
1275 checkbox += b"**"
1275 checkbox += b"**"
1276 if isinstance(item, uiheader):
1276 if isinstance(item, uiheader):
1277 # one of "m", "a", or "d" (modified, added, deleted)
1277 # one of "m", "a", or "d" (modified, added, deleted)
1278 filestatus = item.changetype
1278 filestatus = item.changetype
1279
1279
1280 checkbox += filestatus + b" "
1280 checkbox += filestatus + b" "
1281 else:
1281 else:
1282 checkbox += b" "
1282 checkbox += b" "
1283 if isinstance(item, uiheader):
1283 if isinstance(item, uiheader):
1284 # add two more spaces for headers
1284 # add two more spaces for headers
1285 checkbox += b" "
1285 checkbox += b" "
1286 except AttributeError: # not foldable
1286 except AttributeError: # not foldable
1287 checkbox += b" "
1287 checkbox += b" "
1288
1288
1289 return checkbox
1289 return checkbox
1290
1290
1291 def printheader(
1291 def printheader(
1292 self, header, selected=False, towin=True, ignorefolding=False
1292 self, header, selected=False, towin=True, ignorefolding=False
1293 ):
1293 ):
1294 """
1294 """
1295 print the header to the pad. if countlines is True, don't print
1295 print the header to the pad. if countlines is True, don't print
1296 anything, but just count the number of lines which would be printed.
1296 anything, but just count the number of lines which would be printed.
1297 """
1297 """
1298
1298
1299 outstr = b""
1299 outstr = b""
1300 text = header.prettystr()
1300 text = header.prettystr()
1301 chunkindex = self.chunklist.index(header)
1301 chunkindex = self.chunklist.index(header)
1302
1302
1303 if chunkindex != 0 and not header.folded:
1303 if chunkindex != 0 and not header.folded:
1304 # add separating line before headers
1304 # add separating line before headers
1305 outstr += self.printstring(
1305 outstr += self.printstring(
1306 self.chunkpad, b'_' * self.xscreensize, towin=towin, align=False
1306 self.chunkpad, b'_' * self.xscreensize, towin=towin, align=False
1307 )
1307 )
1308 # select color-pair based on if the header is selected
1308 # select color-pair based on if the header is selected
1309 colorpair = self.getcolorpair(
1309 colorpair = self.getcolorpair(
1310 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1310 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1311 )
1311 )
1312
1312
1313 # print out each line of the chunk, expanding it to screen width
1313 # print out each line of the chunk, expanding it to screen width
1314
1314
1315 # number of characters to indent lines on this level by
1315 # number of characters to indent lines on this level by
1316 indentnumchars = 0
1316 indentnumchars = 0
1317 checkbox = self.getstatusprefixstring(header)
1317 checkbox = self.getstatusprefixstring(header)
1318 if not header.folded or ignorefolding:
1318 if not header.folded or ignorefolding:
1319 textlist = text.split(b"\n")
1319 textlist = text.split(b"\n")
1320 linestr = checkbox + textlist[0]
1320 linestr = checkbox + textlist[0]
1321 else:
1321 else:
1322 linestr = checkbox + header.filename()
1322 linestr = checkbox + header.filename()
1323 outstr += self.printstring(
1323 outstr += self.printstring(
1324 self.chunkpad, linestr, pair=colorpair, towin=towin
1324 self.chunkpad, linestr, pair=colorpair, towin=towin
1325 )
1325 )
1326 if not header.folded or ignorefolding:
1326 if not header.folded or ignorefolding:
1327 if len(textlist) > 1:
1327 if len(textlist) > 1:
1328 for line in textlist[1:]:
1328 for line in textlist[1:]:
1329 linestr = b" " * (indentnumchars + len(checkbox)) + line
1329 linestr = b" " * (indentnumchars + len(checkbox)) + line
1330 outstr += self.printstring(
1330 outstr += self.printstring(
1331 self.chunkpad, linestr, pair=colorpair, towin=towin
1331 self.chunkpad, linestr, pair=colorpair, towin=towin
1332 )
1332 )
1333
1333
1334 return outstr
1334 return outstr
1335
1335
1336 def printhunklinesbefore(
1336 def printhunklinesbefore(
1337 self, hunk, selected=False, towin=True, ignorefolding=False
1337 self, hunk, selected=False, towin=True, ignorefolding=False
1338 ):
1338 ):
1339 """includes start/end line indicator"""
1339 """includes start/end line indicator"""
1340 outstr = b""
1340 outstr = b""
1341 # where hunk is in list of siblings
1341 # where hunk is in list of siblings
1342 hunkindex = hunk.header.hunks.index(hunk)
1342 hunkindex = hunk.header.hunks.index(hunk)
1343
1343
1344 if hunkindex != 0:
1344 if hunkindex != 0:
1345 # add separating line before headers
1345 # add separating line before headers
1346 outstr += self.printstring(
1346 outstr += self.printstring(
1347 self.chunkpad, b' ' * self.xscreensize, towin=towin, align=False
1347 self.chunkpad, b' ' * self.xscreensize, towin=towin, align=False
1348 )
1348 )
1349
1349
1350 colorpair = self.getcolorpair(
1350 colorpair = self.getcolorpair(
1351 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1351 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1352 )
1352 )
1353
1353
1354 # print out from-to line with checkbox
1354 # print out from-to line with checkbox
1355 checkbox = self.getstatusprefixstring(hunk)
1355 checkbox = self.getstatusprefixstring(hunk)
1356
1356
1357 lineprefix = b" " * self.hunkindentnumchars + checkbox
1357 lineprefix = b" " * self.hunkindentnumchars + checkbox
1358 frtoline = b" " + hunk.getfromtoline().strip(b"\n")
1358 frtoline = b" " + hunk.getfromtoline().strip(b"\n")
1359
1359
1360 outstr += self.printstring(
1360 outstr += self.printstring(
1361 self.chunkpad, lineprefix, towin=towin, align=False
1361 self.chunkpad, lineprefix, towin=towin, align=False
1362 ) # add uncolored checkbox/indent
1362 ) # add uncolored checkbox/indent
1363 outstr += self.printstring(
1363 outstr += self.printstring(
1364 self.chunkpad, frtoline, pair=colorpair, towin=towin
1364 self.chunkpad, frtoline, pair=colorpair, towin=towin
1365 )
1365 )
1366
1366
1367 if hunk.folded and not ignorefolding:
1367 if hunk.folded and not ignorefolding:
1368 # skip remainder of output
1368 # skip remainder of output
1369 return outstr
1369 return outstr
1370
1370
1371 # print out lines of the chunk preceeding changed-lines
1371 # print out lines of the chunk preceeding changed-lines
1372 for line in hunk.before:
1372 for line in hunk.before:
1373 linestr = (
1373 linestr = (
1374 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1374 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1375 )
1375 )
1376 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1376 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1377
1377
1378 return outstr
1378 return outstr
1379
1379
1380 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1380 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1381 outstr = b""
1381 outstr = b""
1382 if hunk.folded and not ignorefolding:
1382 if hunk.folded and not ignorefolding:
1383 return outstr
1383 return outstr
1384
1384
1385 # a bit superfluous, but to avoid hard-coding indent amount
1385 # a bit superfluous, but to avoid hard-coding indent amount
1386 checkbox = self.getstatusprefixstring(hunk)
1386 checkbox = self.getstatusprefixstring(hunk)
1387 for line in hunk.after:
1387 for line in hunk.after:
1388 linestr = (
1388 linestr = (
1389 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1389 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1390 )
1390 )
1391 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1391 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1392
1392
1393 return outstr
1393 return outstr
1394
1394
1395 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1395 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1396 outstr = b""
1396 outstr = b""
1397 checkbox = self.getstatusprefixstring(hunkline)
1397 checkbox = self.getstatusprefixstring(hunkline)
1398
1398
1399 linestr = hunkline.prettystr().strip(b"\n")
1399 linestr = hunkline.prettystr().strip(b"\n")
1400
1400
1401 # select color-pair based on whether line is an addition/removal
1401 # select color-pair based on whether line is an addition/removal
1402 if selected:
1402 if selected:
1403 colorpair = self.getcolorpair(name=b"selected")
1403 colorpair = self.getcolorpair(name=b"selected")
1404 elif linestr.startswith(b"+"):
1404 elif linestr.startswith(b"+"):
1405 colorpair = self.getcolorpair(name=b"addition")
1405 colorpair = self.getcolorpair(name=b"addition")
1406 elif linestr.startswith(b"-"):
1406 elif linestr.startswith(b"-"):
1407 colorpair = self.getcolorpair(name=b"deletion")
1407 colorpair = self.getcolorpair(name=b"deletion")
1408 elif linestr.startswith(b"\\"):
1408 elif linestr.startswith(b"\\"):
1409 colorpair = self.getcolorpair(name=b"normal")
1409 colorpair = self.getcolorpair(name=b"normal")
1410
1410
1411 lineprefix = b" " * self.hunklineindentnumchars + checkbox
1411 lineprefix = b" " * self.hunklineindentnumchars + checkbox
1412 outstr += self.printstring(
1412 outstr += self.printstring(
1413 self.chunkpad, lineprefix, towin=towin, align=False
1413 self.chunkpad, lineprefix, towin=towin, align=False
1414 ) # add uncolored checkbox/indent
1414 ) # add uncolored checkbox/indent
1415 outstr += self.printstring(
1415 outstr += self.printstring(
1416 self.chunkpad, linestr, pair=colorpair, towin=towin, showwhtspc=True
1416 self.chunkpad, linestr, pair=colorpair, towin=towin, showwhtspc=True
1417 )
1417 )
1418 return outstr
1418 return outstr
1419
1419
1420 def printitem(
1420 def printitem(
1421 self, item=None, ignorefolding=False, recursechildren=True, towin=True
1421 self, item=None, ignorefolding=False, recursechildren=True, towin=True
1422 ):
1422 ):
1423 """
1423 """
1424 use __printitem() to print the the specified item.applied.
1424 use __printitem() to print the the specified item.applied.
1425 if item is not specified, then print the entire patch.
1425 if item is not specified, then print the entire patch.
1426 (hiding folded elements, etc. -- see __printitem() docstring)
1426 (hiding folded elements, etc. -- see __printitem() docstring)
1427 """
1427 """
1428
1428
1429 if item is None:
1429 if item is None:
1430 item = self.headerlist
1430 item = self.headerlist
1431 if recursechildren:
1431 if recursechildren:
1432 self.linesprintedtopadsofar = 0
1432 self.linesprintedtopadsofar = 0
1433
1433
1434 outstr = []
1434 outstr = []
1435 self.__printitem(
1435 self.__printitem(
1436 item, ignorefolding, recursechildren, outstr, towin=towin
1436 item, ignorefolding, recursechildren, outstr, towin=towin
1437 )
1437 )
1438 return b''.join(outstr)
1438 return b''.join(outstr)
1439
1439
1440 def outofdisplayedarea(self):
1440 def outofdisplayedarea(self):
1441 y, _ = self.chunkpad.getyx() # cursor location
1441 y, _ = self.chunkpad.getyx() # cursor location
1442 # * 2 here works but an optimization would be the max number of
1442 # * 2 here works but an optimization would be the max number of
1443 # consecutive non selectable lines
1443 # consecutive non selectable lines
1444 # i.e the max number of context line for any hunk in the patch
1444 # i.e the max number of context line for any hunk in the patch
1445 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1445 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1446 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1446 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1447 return y < miny or y > maxy
1447 return y < miny or y > maxy
1448
1448
1449 def handleselection(self, item, recursechildren):
1449 def handleselection(self, item, recursechildren):
1450 selected = item is self.currentselecteditem
1450 selected = item is self.currentselecteditem
1451 if selected and recursechildren:
1451 if selected and recursechildren:
1452 # assumes line numbering starting from line 0
1452 # assumes line numbering starting from line 0
1453 self.selecteditemstartline = self.linesprintedtopadsofar
1453 self.selecteditemstartline = self.linesprintedtopadsofar
1454 selecteditemlines = self.getnumlinesdisplayed(
1454 selecteditemlines = self.getnumlinesdisplayed(
1455 item, recursechildren=False
1455 item, recursechildren=False
1456 )
1456 )
1457 self.selecteditemendline = (
1457 self.selecteditemendline = (
1458 self.selecteditemstartline + selecteditemlines - 1
1458 self.selecteditemstartline + selecteditemlines - 1
1459 )
1459 )
1460 return selected
1460 return selected
1461
1461
1462 def __printitem(
1462 def __printitem(
1463 self, item, ignorefolding, recursechildren, outstr, towin=True
1463 self, item, ignorefolding, recursechildren, outstr, towin=True
1464 ):
1464 ):
1465 """
1465 """
1466 recursive method for printing out patch/header/hunk/hunk-line data to
1466 recursive method for printing out patch/header/hunk/hunk-line data to
1467 screen. also returns a string with all of the content of the displayed
1467 screen. also returns a string with all of the content of the displayed
1468 patch (not including coloring, etc.).
1468 patch (not including coloring, etc.).
1469
1469
1470 if ignorefolding is True, then folded items are printed out.
1470 if ignorefolding is True, then folded items are printed out.
1471
1471
1472 if recursechildren is False, then only print the item without its
1472 if recursechildren is False, then only print the item without its
1473 child items.
1473 child items.
1474 """
1474 """
1475
1475
1476 if towin and self.outofdisplayedarea():
1476 if towin and self.outofdisplayedarea():
1477 return
1477 return
1478
1478
1479 selected = self.handleselection(item, recursechildren)
1479 selected = self.handleselection(item, recursechildren)
1480
1480
1481 # patch object is a list of headers
1481 # patch object is a list of headers
1482 if isinstance(item, patch):
1482 if isinstance(item, patch):
1483 if recursechildren:
1483 if recursechildren:
1484 for hdr in item:
1484 for hdr in item:
1485 self.__printitem(
1485 self.__printitem(
1486 hdr, ignorefolding, recursechildren, outstr, towin
1486 hdr, ignorefolding, recursechildren, outstr, towin
1487 )
1487 )
1488 # todo: eliminate all isinstance() calls
1488 # todo: eliminate all isinstance() calls
1489 if isinstance(item, uiheader):
1489 if isinstance(item, uiheader):
1490 outstr.append(
1490 outstr.append(
1491 self.printheader(
1491 self.printheader(
1492 item, selected, towin=towin, ignorefolding=ignorefolding
1492 item, selected, towin=towin, ignorefolding=ignorefolding
1493 )
1493 )
1494 )
1494 )
1495 if recursechildren:
1495 if recursechildren:
1496 for hnk in item.hunks:
1496 for hnk in item.hunks:
1497 self.__printitem(
1497 self.__printitem(
1498 hnk, ignorefolding, recursechildren, outstr, towin
1498 hnk, ignorefolding, recursechildren, outstr, towin
1499 )
1499 )
1500 elif isinstance(item, uihunk) and (
1500 elif isinstance(item, uihunk) and (
1501 (not item.header.folded) or ignorefolding
1501 (not item.header.folded) or ignorefolding
1502 ):
1502 ):
1503 # print the hunk data which comes before the changed-lines
1503 # print the hunk data which comes before the changed-lines
1504 outstr.append(
1504 outstr.append(
1505 self.printhunklinesbefore(
1505 self.printhunklinesbefore(
1506 item, selected, towin=towin, ignorefolding=ignorefolding
1506 item, selected, towin=towin, ignorefolding=ignorefolding
1507 )
1507 )
1508 )
1508 )
1509 if recursechildren:
1509 if recursechildren:
1510 for l in item.changedlines:
1510 for l in item.changedlines:
1511 self.__printitem(
1511 self.__printitem(
1512 l, ignorefolding, recursechildren, outstr, towin
1512 l, ignorefolding, recursechildren, outstr, towin
1513 )
1513 )
1514 outstr.append(
1514 outstr.append(
1515 self.printhunklinesafter(
1515 self.printhunklinesafter(
1516 item, towin=towin, ignorefolding=ignorefolding
1516 item, towin=towin, ignorefolding=ignorefolding
1517 )
1517 )
1518 )
1518 )
1519 elif isinstance(item, uihunkline) and (
1519 elif isinstance(item, uihunkline) and (
1520 (not item.hunk.folded) or ignorefolding
1520 (not item.hunk.folded) or ignorefolding
1521 ):
1521 ):
1522 outstr.append(
1522 outstr.append(
1523 self.printhunkchangedline(item, selected, towin=towin)
1523 self.printhunkchangedline(item, selected, towin=towin)
1524 )
1524 )
1525
1525
1526 return outstr
1526 return outstr
1527
1527
1528 def getnumlinesdisplayed(
1528 def getnumlinesdisplayed(
1529 self, item=None, ignorefolding=False, recursechildren=True
1529 self, item=None, ignorefolding=False, recursechildren=True
1530 ):
1530 ):
1531 """
1531 """
1532 return the number of lines which would be displayed if the item were
1532 return the number of lines which would be displayed if the item were
1533 to be printed to the display. the item will not be printed to the
1533 to be printed to the display. the item will not be printed to the
1534 display (pad).
1534 display (pad).
1535 if no item is given, assume the entire patch.
1535 if no item is given, assume the entire patch.
1536 if ignorefolding is True, folded items will be unfolded when counting
1536 if ignorefolding is True, folded items will be unfolded when counting
1537 the number of lines.
1537 the number of lines.
1538 """
1538 """
1539
1539
1540 # temporarily disable printing to windows by printstring
1540 # temporarily disable printing to windows by printstring
1541 patchdisplaystring = self.printitem(
1541 patchdisplaystring = self.printitem(
1542 item, ignorefolding, recursechildren, towin=False
1542 item, ignorefolding, recursechildren, towin=False
1543 )
1543 )
1544 numlines = len(patchdisplaystring) // self.xscreensize
1544 numlines = len(patchdisplaystring) // self.xscreensize
1545 return numlines
1545 return numlines
1546
1546
1547 def sigwinchhandler(self, n, frame):
1547 def sigwinchhandler(self, n, frame):
1548 """handle window resizing"""
1548 """handle window resizing"""
1549 try:
1549 try:
1550 curses.endwin()
1550 curses.endwin()
1551 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1551 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1552 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1552 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1553 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1553 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1554 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1554 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1555 except curses.error:
1555 except curses.error:
1556 pass
1556 pass
1557
1557
1558 def getcolorpair(
1558 def getcolorpair(
1559 self, fgcolor=None, bgcolor=None, name=None, attrlist=None
1559 self, fgcolor=None, bgcolor=None, name=None, attrlist=None
1560 ):
1560 ):
1561 """
1561 """
1562 get a curses color pair, adding it to self.colorpairs if it is not
1562 get a curses color pair, adding it to self.colorpairs if it is not
1563 already defined. an optional string, name, can be passed as a shortcut
1563 already defined. an optional string, name, can be passed as a shortcut
1564 for referring to the color-pair. by default, if no arguments are
1564 for referring to the color-pair. by default, if no arguments are
1565 specified, the white foreground / black background color-pair is
1565 specified, the white foreground / black background color-pair is
1566 returned.
1566 returned.
1567
1567
1568 it is expected that this function will be used exclusively for
1568 it is expected that this function will be used exclusively for
1569 initializing color pairs, and not curses.init_pair().
1569 initializing color pairs, and not curses.init_pair().
1570
1570
1571 attrlist is used to 'flavor' the returned color-pair. this information
1571 attrlist is used to 'flavor' the returned color-pair. this information
1572 is not stored in self.colorpairs. it contains attribute values like
1572 is not stored in self.colorpairs. it contains attribute values like
1573 curses.A_BOLD.
1573 curses.A_BOLD.
1574 """
1574 """
1575
1575
1576 if (name is not None) and name in self.colorpairnames:
1576 if (name is not None) and name in self.colorpairnames:
1577 # then get the associated color pair and return it
1577 # then get the associated color pair and return it
1578 colorpair = self.colorpairnames[name]
1578 colorpair = self.colorpairnames[name]
1579 else:
1579 else:
1580 if fgcolor is None:
1580 if fgcolor is None:
1581 fgcolor = -1
1581 fgcolor = -1
1582 if bgcolor is None:
1582 if bgcolor is None:
1583 bgcolor = -1
1583 bgcolor = -1
1584 if (fgcolor, bgcolor) in self.colorpairs:
1584 if (fgcolor, bgcolor) in self.colorpairs:
1585 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1585 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1586 else:
1586 else:
1587 pairindex = len(self.colorpairs) + 1
1587 pairindex = len(self.colorpairs) + 1
1588 if self.usecolor:
1588 if self.usecolor:
1589 curses.init_pair(pairindex, fgcolor, bgcolor)
1589 curses.init_pair(pairindex, fgcolor, bgcolor)
1590 colorpair = self.colorpairs[
1590 colorpair = self.colorpairs[
1591 (fgcolor, bgcolor)
1591 (fgcolor, bgcolor)
1592 ] = curses.color_pair(pairindex)
1592 ] = curses.color_pair(pairindex)
1593 if name is not None:
1593 if name is not None:
1594 self.colorpairnames[name] = curses.color_pair(pairindex)
1594 self.colorpairnames[name] = curses.color_pair(pairindex)
1595 else:
1595 else:
1596 cval = 0
1596 cval = 0
1597 if name is not None:
1597 if name is not None:
1598 if name == b'selected':
1598 if name == b'selected':
1599 cval = curses.A_REVERSE
1599 cval = curses.A_REVERSE
1600 self.colorpairnames[name] = cval
1600 self.colorpairnames[name] = cval
1601 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1601 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1602
1602
1603 # add attributes if possible
1603 # add attributes if possible
1604 if attrlist is None:
1604 if attrlist is None:
1605 attrlist = []
1605 attrlist = []
1606 if colorpair < 256:
1606 if colorpair < 256:
1607 # then it is safe to apply all attributes
1607 # then it is safe to apply all attributes
1608 for textattr in attrlist:
1608 for textattr in attrlist:
1609 colorpair |= textattr
1609 colorpair |= textattr
1610 else:
1610 else:
1611 # just apply a select few (safe?) attributes
1611 # just apply a select few (safe?) attributes
1612 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1612 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1613 if textattrib in attrlist:
1613 if textattrib in attrlist:
1614 colorpair |= textattrib
1614 colorpair |= textattrib
1615 return colorpair
1615 return colorpair
1616
1616
1617 def initcolorpair(self, *args, **kwargs):
1617 def initcolorpair(self, *args, **kwargs):
1618 """same as getcolorpair."""
1618 """same as getcolorpair."""
1619 self.getcolorpair(*args, **kwargs)
1619 self.getcolorpair(*args, **kwargs)
1620
1620
1621 def helpwindow(self):
1621 def helpwindow(self):
1622 """print a help window to the screen. exit after any keypress."""
1622 """print a help window to the screen. exit after any keypress."""
1623 helptext = _(
1623 helptext = _(
1624 """ [press any key to return to the patch-display]
1624 """ [press any key to return to the patch-display]
1625
1625
1626 The curses hunk selector allows you to interactively choose among the
1626 The curses hunk selector allows you to interactively choose among the
1627 changes you have made, and confirm only those changes you select for
1627 changes you have made, and confirm only those changes you select for
1628 further processing by the command you are running (such as commit,
1628 further processing by the command you are running (such as commit,
1629 shelve, or revert). After confirming the selected changes, the
1629 shelve, or revert). After confirming the selected changes, the
1630 unselected changes are still present in your working copy, so you can
1630 unselected changes are still present in your working copy, so you can
1631 use the hunk selector multiple times to split large changes into
1631 use the hunk selector multiple times to split large changes into
1632 smaller changesets. the following are valid keystrokes:
1632 smaller changesets. the following are valid keystrokes:
1633
1633
1634 x [space] : (un-)select item ([~]/[x] = partly/fully applied)
1634 x [space] : (un-)select item ([~]/[x] = partly/fully applied)
1635 [enter] : (un-)select item and go to next item of same type
1635 [enter] : (un-)select item and go to next item of same type
1636 A : (un-)select all items
1636 A : (un-)select all items
1637 X : (un-)select all items between current and most-recent
1637 X : (un-)select all items between current and most-recent
1638 up/down-arrow [k/j] : go to previous/next unfolded item
1638 up/down-arrow [k/j] : go to previous/next unfolded item
1639 pgup/pgdn [K/J] : go to previous/next item of same type
1639 pgup/pgdn [K/J] : go to previous/next item of same type
1640 right/left-arrow [l/h] : go to child item / parent item
1640 right/left-arrow [l/h] : go to child item / parent item
1641 shift-left-arrow [H] : go to parent header / fold selected header
1641 shift-left-arrow [H] : go to parent header / fold selected header
1642 g : go to the top
1642 g : go to the top
1643 G : go to the bottom
1643 G : go to the bottom
1644 f : fold / unfold item, hiding/revealing its children
1644 f : fold / unfold item, hiding/revealing its children
1645 F : fold / unfold parent item and all of its ancestors
1645 F : fold / unfold parent item and all of its ancestors
1646 ctrl-l : scroll the selected line to the top of the screen
1646 ctrl-l : scroll the selected line to the top of the screen
1647 m : edit / resume editing the commit message
1647 m : edit / resume editing the commit message
1648 e : edit the currently selected hunk
1648 e : edit the currently selected hunk
1649 a : toggle all selections
1649 a : toggle all selections
1650 c : confirm selected changes
1650 c : confirm selected changes
1651 r : review/edit and confirm selected changes
1651 r : review/edit and confirm selected changes
1652 q : quit without confirming (no changes will be made)
1652 q : quit without confirming (no changes will be made)
1653 ? : help (what you're currently reading)"""
1653 ? : help (what you're currently reading)"""
1654 )
1654 )
1655
1655
1656 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1656 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1657 helplines = helptext.split(b"\n")
1657 helplines = helptext.split(b"\n")
1658 helplines = helplines + [b" "] * (
1658 helplines = helplines + [b" "] * (
1659 self.yscreensize - self.numstatuslines - len(helplines) - 1
1659 self.yscreensize - self.numstatuslines - len(helplines) - 1
1660 )
1660 )
1661 try:
1661 try:
1662 for line in helplines:
1662 for line in helplines:
1663 self.printstring(helpwin, line, pairname=b"legend")
1663 self.printstring(helpwin, line, pairname=b"legend")
1664 except curses.error:
1664 except curses.error:
1665 pass
1665 pass
1666 helpwin.refresh()
1666 helpwin.refresh()
1667 try:
1667 try:
1668 with self.ui.timeblockedsection(b'crecord'):
1668 with self.ui.timeblockedsection(b'crecord'):
1669 helpwin.getkey()
1669 helpwin.getkey()
1670 except curses.error:
1670 except curses.error:
1671 pass
1671 pass
1672
1672
1673 def commitMessageWindow(self):
1673 def commitMessageWindow(self):
1674 """Create a temporary commit message editing window on the screen."""
1674 """Create a temporary commit message editing window on the screen."""
1675
1675
1676 curses.raw()
1676 curses.raw()
1677 curses.def_prog_mode()
1677 curses.def_prog_mode()
1678 curses.endwin()
1678 curses.endwin()
1679 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1679 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1680 curses.cbreak()
1680 curses.cbreak()
1681 self.stdscr.refresh()
1681 self.stdscr.refresh()
1682 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1682 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1683
1683
1684 def handlefirstlineevent(self):
1684 def handlefirstlineevent(self):
1685 """
1685 """
1686 Handle 'g' to navigate to the top most file in the ncurses window.
1686 Handle 'g' to navigate to the top most file in the ncurses window.
1687 """
1687 """
1688 self.currentselecteditem = self.headerlist[0]
1688 self.currentselecteditem = self.headerlist[0]
1689 currentitem = self.currentselecteditem
1689 currentitem = self.currentselecteditem
1690 # select the parent item recursively until we're at a header
1690 # select the parent item recursively until we're at a header
1691 while True:
1691 while True:
1692 nextitem = currentitem.parentitem()
1692 nextitem = currentitem.parentitem()
1693 if nextitem is None:
1693 if nextitem is None:
1694 break
1694 break
1695 else:
1695 else:
1696 currentitem = nextitem
1696 currentitem = nextitem
1697
1697
1698 self.currentselecteditem = currentitem
1698 self.currentselecteditem = currentitem
1699
1699
1700 def handlelastlineevent(self):
1700 def handlelastlineevent(self):
1701 """
1701 """
1702 Handle 'G' to navigate to the bottom most file/hunk/line depending
1702 Handle 'G' to navigate to the bottom most file/hunk/line depending
1703 on the whether the fold is active or not.
1703 on the whether the fold is active or not.
1704
1704
1705 If the bottom most file is folded, it navigates to that file and
1705 If the bottom most file is folded, it navigates to that file and
1706 stops there. If the bottom most file is unfolded, it navigates to
1706 stops there. If the bottom most file is unfolded, it navigates to
1707 the bottom most hunk in that file and stops there. If the bottom most
1707 the bottom most hunk in that file and stops there. If the bottom most
1708 hunk is unfolded, it navigates to the bottom most line in that hunk.
1708 hunk is unfolded, it navigates to the bottom most line in that hunk.
1709 """
1709 """
1710 currentitem = self.currentselecteditem
1710 currentitem = self.currentselecteditem
1711 nextitem = currentitem.nextitem()
1711 nextitem = currentitem.nextitem()
1712 # select the child item recursively until we're at a footer
1712 # select the child item recursively until we're at a footer
1713 while nextitem is not None:
1713 while nextitem is not None:
1714 nextitem = currentitem.nextitem()
1714 nextitem = currentitem.nextitem()
1715 if nextitem is None:
1715 if nextitem is None:
1716 break
1716 break
1717 else:
1717 else:
1718 currentitem = nextitem
1718 currentitem = nextitem
1719
1719
1720 self.currentselecteditem = currentitem
1720 self.currentselecteditem = currentitem
1721 self.recenterdisplayedarea()
1721 self.recenterdisplayedarea()
1722
1722
1723 def confirmationwindow(self, windowtext):
1723 def confirmationwindow(self, windowtext):
1724 """display an informational window, then wait for and return a
1724 """display an informational window, then wait for and return a
1725 keypress."""
1725 keypress."""
1726
1726
1727 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1727 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1728 try:
1728 try:
1729 lines = windowtext.split(b"\n")
1729 lines = windowtext.split(b"\n")
1730 for line in lines:
1730 for line in lines:
1731 self.printstring(confirmwin, line, pairname=b"selected")
1731 self.printstring(confirmwin, line, pairname=b"selected")
1732 except curses.error:
1732 except curses.error:
1733 pass
1733 pass
1734 self.stdscr.refresh()
1734 self.stdscr.refresh()
1735 confirmwin.refresh()
1735 confirmwin.refresh()
1736 try:
1736 try:
1737 with self.ui.timeblockedsection(b'crecord'):
1737 with self.ui.timeblockedsection(b'crecord'):
1738 response = chr(self.stdscr.getch())
1738 response = chr(self.stdscr.getch())
1739 except ValueError:
1739 except ValueError:
1740 response = None
1740 response = None
1741
1741
1742 return response
1742 return response
1743
1743
1744 def reviewcommit(self):
1744 def reviewcommit(self):
1745 """ask for 'y' to be pressed to confirm selected. return True if
1745 """ask for 'y' to be pressed to confirm selected. return True if
1746 confirmed."""
1746 confirmed."""
1747 confirmtext = _(
1747 confirmtext = _(
1748 """If you answer yes to the following, your currently chosen patch chunks
1748 """If you answer yes to the following, your currently chosen patch chunks
1749 will be loaded into an editor. To modify the patch, make the changes in your
1749 will be loaded into an editor. To modify the patch, make the changes in your
1750 editor and save. To accept the current patch as-is, close the editor without
1750 editor and save. To accept the current patch as-is, close the editor without
1751 saving.
1751 saving.
1752
1752
1753 note: don't add/remove lines unless you also modify the range information.
1753 note: don't add/remove lines unless you also modify the range information.
1754 failing to follow this rule will result in the commit aborting.
1754 failing to follow this rule will result in the commit aborting.
1755
1755
1756 are you sure you want to review/edit and confirm the selected changes [yn]?
1756 are you sure you want to review/edit and confirm the selected changes [yn]?
1757 """
1757 """
1758 )
1758 )
1759 with self.ui.timeblockedsection(b'crecord'):
1759 with self.ui.timeblockedsection(b'crecord'):
1760 response = self.confirmationwindow(confirmtext)
1760 response = self.confirmationwindow(confirmtext)
1761 if response is None:
1761 if response is None:
1762 response = "n"
1762 response = "n"
1763 if response.lower().startswith("y"):
1763 if response.lower().startswith("y"):
1764 return True
1764 return True
1765 else:
1765 else:
1766 return False
1766 return False
1767
1767
1768 def recenterdisplayedarea(self):
1768 def recenterdisplayedarea(self):
1769 """
1769 """
1770 once we scrolled with pg up pg down we can be pointing outside of the
1770 once we scrolled with pg up pg down we can be pointing outside of the
1771 display zone. we print the patch with towin=False to compute the
1771 display zone. we print the patch with towin=False to compute the
1772 location of the selected item even though it is outside of the displayed
1772 location of the selected item even though it is outside of the displayed
1773 zone and then update the scroll.
1773 zone and then update the scroll.
1774 """
1774 """
1775 self.printitem(towin=False)
1775 self.printitem(towin=False)
1776 self.updatescroll()
1776 self.updatescroll()
1777
1777
1778 def toggleedit(self, item=None, test=False):
1778 def toggleedit(self, item=None, test=False):
1779 """
1779 """
1780 edit the currently selected chunk
1780 edit the currently selected chunk
1781 """
1781 """
1782
1782
1783 def updateui(self):
1783 def updateui(self):
1784 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1784 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1785 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1785 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1786 self.updatescroll()
1786 self.updatescroll()
1787 self.stdscr.refresh()
1787 self.stdscr.refresh()
1788 self.statuswin.refresh()
1788 self.statuswin.refresh()
1789 self.stdscr.keypad(1)
1789 self.stdscr.keypad(1)
1790
1790
1791 def editpatchwitheditor(self, chunk):
1791 def editpatchwitheditor(self, chunk):
1792 if chunk is None:
1792 if chunk is None:
1793 self.ui.write(_(b'cannot edit patch for whole file'))
1793 self.ui.write(_(b'cannot edit patch for whole file'))
1794 self.ui.write(b"\n")
1794 self.ui.write(b"\n")
1795 return None
1795 return None
1796 if chunk.header.binary():
1796 if chunk.header.binary():
1797 self.ui.write(_(b'cannot edit patch for binary file'))
1797 self.ui.write(_(b'cannot edit patch for binary file'))
1798 self.ui.write(b"\n")
1798 self.ui.write(b"\n")
1799 return None
1799 return None
1800
1800
1801 # write the initial patch
1801 # write the initial patch
1802 patch = stringio()
1802 patch = stringio()
1803 patch.write(diffhelptext + hunkhelptext)
1803 patch.write(diffhelptext + hunkhelptext)
1804 chunk.header.write(patch)
1804 chunk.header.write(patch)
1805 chunk.write(patch)
1805 chunk.write(patch)
1806
1806
1807 # start the editor and wait for it to complete
1807 # start the editor and wait for it to complete
1808 try:
1808 try:
1809 patch = self.ui.edit(patch.getvalue(), b"", action=b"diff")
1809 patch = self.ui.edit(patch.getvalue(), b"", action=b"diff")
1810 except error.Abort as exc:
1810 except error.Abort as exc:
1811 self.errorstr = exc.message
1811 self.errorstr = exc.message
1812 return None
1812 return None
1813 finally:
1813 finally:
1814 self.stdscr.clear()
1814 self.stdscr.clear()
1815 self.stdscr.refresh()
1815 self.stdscr.refresh()
1816
1816
1817 # remove comment lines
1817 # remove comment lines
1818 patch = [
1818 patch = [
1819 line + b'\n'
1819 line + b'\n'
1820 for line in patch.splitlines()
1820 for line in patch.splitlines()
1821 if not line.startswith(b'#')
1821 if not line.startswith(b'#')
1822 ]
1822 ]
1823 return patchmod.parsepatch(patch)
1823 return patchmod.parsepatch(patch)
1824
1824
1825 if item is None:
1825 if item is None:
1826 item = self.currentselecteditem
1826 item = self.currentselecteditem
1827 if isinstance(item, uiheader):
1827 if isinstance(item, uiheader):
1828 return
1828 return
1829 if isinstance(item, uihunkline):
1829 if isinstance(item, uihunkline):
1830 item = item.parentitem()
1830 item = item.parentitem()
1831 if not isinstance(item, uihunk):
1831 if not isinstance(item, uihunk):
1832 return
1832 return
1833
1833
1834 # To go back to that hunk or its replacement at the end of the edit
1834 # To go back to that hunk or its replacement at the end of the edit
1835 itemindex = item.parentitem().hunks.index(item)
1835 itemindex = item.parentitem().hunks.index(item)
1836
1836
1837 beforeadded, beforeremoved = item.added, item.removed
1837 beforeadded, beforeremoved = item.added, item.removed
1838 newpatches = editpatchwitheditor(self, item)
1838 newpatches = editpatchwitheditor(self, item)
1839 if newpatches is None:
1839 if newpatches is None:
1840 if not test:
1840 if not test:
1841 updateui(self)
1841 updateui(self)
1842 return
1842 return
1843 header = item.header
1843 header = item.header
1844 editedhunkindex = header.hunks.index(item)
1844 editedhunkindex = header.hunks.index(item)
1845 hunksbefore = header.hunks[:editedhunkindex]
1845 hunksbefore = header.hunks[:editedhunkindex]
1846 hunksafter = header.hunks[editedhunkindex + 1 :]
1846 hunksafter = header.hunks[editedhunkindex + 1 :]
1847 newpatchheader = newpatches[0]
1847 newpatchheader = newpatches[0]
1848 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1848 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1849 newadded = sum([h.added for h in newhunks])
1849 newadded = sum([h.added for h in newhunks])
1850 newremoved = sum([h.removed for h in newhunks])
1850 newremoved = sum([h.removed for h in newhunks])
1851 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1851 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1852
1852
1853 for h in hunksafter:
1853 for h in hunksafter:
1854 h.toline += offset
1854 h.toline += offset
1855 for h in newhunks:
1855 for h in newhunks:
1856 h.folded = False
1856 h.folded = False
1857 header.hunks = hunksbefore + newhunks + hunksafter
1857 header.hunks = hunksbefore + newhunks + hunksafter
1858 if self.emptypatch():
1858 if self.emptypatch():
1859 header.hunks = hunksbefore + [item] + hunksafter
1859 header.hunks = hunksbefore + [item] + hunksafter
1860 self.currentselecteditem = header
1860 self.currentselecteditem = header
1861 if len(header.hunks) > itemindex:
1861 if len(header.hunks) > itemindex:
1862 self.currentselecteditem = header.hunks[itemindex]
1862 self.currentselecteditem = header.hunks[itemindex]
1863
1863
1864 if not test:
1864 if not test:
1865 updateui(self)
1865 updateui(self)
1866
1866
1867 def emptypatch(self):
1867 def emptypatch(self):
1868 item = self.headerlist
1868 item = self.headerlist
1869 if not item:
1869 if not item:
1870 return True
1870 return True
1871 for header in item:
1871 for header in item:
1872 if header.hunks:
1872 if header.hunks:
1873 return False
1873 return False
1874 return True
1874 return True
1875
1875
1876 def handlekeypressed(self, keypressed, test=False):
1876 def handlekeypressed(self, keypressed, test=False):
1877 """
1877 """
1878 Perform actions based on pressed keys.
1878 Perform actions based on pressed keys.
1879
1879
1880 Return true to exit the main loop.
1880 Return true to exit the main loop.
1881 """
1881 """
1882 if keypressed in ["k", "KEY_UP"]:
1882 if keypressed in ["k", "KEY_UP"]:
1883 self.uparrowevent()
1883 self.uparrowevent()
1884 elif keypressed in ["K", "KEY_PPAGE"]:
1884 elif keypressed in ["K", "KEY_PPAGE"]:
1885 self.uparrowshiftevent()
1885 self.uparrowshiftevent()
1886 elif keypressed in ["j", "KEY_DOWN"]:
1886 elif keypressed in ["j", "KEY_DOWN"]:
1887 self.downarrowevent()
1887 self.downarrowevent()
1888 elif keypressed in ["J", "KEY_NPAGE"]:
1888 elif keypressed in ["J", "KEY_NPAGE"]:
1889 self.downarrowshiftevent()
1889 self.downarrowshiftevent()
1890 elif keypressed in ["l", "KEY_RIGHT"]:
1890 elif keypressed in ["l", "KEY_RIGHT"]:
1891 self.rightarrowevent()
1891 self.rightarrowevent()
1892 elif keypressed in ["h", "KEY_LEFT"]:
1892 elif keypressed in ["h", "KEY_LEFT"]:
1893 self.leftarrowevent()
1893 self.leftarrowevent()
1894 elif keypressed in ["H", "KEY_SLEFT"]:
1894 elif keypressed in ["H", "KEY_SLEFT"]:
1895 self.leftarrowshiftevent()
1895 self.leftarrowshiftevent()
1896 elif keypressed in ["q"]:
1896 elif keypressed in ["q"]:
1897 raise error.Abort(_(b'user quit'))
1897 raise error.Abort(_(b'user quit'))
1898 elif keypressed in ['a']:
1898 elif keypressed in ['a']:
1899 self.flipselections()
1899 self.flipselections()
1900 elif keypressed in ["c"]:
1900 elif keypressed in ["c"]:
1901 return True
1901 return True
1902 elif keypressed in ["r"]:
1902 elif keypressed in ["r"]:
1903 if self.reviewcommit():
1903 if self.reviewcommit():
1904 self.opts[b'review'] = True
1904 self.opts[b'review'] = True
1905 return True
1905 return True
1906 elif test and keypressed in ["R"]:
1906 elif test and keypressed in ["R"]:
1907 self.opts[b'review'] = True
1907 self.opts[b'review'] = True
1908 return True
1908 return True
1909 elif keypressed in [" ", "x"]:
1909 elif keypressed in [" ", "x"]:
1910 self.toggleapply()
1910 self.toggleapply()
1911 elif keypressed in ["\n", "KEY_ENTER"]:
1911 elif keypressed in ["\n", "KEY_ENTER"]:
1912 self.toggleapply()
1912 self.toggleapply()
1913 self.nextsametype(test=test)
1913 self.nextsametype(test=test)
1914 elif keypressed in ["X"]:
1914 elif keypressed in ["X"]:
1915 self.toggleallbetween()
1915 self.toggleallbetween()
1916 elif keypressed in ["A"]:
1916 elif keypressed in ["A"]:
1917 self.toggleall()
1917 self.toggleall()
1918 elif keypressed in ["e"]:
1918 elif keypressed in ["e"]:
1919 self.toggleedit(test=test)
1919 self.toggleedit(test=test)
1920 elif keypressed in ["f"]:
1920 elif keypressed in ["f"]:
1921 self.togglefolded()
1921 self.togglefolded()
1922 elif keypressed in ["F"]:
1922 elif keypressed in ["F"]:
1923 self.togglefolded(foldparent=True)
1923 self.togglefolded(foldparent=True)
1924 elif keypressed in ["m"]:
1924 elif keypressed in ["m"]:
1925 self.commitMessageWindow()
1925 self.commitMessageWindow()
1926 elif keypressed in ["g", "KEY_HOME"]:
1926 elif keypressed in ["g", "KEY_HOME"]:
1927 self.handlefirstlineevent()
1927 self.handlefirstlineevent()
1928 elif keypressed in ["G", "KEY_END"]:
1928 elif keypressed in ["G", "KEY_END"]:
1929 self.handlelastlineevent()
1929 self.handlelastlineevent()
1930 elif keypressed in ["?"]:
1930 elif keypressed in ["?"]:
1931 self.helpwindow()
1931 self.helpwindow()
1932 self.stdscr.clear()
1932 self.stdscr.clear()
1933 self.stdscr.refresh()
1933 self.stdscr.refresh()
1934 elif keypressed in [curses.ascii.ctrl("L")]:
1934 elif keypressed in [curses.ascii.ctrl("L")]:
1935 # scroll the current line to the top of the screen, and redraw
1935 # scroll the current line to the top of the screen, and redraw
1936 # everything
1936 # everything
1937 self.scrolllines(self.selecteditemstartline)
1937 self.scrolllines(self.selecteditemstartline)
1938 self.stdscr.clear()
1938 self.stdscr.clear()
1939 self.stdscr.refresh()
1939 self.stdscr.refresh()
1940
1940
1941 def main(self, stdscr):
1941 def main(self, stdscr):
1942 """
1942 """
1943 method to be wrapped by curses.wrapper() for selecting chunks.
1943 method to be wrapped by curses.wrapper() for selecting chunks.
1944 """
1944 """
1945
1945
1946 origsigwinch = sentinel = object()
1946 origsigwinch = sentinel = object()
1947 if util.safehasattr(signal, b'SIGWINCH'):
1947 if util.safehasattr(signal, b'SIGWINCH'):
1948 origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1948 origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1949 try:
1949 try:
1950 return self._main(stdscr)
1950 return self._main(stdscr)
1951 finally:
1951 finally:
1952 if origsigwinch is not sentinel:
1952 if origsigwinch is not sentinel:
1953 signal.signal(signal.SIGWINCH, origsigwinch)
1953 signal.signal(signal.SIGWINCH, origsigwinch)
1954
1954
1955 def _main(self, stdscr):
1955 def _main(self, stdscr):
1956 self.stdscr = stdscr
1956 self.stdscr = stdscr
1957 # error during initialization, cannot be printed in the curses
1957 # error during initialization, cannot be printed in the curses
1958 # interface, it should be printed by the calling code
1958 # interface, it should be printed by the calling code
1959 self.initexc = None
1959 self.initexc = None
1960 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1960 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1961
1961
1962 curses.start_color()
1962 curses.start_color()
1963 try:
1963 try:
1964 curses.use_default_colors()
1964 curses.use_default_colors()
1965 except curses.error:
1965 except curses.error:
1966 self.usecolor = False
1966 self.usecolor = False
1967
1967
1968 # In some situations we may have some cruft left on the "alternate
1968 # In some situations we may have some cruft left on the "alternate
1969 # screen" from another program (or previous iterations of ourself), and
1969 # screen" from another program (or previous iterations of ourself), and
1970 # we won't clear it if the scroll region is small enough to comfortably
1970 # we won't clear it if the scroll region is small enough to comfortably
1971 # fit on the terminal.
1971 # fit on the terminal.
1972 self.stdscr.clear()
1972 self.stdscr.clear()
1973
1973
1974 # don't display the cursor
1974 # don't display the cursor
1975 try:
1975 try:
1976 curses.curs_set(0)
1976 curses.curs_set(0)
1977 except curses.error:
1977 except curses.error:
1978 pass
1978 pass
1979
1979
1980 # available colors: black, blue, cyan, green, magenta, white, yellow
1980 # available colors: black, blue, cyan, green, magenta, white, yellow
1981 # init_pair(color_id, foreground_color, background_color)
1981 # init_pair(color_id, foreground_color, background_color)
1982 self.initcolorpair(None, None, name=b"normal")
1982 self.initcolorpair(None, None, name=b"normal")
1983 self.initcolorpair(
1983 self.initcolorpair(
1984 curses.COLOR_WHITE, curses.COLOR_MAGENTA, name=b"selected"
1984 curses.COLOR_WHITE, curses.COLOR_MAGENTA, name=b"selected"
1985 )
1985 )
1986 self.initcolorpair(curses.COLOR_RED, None, name=b"deletion")
1986 self.initcolorpair(curses.COLOR_RED, None, name=b"deletion")
1987 self.initcolorpair(curses.COLOR_GREEN, None, name=b"addition")
1987 self.initcolorpair(curses.COLOR_GREEN, None, name=b"addition")
1988 self.initcolorpair(
1988 self.initcolorpair(
1989 curses.COLOR_WHITE, curses.COLOR_BLUE, name=b"legend"
1989 curses.COLOR_WHITE, curses.COLOR_BLUE, name=b"legend"
1990 )
1990 )
1991 # newwin([height, width,] begin_y, begin_x)
1991 # newwin([height, width,] begin_y, begin_x)
1992 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1992 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1993 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1993 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1994
1994
1995 # figure out how much space to allocate for the chunk-pad which is
1995 # figure out how much space to allocate for the chunk-pad which is
1996 # used for displaying the patch
1996 # used for displaying the patch
1997
1997
1998 # stupid hack to prevent getnumlinesdisplayed from failing
1998 # stupid hack to prevent getnumlinesdisplayed from failing
1999 self.chunkpad = curses.newpad(1, self.xscreensize)
1999 self.chunkpad = curses.newpad(1, self.xscreensize)
2000
2000
2001 # add 1 so to account for last line text reaching end of line
2001 # add 1 so to account for last line text reaching end of line
2002 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
2002 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
2003
2003
2004 try:
2004 try:
2005 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
2005 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
2006 except curses.error:
2006 except curses.error:
2007 self.initexc = fallbackerror(
2007 self.initexc = fallbackerror(
2008 _(b'this diff is too large to be displayed')
2008 _(b'this diff is too large to be displayed')
2009 )
2009 )
2010 return
2010 return
2011 # initialize selecteditemendline (initial start-line is 0)
2011 # initialize selecteditemendline (initial start-line is 0)
2012 self.selecteditemendline = self.getnumlinesdisplayed(
2012 self.selecteditemendline = self.getnumlinesdisplayed(
2013 self.currentselecteditem, recursechildren=False
2013 self.currentselecteditem, recursechildren=False
2014 )
2014 )
2015
2015
2016 while True:
2016 while True:
2017 self.updatescreen()
2017 self.updatescreen()
2018 try:
2018 try:
2019 with self.ui.timeblockedsection(b'crecord'):
2019 with self.ui.timeblockedsection(b'crecord'):
2020 keypressed = self.statuswin.getkey()
2020 keypressed = self.statuswin.getkey()
2021 if self.errorstr is not None:
2021 if self.errorstr is not None:
2022 self.errorstr = None
2022 self.errorstr = None
2023 continue
2023 continue
2024 except curses.error:
2024 except curses.error:
2025 keypressed = b"foobar"
2025 keypressed = b"foobar"
2026 if self.handlekeypressed(keypressed):
2026 if self.handlekeypressed(keypressed):
2027 break
2027 break
2028
2028
2029 if self.commenttext != b"":
2029 if self.commenttext != b"":
2030 whitespaceremoved = re.sub(
2030 whitespaceremoved = re.sub(
2031 br"(?m)^\s.*(\n|$)", b"", self.commenttext
2031 br"(?m)^\s.*(\n|$)", b"", self.commenttext
2032 )
2032 )
2033 if whitespaceremoved != b"":
2033 if whitespaceremoved != b"":
2034 self.opts[b'message'] = self.commenttext
2034 self.opts[b'message'] = self.commenttext
General Comments 0
You need to be logged in to leave comments. Login now