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