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