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