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