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