##// END OF EJS Templates
crecord: change help text for the space key dynamically...
Jun Wu -
r30547:5129ed3c default
parent child Browse files
Show More
@@ -1,1655 +1,1656
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 f = signal.getsignal(signal.SIGTSTP)
477 curses.wrapper(chunkselector.main)
477 curses.wrapper(chunkselector.main)
478 if chunkselector.initerr is not None:
478 if chunkselector.initerr is not None:
479 raise error.Abort(chunkselector.initerr)
479 raise error.Abort(chunkselector.initerr)
480 # ncurses does not restore signal handler for SIGTSTP
480 # ncurses does not restore signal handler for SIGTSTP
481 signal.signal(signal.SIGTSTP, f)
481 signal.signal(signal.SIGTSTP, f)
482 return chunkselector.opts
482 return chunkselector.opts
483
483
484 def testdecorator(testfn, f):
484 def testdecorator(testfn, f):
485 def u(*args, **kwargs):
485 def u(*args, **kwargs):
486 return f(testfn, *args, **kwargs)
486 return f(testfn, *args, **kwargs)
487 return u
487 return u
488
488
489 def testchunkselector(testfn, ui, headerlist, operation=None):
489 def testchunkselector(testfn, ui, headerlist, operation=None):
490 """
490 """
491 test interface to get selection of chunks, and mark the applied flags
491 test interface to get selection of chunks, and mark the applied flags
492 of the chosen chunks.
492 of the chosen chunks.
493 """
493 """
494 chunkselector = curseschunkselector(headerlist, ui, operation)
494 chunkselector = curseschunkselector(headerlist, ui, operation)
495 if testfn and os.path.exists(testfn):
495 if testfn and os.path.exists(testfn):
496 testf = open(testfn)
496 testf = open(testfn)
497 testcommands = map(lambda x: x.rstrip('\n'), testf.readlines())
497 testcommands = map(lambda x: x.rstrip('\n'), testf.readlines())
498 testf.close()
498 testf.close()
499 while True:
499 while True:
500 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
500 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
501 break
501 break
502 return chunkselector.opts
502 return chunkselector.opts
503
503
504 class curseschunkselector(object):
504 class curseschunkselector(object):
505 def __init__(self, headerlist, ui, operation=None):
505 def __init__(self, headerlist, ui, operation=None):
506 # put the headers into a patch object
506 # put the headers into a patch object
507 self.headerlist = patch(headerlist)
507 self.headerlist = patch(headerlist)
508
508
509 self.ui = ui
509 self.ui = ui
510 self.opts = {}
510 self.opts = {}
511
511
512 self.errorstr = None
512 self.errorstr = None
513 # list of all chunks
513 # list of all chunks
514 self.chunklist = []
514 self.chunklist = []
515 for h in headerlist:
515 for h in headerlist:
516 self.chunklist.append(h)
516 self.chunklist.append(h)
517 self.chunklist.extend(h.hunks)
517 self.chunklist.extend(h.hunks)
518
518
519 # dictionary mapping (fgcolor, bgcolor) pairs to the
519 # dictionary mapping (fgcolor, bgcolor) pairs to the
520 # corresponding curses color-pair value.
520 # corresponding curses color-pair value.
521 self.colorpairs = {}
521 self.colorpairs = {}
522 # maps custom nicknames of color-pairs to curses color-pair values
522 # maps custom nicknames of color-pairs to curses color-pair values
523 self.colorpairnames = {}
523 self.colorpairnames = {}
524
524
525 # the currently selected header, hunk, or hunk-line
525 # the currently selected header, hunk, or hunk-line
526 self.currentselecteditem = self.headerlist[0]
526 self.currentselecteditem = self.headerlist[0]
527
527
528 # updated when printing out patch-display -- the 'lines' here are the
528 # updated when printing out patch-display -- the 'lines' here are the
529 # line positions *in the pad*, not on the screen.
529 # line positions *in the pad*, not on the screen.
530 self.selecteditemstartline = 0
530 self.selecteditemstartline = 0
531 self.selecteditemendline = None
531 self.selecteditemendline = None
532
532
533 # define indentation levels
533 # define indentation levels
534 self.headerindentnumchars = 0
534 self.headerindentnumchars = 0
535 self.hunkindentnumchars = 3
535 self.hunkindentnumchars = 3
536 self.hunklineindentnumchars = 6
536 self.hunklineindentnumchars = 6
537
537
538 # the first line of the pad to print to the screen
538 # the first line of the pad to print to the screen
539 self.firstlineofpadtoprint = 0
539 self.firstlineofpadtoprint = 0
540
540
541 # keeps track of the number of lines in the pad
541 # keeps track of the number of lines in the pad
542 self.numpadlines = None
542 self.numpadlines = None
543
543
544 self.numstatuslines = 1
544 self.numstatuslines = 1
545
545
546 # keep a running count of the number of lines printed to the pad
546 # keep a running count of the number of lines printed to the pad
547 # (used for determining when the selected item begins/ends)
547 # (used for determining when the selected item begins/ends)
548 self.linesprintedtopadsofar = 0
548 self.linesprintedtopadsofar = 0
549
549
550 # the first line of the pad which is visible on the screen
550 # the first line of the pad which is visible on the screen
551 self.firstlineofpadtoprint = 0
551 self.firstlineofpadtoprint = 0
552
552
553 # stores optional text for a commit comment provided by the user
553 # stores optional text for a commit comment provided by the user
554 self.commenttext = ""
554 self.commenttext = ""
555
555
556 # if the last 'toggle all' command caused all changes to be applied
556 # if the last 'toggle all' command caused all changes to be applied
557 self.waslasttoggleallapplied = True
557 self.waslasttoggleallapplied = True
558
558
559 # affects some ui text
559 # affects some ui text
560 self.operation = operation
560 self.operation = operation
561
561
562 def uparrowevent(self):
562 def uparrowevent(self):
563 """
563 """
564 try to select the previous item to the current item that has the
564 try to select the previous item to the current item that has the
565 most-indented level. for example, if a hunk is selected, try to select
565 most-indented level. for example, if a hunk is selected, try to select
566 the last hunkline of the hunk prior to the selected hunk. or, if
566 the last hunkline of the hunk prior to the selected hunk. or, if
567 the first hunkline of a hunk is currently selected, then select the
567 the first hunkline of a hunk is currently selected, then select the
568 hunk itself.
568 hunk itself.
569 """
569 """
570 currentitem = self.currentselecteditem
570 currentitem = self.currentselecteditem
571
571
572 nextitem = currentitem.previtem()
572 nextitem = currentitem.previtem()
573
573
574 if nextitem is None:
574 if nextitem is None:
575 # if no parent item (i.e. currentitem is the first header), then
575 # if no parent item (i.e. currentitem is the first header), then
576 # no change...
576 # no change...
577 nextitem = currentitem
577 nextitem = currentitem
578
578
579 self.currentselecteditem = nextitem
579 self.currentselecteditem = nextitem
580
580
581 def uparrowshiftevent(self):
581 def uparrowshiftevent(self):
582 """
582 """
583 select (if possible) the previous item on the same level as the
583 select (if possible) the previous item on the same level as the
584 currently selected item. otherwise, select (if possible) the
584 currently selected item. otherwise, select (if possible) the
585 parent-item of the currently selected item.
585 parent-item of the currently selected item.
586 """
586 """
587 currentitem = self.currentselecteditem
587 currentitem = self.currentselecteditem
588 nextitem = currentitem.prevsibling()
588 nextitem = currentitem.prevsibling()
589 # if there's no previous sibling, try choosing the parent
589 # if there's no previous sibling, try choosing the parent
590 if nextitem is None:
590 if nextitem is None:
591 nextitem = currentitem.parentitem()
591 nextitem = currentitem.parentitem()
592 if nextitem is None:
592 if nextitem is None:
593 # if no parent item (i.e. currentitem is the first header), then
593 # if no parent item (i.e. currentitem is the first header), then
594 # no change...
594 # no change...
595 nextitem = currentitem
595 nextitem = currentitem
596
596
597 self.currentselecteditem = nextitem
597 self.currentselecteditem = nextitem
598
598
599 def downarrowevent(self):
599 def downarrowevent(self):
600 """
600 """
601 try to select the next item to the current item that has the
601 try to select the next item to the current item that has the
602 most-indented level. for example, if a hunk is selected, select
602 most-indented level. for example, if a hunk is selected, select
603 the first hunkline of the selected hunk. or, if the last hunkline of
603 the first hunkline of the selected hunk. or, if the last hunkline of
604 a hunk is currently selected, then select the next hunk, if one exists,
604 a hunk is currently selected, then select the next hunk, if one exists,
605 or if not, the next header if one exists.
605 or if not, the next header if one exists.
606 """
606 """
607 #self.startprintline += 1 #debug
607 #self.startprintline += 1 #debug
608 currentitem = self.currentselecteditem
608 currentitem = self.currentselecteditem
609
609
610 nextitem = currentitem.nextitem()
610 nextitem = currentitem.nextitem()
611 # if there's no next item, keep the selection as-is
611 # if there's no next item, keep the selection as-is
612 if nextitem is None:
612 if nextitem is None:
613 nextitem = currentitem
613 nextitem = currentitem
614
614
615 self.currentselecteditem = nextitem
615 self.currentselecteditem = nextitem
616
616
617 def downarrowshiftevent(self):
617 def downarrowshiftevent(self):
618 """
618 """
619 select (if possible) the next item on the same level as the currently
619 select (if possible) the next item on the same level as the currently
620 selected item. otherwise, select (if possible) the next item on the
620 selected item. otherwise, select (if possible) the next item on the
621 same level as the parent item of the currently selected item.
621 same level as the parent item of the currently selected item.
622 """
622 """
623 currentitem = self.currentselecteditem
623 currentitem = self.currentselecteditem
624 nextitem = currentitem.nextsibling()
624 nextitem = currentitem.nextsibling()
625 # if there's no next sibling, try choosing the parent's nextsibling
625 # if there's no next sibling, try choosing the parent's nextsibling
626 if nextitem is None:
626 if nextitem is None:
627 try:
627 try:
628 nextitem = currentitem.parentitem().nextsibling()
628 nextitem = currentitem.parentitem().nextsibling()
629 except AttributeError:
629 except AttributeError:
630 # parentitem returned None, so nextsibling() can't be called
630 # parentitem returned None, so nextsibling() can't be called
631 nextitem = None
631 nextitem = None
632 if nextitem is None:
632 if nextitem is None:
633 # if parent has no next sibling, then no change...
633 # if parent has no next sibling, then no change...
634 nextitem = currentitem
634 nextitem = currentitem
635
635
636 self.currentselecteditem = nextitem
636 self.currentselecteditem = nextitem
637
637
638 def rightarrowevent(self):
638 def rightarrowevent(self):
639 """
639 """
640 select (if possible) the first of this item's child-items.
640 select (if possible) the first of this item's child-items.
641 """
641 """
642 currentitem = self.currentselecteditem
642 currentitem = self.currentselecteditem
643 nextitem = currentitem.firstchild()
643 nextitem = currentitem.firstchild()
644
644
645 # turn off folding if we want to show a child-item
645 # turn off folding if we want to show a child-item
646 if currentitem.folded:
646 if currentitem.folded:
647 self.togglefolded(currentitem)
647 self.togglefolded(currentitem)
648
648
649 if nextitem is None:
649 if nextitem is None:
650 # if no next item on parent-level, then no change...
650 # if no next item on parent-level, then no change...
651 nextitem = currentitem
651 nextitem = currentitem
652
652
653 self.currentselecteditem = nextitem
653 self.currentselecteditem = nextitem
654
654
655 def leftarrowevent(self):
655 def leftarrowevent(self):
656 """
656 """
657 if the current item can be folded (i.e. it is an unfolded header or
657 if the current item can be folded (i.e. it is an unfolded header or
658 hunk), then fold it. otherwise try select (if possible) the parent
658 hunk), then fold it. otherwise try select (if possible) the parent
659 of this item.
659 of this item.
660 """
660 """
661 currentitem = self.currentselecteditem
661 currentitem = self.currentselecteditem
662
662
663 # try to fold the item
663 # try to fold the item
664 if not isinstance(currentitem, uihunkline):
664 if not isinstance(currentitem, uihunkline):
665 if not currentitem.folded:
665 if not currentitem.folded:
666 self.togglefolded(item=currentitem)
666 self.togglefolded(item=currentitem)
667 return
667 return
668
668
669 # if it can't be folded, try to select the parent item
669 # if it can't be folded, try to select the parent item
670 nextitem = currentitem.parentitem()
670 nextitem = currentitem.parentitem()
671
671
672 if nextitem is None:
672 if nextitem is None:
673 # if no item on parent-level, then no change...
673 # if no item on parent-level, then no change...
674 nextitem = currentitem
674 nextitem = currentitem
675 if not nextitem.folded:
675 if not nextitem.folded:
676 self.togglefolded(item=nextitem)
676 self.togglefolded(item=nextitem)
677
677
678 self.currentselecteditem = nextitem
678 self.currentselecteditem = nextitem
679
679
680 def leftarrowshiftevent(self):
680 def leftarrowshiftevent(self):
681 """
681 """
682 select the header of the current item (or fold current item if the
682 select the header of the current item (or fold current item if the
683 current item is already a header).
683 current item is already a header).
684 """
684 """
685 currentitem = self.currentselecteditem
685 currentitem = self.currentselecteditem
686
686
687 if isinstance(currentitem, uiheader):
687 if isinstance(currentitem, uiheader):
688 if not currentitem.folded:
688 if not currentitem.folded:
689 self.togglefolded(item=currentitem)
689 self.togglefolded(item=currentitem)
690 return
690 return
691
691
692 # select the parent item recursively until we're at a header
692 # select the parent item recursively until we're at a header
693 while True:
693 while True:
694 nextitem = currentitem.parentitem()
694 nextitem = currentitem.parentitem()
695 if nextitem is None:
695 if nextitem is None:
696 break
696 break
697 else:
697 else:
698 currentitem = nextitem
698 currentitem = nextitem
699
699
700 self.currentselecteditem = currentitem
700 self.currentselecteditem = currentitem
701
701
702 def updatescroll(self):
702 def updatescroll(self):
703 "scroll the screen to fully show the currently-selected"
703 "scroll the screen to fully show the currently-selected"
704 selstart = self.selecteditemstartline
704 selstart = self.selecteditemstartline
705 selend = self.selecteditemendline
705 selend = self.selecteditemendline
706
706
707 padstart = self.firstlineofpadtoprint
707 padstart = self.firstlineofpadtoprint
708 padend = padstart + self.yscreensize - self.numstatuslines - 1
708 padend = padstart + self.yscreensize - self.numstatuslines - 1
709 # 'buffered' pad start/end values which scroll with a certain
709 # 'buffered' pad start/end values which scroll with a certain
710 # top/bottom context margin
710 # top/bottom context margin
711 padstartbuffered = padstart + 3
711 padstartbuffered = padstart + 3
712 padendbuffered = padend - 3
712 padendbuffered = padend - 3
713
713
714 if selend > padendbuffered:
714 if selend > padendbuffered:
715 self.scrolllines(selend - padendbuffered)
715 self.scrolllines(selend - padendbuffered)
716 elif selstart < padstartbuffered:
716 elif selstart < padstartbuffered:
717 # negative values scroll in pgup direction
717 # negative values scroll in pgup direction
718 self.scrolllines(selstart - padstartbuffered)
718 self.scrolllines(selstart - padstartbuffered)
719
719
720 def scrolllines(self, numlines):
720 def scrolllines(self, numlines):
721 "scroll the screen up (down) by numlines when numlines >0 (<0)."
721 "scroll the screen up (down) by numlines when numlines >0 (<0)."
722 self.firstlineofpadtoprint += numlines
722 self.firstlineofpadtoprint += numlines
723 if self.firstlineofpadtoprint < 0:
723 if self.firstlineofpadtoprint < 0:
724 self.firstlineofpadtoprint = 0
724 self.firstlineofpadtoprint = 0
725 if self.firstlineofpadtoprint > self.numpadlines - 1:
725 if self.firstlineofpadtoprint > self.numpadlines - 1:
726 self.firstlineofpadtoprint = self.numpadlines - 1
726 self.firstlineofpadtoprint = self.numpadlines - 1
727
727
728 def toggleapply(self, item=None):
728 def toggleapply(self, item=None):
729 """
729 """
730 toggle the applied flag of the specified item. if no item is specified,
730 toggle the applied flag of the specified item. if no item is specified,
731 toggle the flag of the currently selected item.
731 toggle the flag of the currently selected item.
732 """
732 """
733 if item is None:
733 if item is None:
734 item = self.currentselecteditem
734 item = self.currentselecteditem
735
735
736 item.applied = not item.applied
736 item.applied = not item.applied
737
737
738 if isinstance(item, uiheader):
738 if isinstance(item, uiheader):
739 item.partial = False
739 item.partial = False
740 if item.applied:
740 if item.applied:
741 # apply all its hunks
741 # apply all its hunks
742 for hnk in item.hunks:
742 for hnk in item.hunks:
743 hnk.applied = True
743 hnk.applied = True
744 # apply all their hunklines
744 # apply all their hunklines
745 for hunkline in hnk.changedlines:
745 for hunkline in hnk.changedlines:
746 hunkline.applied = True
746 hunkline.applied = True
747 else:
747 else:
748 # un-apply all its hunks
748 # un-apply all its hunks
749 for hnk in item.hunks:
749 for hnk in item.hunks:
750 hnk.applied = False
750 hnk.applied = False
751 hnk.partial = False
751 hnk.partial = False
752 # un-apply all their hunklines
752 # un-apply all their hunklines
753 for hunkline in hnk.changedlines:
753 for hunkline in hnk.changedlines:
754 hunkline.applied = False
754 hunkline.applied = False
755 elif isinstance(item, uihunk):
755 elif isinstance(item, uihunk):
756 item.partial = False
756 item.partial = False
757 # apply all it's hunklines
757 # apply all it's hunklines
758 for hunkline in item.changedlines:
758 for hunkline in item.changedlines:
759 hunkline.applied = item.applied
759 hunkline.applied = item.applied
760
760
761 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
761 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
762 allsiblingsapplied = not (False in siblingappliedstatus)
762 allsiblingsapplied = not (False in siblingappliedstatus)
763 nosiblingsapplied = not (True in siblingappliedstatus)
763 nosiblingsapplied = not (True in siblingappliedstatus)
764
764
765 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
765 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
766 somesiblingspartial = (True in siblingspartialstatus)
766 somesiblingspartial = (True in siblingspartialstatus)
767
767
768 #cases where applied or partial should be removed from header
768 #cases where applied or partial should be removed from header
769
769
770 # if no 'sibling' hunks are applied (including this hunk)
770 # if no 'sibling' hunks are applied (including this hunk)
771 if nosiblingsapplied:
771 if nosiblingsapplied:
772 if not item.header.special():
772 if not item.header.special():
773 item.header.applied = False
773 item.header.applied = False
774 item.header.partial = False
774 item.header.partial = False
775 else: # some/all parent siblings are applied
775 else: # some/all parent siblings are applied
776 item.header.applied = True
776 item.header.applied = True
777 item.header.partial = (somesiblingspartial or
777 item.header.partial = (somesiblingspartial or
778 not allsiblingsapplied)
778 not allsiblingsapplied)
779
779
780 elif isinstance(item, uihunkline):
780 elif isinstance(item, uihunkline):
781 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
781 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
782 allsiblingsapplied = not (False in siblingappliedstatus)
782 allsiblingsapplied = not (False in siblingappliedstatus)
783 nosiblingsapplied = not (True in siblingappliedstatus)
783 nosiblingsapplied = not (True in siblingappliedstatus)
784
784
785 # if no 'sibling' lines are applied
785 # if no 'sibling' lines are applied
786 if nosiblingsapplied:
786 if nosiblingsapplied:
787 item.hunk.applied = False
787 item.hunk.applied = False
788 item.hunk.partial = False
788 item.hunk.partial = False
789 elif allsiblingsapplied:
789 elif allsiblingsapplied:
790 item.hunk.applied = True
790 item.hunk.applied = True
791 item.hunk.partial = False
791 item.hunk.partial = False
792 else: # some siblings applied
792 else: # some siblings applied
793 item.hunk.applied = True
793 item.hunk.applied = True
794 item.hunk.partial = True
794 item.hunk.partial = True
795
795
796 parentsiblingsapplied = [hnk.applied for hnk
796 parentsiblingsapplied = [hnk.applied for hnk
797 in item.hunk.header.hunks]
797 in item.hunk.header.hunks]
798 noparentsiblingsapplied = not (True in parentsiblingsapplied)
798 noparentsiblingsapplied = not (True in parentsiblingsapplied)
799 allparentsiblingsapplied = not (False in parentsiblingsapplied)
799 allparentsiblingsapplied = not (False in parentsiblingsapplied)
800
800
801 parentsiblingspartial = [hnk.partial for hnk
801 parentsiblingspartial = [hnk.partial for hnk
802 in item.hunk.header.hunks]
802 in item.hunk.header.hunks]
803 someparentsiblingspartial = (True in parentsiblingspartial)
803 someparentsiblingspartial = (True in parentsiblingspartial)
804
804
805 # if all parent hunks are not applied, un-apply header
805 # if all parent hunks are not applied, un-apply header
806 if noparentsiblingsapplied:
806 if noparentsiblingsapplied:
807 if not item.hunk.header.special():
807 if not item.hunk.header.special():
808 item.hunk.header.applied = False
808 item.hunk.header.applied = False
809 item.hunk.header.partial = False
809 item.hunk.header.partial = False
810 # set the applied and partial status of the header if needed
810 # set the applied and partial status of the header if needed
811 else: # some/all parent siblings are applied
811 else: # some/all parent siblings are applied
812 item.hunk.header.applied = True
812 item.hunk.header.applied = True
813 item.hunk.header.partial = (someparentsiblingspartial or
813 item.hunk.header.partial = (someparentsiblingspartial or
814 not allparentsiblingsapplied)
814 not allparentsiblingsapplied)
815
815
816 def toggleall(self):
816 def toggleall(self):
817 "toggle the applied flag of all items."
817 "toggle the applied flag of all items."
818 if self.waslasttoggleallapplied: # then unapply them this time
818 if self.waslasttoggleallapplied: # then unapply them this time
819 for item in self.headerlist:
819 for item in self.headerlist:
820 if item.applied:
820 if item.applied:
821 self.toggleapply(item)
821 self.toggleapply(item)
822 else:
822 else:
823 for item in self.headerlist:
823 for item in self.headerlist:
824 if not item.applied:
824 if not item.applied:
825 self.toggleapply(item)
825 self.toggleapply(item)
826 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
826 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
827
827
828 def togglefolded(self, item=None, foldparent=False):
828 def togglefolded(self, item=None, foldparent=False):
829 "toggle folded flag of specified item (defaults to currently selected)"
829 "toggle folded flag of specified item (defaults to currently selected)"
830 if item is None:
830 if item is None:
831 item = self.currentselecteditem
831 item = self.currentselecteditem
832 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
832 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
833 if not isinstance(item, uiheader):
833 if not isinstance(item, uiheader):
834 # we need to select the parent item in this case
834 # we need to select the parent item in this case
835 self.currentselecteditem = item = item.parentitem()
835 self.currentselecteditem = item = item.parentitem()
836 elif item.neverunfolded:
836 elif item.neverunfolded:
837 item.neverunfolded = False
837 item.neverunfolded = False
838
838
839 # also fold any foldable children of the parent/current item
839 # also fold any foldable children of the parent/current item
840 if isinstance(item, uiheader): # the original or 'new' item
840 if isinstance(item, uiheader): # the original or 'new' item
841 for child in item.allchildren():
841 for child in item.allchildren():
842 child.folded = not item.folded
842 child.folded = not item.folded
843
843
844 if isinstance(item, (uiheader, uihunk)):
844 if isinstance(item, (uiheader, uihunk)):
845 item.folded = not item.folded
845 item.folded = not item.folded
846
846
847 def alignstring(self, instr, window):
847 def alignstring(self, instr, window):
848 """
848 """
849 add whitespace to the end of a string in order to make it fill
849 add whitespace to the end of a string in order to make it fill
850 the screen in the x direction. the current cursor position is
850 the screen in the x direction. the current cursor position is
851 taken into account when making this calculation. the string can span
851 taken into account when making this calculation. the string can span
852 multiple lines.
852 multiple lines.
853 """
853 """
854 y, xstart = window.getyx()
854 y, xstart = window.getyx()
855 width = self.xscreensize
855 width = self.xscreensize
856 # turn tabs into spaces
856 # turn tabs into spaces
857 instr = instr.expandtabs(4)
857 instr = instr.expandtabs(4)
858 strwidth = encoding.colwidth(instr)
858 strwidth = encoding.colwidth(instr)
859 numspaces = (width - ((strwidth + xstart) % width) - 1)
859 numspaces = (width - ((strwidth + xstart) % width) - 1)
860 return instr + " " * numspaces + "\n"
860 return instr + " " * numspaces + "\n"
861
861
862 def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None,
862 def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None,
863 pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False):
863 pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False):
864 """
864 """
865 print the string, text, with the specified colors and attributes, to
865 print the string, text, with the specified colors and attributes, to
866 the specified curses window object.
866 the specified curses window object.
867
867
868 the foreground and background colors are of the form
868 the foreground and background colors are of the form
869 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
869 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
870 magenta, red, white, yellow]. if pairname is provided, a color
870 magenta, red, white, yellow]. if pairname is provided, a color
871 pair will be looked up in the self.colorpairnames dictionary.
871 pair will be looked up in the self.colorpairnames dictionary.
872
872
873 attrlist is a list containing text attributes in the form of
873 attrlist is a list containing text attributes in the form of
874 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
874 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
875 underline].
875 underline].
876
876
877 if align == True, whitespace is added to the printed string such that
877 if align == True, whitespace is added to the printed string such that
878 the string stretches to the right border of the window.
878 the string stretches to the right border of the window.
879
879
880 if showwhtspc == True, trailing whitespace of a string is highlighted.
880 if showwhtspc == True, trailing whitespace of a string is highlighted.
881 """
881 """
882 # preprocess the text, converting tabs to spaces
882 # preprocess the text, converting tabs to spaces
883 text = text.expandtabs(4)
883 text = text.expandtabs(4)
884 # strip \n, and convert control characters to ^[char] representation
884 # strip \n, and convert control characters to ^[char] representation
885 text = re.sub(r'[\x00-\x08\x0a-\x1f]',
885 text = re.sub(r'[\x00-\x08\x0a-\x1f]',
886 lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n'))
886 lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n'))
887
887
888 if pair is not None:
888 if pair is not None:
889 colorpair = pair
889 colorpair = pair
890 elif pairname is not None:
890 elif pairname is not None:
891 colorpair = self.colorpairnames[pairname]
891 colorpair = self.colorpairnames[pairname]
892 else:
892 else:
893 if fgcolor is None:
893 if fgcolor is None:
894 fgcolor = -1
894 fgcolor = -1
895 if bgcolor is None:
895 if bgcolor is None:
896 bgcolor = -1
896 bgcolor = -1
897 if (fgcolor, bgcolor) in self.colorpairs:
897 if (fgcolor, bgcolor) in self.colorpairs:
898 colorpair = self.colorpairs[(fgcolor, bgcolor)]
898 colorpair = self.colorpairs[(fgcolor, bgcolor)]
899 else:
899 else:
900 colorpair = self.getcolorpair(fgcolor, bgcolor)
900 colorpair = self.getcolorpair(fgcolor, bgcolor)
901 # add attributes if possible
901 # add attributes if possible
902 if attrlist is None:
902 if attrlist is None:
903 attrlist = []
903 attrlist = []
904 if colorpair < 256:
904 if colorpair < 256:
905 # then it is safe to apply all attributes
905 # then it is safe to apply all attributes
906 for textattr in attrlist:
906 for textattr in attrlist:
907 colorpair |= textattr
907 colorpair |= textattr
908 else:
908 else:
909 # just apply a select few (safe?) attributes
909 # just apply a select few (safe?) attributes
910 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
910 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
911 if textattr in attrlist:
911 if textattr in attrlist:
912 colorpair |= textattr
912 colorpair |= textattr
913
913
914 y, xstart = self.chunkpad.getyx()
914 y, xstart = self.chunkpad.getyx()
915 t = "" # variable for counting lines printed
915 t = "" # variable for counting lines printed
916 # if requested, show trailing whitespace
916 # if requested, show trailing whitespace
917 if showwhtspc:
917 if showwhtspc:
918 origlen = len(text)
918 origlen = len(text)
919 text = text.rstrip(' \n') # tabs have already been expanded
919 text = text.rstrip(' \n') # tabs have already been expanded
920 strippedlen = len(text)
920 strippedlen = len(text)
921 numtrailingspaces = origlen - strippedlen
921 numtrailingspaces = origlen - strippedlen
922
922
923 if towin:
923 if towin:
924 window.addstr(text, colorpair)
924 window.addstr(text, colorpair)
925 t += text
925 t += text
926
926
927 if showwhtspc:
927 if showwhtspc:
928 wscolorpair = colorpair | curses.A_REVERSE
928 wscolorpair = colorpair | curses.A_REVERSE
929 if towin:
929 if towin:
930 for i in range(numtrailingspaces):
930 for i in range(numtrailingspaces):
931 window.addch(curses.ACS_CKBOARD, wscolorpair)
931 window.addch(curses.ACS_CKBOARD, wscolorpair)
932 t += " " * numtrailingspaces
932 t += " " * numtrailingspaces
933
933
934 if align:
934 if align:
935 if towin:
935 if towin:
936 extrawhitespace = self.alignstring("", window)
936 extrawhitespace = self.alignstring("", window)
937 window.addstr(extrawhitespace, colorpair)
937 window.addstr(extrawhitespace, colorpair)
938 else:
938 else:
939 # need to use t, since the x position hasn't incremented
939 # need to use t, since the x position hasn't incremented
940 extrawhitespace = self.alignstring(t, window)
940 extrawhitespace = self.alignstring(t, window)
941 t += extrawhitespace
941 t += extrawhitespace
942
942
943 # is reset to 0 at the beginning of printitem()
943 # is reset to 0 at the beginning of printitem()
944
944
945 linesprinted = (xstart + len(t)) / self.xscreensize
945 linesprinted = (xstart + len(t)) / self.xscreensize
946 self.linesprintedtopadsofar += linesprinted
946 self.linesprintedtopadsofar += linesprinted
947 return t
947 return t
948
948
949 def _getstatuslinesegments(self):
949 def _getstatuslinesegments(self):
950 """-> [str]. return segments"""
950 """-> [str]. return segments"""
951 selected = self.currentselecteditem.applied
951 segments = [
952 segments = [
952 _('Select hunks to record'),
953 _('Select hunks to record'),
953 '-',
954 '-',
954 _('[x]=selected **=collapsed'),
955 _('[x]=selected **=collapsed'),
955 _('c: confirm'),
956 _('c: confirm'),
956 _('q: abort'),
957 _('q: abort'),
957 _('arrow keys: move/expand/collapse'),
958 _('arrow keys: move/expand/collapse'),
958 _('space: select'),
959 _('space: deselect') if selected else _('space: select'),
959 _('?: help'),
960 _('?: help'),
960 ]
961 ]
961 return segments
962 return segments
962
963
963 def _getstatuslines(self):
964 def _getstatuslines(self):
964 """() -> [str]. return short help used in the top status window"""
965 """() -> [str]. return short help used in the top status window"""
965 if self.errorstr is not None:
966 if self.errorstr is not None:
966 lines = [self.errorstr, _('Press any key to continue')]
967 lines = [self.errorstr, _('Press any key to continue')]
967 else:
968 else:
968 # wrap segments to lines
969 # wrap segments to lines
969 segments = self._getstatuslinesegments()
970 segments = self._getstatuslinesegments()
970 width = self.xscreensize
971 width = self.xscreensize
971 lines = []
972 lines = []
972 lastwidth = width
973 lastwidth = width
973 for s in segments:
974 for s in segments:
974 w = encoding.colwidth(s)
975 w = encoding.colwidth(s)
975 sep = ' ' * (1 + (s and s[0] not in '-['))
976 sep = ' ' * (1 + (s and s[0] not in '-['))
976 if lastwidth + w + len(sep) >= width:
977 if lastwidth + w + len(sep) >= width:
977 lines.append(s)
978 lines.append(s)
978 lastwidth = w
979 lastwidth = w
979 else:
980 else:
980 lines[-1] += sep + s
981 lines[-1] += sep + s
981 lastwidth += w + len(sep)
982 lastwidth += w + len(sep)
982 if len(lines) != self.numstatuslines:
983 if len(lines) != self.numstatuslines:
983 self.numstatuslines = len(lines)
984 self.numstatuslines = len(lines)
984 self.statuswin.resize(self.numstatuslines, self.xscreensize)
985 self.statuswin.resize(self.numstatuslines, self.xscreensize)
985 return [util.ellipsis(l, self.xscreensize - 1) for l in lines]
986 return [util.ellipsis(l, self.xscreensize - 1) for l in lines]
986
987
987 def updatescreen(self):
988 def updatescreen(self):
988 self.statuswin.erase()
989 self.statuswin.erase()
989 self.chunkpad.erase()
990 self.chunkpad.erase()
990
991
991 printstring = self.printstring
992 printstring = self.printstring
992
993
993 # print out the status lines at the top
994 # print out the status lines at the top
994 try:
995 try:
995 for line in self._getstatuslines():
996 for line in self._getstatuslines():
996 printstring(self.statuswin, line, pairname="legend")
997 printstring(self.statuswin, line, pairname="legend")
997 self.statuswin.refresh()
998 self.statuswin.refresh()
998 except curses.error:
999 except curses.error:
999 pass
1000 pass
1000 if self.errorstr is not None:
1001 if self.errorstr is not None:
1001 return
1002 return
1002
1003
1003 # print out the patch in the remaining part of the window
1004 # print out the patch in the remaining part of the window
1004 try:
1005 try:
1005 self.printitem()
1006 self.printitem()
1006 self.updatescroll()
1007 self.updatescroll()
1007 self.chunkpad.refresh(self.firstlineofpadtoprint, 0,
1008 self.chunkpad.refresh(self.firstlineofpadtoprint, 0,
1008 self.numstatuslines, 0,
1009 self.numstatuslines, 0,
1009 self.yscreensize - self.numstatuslines,
1010 self.yscreensize - self.numstatuslines,
1010 self.xscreensize)
1011 self.xscreensize)
1011 except curses.error:
1012 except curses.error:
1012 pass
1013 pass
1013
1014
1014 def getstatusprefixstring(self, item):
1015 def getstatusprefixstring(self, item):
1015 """
1016 """
1016 create a string to prefix a line with which indicates whether 'item'
1017 create a string to prefix a line with which indicates whether 'item'
1017 is applied and/or folded.
1018 is applied and/or folded.
1018 """
1019 """
1019
1020
1020 # create checkbox string
1021 # create checkbox string
1021 if item.applied:
1022 if item.applied:
1022 if not isinstance(item, uihunkline) and item.partial:
1023 if not isinstance(item, uihunkline) and item.partial:
1023 checkbox = "[~]"
1024 checkbox = "[~]"
1024 else:
1025 else:
1025 checkbox = "[x]"
1026 checkbox = "[x]"
1026 else:
1027 else:
1027 checkbox = "[ ]"
1028 checkbox = "[ ]"
1028
1029
1029 try:
1030 try:
1030 if item.folded:
1031 if item.folded:
1031 checkbox += "**"
1032 checkbox += "**"
1032 if isinstance(item, uiheader):
1033 if isinstance(item, uiheader):
1033 # one of "m", "a", or "d" (modified, added, deleted)
1034 # one of "m", "a", or "d" (modified, added, deleted)
1034 filestatus = item.changetype
1035 filestatus = item.changetype
1035
1036
1036 checkbox += filestatus + " "
1037 checkbox += filestatus + " "
1037 else:
1038 else:
1038 checkbox += " "
1039 checkbox += " "
1039 if isinstance(item, uiheader):
1040 if isinstance(item, uiheader):
1040 # add two more spaces for headers
1041 # add two more spaces for headers
1041 checkbox += " "
1042 checkbox += " "
1042 except AttributeError: # not foldable
1043 except AttributeError: # not foldable
1043 checkbox += " "
1044 checkbox += " "
1044
1045
1045 return checkbox
1046 return checkbox
1046
1047
1047 def printheader(self, header, selected=False, towin=True,
1048 def printheader(self, header, selected=False, towin=True,
1048 ignorefolding=False):
1049 ignorefolding=False):
1049 """
1050 """
1050 print the header to the pad. if countlines is True, don't print
1051 print the header to the pad. if countlines is True, don't print
1051 anything, but just count the number of lines which would be printed.
1052 anything, but just count the number of lines which would be printed.
1052 """
1053 """
1053
1054
1054 outstr = ""
1055 outstr = ""
1055 text = header.prettystr()
1056 text = header.prettystr()
1056 chunkindex = self.chunklist.index(header)
1057 chunkindex = self.chunklist.index(header)
1057
1058
1058 if chunkindex != 0 and not header.folded:
1059 if chunkindex != 0 and not header.folded:
1059 # add separating line before headers
1060 # add separating line before headers
1060 outstr += self.printstring(self.chunkpad, '_' * self.xscreensize,
1061 outstr += self.printstring(self.chunkpad, '_' * self.xscreensize,
1061 towin=towin, align=False)
1062 towin=towin, align=False)
1062 # select color-pair based on if the header is selected
1063 # select color-pair based on if the header is selected
1063 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1064 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1064 attrlist=[curses.A_BOLD])
1065 attrlist=[curses.A_BOLD])
1065
1066
1066 # print out each line of the chunk, expanding it to screen width
1067 # print out each line of the chunk, expanding it to screen width
1067
1068
1068 # number of characters to indent lines on this level by
1069 # number of characters to indent lines on this level by
1069 indentnumchars = 0
1070 indentnumchars = 0
1070 checkbox = self.getstatusprefixstring(header)
1071 checkbox = self.getstatusprefixstring(header)
1071 if not header.folded or ignorefolding:
1072 if not header.folded or ignorefolding:
1072 textlist = text.split("\n")
1073 textlist = text.split("\n")
1073 linestr = checkbox + textlist[0]
1074 linestr = checkbox + textlist[0]
1074 else:
1075 else:
1075 linestr = checkbox + header.filename()
1076 linestr = checkbox + header.filename()
1076 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1077 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1077 towin=towin)
1078 towin=towin)
1078 if not header.folded or ignorefolding:
1079 if not header.folded or ignorefolding:
1079 if len(textlist) > 1:
1080 if len(textlist) > 1:
1080 for line in textlist[1:]:
1081 for line in textlist[1:]:
1081 linestr = " "*(indentnumchars + len(checkbox)) + line
1082 linestr = " "*(indentnumchars + len(checkbox)) + line
1082 outstr += self.printstring(self.chunkpad, linestr,
1083 outstr += self.printstring(self.chunkpad, linestr,
1083 pair=colorpair, towin=towin)
1084 pair=colorpair, towin=towin)
1084
1085
1085 return outstr
1086 return outstr
1086
1087
1087 def printhunklinesbefore(self, hunk, selected=False, towin=True,
1088 def printhunklinesbefore(self, hunk, selected=False, towin=True,
1088 ignorefolding=False):
1089 ignorefolding=False):
1089 "includes start/end line indicator"
1090 "includes start/end line indicator"
1090 outstr = ""
1091 outstr = ""
1091 # where hunk is in list of siblings
1092 # where hunk is in list of siblings
1092 hunkindex = hunk.header.hunks.index(hunk)
1093 hunkindex = hunk.header.hunks.index(hunk)
1093
1094
1094 if hunkindex != 0:
1095 if hunkindex != 0:
1095 # add separating line before headers
1096 # add separating line before headers
1096 outstr += self.printstring(self.chunkpad, ' '*self.xscreensize,
1097 outstr += self.printstring(self.chunkpad, ' '*self.xscreensize,
1097 towin=towin, align=False)
1098 towin=towin, align=False)
1098
1099
1099 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1100 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1100 attrlist=[curses.A_BOLD])
1101 attrlist=[curses.A_BOLD])
1101
1102
1102 # print out from-to line with checkbox
1103 # print out from-to line with checkbox
1103 checkbox = self.getstatusprefixstring(hunk)
1104 checkbox = self.getstatusprefixstring(hunk)
1104
1105
1105 lineprefix = " "*self.hunkindentnumchars + checkbox
1106 lineprefix = " "*self.hunkindentnumchars + checkbox
1106 frtoline = " " + hunk.getfromtoline().strip("\n")
1107 frtoline = " " + hunk.getfromtoline().strip("\n")
1107
1108
1108 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1109 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1109 align=False) # add uncolored checkbox/indent
1110 align=False) # add uncolored checkbox/indent
1110 outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair,
1111 outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair,
1111 towin=towin)
1112 towin=towin)
1112
1113
1113 if hunk.folded and not ignorefolding:
1114 if hunk.folded and not ignorefolding:
1114 # skip remainder of output
1115 # skip remainder of output
1115 return outstr
1116 return outstr
1116
1117
1117 # print out lines of the chunk preceeding changed-lines
1118 # print out lines of the chunk preceeding changed-lines
1118 for line in hunk.before:
1119 for line in hunk.before:
1119 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1120 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1120 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1121 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1121
1122
1122 return outstr
1123 return outstr
1123
1124
1124 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1125 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1125 outstr = ""
1126 outstr = ""
1126 if hunk.folded and not ignorefolding:
1127 if hunk.folded and not ignorefolding:
1127 return outstr
1128 return outstr
1128
1129
1129 # a bit superfluous, but to avoid hard-coding indent amount
1130 # a bit superfluous, but to avoid hard-coding indent amount
1130 checkbox = self.getstatusprefixstring(hunk)
1131 checkbox = self.getstatusprefixstring(hunk)
1131 for line in hunk.after:
1132 for line in hunk.after:
1132 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1133 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1133 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1134 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1134
1135
1135 return outstr
1136 return outstr
1136
1137
1137 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1138 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1138 outstr = ""
1139 outstr = ""
1139 checkbox = self.getstatusprefixstring(hunkline)
1140 checkbox = self.getstatusprefixstring(hunkline)
1140
1141
1141 linestr = hunkline.prettystr().strip("\n")
1142 linestr = hunkline.prettystr().strip("\n")
1142
1143
1143 # select color-pair based on whether line is an addition/removal
1144 # select color-pair based on whether line is an addition/removal
1144 if selected:
1145 if selected:
1145 colorpair = self.getcolorpair(name="selected")
1146 colorpair = self.getcolorpair(name="selected")
1146 elif linestr.startswith("+"):
1147 elif linestr.startswith("+"):
1147 colorpair = self.getcolorpair(name="addition")
1148 colorpair = self.getcolorpair(name="addition")
1148 elif linestr.startswith("-"):
1149 elif linestr.startswith("-"):
1149 colorpair = self.getcolorpair(name="deletion")
1150 colorpair = self.getcolorpair(name="deletion")
1150 elif linestr.startswith("\\"):
1151 elif linestr.startswith("\\"):
1151 colorpair = self.getcolorpair(name="normal")
1152 colorpair = self.getcolorpair(name="normal")
1152
1153
1153 lineprefix = " "*self.hunklineindentnumchars + checkbox
1154 lineprefix = " "*self.hunklineindentnumchars + checkbox
1154 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1155 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1155 align=False) # add uncolored checkbox/indent
1156 align=False) # add uncolored checkbox/indent
1156 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1157 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1157 towin=towin, showwhtspc=True)
1158 towin=towin, showwhtspc=True)
1158 return outstr
1159 return outstr
1159
1160
1160 def printitem(self, item=None, ignorefolding=False, recursechildren=True,
1161 def printitem(self, item=None, ignorefolding=False, recursechildren=True,
1161 towin=True):
1162 towin=True):
1162 """
1163 """
1163 use __printitem() to print the the specified item.applied.
1164 use __printitem() to print the the specified item.applied.
1164 if item is not specified, then print the entire patch.
1165 if item is not specified, then print the entire patch.
1165 (hiding folded elements, etc. -- see __printitem() docstring)
1166 (hiding folded elements, etc. -- see __printitem() docstring)
1166 """
1167 """
1167
1168
1168 if item is None:
1169 if item is None:
1169 item = self.headerlist
1170 item = self.headerlist
1170 if recursechildren:
1171 if recursechildren:
1171 self.linesprintedtopadsofar = 0
1172 self.linesprintedtopadsofar = 0
1172
1173
1173 outstr = []
1174 outstr = []
1174 self.__printitem(item, ignorefolding, recursechildren, outstr,
1175 self.__printitem(item, ignorefolding, recursechildren, outstr,
1175 towin=towin)
1176 towin=towin)
1176 return ''.join(outstr)
1177 return ''.join(outstr)
1177
1178
1178 def outofdisplayedarea(self):
1179 def outofdisplayedarea(self):
1179 y, _ = self.chunkpad.getyx() # cursor location
1180 y, _ = self.chunkpad.getyx() # cursor location
1180 # * 2 here works but an optimization would be the max number of
1181 # * 2 here works but an optimization would be the max number of
1181 # consecutive non selectable lines
1182 # consecutive non selectable lines
1182 # i.e the max number of context line for any hunk in the patch
1183 # i.e the max number of context line for any hunk in the patch
1183 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1184 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1184 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1185 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1185 return y < miny or y > maxy
1186 return y < miny or y > maxy
1186
1187
1187 def handleselection(self, item, recursechildren):
1188 def handleselection(self, item, recursechildren):
1188 selected = (item is self.currentselecteditem)
1189 selected = (item is self.currentselecteditem)
1189 if selected and recursechildren:
1190 if selected and recursechildren:
1190 # assumes line numbering starting from line 0
1191 # assumes line numbering starting from line 0
1191 self.selecteditemstartline = self.linesprintedtopadsofar
1192 self.selecteditemstartline = self.linesprintedtopadsofar
1192 selecteditemlines = self.getnumlinesdisplayed(item,
1193 selecteditemlines = self.getnumlinesdisplayed(item,
1193 recursechildren=False)
1194 recursechildren=False)
1194 self.selecteditemendline = (self.selecteditemstartline +
1195 self.selecteditemendline = (self.selecteditemstartline +
1195 selecteditemlines - 1)
1196 selecteditemlines - 1)
1196 return selected
1197 return selected
1197
1198
1198 def __printitem(self, item, ignorefolding, recursechildren, outstr,
1199 def __printitem(self, item, ignorefolding, recursechildren, outstr,
1199 towin=True):
1200 towin=True):
1200 """
1201 """
1201 recursive method for printing out patch/header/hunk/hunk-line data to
1202 recursive method for printing out patch/header/hunk/hunk-line data to
1202 screen. also returns a string with all of the content of the displayed
1203 screen. also returns a string with all of the content of the displayed
1203 patch (not including coloring, etc.).
1204 patch (not including coloring, etc.).
1204
1205
1205 if ignorefolding is True, then folded items are printed out.
1206 if ignorefolding is True, then folded items are printed out.
1206
1207
1207 if recursechildren is False, then only print the item without its
1208 if recursechildren is False, then only print the item without its
1208 child items.
1209 child items.
1209 """
1210 """
1210
1211
1211 if towin and self.outofdisplayedarea():
1212 if towin and self.outofdisplayedarea():
1212 return
1213 return
1213
1214
1214 selected = self.handleselection(item, recursechildren)
1215 selected = self.handleselection(item, recursechildren)
1215
1216
1216 # patch object is a list of headers
1217 # patch object is a list of headers
1217 if isinstance(item, patch):
1218 if isinstance(item, patch):
1218 if recursechildren:
1219 if recursechildren:
1219 for hdr in item:
1220 for hdr in item:
1220 self.__printitem(hdr, ignorefolding,
1221 self.__printitem(hdr, ignorefolding,
1221 recursechildren, outstr, towin)
1222 recursechildren, outstr, towin)
1222 # todo: eliminate all isinstance() calls
1223 # todo: eliminate all isinstance() calls
1223 if isinstance(item, uiheader):
1224 if isinstance(item, uiheader):
1224 outstr.append(self.printheader(item, selected, towin=towin,
1225 outstr.append(self.printheader(item, selected, towin=towin,
1225 ignorefolding=ignorefolding))
1226 ignorefolding=ignorefolding))
1226 if recursechildren:
1227 if recursechildren:
1227 for hnk in item.hunks:
1228 for hnk in item.hunks:
1228 self.__printitem(hnk, ignorefolding,
1229 self.__printitem(hnk, ignorefolding,
1229 recursechildren, outstr, towin)
1230 recursechildren, outstr, towin)
1230 elif (isinstance(item, uihunk) and
1231 elif (isinstance(item, uihunk) and
1231 ((not item.header.folded) or ignorefolding)):
1232 ((not item.header.folded) or ignorefolding)):
1232 # print the hunk data which comes before the changed-lines
1233 # print the hunk data which comes before the changed-lines
1233 outstr.append(self.printhunklinesbefore(item, selected, towin=towin,
1234 outstr.append(self.printhunklinesbefore(item, selected, towin=towin,
1234 ignorefolding=ignorefolding))
1235 ignorefolding=ignorefolding))
1235 if recursechildren:
1236 if recursechildren:
1236 for l in item.changedlines:
1237 for l in item.changedlines:
1237 self.__printitem(l, ignorefolding,
1238 self.__printitem(l, ignorefolding,
1238 recursechildren, outstr, towin)
1239 recursechildren, outstr, towin)
1239 outstr.append(self.printhunklinesafter(item, towin=towin,
1240 outstr.append(self.printhunklinesafter(item, towin=towin,
1240 ignorefolding=ignorefolding))
1241 ignorefolding=ignorefolding))
1241 elif (isinstance(item, uihunkline) and
1242 elif (isinstance(item, uihunkline) and
1242 ((not item.hunk.folded) or ignorefolding)):
1243 ((not item.hunk.folded) or ignorefolding)):
1243 outstr.append(self.printhunkchangedline(item, selected,
1244 outstr.append(self.printhunkchangedline(item, selected,
1244 towin=towin))
1245 towin=towin))
1245
1246
1246 return outstr
1247 return outstr
1247
1248
1248 def getnumlinesdisplayed(self, item=None, ignorefolding=False,
1249 def getnumlinesdisplayed(self, item=None, ignorefolding=False,
1249 recursechildren=True):
1250 recursechildren=True):
1250 """
1251 """
1251 return the number of lines which would be displayed if the item were
1252 return the number of lines which would be displayed if the item were
1252 to be printed to the display. the item will not be printed to the
1253 to be printed to the display. the item will not be printed to the
1253 display (pad).
1254 display (pad).
1254 if no item is given, assume the entire patch.
1255 if no item is given, assume the entire patch.
1255 if ignorefolding is True, folded items will be unfolded when counting
1256 if ignorefolding is True, folded items will be unfolded when counting
1256 the number of lines.
1257 the number of lines.
1257 """
1258 """
1258
1259
1259 # temporarily disable printing to windows by printstring
1260 # temporarily disable printing to windows by printstring
1260 patchdisplaystring = self.printitem(item, ignorefolding,
1261 patchdisplaystring = self.printitem(item, ignorefolding,
1261 recursechildren, towin=False)
1262 recursechildren, towin=False)
1262 numlines = len(patchdisplaystring) / self.xscreensize
1263 numlines = len(patchdisplaystring) / self.xscreensize
1263 return numlines
1264 return numlines
1264
1265
1265 def sigwinchhandler(self, n, frame):
1266 def sigwinchhandler(self, n, frame):
1266 "handle window resizing"
1267 "handle window resizing"
1267 try:
1268 try:
1268 curses.endwin()
1269 curses.endwin()
1269 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1270 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1270 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1271 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1271 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1272 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1272 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1273 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1273 except curses.error:
1274 except curses.error:
1274 pass
1275 pass
1275
1276
1276 def getcolorpair(self, fgcolor=None, bgcolor=None, name=None,
1277 def getcolorpair(self, fgcolor=None, bgcolor=None, name=None,
1277 attrlist=None):
1278 attrlist=None):
1278 """
1279 """
1279 get a curses color pair, adding it to self.colorpairs if it is not
1280 get a curses color pair, adding it to self.colorpairs if it is not
1280 already defined. an optional string, name, can be passed as a shortcut
1281 already defined. an optional string, name, can be passed as a shortcut
1281 for referring to the color-pair. by default, if no arguments are
1282 for referring to the color-pair. by default, if no arguments are
1282 specified, the white foreground / black background color-pair is
1283 specified, the white foreground / black background color-pair is
1283 returned.
1284 returned.
1284
1285
1285 it is expected that this function will be used exclusively for
1286 it is expected that this function will be used exclusively for
1286 initializing color pairs, and not curses.init_pair().
1287 initializing color pairs, and not curses.init_pair().
1287
1288
1288 attrlist is used to 'flavor' the returned color-pair. this information
1289 attrlist is used to 'flavor' the returned color-pair. this information
1289 is not stored in self.colorpairs. it contains attribute values like
1290 is not stored in self.colorpairs. it contains attribute values like
1290 curses.A_BOLD.
1291 curses.A_BOLD.
1291 """
1292 """
1292
1293
1293 if (name is not None) and name in self.colorpairnames:
1294 if (name is not None) and name in self.colorpairnames:
1294 # then get the associated color pair and return it
1295 # then get the associated color pair and return it
1295 colorpair = self.colorpairnames[name]
1296 colorpair = self.colorpairnames[name]
1296 else:
1297 else:
1297 if fgcolor is None:
1298 if fgcolor is None:
1298 fgcolor = -1
1299 fgcolor = -1
1299 if bgcolor is None:
1300 if bgcolor is None:
1300 bgcolor = -1
1301 bgcolor = -1
1301 if (fgcolor, bgcolor) in self.colorpairs:
1302 if (fgcolor, bgcolor) in self.colorpairs:
1302 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1303 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1303 else:
1304 else:
1304 pairindex = len(self.colorpairs) + 1
1305 pairindex = len(self.colorpairs) + 1
1305 curses.init_pair(pairindex, fgcolor, bgcolor)
1306 curses.init_pair(pairindex, fgcolor, bgcolor)
1306 colorpair = self.colorpairs[(fgcolor, bgcolor)] = (
1307 colorpair = self.colorpairs[(fgcolor, bgcolor)] = (
1307 curses.color_pair(pairindex))
1308 curses.color_pair(pairindex))
1308 if name is not None:
1309 if name is not None:
1309 self.colorpairnames[name] = curses.color_pair(pairindex)
1310 self.colorpairnames[name] = curses.color_pair(pairindex)
1310
1311
1311 # add attributes if possible
1312 # add attributes if possible
1312 if attrlist is None:
1313 if attrlist is None:
1313 attrlist = []
1314 attrlist = []
1314 if colorpair < 256:
1315 if colorpair < 256:
1315 # then it is safe to apply all attributes
1316 # then it is safe to apply all attributes
1316 for textattr in attrlist:
1317 for textattr in attrlist:
1317 colorpair |= textattr
1318 colorpair |= textattr
1318 else:
1319 else:
1319 # just apply a select few (safe?) attributes
1320 # just apply a select few (safe?) attributes
1320 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1321 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1321 if textattrib in attrlist:
1322 if textattrib in attrlist:
1322 colorpair |= textattrib
1323 colorpair |= textattrib
1323 return colorpair
1324 return colorpair
1324
1325
1325 def initcolorpair(self, *args, **kwargs):
1326 def initcolorpair(self, *args, **kwargs):
1326 "same as getcolorpair."
1327 "same as getcolorpair."
1327 self.getcolorpair(*args, **kwargs)
1328 self.getcolorpair(*args, **kwargs)
1328
1329
1329 def helpwindow(self):
1330 def helpwindow(self):
1330 "print a help window to the screen. exit after any keypress."
1331 "print a help window to the screen. exit after any keypress."
1331 helptext = _(
1332 helptext = _(
1332 """ [press any key to return to the patch-display]
1333 """ [press any key to return to the patch-display]
1333
1334
1334 crecord allows you to interactively choose among the changes you have made,
1335 crecord allows you to interactively choose among the changes you have made,
1335 and confirm only those changes you select for further processing by the command
1336 and confirm only those changes you select for further processing by the command
1336 you are running (commit/shelve/revert), after confirming the selected
1337 you are running (commit/shelve/revert), after confirming the selected
1337 changes, the unselected changes are still present in your working copy, so you
1338 changes, the unselected changes are still present in your working copy, so you
1338 can use crecord multiple times to split large changes into smaller changesets.
1339 can use crecord multiple times to split large changes into smaller changesets.
1339 the following are valid keystrokes:
1340 the following are valid keystrokes:
1340
1341
1341 [space] : (un-)select item ([~]/[x] = partly/fully applied)
1342 [space] : (un-)select item ([~]/[x] = partly/fully applied)
1342 A : (un-)select all items
1343 A : (un-)select all items
1343 up/down-arrow [k/j] : go to previous/next unfolded item
1344 up/down-arrow [k/j] : go to previous/next unfolded item
1344 pgup/pgdn [K/J] : go to previous/next item of same type
1345 pgup/pgdn [K/J] : go to previous/next item of same type
1345 right/left-arrow [l/h] : go to child item / parent item
1346 right/left-arrow [l/h] : go to child item / parent item
1346 shift-left-arrow [H] : go to parent header / fold selected header
1347 shift-left-arrow [H] : go to parent header / fold selected header
1347 f : fold / unfold item, hiding/revealing its children
1348 f : fold / unfold item, hiding/revealing its children
1348 F : fold / unfold parent item and all of its ancestors
1349 F : fold / unfold parent item and all of its ancestors
1349 ctrl-l : scroll the selected line to the top of the screen
1350 ctrl-l : scroll the selected line to the top of the screen
1350 m : edit / resume editing the commit message
1351 m : edit / resume editing the commit message
1351 e : edit the currently selected hunk
1352 e : edit the currently selected hunk
1352 a : toggle amend mode, only with commit -i
1353 a : toggle amend mode, only with commit -i
1353 c : confirm selected changes
1354 c : confirm selected changes
1354 r : review/edit and confirm selected changes
1355 r : review/edit and confirm selected changes
1355 q : quit without confirming (no changes will be made)
1356 q : quit without confirming (no changes will be made)
1356 ? : help (what you're currently reading)""")
1357 ? : help (what you're currently reading)""")
1357
1358
1358 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1359 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1359 helplines = helptext.split("\n")
1360 helplines = helptext.split("\n")
1360 helplines = helplines + [" "]*(
1361 helplines = helplines + [" "]*(
1361 self.yscreensize - self.numstatuslines - len(helplines) - 1)
1362 self.yscreensize - self.numstatuslines - len(helplines) - 1)
1362 try:
1363 try:
1363 for line in helplines:
1364 for line in helplines:
1364 self.printstring(helpwin, line, pairname="legend")
1365 self.printstring(helpwin, line, pairname="legend")
1365 except curses.error:
1366 except curses.error:
1366 pass
1367 pass
1367 helpwin.refresh()
1368 helpwin.refresh()
1368 try:
1369 try:
1369 helpwin.getkey()
1370 helpwin.getkey()
1370 except curses.error:
1371 except curses.error:
1371 pass
1372 pass
1372
1373
1373 def confirmationwindow(self, windowtext):
1374 def confirmationwindow(self, windowtext):
1374 "display an informational window, then wait for and return a keypress."
1375 "display an informational window, then wait for and return a keypress."
1375
1376
1376 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1377 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1377 try:
1378 try:
1378 lines = windowtext.split("\n")
1379 lines = windowtext.split("\n")
1379 for line in lines:
1380 for line in lines:
1380 self.printstring(confirmwin, line, pairname="selected")
1381 self.printstring(confirmwin, line, pairname="selected")
1381 except curses.error:
1382 except curses.error:
1382 pass
1383 pass
1383 self.stdscr.refresh()
1384 self.stdscr.refresh()
1384 confirmwin.refresh()
1385 confirmwin.refresh()
1385 try:
1386 try:
1386 response = chr(self.stdscr.getch())
1387 response = chr(self.stdscr.getch())
1387 except ValueError:
1388 except ValueError:
1388 response = None
1389 response = None
1389
1390
1390 return response
1391 return response
1391
1392
1392 def reviewcommit(self):
1393 def reviewcommit(self):
1393 """ask for 'y' to be pressed to confirm selected. return True if
1394 """ask for 'y' to be pressed to confirm selected. return True if
1394 confirmed."""
1395 confirmed."""
1395 confirmtext = _(
1396 confirmtext = _(
1396 """if you answer yes to the following, the your currently chosen patch chunks
1397 """if you answer yes to the following, the your currently chosen patch chunks
1397 will be loaded into an editor. you may modify the patch from the editor, and
1398 will be loaded into an editor. you may modify the patch from the editor, and
1398 save the changes if you wish to change the patch. otherwise, you can just
1399 save the changes if you wish to change the patch. otherwise, you can just
1399 close the editor without saving to accept the current patch as-is.
1400 close the editor without saving to accept the current patch as-is.
1400
1401
1401 note: don't add/remove lines unless you also modify the range information.
1402 note: don't add/remove lines unless you also modify the range information.
1402 failing to follow this rule will result in the commit aborting.
1403 failing to follow this rule will result in the commit aborting.
1403
1404
1404 are you sure you want to review/edit and confirm the selected changes [yn]?
1405 are you sure you want to review/edit and confirm the selected changes [yn]?
1405 """)
1406 """)
1406 response = self.confirmationwindow(confirmtext)
1407 response = self.confirmationwindow(confirmtext)
1407 if response is None:
1408 if response is None:
1408 response = "n"
1409 response = "n"
1409 if response.lower().startswith("y"):
1410 if response.lower().startswith("y"):
1410 return True
1411 return True
1411 else:
1412 else:
1412 return False
1413 return False
1413
1414
1414 def toggleamend(self, opts, test):
1415 def toggleamend(self, opts, test):
1415 """Toggle the amend flag.
1416 """Toggle the amend flag.
1416
1417
1417 When the amend flag is set, a commit will modify the most recently
1418 When the amend flag is set, a commit will modify the most recently
1418 committed changeset, instead of creating a new changeset. Otherwise, a
1419 committed changeset, instead of creating a new changeset. Otherwise, a
1419 new changeset will be created (the normal commit behavior).
1420 new changeset will be created (the normal commit behavior).
1420 """
1421 """
1421
1422
1422 try:
1423 try:
1423 ver = float(util.version()[:3])
1424 ver = float(util.version()[:3])
1424 except ValueError:
1425 except ValueError:
1425 ver = 1
1426 ver = 1
1426 if ver < 2.19:
1427 if ver < 2.19:
1427 msg = _("The amend option is unavailable with hg versions < 2.2\n\n"
1428 msg = _("The amend option is unavailable with hg versions < 2.2\n\n"
1428 "Press any key to continue.")
1429 "Press any key to continue.")
1429 elif opts.get('amend') is None:
1430 elif opts.get('amend') is None:
1430 opts['amend'] = True
1431 opts['amend'] = True
1431 msg = _("Amend option is turned on -- committing the currently "
1432 msg = _("Amend option is turned on -- committing the currently "
1432 "selected changes will not create a new changeset, but "
1433 "selected changes will not create a new changeset, but "
1433 "instead update the most recently committed changeset.\n\n"
1434 "instead update the most recently committed changeset.\n\n"
1434 "Press any key to continue.")
1435 "Press any key to continue.")
1435 elif opts.get('amend') is True:
1436 elif opts.get('amend') is True:
1436 opts['amend'] = None
1437 opts['amend'] = None
1437 msg = _("Amend option is turned off -- committing the currently "
1438 msg = _("Amend option is turned off -- committing the currently "
1438 "selected changes will create a new changeset.\n\n"
1439 "selected changes will create a new changeset.\n\n"
1439 "Press any key to continue.")
1440 "Press any key to continue.")
1440 if not test:
1441 if not test:
1441 self.confirmationwindow(msg)
1442 self.confirmationwindow(msg)
1442
1443
1443 def recenterdisplayedarea(self):
1444 def recenterdisplayedarea(self):
1444 """
1445 """
1445 once we scrolled with pg up pg down we can be pointing outside of the
1446 once we scrolled with pg up pg down we can be pointing outside of the
1446 display zone. we print the patch with towin=False to compute the
1447 display zone. we print the patch with towin=False to compute the
1447 location of the selected item even though it is outside of the displayed
1448 location of the selected item even though it is outside of the displayed
1448 zone and then update the scroll.
1449 zone and then update the scroll.
1449 """
1450 """
1450 self.printitem(towin=False)
1451 self.printitem(towin=False)
1451 self.updatescroll()
1452 self.updatescroll()
1452
1453
1453 def toggleedit(self, item=None, test=False):
1454 def toggleedit(self, item=None, test=False):
1454 """
1455 """
1455 edit the currently selected chunk
1456 edit the currently selected chunk
1456 """
1457 """
1457 def updateui(self):
1458 def updateui(self):
1458 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1459 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1459 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1460 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1460 self.updatescroll()
1461 self.updatescroll()
1461 self.stdscr.refresh()
1462 self.stdscr.refresh()
1462 self.statuswin.refresh()
1463 self.statuswin.refresh()
1463 self.stdscr.keypad(1)
1464 self.stdscr.keypad(1)
1464
1465
1465 def editpatchwitheditor(self, chunk):
1466 def editpatchwitheditor(self, chunk):
1466 if chunk is None:
1467 if chunk is None:
1467 self.ui.write(_('cannot edit patch for whole file'))
1468 self.ui.write(_('cannot edit patch for whole file'))
1468 self.ui.write("\n")
1469 self.ui.write("\n")
1469 return None
1470 return None
1470 if chunk.header.binary():
1471 if chunk.header.binary():
1471 self.ui.write(_('cannot edit patch for binary file'))
1472 self.ui.write(_('cannot edit patch for binary file'))
1472 self.ui.write("\n")
1473 self.ui.write("\n")
1473 return None
1474 return None
1474
1475
1475 # write the initial patch
1476 # write the initial patch
1476 patch = stringio()
1477 patch = stringio()
1477 patch.write(diffhelptext + hunkhelptext)
1478 patch.write(diffhelptext + hunkhelptext)
1478 chunk.header.write(patch)
1479 chunk.header.write(patch)
1479 chunk.write(patch)
1480 chunk.write(patch)
1480
1481
1481 # start the editor and wait for it to complete
1482 # start the editor and wait for it to complete
1482 try:
1483 try:
1483 patch = self.ui.edit(patch.getvalue(), "",
1484 patch = self.ui.edit(patch.getvalue(), "",
1484 extra={"suffix": ".diff"})
1485 extra={"suffix": ".diff"})
1485 except error.Abort as exc:
1486 except error.Abort as exc:
1486 self.errorstr = str(exc)
1487 self.errorstr = str(exc)
1487 return None
1488 return None
1488
1489
1489 # remove comment lines
1490 # remove comment lines
1490 patch = [line + '\n' for line in patch.splitlines()
1491 patch = [line + '\n' for line in patch.splitlines()
1491 if not line.startswith('#')]
1492 if not line.startswith('#')]
1492 return patchmod.parsepatch(patch)
1493 return patchmod.parsepatch(patch)
1493
1494
1494 if item is None:
1495 if item is None:
1495 item = self.currentselecteditem
1496 item = self.currentselecteditem
1496 if isinstance(item, uiheader):
1497 if isinstance(item, uiheader):
1497 return
1498 return
1498 if isinstance(item, uihunkline):
1499 if isinstance(item, uihunkline):
1499 item = item.parentitem()
1500 item = item.parentitem()
1500 if not isinstance(item, uihunk):
1501 if not isinstance(item, uihunk):
1501 return
1502 return
1502
1503
1503 # To go back to that hunk or its replacement at the end of the edit
1504 # To go back to that hunk or its replacement at the end of the edit
1504 itemindex = item.parentitem().hunks.index(item)
1505 itemindex = item.parentitem().hunks.index(item)
1505
1506
1506 beforeadded, beforeremoved = item.added, item.removed
1507 beforeadded, beforeremoved = item.added, item.removed
1507 newpatches = editpatchwitheditor(self, item)
1508 newpatches = editpatchwitheditor(self, item)
1508 if newpatches is None:
1509 if newpatches is None:
1509 if not test:
1510 if not test:
1510 updateui(self)
1511 updateui(self)
1511 return
1512 return
1512 header = item.header
1513 header = item.header
1513 editedhunkindex = header.hunks.index(item)
1514 editedhunkindex = header.hunks.index(item)
1514 hunksbefore = header.hunks[:editedhunkindex]
1515 hunksbefore = header.hunks[:editedhunkindex]
1515 hunksafter = header.hunks[editedhunkindex + 1:]
1516 hunksafter = header.hunks[editedhunkindex + 1:]
1516 newpatchheader = newpatches[0]
1517 newpatchheader = newpatches[0]
1517 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1518 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1518 newadded = sum([h.added for h in newhunks])
1519 newadded = sum([h.added for h in newhunks])
1519 newremoved = sum([h.removed for h in newhunks])
1520 newremoved = sum([h.removed for h in newhunks])
1520 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1521 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1521
1522
1522 for h in hunksafter:
1523 for h in hunksafter:
1523 h.toline += offset
1524 h.toline += offset
1524 for h in newhunks:
1525 for h in newhunks:
1525 h.folded = False
1526 h.folded = False
1526 header.hunks = hunksbefore + newhunks + hunksafter
1527 header.hunks = hunksbefore + newhunks + hunksafter
1527 if self.emptypatch():
1528 if self.emptypatch():
1528 header.hunks = hunksbefore + [item] + hunksafter
1529 header.hunks = hunksbefore + [item] + hunksafter
1529 self.currentselecteditem = header
1530 self.currentselecteditem = header
1530 if len(header.hunks) > itemindex:
1531 if len(header.hunks) > itemindex:
1531 self.currentselecteditem = header.hunks[itemindex]
1532 self.currentselecteditem = header.hunks[itemindex]
1532
1533
1533 if not test:
1534 if not test:
1534 updateui(self)
1535 updateui(self)
1535
1536
1536 def emptypatch(self):
1537 def emptypatch(self):
1537 item = self.headerlist
1538 item = self.headerlist
1538 if not item:
1539 if not item:
1539 return True
1540 return True
1540 for header in item:
1541 for header in item:
1541 if header.hunks:
1542 if header.hunks:
1542 return False
1543 return False
1543 return True
1544 return True
1544
1545
1545 def handlekeypressed(self, keypressed, test=False):
1546 def handlekeypressed(self, keypressed, test=False):
1546 """
1547 """
1547 Perform actions based on pressed keys.
1548 Perform actions based on pressed keys.
1548
1549
1549 Return true to exit the main loop.
1550 Return true to exit the main loop.
1550 """
1551 """
1551 if keypressed in ["k", "KEY_UP"]:
1552 if keypressed in ["k", "KEY_UP"]:
1552 self.uparrowevent()
1553 self.uparrowevent()
1553 if keypressed in ["K", "KEY_PPAGE"]:
1554 if keypressed in ["K", "KEY_PPAGE"]:
1554 self.uparrowshiftevent()
1555 self.uparrowshiftevent()
1555 elif keypressed in ["j", "KEY_DOWN"]:
1556 elif keypressed in ["j", "KEY_DOWN"]:
1556 self.downarrowevent()
1557 self.downarrowevent()
1557 elif keypressed in ["J", "KEY_NPAGE"]:
1558 elif keypressed in ["J", "KEY_NPAGE"]:
1558 self.downarrowshiftevent()
1559 self.downarrowshiftevent()
1559 elif keypressed in ["l", "KEY_RIGHT"]:
1560 elif keypressed in ["l", "KEY_RIGHT"]:
1560 self.rightarrowevent()
1561 self.rightarrowevent()
1561 elif keypressed in ["h", "KEY_LEFT"]:
1562 elif keypressed in ["h", "KEY_LEFT"]:
1562 self.leftarrowevent()
1563 self.leftarrowevent()
1563 elif keypressed in ["H", "KEY_SLEFT"]:
1564 elif keypressed in ["H", "KEY_SLEFT"]:
1564 self.leftarrowshiftevent()
1565 self.leftarrowshiftevent()
1565 elif keypressed in ["q"]:
1566 elif keypressed in ["q"]:
1566 raise error.Abort(_('user quit'))
1567 raise error.Abort(_('user quit'))
1567 elif keypressed in ['a']:
1568 elif keypressed in ['a']:
1568 self.toggleamend(self.opts, test)
1569 self.toggleamend(self.opts, test)
1569 elif keypressed in ["c"]:
1570 elif keypressed in ["c"]:
1570 return True
1571 return True
1571 elif test and keypressed in ['X']:
1572 elif test and keypressed in ['X']:
1572 return True
1573 return True
1573 elif keypressed in ["r"]:
1574 elif keypressed in ["r"]:
1574 if self.reviewcommit():
1575 if self.reviewcommit():
1575 self.opts['review'] = True
1576 self.opts['review'] = True
1576 return True
1577 return True
1577 elif test and keypressed in ['R']:
1578 elif test and keypressed in ['R']:
1578 self.opts['review'] = True
1579 self.opts['review'] = True
1579 return True
1580 return True
1580 elif keypressed in [' '] or (test and keypressed in ["TOGGLE"]):
1581 elif keypressed in [' '] or (test and keypressed in ["TOGGLE"]):
1581 self.toggleapply()
1582 self.toggleapply()
1582 elif keypressed in ['A']:
1583 elif keypressed in ['A']:
1583 self.toggleall()
1584 self.toggleall()
1584 elif keypressed in ['e']:
1585 elif keypressed in ['e']:
1585 self.toggleedit(test=test)
1586 self.toggleedit(test=test)
1586 elif keypressed in ["f"]:
1587 elif keypressed in ["f"]:
1587 self.togglefolded()
1588 self.togglefolded()
1588 elif keypressed in ["F"]:
1589 elif keypressed in ["F"]:
1589 self.togglefolded(foldparent=True)
1590 self.togglefolded(foldparent=True)
1590 elif keypressed in ["?"]:
1591 elif keypressed in ["?"]:
1591 self.helpwindow()
1592 self.helpwindow()
1592 self.stdscr.clear()
1593 self.stdscr.clear()
1593 self.stdscr.refresh()
1594 self.stdscr.refresh()
1594 elif curses.unctrl(keypressed) in ["^L"]:
1595 elif curses.unctrl(keypressed) in ["^L"]:
1595 # scroll the current line to the top of the screen
1596 # scroll the current line to the top of the screen
1596 self.scrolllines(self.selecteditemstartline)
1597 self.scrolllines(self.selecteditemstartline)
1597
1598
1598 def main(self, stdscr):
1599 def main(self, stdscr):
1599 """
1600 """
1600 method to be wrapped by curses.wrapper() for selecting chunks.
1601 method to be wrapped by curses.wrapper() for selecting chunks.
1601 """
1602 """
1602
1603
1603 origsigwinchhandler = signal.signal(signal.SIGWINCH,
1604 origsigwinchhandler = signal.signal(signal.SIGWINCH,
1604 self.sigwinchhandler)
1605 self.sigwinchhandler)
1605 self.stdscr = stdscr
1606 self.stdscr = stdscr
1606 # error during initialization, cannot be printed in the curses
1607 # error during initialization, cannot be printed in the curses
1607 # interface, it should be printed by the calling code
1608 # interface, it should be printed by the calling code
1608 self.initerr = None
1609 self.initerr = None
1609 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1610 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1610
1611
1611 curses.start_color()
1612 curses.start_color()
1612 curses.use_default_colors()
1613 curses.use_default_colors()
1613
1614
1614 # available colors: black, blue, cyan, green, magenta, white, yellow
1615 # available colors: black, blue, cyan, green, magenta, white, yellow
1615 # init_pair(color_id, foreground_color, background_color)
1616 # init_pair(color_id, foreground_color, background_color)
1616 self.initcolorpair(None, None, name="normal")
1617 self.initcolorpair(None, None, name="normal")
1617 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA,
1618 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA,
1618 name="selected")
1619 name="selected")
1619 self.initcolorpair(curses.COLOR_RED, None, name="deletion")
1620 self.initcolorpair(curses.COLOR_RED, None, name="deletion")
1620 self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
1621 self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
1621 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
1622 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
1622 # newwin([height, width,] begin_y, begin_x)
1623 # newwin([height, width,] begin_y, begin_x)
1623 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1624 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1624 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1625 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1625
1626
1626 # figure out how much space to allocate for the chunk-pad which is
1627 # figure out how much space to allocate for the chunk-pad which is
1627 # used for displaying the patch
1628 # used for displaying the patch
1628
1629
1629 # stupid hack to prevent getnumlinesdisplayed from failing
1630 # stupid hack to prevent getnumlinesdisplayed from failing
1630 self.chunkpad = curses.newpad(1, self.xscreensize)
1631 self.chunkpad = curses.newpad(1, self.xscreensize)
1631
1632
1632 # add 1 so to account for last line text reaching end of line
1633 # add 1 so to account for last line text reaching end of line
1633 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1634 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1634
1635
1635 try:
1636 try:
1636 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1637 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1637 except curses.error:
1638 except curses.error:
1638 self.initerr = _('this diff is too large to be displayed')
1639 self.initerr = _('this diff is too large to be displayed')
1639 return
1640 return
1640 # initialize selecteditemendline (initial start-line is 0)
1641 # initialize selecteditemendline (initial start-line is 0)
1641 self.selecteditemendline = self.getnumlinesdisplayed(
1642 self.selecteditemendline = self.getnumlinesdisplayed(
1642 self.currentselecteditem, recursechildren=False)
1643 self.currentselecteditem, recursechildren=False)
1643
1644
1644 while True:
1645 while True:
1645 self.updatescreen()
1646 self.updatescreen()
1646 try:
1647 try:
1647 keypressed = self.statuswin.getkey()
1648 keypressed = self.statuswin.getkey()
1648 if self.errorstr is not None:
1649 if self.errorstr is not None:
1649 self.errorstr = None
1650 self.errorstr = None
1650 continue
1651 continue
1651 except curses.error:
1652 except curses.error:
1652 keypressed = "foobar"
1653 keypressed = "foobar"
1653 if self.handlekeypressed(keypressed):
1654 if self.handlekeypressed(keypressed):
1654 break
1655 break
1655 signal.signal(signal.SIGWINCH, origsigwinchhandler)
1656 signal.signal(signal.SIGWINCH, origsigwinchhandler)
General Comments 0
You need to be logged in to leave comments. Login now