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