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