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