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