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