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