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