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