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