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