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