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