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