##// END OF EJS Templates
py3: make sure regex is bytes in crecord.py...
Pulkit Goyal -
r36323:a8bca9d0 default
parent child Browse files
Show More
@@ -1,1773 +1,1773 b''
1 # stuff related specifically to patch manipulation / parsing
1 # stuff related specifically to patch manipulation / parsing
2 #
2 #
3 # Copyright 2008 Mark Edgington <edgimar@gmail.com>
3 # Copyright 2008 Mark Edgington <edgimar@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 #
7 #
8 # This code is based on the Mark Edgington's crecord extension.
8 # This code is based on the Mark Edgington's crecord extension.
9 # (Itself based on Bryan O'Sullivan's record extension.)
9 # (Itself based on Bryan O'Sullivan's record extension.)
10
10
11 from __future__ import absolute_import
11 from __future__ import absolute_import
12
12
13 import 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 reversehunk(self):
430 def reversehunk(self):
431 """return a recordhunk which is the reverse of the hunk
431 """return a recordhunk which is the reverse of the hunk
432
432
433 Assuming the displayed patch is diff(A, B) result. The returned hunk is
433 Assuming the displayed patch is diff(A, B) result. The returned hunk is
434 intended to be applied to B, instead of A.
434 intended to be applied to B, instead of A.
435
435
436 For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and
436 For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and
437 the user made the following selection:
437 the user made the following selection:
438
438
439 0
439 0
440 [x] -1 [x]: selected
440 [x] -1 [x]: selected
441 [ ] -2 [ ]: not selected
441 [ ] -2 [ ]: not selected
442 [x] +3
442 [x] +3
443 [ ] +4
443 [ ] +4
444 [x] +5
444 [x] +5
445 6
445 6
446
446
447 This function returns a hunk like:
447 This function returns a hunk like:
448
448
449 0
449 0
450 -3
450 -3
451 -4
451 -4
452 -5
452 -5
453 +1
453 +1
454 +4
454 +4
455 6
455 6
456
456
457 Note "4" was first deleted then added. That's because "4" exists in B
457 Note "4" was first deleted then added. That's because "4" exists in B
458 side and "-4" must exist between "-3" and "-5" to make the patch
458 side and "-4" must exist between "-3" and "-5" to make the patch
459 applicable to B.
459 applicable to B.
460 """
460 """
461 dels = []
461 dels = []
462 adds = []
462 adds = []
463 for line in self.changedlines:
463 for line in self.changedlines:
464 text = line.linetext
464 text = line.linetext
465 if line.applied:
465 if line.applied:
466 if text[0] == '+':
466 if text[0] == '+':
467 dels.append(text[1:])
467 dels.append(text[1:])
468 elif text[0] == '-':
468 elif text[0] == '-':
469 adds.append(text[1:])
469 adds.append(text[1:])
470 elif text[0] == '+':
470 elif text[0] == '+':
471 dels.append(text[1:])
471 dels.append(text[1:])
472 adds.append(text[1:])
472 adds.append(text[1:])
473 hunk = ['-%s' % l for l in dels] + ['+%s' % l for l in adds]
473 hunk = ['-%s' % l for l in dels] + ['+%s' % l for l in adds]
474 h = self._hunk
474 h = self._hunk
475 return patchmod.recordhunk(h.header, h.toline, h.fromline, h.proc,
475 return patchmod.recordhunk(h.header, h.toline, h.fromline, h.proc,
476 h.before, hunk, h.after)
476 h.before, hunk, h.after)
477
477
478 def __getattr__(self, name):
478 def __getattr__(self, name):
479 return getattr(self._hunk, name)
479 return getattr(self._hunk, name)
480
480
481 def __repr__(self):
481 def __repr__(self):
482 return '<hunk %r@%d>' % (self.filename(), self.fromline)
482 return '<hunk %r@%d>' % (self.filename(), self.fromline)
483
483
484 def filterpatch(ui, chunks, chunkselector, operation=None):
484 def filterpatch(ui, chunks, chunkselector, operation=None):
485 """interactively filter patch chunks into applied-only chunks"""
485 """interactively filter patch chunks into applied-only chunks"""
486 chunks = list(chunks)
486 chunks = list(chunks)
487 # convert chunks list into structure suitable for displaying/modifying
487 # convert chunks list into structure suitable for displaying/modifying
488 # with curses. create a list of headers only.
488 # with curses. create a list of headers only.
489 headers = [c for c in chunks if isinstance(c, patchmod.header)]
489 headers = [c for c in chunks if isinstance(c, patchmod.header)]
490
490
491 # if there are no changed files
491 # if there are no changed files
492 if len(headers) == 0:
492 if len(headers) == 0:
493 return [], {}
493 return [], {}
494 uiheaders = [uiheader(h) for h in headers]
494 uiheaders = [uiheader(h) for h in headers]
495 # let user choose headers/hunks/lines, and mark their applied flags
495 # let user choose headers/hunks/lines, and mark their applied flags
496 # accordingly
496 # accordingly
497 ret = chunkselector(ui, uiheaders, operation=operation)
497 ret = chunkselector(ui, uiheaders, operation=operation)
498 appliedhunklist = []
498 appliedhunklist = []
499 for hdr in uiheaders:
499 for hdr in uiheaders:
500 if (hdr.applied and
500 if (hdr.applied and
501 (hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0)):
501 (hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0)):
502 appliedhunklist.append(hdr)
502 appliedhunklist.append(hdr)
503 fixoffset = 0
503 fixoffset = 0
504 for hnk in hdr.hunks:
504 for hnk in hdr.hunks:
505 if hnk.applied:
505 if hnk.applied:
506 appliedhunklist.append(hnk)
506 appliedhunklist.append(hnk)
507 # adjust the 'to'-line offset of the hunk to be correct
507 # adjust the 'to'-line offset of the hunk to be correct
508 # after de-activating some of the other hunks for this file
508 # after de-activating some of the other hunks for this file
509 if fixoffset:
509 if fixoffset:
510 #hnk = copy.copy(hnk) # necessary??
510 #hnk = copy.copy(hnk) # necessary??
511 hnk.toline += fixoffset
511 hnk.toline += fixoffset
512 else:
512 else:
513 fixoffset += hnk.removed - hnk.added
513 fixoffset += hnk.removed - hnk.added
514
514
515 return (appliedhunklist, ret)
515 return (appliedhunklist, ret)
516
516
517 def chunkselector(ui, headerlist, operation=None):
517 def chunkselector(ui, headerlist, operation=None):
518 """
518 """
519 curses interface to get selection of chunks, and mark the applied flags
519 curses interface to get selection of chunks, and mark the applied flags
520 of the chosen chunks.
520 of the chosen chunks.
521 """
521 """
522 ui.write(_('starting interactive selection\n'))
522 ui.write(_('starting interactive selection\n'))
523 chunkselector = curseschunkselector(headerlist, ui, operation)
523 chunkselector = curseschunkselector(headerlist, ui, operation)
524 origsigtstp = sentinel = object()
524 origsigtstp = sentinel = object()
525 if util.safehasattr(signal, 'SIGTSTP'):
525 if util.safehasattr(signal, 'SIGTSTP'):
526 origsigtstp = signal.getsignal(signal.SIGTSTP)
526 origsigtstp = signal.getsignal(signal.SIGTSTP)
527 try:
527 try:
528 curses.wrapper(chunkselector.main)
528 curses.wrapper(chunkselector.main)
529 if chunkselector.initerr is not None:
529 if chunkselector.initerr is not None:
530 raise error.Abort(chunkselector.initerr)
530 raise error.Abort(chunkselector.initerr)
531 # ncurses does not restore signal handler for SIGTSTP
531 # ncurses does not restore signal handler for SIGTSTP
532 finally:
532 finally:
533 if origsigtstp is not sentinel:
533 if origsigtstp is not sentinel:
534 signal.signal(signal.SIGTSTP, origsigtstp)
534 signal.signal(signal.SIGTSTP, origsigtstp)
535 return chunkselector.opts
535 return chunkselector.opts
536
536
537 def testdecorator(testfn, f):
537 def testdecorator(testfn, f):
538 def u(*args, **kwargs):
538 def u(*args, **kwargs):
539 return f(testfn, *args, **kwargs)
539 return f(testfn, *args, **kwargs)
540 return u
540 return u
541
541
542 def testchunkselector(testfn, ui, headerlist, operation=None):
542 def testchunkselector(testfn, ui, headerlist, operation=None):
543 """
543 """
544 test interface to get selection of chunks, and mark the applied flags
544 test interface to get selection of chunks, and mark the applied flags
545 of the chosen chunks.
545 of the chosen chunks.
546 """
546 """
547 chunkselector = curseschunkselector(headerlist, ui, operation)
547 chunkselector = curseschunkselector(headerlist, ui, operation)
548 if testfn and os.path.exists(testfn):
548 if testfn and os.path.exists(testfn):
549 testf = open(testfn)
549 testf = open(testfn)
550 testcommands = map(lambda x: x.rstrip('\n'), testf.readlines())
550 testcommands = map(lambda x: x.rstrip('\n'), testf.readlines())
551 testf.close()
551 testf.close()
552 while True:
552 while True:
553 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
553 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
554 break
554 break
555 return chunkselector.opts
555 return chunkselector.opts
556
556
557 _headermessages = { # {operation: text}
557 _headermessages = { # {operation: text}
558 'apply': _('Select hunks to apply'),
558 'apply': _('Select hunks to apply'),
559 'discard': _('Select hunks to discard'),
559 'discard': _('Select hunks to discard'),
560 None: _('Select hunks to record'),
560 None: _('Select hunks to record'),
561 }
561 }
562
562
563 class curseschunkselector(object):
563 class curseschunkselector(object):
564 def __init__(self, headerlist, ui, operation=None):
564 def __init__(self, headerlist, ui, operation=None):
565 # put the headers into a patch object
565 # put the headers into a patch object
566 self.headerlist = patch(headerlist)
566 self.headerlist = patch(headerlist)
567
567
568 self.ui = ui
568 self.ui = ui
569 self.opts = {}
569 self.opts = {}
570
570
571 self.errorstr = None
571 self.errorstr = None
572 # list of all chunks
572 # list of all chunks
573 self.chunklist = []
573 self.chunklist = []
574 for h in headerlist:
574 for h in headerlist:
575 self.chunklist.append(h)
575 self.chunklist.append(h)
576 self.chunklist.extend(h.hunks)
576 self.chunklist.extend(h.hunks)
577
577
578 # dictionary mapping (fgcolor, bgcolor) pairs to the
578 # dictionary mapping (fgcolor, bgcolor) pairs to the
579 # corresponding curses color-pair value.
579 # corresponding curses color-pair value.
580 self.colorpairs = {}
580 self.colorpairs = {}
581 # maps custom nicknames of color-pairs to curses color-pair values
581 # maps custom nicknames of color-pairs to curses color-pair values
582 self.colorpairnames = {}
582 self.colorpairnames = {}
583
583
584 # Honor color setting of ui section. Keep colored setup as
584 # Honor color setting of ui section. Keep colored setup as
585 # long as not explicitly set to a falsy value - especially,
585 # long as not explicitly set to a falsy value - especially,
586 # when not set at all. This is to stay most compatible with
586 # when not set at all. This is to stay most compatible with
587 # previous (color only) behaviour.
587 # previous (color only) behaviour.
588 uicolor = util.parsebool(self.ui.config('ui', 'color'))
588 uicolor = util.parsebool(self.ui.config('ui', 'color'))
589 self.usecolor = uicolor is not False
589 self.usecolor = uicolor is not False
590
590
591 # the currently selected header, hunk, or hunk-line
591 # the currently selected header, hunk, or hunk-line
592 self.currentselecteditem = self.headerlist[0]
592 self.currentselecteditem = self.headerlist[0]
593
593
594 # updated when printing out patch-display -- the 'lines' here are the
594 # updated when printing out patch-display -- the 'lines' here are the
595 # line positions *in the pad*, not on the screen.
595 # line positions *in the pad*, not on the screen.
596 self.selecteditemstartline = 0
596 self.selecteditemstartline = 0
597 self.selecteditemendline = None
597 self.selecteditemendline = None
598
598
599 # define indentation levels
599 # define indentation levels
600 self.headerindentnumchars = 0
600 self.headerindentnumchars = 0
601 self.hunkindentnumchars = 3
601 self.hunkindentnumchars = 3
602 self.hunklineindentnumchars = 6
602 self.hunklineindentnumchars = 6
603
603
604 # the first line of the pad to print to the screen
604 # the first line of the pad to print to the screen
605 self.firstlineofpadtoprint = 0
605 self.firstlineofpadtoprint = 0
606
606
607 # keeps track of the number of lines in the pad
607 # keeps track of the number of lines in the pad
608 self.numpadlines = None
608 self.numpadlines = None
609
609
610 self.numstatuslines = 1
610 self.numstatuslines = 1
611
611
612 # keep a running count of the number of lines printed to the pad
612 # keep a running count of the number of lines printed to the pad
613 # (used for determining when the selected item begins/ends)
613 # (used for determining when the selected item begins/ends)
614 self.linesprintedtopadsofar = 0
614 self.linesprintedtopadsofar = 0
615
615
616 # the first line of the pad which is visible on the screen
616 # the first line of the pad which is visible on the screen
617 self.firstlineofpadtoprint = 0
617 self.firstlineofpadtoprint = 0
618
618
619 # stores optional text for a commit comment provided by the user
619 # stores optional text for a commit comment provided by the user
620 self.commenttext = ""
620 self.commenttext = ""
621
621
622 # if the last 'toggle all' command caused all changes to be applied
622 # if the last 'toggle all' command caused all changes to be applied
623 self.waslasttoggleallapplied = True
623 self.waslasttoggleallapplied = True
624
624
625 # affects some ui text
625 # affects some ui text
626 if operation not in _headermessages:
626 if operation not in _headermessages:
627 raise error.ProgrammingError('unexpected operation: %s' % operation)
627 raise error.ProgrammingError('unexpected operation: %s' % operation)
628 self.operation = operation
628 self.operation = operation
629
629
630 def uparrowevent(self):
630 def uparrowevent(self):
631 """
631 """
632 try to select the previous item to the current item that has the
632 try to select the previous item to the current item that has the
633 most-indented level. for example, if a hunk is selected, try to select
633 most-indented level. for example, if a hunk is selected, try to select
634 the last hunkline of the hunk prior to the selected hunk. or, if
634 the last hunkline of the hunk prior to the selected hunk. or, if
635 the first hunkline of a hunk is currently selected, then select the
635 the first hunkline of a hunk is currently selected, then select the
636 hunk itself.
636 hunk itself.
637 """
637 """
638 currentitem = self.currentselecteditem
638 currentitem = self.currentselecteditem
639
639
640 nextitem = currentitem.previtem()
640 nextitem = currentitem.previtem()
641
641
642 if nextitem is None:
642 if nextitem is None:
643 # if no parent item (i.e. currentitem is the first header), then
643 # if no parent item (i.e. currentitem is the first header), then
644 # no change...
644 # no change...
645 nextitem = currentitem
645 nextitem = currentitem
646
646
647 self.currentselecteditem = nextitem
647 self.currentselecteditem = nextitem
648
648
649 def uparrowshiftevent(self):
649 def uparrowshiftevent(self):
650 """
650 """
651 select (if possible) the previous item on the same level as the
651 select (if possible) the previous item on the same level as the
652 currently selected item. otherwise, select (if possible) the
652 currently selected item. otherwise, select (if possible) the
653 parent-item of the currently selected item.
653 parent-item of the currently selected item.
654 """
654 """
655 currentitem = self.currentselecteditem
655 currentitem = self.currentselecteditem
656 nextitem = currentitem.prevsibling()
656 nextitem = currentitem.prevsibling()
657 # if there's no previous sibling, try choosing the parent
657 # if there's no previous sibling, try choosing the parent
658 if nextitem is None:
658 if nextitem is None:
659 nextitem = currentitem.parentitem()
659 nextitem = currentitem.parentitem()
660 if nextitem is None:
660 if nextitem is None:
661 # if no parent item (i.e. currentitem is the first header), then
661 # if no parent item (i.e. currentitem is the first header), then
662 # no change...
662 # no change...
663 nextitem = currentitem
663 nextitem = currentitem
664
664
665 self.currentselecteditem = nextitem
665 self.currentselecteditem = nextitem
666
666
667 def downarrowevent(self):
667 def downarrowevent(self):
668 """
668 """
669 try to select the next item to the current item that has the
669 try to select the next item to the current item that has the
670 most-indented level. for example, if a hunk is selected, select
670 most-indented level. for example, if a hunk is selected, select
671 the first hunkline of the selected hunk. or, if the last hunkline of
671 the first hunkline of the selected hunk. or, if the last hunkline of
672 a hunk is currently selected, then select the next hunk, if one exists,
672 a hunk is currently selected, then select the next hunk, if one exists,
673 or if not, the next header if one exists.
673 or if not, the next header if one exists.
674 """
674 """
675 #self.startprintline += 1 #debug
675 #self.startprintline += 1 #debug
676 currentitem = self.currentselecteditem
676 currentitem = self.currentselecteditem
677
677
678 nextitem = currentitem.nextitem()
678 nextitem = currentitem.nextitem()
679 # if there's no next item, keep the selection as-is
679 # if there's no next item, keep the selection as-is
680 if nextitem is None:
680 if nextitem is None:
681 nextitem = currentitem
681 nextitem = currentitem
682
682
683 self.currentselecteditem = nextitem
683 self.currentselecteditem = nextitem
684
684
685 def downarrowshiftevent(self):
685 def downarrowshiftevent(self):
686 """
686 """
687 select (if possible) the next item on the same level as the currently
687 select (if possible) the next item on the same level as the currently
688 selected item. otherwise, select (if possible) the next item on the
688 selected item. otherwise, select (if possible) the next item on the
689 same level as the parent item of the currently selected item.
689 same level as the parent item of the currently selected item.
690 """
690 """
691 currentitem = self.currentselecteditem
691 currentitem = self.currentselecteditem
692 nextitem = currentitem.nextsibling()
692 nextitem = currentitem.nextsibling()
693 # if there's no next sibling, try choosing the parent's nextsibling
693 # if there's no next sibling, try choosing the parent's nextsibling
694 if nextitem is None:
694 if nextitem is None:
695 try:
695 try:
696 nextitem = currentitem.parentitem().nextsibling()
696 nextitem = currentitem.parentitem().nextsibling()
697 except AttributeError:
697 except AttributeError:
698 # parentitem returned None, so nextsibling() can't be called
698 # parentitem returned None, so nextsibling() can't be called
699 nextitem = None
699 nextitem = None
700 if nextitem is None:
700 if nextitem is None:
701 # if parent has no next sibling, then no change...
701 # if parent has no next sibling, then no change...
702 nextitem = currentitem
702 nextitem = currentitem
703
703
704 self.currentselecteditem = nextitem
704 self.currentselecteditem = nextitem
705
705
706 def rightarrowevent(self):
706 def rightarrowevent(self):
707 """
707 """
708 select (if possible) the first of this item's child-items.
708 select (if possible) the first of this item's child-items.
709 """
709 """
710 currentitem = self.currentselecteditem
710 currentitem = self.currentselecteditem
711 nextitem = currentitem.firstchild()
711 nextitem = currentitem.firstchild()
712
712
713 # turn off folding if we want to show a child-item
713 # turn off folding if we want to show a child-item
714 if currentitem.folded:
714 if currentitem.folded:
715 self.togglefolded(currentitem)
715 self.togglefolded(currentitem)
716
716
717 if nextitem is None:
717 if nextitem is None:
718 # if no next item on parent-level, then no change...
718 # if no next item on parent-level, then no change...
719 nextitem = currentitem
719 nextitem = currentitem
720
720
721 self.currentselecteditem = nextitem
721 self.currentselecteditem = nextitem
722
722
723 def leftarrowevent(self):
723 def leftarrowevent(self):
724 """
724 """
725 if the current item can be folded (i.e. it is an unfolded header or
725 if the current item can be folded (i.e. it is an unfolded header or
726 hunk), then fold it. otherwise try select (if possible) the parent
726 hunk), then fold it. otherwise try select (if possible) the parent
727 of this item.
727 of this item.
728 """
728 """
729 currentitem = self.currentselecteditem
729 currentitem = self.currentselecteditem
730
730
731 # try to fold the item
731 # try to fold the item
732 if not isinstance(currentitem, uihunkline):
732 if not isinstance(currentitem, uihunkline):
733 if not currentitem.folded:
733 if not currentitem.folded:
734 self.togglefolded(item=currentitem)
734 self.togglefolded(item=currentitem)
735 return
735 return
736
736
737 # if it can't be folded, try to select the parent item
737 # if it can't be folded, try to select the parent item
738 nextitem = currentitem.parentitem()
738 nextitem = currentitem.parentitem()
739
739
740 if nextitem is None:
740 if nextitem is None:
741 # if no item on parent-level, then no change...
741 # if no item on parent-level, then no change...
742 nextitem = currentitem
742 nextitem = currentitem
743 if not nextitem.folded:
743 if not nextitem.folded:
744 self.togglefolded(item=nextitem)
744 self.togglefolded(item=nextitem)
745
745
746 self.currentselecteditem = nextitem
746 self.currentselecteditem = nextitem
747
747
748 def leftarrowshiftevent(self):
748 def leftarrowshiftevent(self):
749 """
749 """
750 select the header of the current item (or fold current item if the
750 select the header of the current item (or fold current item if the
751 current item is already a header).
751 current item is already a header).
752 """
752 """
753 currentitem = self.currentselecteditem
753 currentitem = self.currentselecteditem
754
754
755 if isinstance(currentitem, uiheader):
755 if isinstance(currentitem, uiheader):
756 if not currentitem.folded:
756 if not currentitem.folded:
757 self.togglefolded(item=currentitem)
757 self.togglefolded(item=currentitem)
758 return
758 return
759
759
760 # select the parent item recursively until we're at a header
760 # select the parent item recursively until we're at a header
761 while True:
761 while True:
762 nextitem = currentitem.parentitem()
762 nextitem = currentitem.parentitem()
763 if nextitem is None:
763 if nextitem is None:
764 break
764 break
765 else:
765 else:
766 currentitem = nextitem
766 currentitem = nextitem
767
767
768 self.currentselecteditem = currentitem
768 self.currentselecteditem = currentitem
769
769
770 def updatescroll(self):
770 def updatescroll(self):
771 "scroll the screen to fully show the currently-selected"
771 "scroll the screen to fully show the currently-selected"
772 selstart = self.selecteditemstartline
772 selstart = self.selecteditemstartline
773 selend = self.selecteditemendline
773 selend = self.selecteditemendline
774
774
775 padstart = self.firstlineofpadtoprint
775 padstart = self.firstlineofpadtoprint
776 padend = padstart + self.yscreensize - self.numstatuslines - 1
776 padend = padstart + self.yscreensize - self.numstatuslines - 1
777 # 'buffered' pad start/end values which scroll with a certain
777 # 'buffered' pad start/end values which scroll with a certain
778 # top/bottom context margin
778 # top/bottom context margin
779 padstartbuffered = padstart + 3
779 padstartbuffered = padstart + 3
780 padendbuffered = padend - 3
780 padendbuffered = padend - 3
781
781
782 if selend > padendbuffered:
782 if selend > padendbuffered:
783 self.scrolllines(selend - padendbuffered)
783 self.scrolllines(selend - padendbuffered)
784 elif selstart < padstartbuffered:
784 elif selstart < padstartbuffered:
785 # negative values scroll in pgup direction
785 # negative values scroll in pgup direction
786 self.scrolllines(selstart - padstartbuffered)
786 self.scrolllines(selstart - padstartbuffered)
787
787
788 def scrolllines(self, numlines):
788 def scrolllines(self, numlines):
789 "scroll the screen up (down) by numlines when numlines >0 (<0)."
789 "scroll the screen up (down) by numlines when numlines >0 (<0)."
790 self.firstlineofpadtoprint += numlines
790 self.firstlineofpadtoprint += numlines
791 if self.firstlineofpadtoprint < 0:
791 if self.firstlineofpadtoprint < 0:
792 self.firstlineofpadtoprint = 0
792 self.firstlineofpadtoprint = 0
793 if self.firstlineofpadtoprint > self.numpadlines - 1:
793 if self.firstlineofpadtoprint > self.numpadlines - 1:
794 self.firstlineofpadtoprint = self.numpadlines - 1
794 self.firstlineofpadtoprint = self.numpadlines - 1
795
795
796 def toggleapply(self, item=None):
796 def toggleapply(self, item=None):
797 """
797 """
798 toggle the applied flag of the specified item. if no item is specified,
798 toggle the applied flag of the specified item. if no item is specified,
799 toggle the flag of the currently selected item.
799 toggle the flag of the currently selected item.
800 """
800 """
801 if item is None:
801 if item is None:
802 item = self.currentselecteditem
802 item = self.currentselecteditem
803
803
804 item.applied = not item.applied
804 item.applied = not item.applied
805
805
806 if isinstance(item, uiheader):
806 if isinstance(item, uiheader):
807 item.partial = False
807 item.partial = False
808 if item.applied:
808 if item.applied:
809 # apply all its hunks
809 # apply all its hunks
810 for hnk in item.hunks:
810 for hnk in item.hunks:
811 hnk.applied = True
811 hnk.applied = True
812 # apply all their hunklines
812 # apply all their hunklines
813 for hunkline in hnk.changedlines:
813 for hunkline in hnk.changedlines:
814 hunkline.applied = True
814 hunkline.applied = True
815 else:
815 else:
816 # un-apply all its hunks
816 # un-apply all its hunks
817 for hnk in item.hunks:
817 for hnk in item.hunks:
818 hnk.applied = False
818 hnk.applied = False
819 hnk.partial = False
819 hnk.partial = False
820 # un-apply all their hunklines
820 # un-apply all their hunklines
821 for hunkline in hnk.changedlines:
821 for hunkline in hnk.changedlines:
822 hunkline.applied = False
822 hunkline.applied = False
823 elif isinstance(item, uihunk):
823 elif isinstance(item, uihunk):
824 item.partial = False
824 item.partial = False
825 # apply all it's hunklines
825 # apply all it's hunklines
826 for hunkline in item.changedlines:
826 for hunkline in item.changedlines:
827 hunkline.applied = item.applied
827 hunkline.applied = item.applied
828
828
829 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
829 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
830 allsiblingsapplied = not (False in siblingappliedstatus)
830 allsiblingsapplied = not (False in siblingappliedstatus)
831 nosiblingsapplied = not (True in siblingappliedstatus)
831 nosiblingsapplied = not (True in siblingappliedstatus)
832
832
833 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
833 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
834 somesiblingspartial = (True in siblingspartialstatus)
834 somesiblingspartial = (True in siblingspartialstatus)
835
835
836 #cases where applied or partial should be removed from header
836 #cases where applied or partial should be removed from header
837
837
838 # if no 'sibling' hunks are applied (including this hunk)
838 # if no 'sibling' hunks are applied (including this hunk)
839 if nosiblingsapplied:
839 if nosiblingsapplied:
840 if not item.header.special():
840 if not item.header.special():
841 item.header.applied = False
841 item.header.applied = False
842 item.header.partial = False
842 item.header.partial = False
843 else: # some/all parent siblings are applied
843 else: # some/all parent siblings are applied
844 item.header.applied = True
844 item.header.applied = True
845 item.header.partial = (somesiblingspartial or
845 item.header.partial = (somesiblingspartial or
846 not allsiblingsapplied)
846 not allsiblingsapplied)
847
847
848 elif isinstance(item, uihunkline):
848 elif isinstance(item, uihunkline):
849 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
849 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
850 allsiblingsapplied = not (False in siblingappliedstatus)
850 allsiblingsapplied = not (False in siblingappliedstatus)
851 nosiblingsapplied = not (True in siblingappliedstatus)
851 nosiblingsapplied = not (True in siblingappliedstatus)
852
852
853 # if no 'sibling' lines are applied
853 # if no 'sibling' lines are applied
854 if nosiblingsapplied:
854 if nosiblingsapplied:
855 item.hunk.applied = False
855 item.hunk.applied = False
856 item.hunk.partial = False
856 item.hunk.partial = False
857 elif allsiblingsapplied:
857 elif allsiblingsapplied:
858 item.hunk.applied = True
858 item.hunk.applied = True
859 item.hunk.partial = False
859 item.hunk.partial = False
860 else: # some siblings applied
860 else: # some siblings applied
861 item.hunk.applied = True
861 item.hunk.applied = True
862 item.hunk.partial = True
862 item.hunk.partial = True
863
863
864 parentsiblingsapplied = [hnk.applied for hnk
864 parentsiblingsapplied = [hnk.applied for hnk
865 in item.hunk.header.hunks]
865 in item.hunk.header.hunks]
866 noparentsiblingsapplied = not (True in parentsiblingsapplied)
866 noparentsiblingsapplied = not (True in parentsiblingsapplied)
867 allparentsiblingsapplied = not (False in parentsiblingsapplied)
867 allparentsiblingsapplied = not (False in parentsiblingsapplied)
868
868
869 parentsiblingspartial = [hnk.partial for hnk
869 parentsiblingspartial = [hnk.partial for hnk
870 in item.hunk.header.hunks]
870 in item.hunk.header.hunks]
871 someparentsiblingspartial = (True in parentsiblingspartial)
871 someparentsiblingspartial = (True in parentsiblingspartial)
872
872
873 # if all parent hunks are not applied, un-apply header
873 # if all parent hunks are not applied, un-apply header
874 if noparentsiblingsapplied:
874 if noparentsiblingsapplied:
875 if not item.hunk.header.special():
875 if not item.hunk.header.special():
876 item.hunk.header.applied = False
876 item.hunk.header.applied = False
877 item.hunk.header.partial = False
877 item.hunk.header.partial = False
878 # set the applied and partial status of the header if needed
878 # set the applied and partial status of the header if needed
879 else: # some/all parent siblings are applied
879 else: # some/all parent siblings are applied
880 item.hunk.header.applied = True
880 item.hunk.header.applied = True
881 item.hunk.header.partial = (someparentsiblingspartial or
881 item.hunk.header.partial = (someparentsiblingspartial or
882 not allparentsiblingsapplied)
882 not allparentsiblingsapplied)
883
883
884 def toggleall(self):
884 def toggleall(self):
885 "toggle the applied flag of all items."
885 "toggle the applied flag of all items."
886 if self.waslasttoggleallapplied: # then unapply them this time
886 if self.waslasttoggleallapplied: # then unapply them this time
887 for item in self.headerlist:
887 for item in self.headerlist:
888 if item.applied:
888 if item.applied:
889 self.toggleapply(item)
889 self.toggleapply(item)
890 else:
890 else:
891 for item in self.headerlist:
891 for item in self.headerlist:
892 if not item.applied:
892 if not item.applied:
893 self.toggleapply(item)
893 self.toggleapply(item)
894 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
894 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
895
895
896 def togglefolded(self, item=None, foldparent=False):
896 def togglefolded(self, item=None, foldparent=False):
897 "toggle folded flag of specified item (defaults to currently selected)"
897 "toggle folded flag of specified item (defaults to currently selected)"
898 if item is None:
898 if item is None:
899 item = self.currentselecteditem
899 item = self.currentselecteditem
900 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
900 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
901 if not isinstance(item, uiheader):
901 if not isinstance(item, uiheader):
902 # we need to select the parent item in this case
902 # we need to select the parent item in this case
903 self.currentselecteditem = item = item.parentitem()
903 self.currentselecteditem = item = item.parentitem()
904 elif item.neverunfolded:
904 elif item.neverunfolded:
905 item.neverunfolded = False
905 item.neverunfolded = False
906
906
907 # also fold any foldable children of the parent/current item
907 # also fold any foldable children of the parent/current item
908 if isinstance(item, uiheader): # the original or 'new' item
908 if isinstance(item, uiheader): # the original or 'new' item
909 for child in item.allchildren():
909 for child in item.allchildren():
910 child.folded = not item.folded
910 child.folded = not item.folded
911
911
912 if isinstance(item, (uiheader, uihunk)):
912 if isinstance(item, (uiheader, uihunk)):
913 item.folded = not item.folded
913 item.folded = not item.folded
914
914
915 def alignstring(self, instr, window):
915 def alignstring(self, instr, window):
916 """
916 """
917 add whitespace to the end of a string in order to make it fill
917 add whitespace to the end of a string in order to make it fill
918 the screen in the x direction. the current cursor position is
918 the screen in the x direction. the current cursor position is
919 taken into account when making this calculation. the string can span
919 taken into account when making this calculation. the string can span
920 multiple lines.
920 multiple lines.
921 """
921 """
922 y, xstart = window.getyx()
922 y, xstart = window.getyx()
923 width = self.xscreensize
923 width = self.xscreensize
924 # turn tabs into spaces
924 # turn tabs into spaces
925 instr = instr.expandtabs(4)
925 instr = instr.expandtabs(4)
926 strwidth = encoding.colwidth(instr)
926 strwidth = encoding.colwidth(instr)
927 numspaces = (width - ((strwidth + xstart) % width) - 1)
927 numspaces = (width - ((strwidth + xstart) % width) - 1)
928 return instr + " " * numspaces + "\n"
928 return instr + " " * numspaces + "\n"
929
929
930 def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None,
930 def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None,
931 pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False):
931 pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False):
932 """
932 """
933 print the string, text, with the specified colors and attributes, to
933 print the string, text, with the specified colors and attributes, to
934 the specified curses window object.
934 the specified curses window object.
935
935
936 the foreground and background colors are of the form
936 the foreground and background colors are of the form
937 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
937 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
938 magenta, red, white, yellow]. if pairname is provided, a color
938 magenta, red, white, yellow]. if pairname is provided, a color
939 pair will be looked up in the self.colorpairnames dictionary.
939 pair will be looked up in the self.colorpairnames dictionary.
940
940
941 attrlist is a list containing text attributes in the form of
941 attrlist is a list containing text attributes in the form of
942 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
942 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
943 underline].
943 underline].
944
944
945 if align == True, whitespace is added to the printed string such that
945 if align == True, whitespace is added to the printed string such that
946 the string stretches to the right border of the window.
946 the string stretches to the right border of the window.
947
947
948 if showwhtspc == True, trailing whitespace of a string is highlighted.
948 if showwhtspc == True, trailing whitespace of a string is highlighted.
949 """
949 """
950 # preprocess the text, converting tabs to spaces
950 # preprocess the text, converting tabs to spaces
951 text = text.expandtabs(4)
951 text = text.expandtabs(4)
952 # strip \n, and convert control characters to ^[char] representation
952 # strip \n, and convert control characters to ^[char] representation
953 text = re.sub(r'[\x00-\x08\x0a-\x1f]',
953 text = re.sub(br'[\x00-\x08\x0a-\x1f]',
954 lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n'))
954 lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n'))
955
955
956 if pair is not None:
956 if pair is not None:
957 colorpair = pair
957 colorpair = pair
958 elif pairname is not None:
958 elif pairname is not None:
959 colorpair = self.colorpairnames[pairname]
959 colorpair = self.colorpairnames[pairname]
960 else:
960 else:
961 if fgcolor is None:
961 if fgcolor is None:
962 fgcolor = -1
962 fgcolor = -1
963 if bgcolor is None:
963 if bgcolor is None:
964 bgcolor = -1
964 bgcolor = -1
965 if (fgcolor, bgcolor) in self.colorpairs:
965 if (fgcolor, bgcolor) in self.colorpairs:
966 colorpair = self.colorpairs[(fgcolor, bgcolor)]
966 colorpair = self.colorpairs[(fgcolor, bgcolor)]
967 else:
967 else:
968 colorpair = self.getcolorpair(fgcolor, bgcolor)
968 colorpair = self.getcolorpair(fgcolor, bgcolor)
969 # add attributes if possible
969 # add attributes if possible
970 if attrlist is None:
970 if attrlist is None:
971 attrlist = []
971 attrlist = []
972 if colorpair < 256:
972 if colorpair < 256:
973 # then it is safe to apply all attributes
973 # then it is safe to apply all attributes
974 for textattr in attrlist:
974 for textattr in attrlist:
975 colorpair |= textattr
975 colorpair |= textattr
976 else:
976 else:
977 # just apply a select few (safe?) attributes
977 # just apply a select few (safe?) attributes
978 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
978 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
979 if textattr in attrlist:
979 if textattr in attrlist:
980 colorpair |= textattr
980 colorpair |= textattr
981
981
982 y, xstart = self.chunkpad.getyx()
982 y, xstart = self.chunkpad.getyx()
983 t = "" # variable for counting lines printed
983 t = "" # variable for counting lines printed
984 # if requested, show trailing whitespace
984 # if requested, show trailing whitespace
985 if showwhtspc:
985 if showwhtspc:
986 origlen = len(text)
986 origlen = len(text)
987 text = text.rstrip(' \n') # tabs have already been expanded
987 text = text.rstrip(' \n') # tabs have already been expanded
988 strippedlen = len(text)
988 strippedlen = len(text)
989 numtrailingspaces = origlen - strippedlen
989 numtrailingspaces = origlen - strippedlen
990
990
991 if towin:
991 if towin:
992 window.addstr(text, colorpair)
992 window.addstr(text, colorpair)
993 t += text
993 t += text
994
994
995 if showwhtspc:
995 if showwhtspc:
996 wscolorpair = colorpair | curses.A_REVERSE
996 wscolorpair = colorpair | curses.A_REVERSE
997 if towin:
997 if towin:
998 for i in range(numtrailingspaces):
998 for i in range(numtrailingspaces):
999 window.addch(curses.ACS_CKBOARD, wscolorpair)
999 window.addch(curses.ACS_CKBOARD, wscolorpair)
1000 t += " " * numtrailingspaces
1000 t += " " * numtrailingspaces
1001
1001
1002 if align:
1002 if align:
1003 if towin:
1003 if towin:
1004 extrawhitespace = self.alignstring("", window)
1004 extrawhitespace = self.alignstring("", window)
1005 window.addstr(extrawhitespace, colorpair)
1005 window.addstr(extrawhitespace, colorpair)
1006 else:
1006 else:
1007 # need to use t, since the x position hasn't incremented
1007 # need to use t, since the x position hasn't incremented
1008 extrawhitespace = self.alignstring(t, window)
1008 extrawhitespace = self.alignstring(t, window)
1009 t += extrawhitespace
1009 t += extrawhitespace
1010
1010
1011 # is reset to 0 at the beginning of printitem()
1011 # is reset to 0 at the beginning of printitem()
1012
1012
1013 linesprinted = (xstart + len(t)) / self.xscreensize
1013 linesprinted = (xstart + len(t)) / self.xscreensize
1014 self.linesprintedtopadsofar += linesprinted
1014 self.linesprintedtopadsofar += linesprinted
1015 return t
1015 return t
1016
1016
1017 def _getstatuslinesegments(self):
1017 def _getstatuslinesegments(self):
1018 """-> [str]. return segments"""
1018 """-> [str]. return segments"""
1019 selected = self.currentselecteditem.applied
1019 selected = self.currentselecteditem.applied
1020 spaceselect = _('space: select')
1020 spaceselect = _('space: select')
1021 spacedeselect = _('space: deselect')
1021 spacedeselect = _('space: deselect')
1022 # Format the selected label into a place as long as the longer of the
1022 # Format the selected label into a place as long as the longer of the
1023 # two possible labels. This may vary by language.
1023 # two possible labels. This may vary by language.
1024 spacelen = max(len(spaceselect), len(spacedeselect))
1024 spacelen = max(len(spaceselect), len(spacedeselect))
1025 selectedlabel = '%-*s' % (spacelen,
1025 selectedlabel = '%-*s' % (spacelen,
1026 spacedeselect if selected else spaceselect)
1026 spacedeselect if selected else spaceselect)
1027 segments = [
1027 segments = [
1028 _headermessages[self.operation],
1028 _headermessages[self.operation],
1029 '-',
1029 '-',
1030 _('[x]=selected **=collapsed'),
1030 _('[x]=selected **=collapsed'),
1031 _('c: confirm'),
1031 _('c: confirm'),
1032 _('q: abort'),
1032 _('q: abort'),
1033 _('arrow keys: move/expand/collapse'),
1033 _('arrow keys: move/expand/collapse'),
1034 selectedlabel,
1034 selectedlabel,
1035 _('?: help'),
1035 _('?: help'),
1036 ]
1036 ]
1037 return segments
1037 return segments
1038
1038
1039 def _getstatuslines(self):
1039 def _getstatuslines(self):
1040 """() -> [str]. return short help used in the top status window"""
1040 """() -> [str]. return short help used in the top status window"""
1041 if self.errorstr is not None:
1041 if self.errorstr is not None:
1042 lines = [self.errorstr, _('Press any key to continue')]
1042 lines = [self.errorstr, _('Press any key to continue')]
1043 else:
1043 else:
1044 # wrap segments to lines
1044 # wrap segments to lines
1045 segments = self._getstatuslinesegments()
1045 segments = self._getstatuslinesegments()
1046 width = self.xscreensize
1046 width = self.xscreensize
1047 lines = []
1047 lines = []
1048 lastwidth = width
1048 lastwidth = width
1049 for s in segments:
1049 for s in segments:
1050 w = encoding.colwidth(s)
1050 w = encoding.colwidth(s)
1051 sep = ' ' * (1 + (s and s[0] not in '-['))
1051 sep = ' ' * (1 + (s and s[0] not in '-['))
1052 if lastwidth + w + len(sep) >= width:
1052 if lastwidth + w + len(sep) >= width:
1053 lines.append(s)
1053 lines.append(s)
1054 lastwidth = w
1054 lastwidth = w
1055 else:
1055 else:
1056 lines[-1] += sep + s
1056 lines[-1] += sep + s
1057 lastwidth += w + len(sep)
1057 lastwidth += w + len(sep)
1058 if len(lines) != self.numstatuslines:
1058 if len(lines) != self.numstatuslines:
1059 self.numstatuslines = len(lines)
1059 self.numstatuslines = len(lines)
1060 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1060 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1061 return [util.ellipsis(l, self.xscreensize - 1) for l in lines]
1061 return [util.ellipsis(l, self.xscreensize - 1) for l in lines]
1062
1062
1063 def updatescreen(self):
1063 def updatescreen(self):
1064 self.statuswin.erase()
1064 self.statuswin.erase()
1065 self.chunkpad.erase()
1065 self.chunkpad.erase()
1066
1066
1067 printstring = self.printstring
1067 printstring = self.printstring
1068
1068
1069 # print out the status lines at the top
1069 # print out the status lines at the top
1070 try:
1070 try:
1071 for line in self._getstatuslines():
1071 for line in self._getstatuslines():
1072 printstring(self.statuswin, line, pairname="legend")
1072 printstring(self.statuswin, line, pairname="legend")
1073 self.statuswin.refresh()
1073 self.statuswin.refresh()
1074 except curses.error:
1074 except curses.error:
1075 pass
1075 pass
1076 if self.errorstr is not None:
1076 if self.errorstr is not None:
1077 return
1077 return
1078
1078
1079 # print out the patch in the remaining part of the window
1079 # print out the patch in the remaining part of the window
1080 try:
1080 try:
1081 self.printitem()
1081 self.printitem()
1082 self.updatescroll()
1082 self.updatescroll()
1083 self.chunkpad.refresh(self.firstlineofpadtoprint, 0,
1083 self.chunkpad.refresh(self.firstlineofpadtoprint, 0,
1084 self.numstatuslines, 0,
1084 self.numstatuslines, 0,
1085 self.yscreensize - self.numstatuslines,
1085 self.yscreensize - self.numstatuslines,
1086 self.xscreensize)
1086 self.xscreensize)
1087 except curses.error:
1087 except curses.error:
1088 pass
1088 pass
1089
1089
1090 def getstatusprefixstring(self, item):
1090 def getstatusprefixstring(self, item):
1091 """
1091 """
1092 create a string to prefix a line with which indicates whether 'item'
1092 create a string to prefix a line with which indicates whether 'item'
1093 is applied and/or folded.
1093 is applied and/or folded.
1094 """
1094 """
1095
1095
1096 # create checkbox string
1096 # create checkbox string
1097 if item.applied:
1097 if item.applied:
1098 if not isinstance(item, uihunkline) and item.partial:
1098 if not isinstance(item, uihunkline) and item.partial:
1099 checkbox = "[~]"
1099 checkbox = "[~]"
1100 else:
1100 else:
1101 checkbox = "[x]"
1101 checkbox = "[x]"
1102 else:
1102 else:
1103 checkbox = "[ ]"
1103 checkbox = "[ ]"
1104
1104
1105 try:
1105 try:
1106 if item.folded:
1106 if item.folded:
1107 checkbox += "**"
1107 checkbox += "**"
1108 if isinstance(item, uiheader):
1108 if isinstance(item, uiheader):
1109 # one of "m", "a", or "d" (modified, added, deleted)
1109 # one of "m", "a", or "d" (modified, added, deleted)
1110 filestatus = item.changetype
1110 filestatus = item.changetype
1111
1111
1112 checkbox += filestatus + " "
1112 checkbox += filestatus + " "
1113 else:
1113 else:
1114 checkbox += " "
1114 checkbox += " "
1115 if isinstance(item, uiheader):
1115 if isinstance(item, uiheader):
1116 # add two more spaces for headers
1116 # add two more spaces for headers
1117 checkbox += " "
1117 checkbox += " "
1118 except AttributeError: # not foldable
1118 except AttributeError: # not foldable
1119 checkbox += " "
1119 checkbox += " "
1120
1120
1121 return checkbox
1121 return checkbox
1122
1122
1123 def printheader(self, header, selected=False, towin=True,
1123 def printheader(self, header, selected=False, towin=True,
1124 ignorefolding=False):
1124 ignorefolding=False):
1125 """
1125 """
1126 print the header to the pad. if countlines is True, don't print
1126 print the header to the pad. if countlines is True, don't print
1127 anything, but just count the number of lines which would be printed.
1127 anything, but just count the number of lines which would be printed.
1128 """
1128 """
1129
1129
1130 outstr = ""
1130 outstr = ""
1131 text = header.prettystr()
1131 text = header.prettystr()
1132 chunkindex = self.chunklist.index(header)
1132 chunkindex = self.chunklist.index(header)
1133
1133
1134 if chunkindex != 0 and not header.folded:
1134 if chunkindex != 0 and not header.folded:
1135 # add separating line before headers
1135 # add separating line before headers
1136 outstr += self.printstring(self.chunkpad, '_' * self.xscreensize,
1136 outstr += self.printstring(self.chunkpad, '_' * self.xscreensize,
1137 towin=towin, align=False)
1137 towin=towin, align=False)
1138 # select color-pair based on if the header is selected
1138 # select color-pair based on if the header is selected
1139 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1139 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1140 attrlist=[curses.A_BOLD])
1140 attrlist=[curses.A_BOLD])
1141
1141
1142 # print out each line of the chunk, expanding it to screen width
1142 # print out each line of the chunk, expanding it to screen width
1143
1143
1144 # number of characters to indent lines on this level by
1144 # number of characters to indent lines on this level by
1145 indentnumchars = 0
1145 indentnumchars = 0
1146 checkbox = self.getstatusprefixstring(header)
1146 checkbox = self.getstatusprefixstring(header)
1147 if not header.folded or ignorefolding:
1147 if not header.folded or ignorefolding:
1148 textlist = text.split("\n")
1148 textlist = text.split("\n")
1149 linestr = checkbox + textlist[0]
1149 linestr = checkbox + textlist[0]
1150 else:
1150 else:
1151 linestr = checkbox + header.filename()
1151 linestr = checkbox + header.filename()
1152 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1152 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1153 towin=towin)
1153 towin=towin)
1154 if not header.folded or ignorefolding:
1154 if not header.folded or ignorefolding:
1155 if len(textlist) > 1:
1155 if len(textlist) > 1:
1156 for line in textlist[1:]:
1156 for line in textlist[1:]:
1157 linestr = " "*(indentnumchars + len(checkbox)) + line
1157 linestr = " "*(indentnumchars + len(checkbox)) + line
1158 outstr += self.printstring(self.chunkpad, linestr,
1158 outstr += self.printstring(self.chunkpad, linestr,
1159 pair=colorpair, towin=towin)
1159 pair=colorpair, towin=towin)
1160
1160
1161 return outstr
1161 return outstr
1162
1162
1163 def printhunklinesbefore(self, hunk, selected=False, towin=True,
1163 def printhunklinesbefore(self, hunk, selected=False, towin=True,
1164 ignorefolding=False):
1164 ignorefolding=False):
1165 "includes start/end line indicator"
1165 "includes start/end line indicator"
1166 outstr = ""
1166 outstr = ""
1167 # where hunk is in list of siblings
1167 # where hunk is in list of siblings
1168 hunkindex = hunk.header.hunks.index(hunk)
1168 hunkindex = hunk.header.hunks.index(hunk)
1169
1169
1170 if hunkindex != 0:
1170 if hunkindex != 0:
1171 # add separating line before headers
1171 # add separating line before headers
1172 outstr += self.printstring(self.chunkpad, ' '*self.xscreensize,
1172 outstr += self.printstring(self.chunkpad, ' '*self.xscreensize,
1173 towin=towin, align=False)
1173 towin=towin, align=False)
1174
1174
1175 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1175 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1176 attrlist=[curses.A_BOLD])
1176 attrlist=[curses.A_BOLD])
1177
1177
1178 # print out from-to line with checkbox
1178 # print out from-to line with checkbox
1179 checkbox = self.getstatusprefixstring(hunk)
1179 checkbox = self.getstatusprefixstring(hunk)
1180
1180
1181 lineprefix = " "*self.hunkindentnumchars + checkbox
1181 lineprefix = " "*self.hunkindentnumchars + checkbox
1182 frtoline = " " + hunk.getfromtoline().strip("\n")
1182 frtoline = " " + hunk.getfromtoline().strip("\n")
1183
1183
1184 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1184 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1185 align=False) # add uncolored checkbox/indent
1185 align=False) # add uncolored checkbox/indent
1186 outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair,
1186 outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair,
1187 towin=towin)
1187 towin=towin)
1188
1188
1189 if hunk.folded and not ignorefolding:
1189 if hunk.folded and not ignorefolding:
1190 # skip remainder of output
1190 # skip remainder of output
1191 return outstr
1191 return outstr
1192
1192
1193 # print out lines of the chunk preceeding changed-lines
1193 # print out lines of the chunk preceeding changed-lines
1194 for line in hunk.before:
1194 for line in hunk.before:
1195 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1195 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1196 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1196 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1197
1197
1198 return outstr
1198 return outstr
1199
1199
1200 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1200 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1201 outstr = ""
1201 outstr = ""
1202 if hunk.folded and not ignorefolding:
1202 if hunk.folded and not ignorefolding:
1203 return outstr
1203 return outstr
1204
1204
1205 # a bit superfluous, but to avoid hard-coding indent amount
1205 # a bit superfluous, but to avoid hard-coding indent amount
1206 checkbox = self.getstatusprefixstring(hunk)
1206 checkbox = self.getstatusprefixstring(hunk)
1207 for line in hunk.after:
1207 for line in hunk.after:
1208 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1208 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1209 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1209 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1210
1210
1211 return outstr
1211 return outstr
1212
1212
1213 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1213 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1214 outstr = ""
1214 outstr = ""
1215 checkbox = self.getstatusprefixstring(hunkline)
1215 checkbox = self.getstatusprefixstring(hunkline)
1216
1216
1217 linestr = hunkline.prettystr().strip("\n")
1217 linestr = hunkline.prettystr().strip("\n")
1218
1218
1219 # select color-pair based on whether line is an addition/removal
1219 # select color-pair based on whether line is an addition/removal
1220 if selected:
1220 if selected:
1221 colorpair = self.getcolorpair(name="selected")
1221 colorpair = self.getcolorpair(name="selected")
1222 elif linestr.startswith("+"):
1222 elif linestr.startswith("+"):
1223 colorpair = self.getcolorpair(name="addition")
1223 colorpair = self.getcolorpair(name="addition")
1224 elif linestr.startswith("-"):
1224 elif linestr.startswith("-"):
1225 colorpair = self.getcolorpair(name="deletion")
1225 colorpair = self.getcolorpair(name="deletion")
1226 elif linestr.startswith("\\"):
1226 elif linestr.startswith("\\"):
1227 colorpair = self.getcolorpair(name="normal")
1227 colorpair = self.getcolorpair(name="normal")
1228
1228
1229 lineprefix = " "*self.hunklineindentnumchars + checkbox
1229 lineprefix = " "*self.hunklineindentnumchars + checkbox
1230 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1230 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1231 align=False) # add uncolored checkbox/indent
1231 align=False) # add uncolored checkbox/indent
1232 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1232 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1233 towin=towin, showwhtspc=True)
1233 towin=towin, showwhtspc=True)
1234 return outstr
1234 return outstr
1235
1235
1236 def printitem(self, item=None, ignorefolding=False, recursechildren=True,
1236 def printitem(self, item=None, ignorefolding=False, recursechildren=True,
1237 towin=True):
1237 towin=True):
1238 """
1238 """
1239 use __printitem() to print the the specified item.applied.
1239 use __printitem() to print the the specified item.applied.
1240 if item is not specified, then print the entire patch.
1240 if item is not specified, then print the entire patch.
1241 (hiding folded elements, etc. -- see __printitem() docstring)
1241 (hiding folded elements, etc. -- see __printitem() docstring)
1242 """
1242 """
1243
1243
1244 if item is None:
1244 if item is None:
1245 item = self.headerlist
1245 item = self.headerlist
1246 if recursechildren:
1246 if recursechildren:
1247 self.linesprintedtopadsofar = 0
1247 self.linesprintedtopadsofar = 0
1248
1248
1249 outstr = []
1249 outstr = []
1250 self.__printitem(item, ignorefolding, recursechildren, outstr,
1250 self.__printitem(item, ignorefolding, recursechildren, outstr,
1251 towin=towin)
1251 towin=towin)
1252 return ''.join(outstr)
1252 return ''.join(outstr)
1253
1253
1254 def outofdisplayedarea(self):
1254 def outofdisplayedarea(self):
1255 y, _ = self.chunkpad.getyx() # cursor location
1255 y, _ = self.chunkpad.getyx() # cursor location
1256 # * 2 here works but an optimization would be the max number of
1256 # * 2 here works but an optimization would be the max number of
1257 # consecutive non selectable lines
1257 # consecutive non selectable lines
1258 # i.e the max number of context line for any hunk in the patch
1258 # i.e the max number of context line for any hunk in the patch
1259 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1259 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1260 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1260 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1261 return y < miny or y > maxy
1261 return y < miny or y > maxy
1262
1262
1263 def handleselection(self, item, recursechildren):
1263 def handleselection(self, item, recursechildren):
1264 selected = (item is self.currentselecteditem)
1264 selected = (item is self.currentselecteditem)
1265 if selected and recursechildren:
1265 if selected and recursechildren:
1266 # assumes line numbering starting from line 0
1266 # assumes line numbering starting from line 0
1267 self.selecteditemstartline = self.linesprintedtopadsofar
1267 self.selecteditemstartline = self.linesprintedtopadsofar
1268 selecteditemlines = self.getnumlinesdisplayed(item,
1268 selecteditemlines = self.getnumlinesdisplayed(item,
1269 recursechildren=False)
1269 recursechildren=False)
1270 self.selecteditemendline = (self.selecteditemstartline +
1270 self.selecteditemendline = (self.selecteditemstartline +
1271 selecteditemlines - 1)
1271 selecteditemlines - 1)
1272 return selected
1272 return selected
1273
1273
1274 def __printitem(self, item, ignorefolding, recursechildren, outstr,
1274 def __printitem(self, item, ignorefolding, recursechildren, outstr,
1275 towin=True):
1275 towin=True):
1276 """
1276 """
1277 recursive method for printing out patch/header/hunk/hunk-line data to
1277 recursive method for printing out patch/header/hunk/hunk-line data to
1278 screen. also returns a string with all of the content of the displayed
1278 screen. also returns a string with all of the content of the displayed
1279 patch (not including coloring, etc.).
1279 patch (not including coloring, etc.).
1280
1280
1281 if ignorefolding is True, then folded items are printed out.
1281 if ignorefolding is True, then folded items are printed out.
1282
1282
1283 if recursechildren is False, then only print the item without its
1283 if recursechildren is False, then only print the item without its
1284 child items.
1284 child items.
1285 """
1285 """
1286
1286
1287 if towin and self.outofdisplayedarea():
1287 if towin and self.outofdisplayedarea():
1288 return
1288 return
1289
1289
1290 selected = self.handleselection(item, recursechildren)
1290 selected = self.handleselection(item, recursechildren)
1291
1291
1292 # patch object is a list of headers
1292 # patch object is a list of headers
1293 if isinstance(item, patch):
1293 if isinstance(item, patch):
1294 if recursechildren:
1294 if recursechildren:
1295 for hdr in item:
1295 for hdr in item:
1296 self.__printitem(hdr, ignorefolding,
1296 self.__printitem(hdr, ignorefolding,
1297 recursechildren, outstr, towin)
1297 recursechildren, outstr, towin)
1298 # todo: eliminate all isinstance() calls
1298 # todo: eliminate all isinstance() calls
1299 if isinstance(item, uiheader):
1299 if isinstance(item, uiheader):
1300 outstr.append(self.printheader(item, selected, towin=towin,
1300 outstr.append(self.printheader(item, selected, towin=towin,
1301 ignorefolding=ignorefolding))
1301 ignorefolding=ignorefolding))
1302 if recursechildren:
1302 if recursechildren:
1303 for hnk in item.hunks:
1303 for hnk in item.hunks:
1304 self.__printitem(hnk, ignorefolding,
1304 self.__printitem(hnk, ignorefolding,
1305 recursechildren, outstr, towin)
1305 recursechildren, outstr, towin)
1306 elif (isinstance(item, uihunk) and
1306 elif (isinstance(item, uihunk) and
1307 ((not item.header.folded) or ignorefolding)):
1307 ((not item.header.folded) or ignorefolding)):
1308 # print the hunk data which comes before the changed-lines
1308 # print the hunk data which comes before the changed-lines
1309 outstr.append(self.printhunklinesbefore(item, selected, towin=towin,
1309 outstr.append(self.printhunklinesbefore(item, selected, towin=towin,
1310 ignorefolding=ignorefolding))
1310 ignorefolding=ignorefolding))
1311 if recursechildren:
1311 if recursechildren:
1312 for l in item.changedlines:
1312 for l in item.changedlines:
1313 self.__printitem(l, ignorefolding,
1313 self.__printitem(l, ignorefolding,
1314 recursechildren, outstr, towin)
1314 recursechildren, outstr, towin)
1315 outstr.append(self.printhunklinesafter(item, towin=towin,
1315 outstr.append(self.printhunklinesafter(item, towin=towin,
1316 ignorefolding=ignorefolding))
1316 ignorefolding=ignorefolding))
1317 elif (isinstance(item, uihunkline) and
1317 elif (isinstance(item, uihunkline) and
1318 ((not item.hunk.folded) or ignorefolding)):
1318 ((not item.hunk.folded) or ignorefolding)):
1319 outstr.append(self.printhunkchangedline(item, selected,
1319 outstr.append(self.printhunkchangedline(item, selected,
1320 towin=towin))
1320 towin=towin))
1321
1321
1322 return outstr
1322 return outstr
1323
1323
1324 def getnumlinesdisplayed(self, item=None, ignorefolding=False,
1324 def getnumlinesdisplayed(self, item=None, ignorefolding=False,
1325 recursechildren=True):
1325 recursechildren=True):
1326 """
1326 """
1327 return the number of lines which would be displayed if the item were
1327 return the number of lines which would be displayed if the item were
1328 to be printed to the display. the item will not be printed to the
1328 to be printed to the display. the item will not be printed to the
1329 display (pad).
1329 display (pad).
1330 if no item is given, assume the entire patch.
1330 if no item is given, assume the entire patch.
1331 if ignorefolding is True, folded items will be unfolded when counting
1331 if ignorefolding is True, folded items will be unfolded when counting
1332 the number of lines.
1332 the number of lines.
1333 """
1333 """
1334
1334
1335 # temporarily disable printing to windows by printstring
1335 # temporarily disable printing to windows by printstring
1336 patchdisplaystring = self.printitem(item, ignorefolding,
1336 patchdisplaystring = self.printitem(item, ignorefolding,
1337 recursechildren, towin=False)
1337 recursechildren, towin=False)
1338 numlines = len(patchdisplaystring) / self.xscreensize
1338 numlines = len(patchdisplaystring) / self.xscreensize
1339 return numlines
1339 return numlines
1340
1340
1341 def sigwinchhandler(self, n, frame):
1341 def sigwinchhandler(self, n, frame):
1342 "handle window resizing"
1342 "handle window resizing"
1343 try:
1343 try:
1344 curses.endwin()
1344 curses.endwin()
1345 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1345 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1346 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1346 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1347 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1347 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1348 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1348 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1349 except curses.error:
1349 except curses.error:
1350 pass
1350 pass
1351
1351
1352 def getcolorpair(self, fgcolor=None, bgcolor=None, name=None,
1352 def getcolorpair(self, fgcolor=None, bgcolor=None, name=None,
1353 attrlist=None):
1353 attrlist=None):
1354 """
1354 """
1355 get a curses color pair, adding it to self.colorpairs if it is not
1355 get a curses color pair, adding it to self.colorpairs if it is not
1356 already defined. an optional string, name, can be passed as a shortcut
1356 already defined. an optional string, name, can be passed as a shortcut
1357 for referring to the color-pair. by default, if no arguments are
1357 for referring to the color-pair. by default, if no arguments are
1358 specified, the white foreground / black background color-pair is
1358 specified, the white foreground / black background color-pair is
1359 returned.
1359 returned.
1360
1360
1361 it is expected that this function will be used exclusively for
1361 it is expected that this function will be used exclusively for
1362 initializing color pairs, and not curses.init_pair().
1362 initializing color pairs, and not curses.init_pair().
1363
1363
1364 attrlist is used to 'flavor' the returned color-pair. this information
1364 attrlist is used to 'flavor' the returned color-pair. this information
1365 is not stored in self.colorpairs. it contains attribute values like
1365 is not stored in self.colorpairs. it contains attribute values like
1366 curses.A_BOLD.
1366 curses.A_BOLD.
1367 """
1367 """
1368
1368
1369 if (name is not None) and name in self.colorpairnames:
1369 if (name is not None) and name in self.colorpairnames:
1370 # then get the associated color pair and return it
1370 # then get the associated color pair and return it
1371 colorpair = self.colorpairnames[name]
1371 colorpair = self.colorpairnames[name]
1372 else:
1372 else:
1373 if fgcolor is None:
1373 if fgcolor is None:
1374 fgcolor = -1
1374 fgcolor = -1
1375 if bgcolor is None:
1375 if bgcolor is None:
1376 bgcolor = -1
1376 bgcolor = -1
1377 if (fgcolor, bgcolor) in self.colorpairs:
1377 if (fgcolor, bgcolor) in self.colorpairs:
1378 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1378 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1379 else:
1379 else:
1380 pairindex = len(self.colorpairs) + 1
1380 pairindex = len(self.colorpairs) + 1
1381 if self.usecolor:
1381 if self.usecolor:
1382 curses.init_pair(pairindex, fgcolor, bgcolor)
1382 curses.init_pair(pairindex, fgcolor, bgcolor)
1383 colorpair = self.colorpairs[(fgcolor, bgcolor)] = (
1383 colorpair = self.colorpairs[(fgcolor, bgcolor)] = (
1384 curses.color_pair(pairindex))
1384 curses.color_pair(pairindex))
1385 if name is not None:
1385 if name is not None:
1386 self.colorpairnames[name] = curses.color_pair(pairindex)
1386 self.colorpairnames[name] = curses.color_pair(pairindex)
1387 else:
1387 else:
1388 cval = 0
1388 cval = 0
1389 if name is not None:
1389 if name is not None:
1390 if name == 'selected':
1390 if name == 'selected':
1391 cval = curses.A_REVERSE
1391 cval = curses.A_REVERSE
1392 self.colorpairnames[name] = cval
1392 self.colorpairnames[name] = cval
1393 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1393 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1394
1394
1395 # add attributes if possible
1395 # add attributes if possible
1396 if attrlist is None:
1396 if attrlist is None:
1397 attrlist = []
1397 attrlist = []
1398 if colorpair < 256:
1398 if colorpair < 256:
1399 # then it is safe to apply all attributes
1399 # then it is safe to apply all attributes
1400 for textattr in attrlist:
1400 for textattr in attrlist:
1401 colorpair |= textattr
1401 colorpair |= textattr
1402 else:
1402 else:
1403 # just apply a select few (safe?) attributes
1403 # just apply a select few (safe?) attributes
1404 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1404 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1405 if textattrib in attrlist:
1405 if textattrib in attrlist:
1406 colorpair |= textattrib
1406 colorpair |= textattrib
1407 return colorpair
1407 return colorpair
1408
1408
1409 def initcolorpair(self, *args, **kwargs):
1409 def initcolorpair(self, *args, **kwargs):
1410 "same as getcolorpair."
1410 "same as getcolorpair."
1411 self.getcolorpair(*args, **kwargs)
1411 self.getcolorpair(*args, **kwargs)
1412
1412
1413 def helpwindow(self):
1413 def helpwindow(self):
1414 "print a help window to the screen. exit after any keypress."
1414 "print a help window to the screen. exit after any keypress."
1415 helptext = _(
1415 helptext = _(
1416 """ [press any key to return to the patch-display]
1416 """ [press any key to return to the patch-display]
1417
1417
1418 crecord allows you to interactively choose among the changes you have made,
1418 crecord allows you to interactively choose among the changes you have made,
1419 and confirm only those changes you select for further processing by the command
1419 and confirm only those changes you select for further processing by the command
1420 you are running (commit/shelve/revert), after confirming the selected
1420 you are running (commit/shelve/revert), after confirming the selected
1421 changes, the unselected changes are still present in your working copy, so you
1421 changes, the unselected changes are still present in your working copy, so you
1422 can use crecord multiple times to split large changes into smaller changesets.
1422 can use crecord multiple times to split large changes into smaller changesets.
1423 the following are valid keystrokes:
1423 the following are valid keystrokes:
1424
1424
1425 [space] : (un-)select item ([~]/[x] = partly/fully applied)
1425 [space] : (un-)select item ([~]/[x] = partly/fully applied)
1426 A : (un-)select all items
1426 A : (un-)select all items
1427 up/down-arrow [k/j] : go to previous/next unfolded item
1427 up/down-arrow [k/j] : go to previous/next unfolded item
1428 pgup/pgdn [K/J] : go to previous/next item of same type
1428 pgup/pgdn [K/J] : go to previous/next item of same type
1429 right/left-arrow [l/h] : go to child item / parent item
1429 right/left-arrow [l/h] : go to child item / parent item
1430 shift-left-arrow [H] : go to parent header / fold selected header
1430 shift-left-arrow [H] : go to parent header / fold selected header
1431 f : fold / unfold item, hiding/revealing its children
1431 f : fold / unfold item, hiding/revealing its children
1432 F : fold / unfold parent item and all of its ancestors
1432 F : fold / unfold parent item and all of its ancestors
1433 ctrl-l : scroll the selected line to the top of the screen
1433 ctrl-l : scroll the selected line to the top of the screen
1434 m : edit / resume editing the commit message
1434 m : edit / resume editing the commit message
1435 e : edit the currently selected hunk
1435 e : edit the currently selected hunk
1436 a : toggle amend mode, only with commit -i
1436 a : toggle amend mode, only with commit -i
1437 c : confirm selected changes
1437 c : confirm selected changes
1438 r : review/edit and confirm selected changes
1438 r : review/edit and confirm selected changes
1439 q : quit without confirming (no changes will be made)
1439 q : quit without confirming (no changes will be made)
1440 ? : help (what you're currently reading)""")
1440 ? : help (what you're currently reading)""")
1441
1441
1442 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1442 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1443 helplines = helptext.split("\n")
1443 helplines = helptext.split("\n")
1444 helplines = helplines + [" "]*(
1444 helplines = helplines + [" "]*(
1445 self.yscreensize - self.numstatuslines - len(helplines) - 1)
1445 self.yscreensize - self.numstatuslines - len(helplines) - 1)
1446 try:
1446 try:
1447 for line in helplines:
1447 for line in helplines:
1448 self.printstring(helpwin, line, pairname="legend")
1448 self.printstring(helpwin, line, pairname="legend")
1449 except curses.error:
1449 except curses.error:
1450 pass
1450 pass
1451 helpwin.refresh()
1451 helpwin.refresh()
1452 try:
1452 try:
1453 with self.ui.timeblockedsection('crecord'):
1453 with self.ui.timeblockedsection('crecord'):
1454 helpwin.getkey()
1454 helpwin.getkey()
1455 except curses.error:
1455 except curses.error:
1456 pass
1456 pass
1457
1457
1458 def commitMessageWindow(self):
1458 def commitMessageWindow(self):
1459 "Create a temporary commit message editing window on the screen."
1459 "Create a temporary commit message editing window on the screen."
1460
1460
1461 curses.raw()
1461 curses.raw()
1462 curses.def_prog_mode()
1462 curses.def_prog_mode()
1463 curses.endwin()
1463 curses.endwin()
1464 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1464 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1465 curses.cbreak()
1465 curses.cbreak()
1466 self.stdscr.refresh()
1466 self.stdscr.refresh()
1467 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1467 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1468
1468
1469 def confirmationwindow(self, windowtext):
1469 def confirmationwindow(self, windowtext):
1470 "display an informational window, then wait for and return a keypress."
1470 "display an informational window, then wait for and return a keypress."
1471
1471
1472 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1472 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1473 try:
1473 try:
1474 lines = windowtext.split("\n")
1474 lines = windowtext.split("\n")
1475 for line in lines:
1475 for line in lines:
1476 self.printstring(confirmwin, line, pairname="selected")
1476 self.printstring(confirmwin, line, pairname="selected")
1477 except curses.error:
1477 except curses.error:
1478 pass
1478 pass
1479 self.stdscr.refresh()
1479 self.stdscr.refresh()
1480 confirmwin.refresh()
1480 confirmwin.refresh()
1481 try:
1481 try:
1482 with self.ui.timeblockedsection('crecord'):
1482 with self.ui.timeblockedsection('crecord'):
1483 response = chr(self.stdscr.getch())
1483 response = chr(self.stdscr.getch())
1484 except ValueError:
1484 except ValueError:
1485 response = None
1485 response = None
1486
1486
1487 return response
1487 return response
1488
1488
1489 def reviewcommit(self):
1489 def reviewcommit(self):
1490 """ask for 'y' to be pressed to confirm selected. return True if
1490 """ask for 'y' to be pressed to confirm selected. return True if
1491 confirmed."""
1491 confirmed."""
1492 confirmtext = _(
1492 confirmtext = _(
1493 """if you answer yes to the following, the your currently chosen patch chunks
1493 """if you answer yes to the following, the your currently chosen patch chunks
1494 will be loaded into an editor. you may modify the patch from the editor, and
1494 will be loaded into an editor. you may modify the patch from the editor, and
1495 save the changes if you wish to change the patch. otherwise, you can just
1495 save the changes if you wish to change the patch. otherwise, you can just
1496 close the editor without saving to accept the current patch as-is.
1496 close the editor without saving to accept the current patch as-is.
1497
1497
1498 note: don't add/remove lines unless you also modify the range information.
1498 note: don't add/remove lines unless you also modify the range information.
1499 failing to follow this rule will result in the commit aborting.
1499 failing to follow this rule will result in the commit aborting.
1500
1500
1501 are you sure you want to review/edit and confirm the selected changes [yn]?
1501 are you sure you want to review/edit and confirm the selected changes [yn]?
1502 """)
1502 """)
1503 with self.ui.timeblockedsection('crecord'):
1503 with self.ui.timeblockedsection('crecord'):
1504 response = self.confirmationwindow(confirmtext)
1504 response = self.confirmationwindow(confirmtext)
1505 if response is None:
1505 if response is None:
1506 response = "n"
1506 response = "n"
1507 if response.lower().startswith("y"):
1507 if response.lower().startswith("y"):
1508 return True
1508 return True
1509 else:
1509 else:
1510 return False
1510 return False
1511
1511
1512 def toggleamend(self, opts, test):
1512 def toggleamend(self, opts, test):
1513 """Toggle the amend flag.
1513 """Toggle the amend flag.
1514
1514
1515 When the amend flag is set, a commit will modify the most recently
1515 When the amend flag is set, a commit will modify the most recently
1516 committed changeset, instead of creating a new changeset. Otherwise, a
1516 committed changeset, instead of creating a new changeset. Otherwise, a
1517 new changeset will be created (the normal commit behavior).
1517 new changeset will be created (the normal commit behavior).
1518 """
1518 """
1519
1519
1520 try:
1520 try:
1521 ver = float(util.version()[:3])
1521 ver = float(util.version()[:3])
1522 except ValueError:
1522 except ValueError:
1523 ver = 1
1523 ver = 1
1524 if ver < 2.19:
1524 if ver < 2.19:
1525 msg = _("The amend option is unavailable with hg versions < 2.2\n\n"
1525 msg = _("The amend option is unavailable with hg versions < 2.2\n\n"
1526 "Press any key to continue.")
1526 "Press any key to continue.")
1527 elif opts.get('amend') is None:
1527 elif opts.get('amend') is None:
1528 opts['amend'] = True
1528 opts['amend'] = True
1529 msg = _("Amend option is turned on -- committing the currently "
1529 msg = _("Amend option is turned on -- committing the currently "
1530 "selected changes will not create a new changeset, but "
1530 "selected changes will not create a new changeset, but "
1531 "instead update the most recently committed changeset.\n\n"
1531 "instead update the most recently committed changeset.\n\n"
1532 "Press any key to continue.")
1532 "Press any key to continue.")
1533 elif opts.get('amend') is True:
1533 elif opts.get('amend') is True:
1534 opts['amend'] = None
1534 opts['amend'] = None
1535 msg = _("Amend option is turned off -- committing the currently "
1535 msg = _("Amend option is turned off -- committing the currently "
1536 "selected changes will create a new changeset.\n\n"
1536 "selected changes will create a new changeset.\n\n"
1537 "Press any key to continue.")
1537 "Press any key to continue.")
1538 if not test:
1538 if not test:
1539 self.confirmationwindow(msg)
1539 self.confirmationwindow(msg)
1540
1540
1541 def recenterdisplayedarea(self):
1541 def recenterdisplayedarea(self):
1542 """
1542 """
1543 once we scrolled with pg up pg down we can be pointing outside of the
1543 once we scrolled with pg up pg down we can be pointing outside of the
1544 display zone. we print the patch with towin=False to compute the
1544 display zone. we print the patch with towin=False to compute the
1545 location of the selected item even though it is outside of the displayed
1545 location of the selected item even though it is outside of the displayed
1546 zone and then update the scroll.
1546 zone and then update the scroll.
1547 """
1547 """
1548 self.printitem(towin=False)
1548 self.printitem(towin=False)
1549 self.updatescroll()
1549 self.updatescroll()
1550
1550
1551 def toggleedit(self, item=None, test=False):
1551 def toggleedit(self, item=None, test=False):
1552 """
1552 """
1553 edit the currently selected chunk
1553 edit the currently selected chunk
1554 """
1554 """
1555 def updateui(self):
1555 def updateui(self):
1556 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1556 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1557 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1557 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1558 self.updatescroll()
1558 self.updatescroll()
1559 self.stdscr.refresh()
1559 self.stdscr.refresh()
1560 self.statuswin.refresh()
1560 self.statuswin.refresh()
1561 self.stdscr.keypad(1)
1561 self.stdscr.keypad(1)
1562
1562
1563 def editpatchwitheditor(self, chunk):
1563 def editpatchwitheditor(self, chunk):
1564 if chunk is None:
1564 if chunk is None:
1565 self.ui.write(_('cannot edit patch for whole file'))
1565 self.ui.write(_('cannot edit patch for whole file'))
1566 self.ui.write("\n")
1566 self.ui.write("\n")
1567 return None
1567 return None
1568 if chunk.header.binary():
1568 if chunk.header.binary():
1569 self.ui.write(_('cannot edit patch for binary file'))
1569 self.ui.write(_('cannot edit patch for binary file'))
1570 self.ui.write("\n")
1570 self.ui.write("\n")
1571 return None
1571 return None
1572
1572
1573 # write the initial patch
1573 # write the initial patch
1574 patch = stringio()
1574 patch = stringio()
1575 patch.write(diffhelptext + hunkhelptext)
1575 patch.write(diffhelptext + hunkhelptext)
1576 chunk.header.write(patch)
1576 chunk.header.write(patch)
1577 chunk.write(patch)
1577 chunk.write(patch)
1578
1578
1579 # start the editor and wait for it to complete
1579 # start the editor and wait for it to complete
1580 try:
1580 try:
1581 patch = self.ui.edit(patch.getvalue(), "", action="diff")
1581 patch = self.ui.edit(patch.getvalue(), "", action="diff")
1582 except error.Abort as exc:
1582 except error.Abort as exc:
1583 self.errorstr = str(exc)
1583 self.errorstr = str(exc)
1584 return None
1584 return None
1585
1585
1586 # remove comment lines
1586 # remove comment lines
1587 patch = [line + '\n' for line in patch.splitlines()
1587 patch = [line + '\n' for line in patch.splitlines()
1588 if not line.startswith('#')]
1588 if not line.startswith('#')]
1589 return patchmod.parsepatch(patch)
1589 return patchmod.parsepatch(patch)
1590
1590
1591 if item is None:
1591 if item is None:
1592 item = self.currentselecteditem
1592 item = self.currentselecteditem
1593 if isinstance(item, uiheader):
1593 if isinstance(item, uiheader):
1594 return
1594 return
1595 if isinstance(item, uihunkline):
1595 if isinstance(item, uihunkline):
1596 item = item.parentitem()
1596 item = item.parentitem()
1597 if not isinstance(item, uihunk):
1597 if not isinstance(item, uihunk):
1598 return
1598 return
1599
1599
1600 # To go back to that hunk or its replacement at the end of the edit
1600 # To go back to that hunk or its replacement at the end of the edit
1601 itemindex = item.parentitem().hunks.index(item)
1601 itemindex = item.parentitem().hunks.index(item)
1602
1602
1603 beforeadded, beforeremoved = item.added, item.removed
1603 beforeadded, beforeremoved = item.added, item.removed
1604 newpatches = editpatchwitheditor(self, item)
1604 newpatches = editpatchwitheditor(self, item)
1605 if newpatches is None:
1605 if newpatches is None:
1606 if not test:
1606 if not test:
1607 updateui(self)
1607 updateui(self)
1608 return
1608 return
1609 header = item.header
1609 header = item.header
1610 editedhunkindex = header.hunks.index(item)
1610 editedhunkindex = header.hunks.index(item)
1611 hunksbefore = header.hunks[:editedhunkindex]
1611 hunksbefore = header.hunks[:editedhunkindex]
1612 hunksafter = header.hunks[editedhunkindex + 1:]
1612 hunksafter = header.hunks[editedhunkindex + 1:]
1613 newpatchheader = newpatches[0]
1613 newpatchheader = newpatches[0]
1614 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1614 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1615 newadded = sum([h.added for h in newhunks])
1615 newadded = sum([h.added for h in newhunks])
1616 newremoved = sum([h.removed for h in newhunks])
1616 newremoved = sum([h.removed for h in newhunks])
1617 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1617 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1618
1618
1619 for h in hunksafter:
1619 for h in hunksafter:
1620 h.toline += offset
1620 h.toline += offset
1621 for h in newhunks:
1621 for h in newhunks:
1622 h.folded = False
1622 h.folded = False
1623 header.hunks = hunksbefore + newhunks + hunksafter
1623 header.hunks = hunksbefore + newhunks + hunksafter
1624 if self.emptypatch():
1624 if self.emptypatch():
1625 header.hunks = hunksbefore + [item] + hunksafter
1625 header.hunks = hunksbefore + [item] + hunksafter
1626 self.currentselecteditem = header
1626 self.currentselecteditem = header
1627 if len(header.hunks) > itemindex:
1627 if len(header.hunks) > itemindex:
1628 self.currentselecteditem = header.hunks[itemindex]
1628 self.currentselecteditem = header.hunks[itemindex]
1629
1629
1630 if not test:
1630 if not test:
1631 updateui(self)
1631 updateui(self)
1632
1632
1633 def emptypatch(self):
1633 def emptypatch(self):
1634 item = self.headerlist
1634 item = self.headerlist
1635 if not item:
1635 if not item:
1636 return True
1636 return True
1637 for header in item:
1637 for header in item:
1638 if header.hunks:
1638 if header.hunks:
1639 return False
1639 return False
1640 return True
1640 return True
1641
1641
1642 def handlekeypressed(self, keypressed, test=False):
1642 def handlekeypressed(self, keypressed, test=False):
1643 """
1643 """
1644 Perform actions based on pressed keys.
1644 Perform actions based on pressed keys.
1645
1645
1646 Return true to exit the main loop.
1646 Return true to exit the main loop.
1647 """
1647 """
1648 if keypressed in ["k", "KEY_UP"]:
1648 if keypressed in ["k", "KEY_UP"]:
1649 self.uparrowevent()
1649 self.uparrowevent()
1650 if keypressed in ["K", "KEY_PPAGE"]:
1650 if keypressed in ["K", "KEY_PPAGE"]:
1651 self.uparrowshiftevent()
1651 self.uparrowshiftevent()
1652 elif keypressed in ["j", "KEY_DOWN"]:
1652 elif keypressed in ["j", "KEY_DOWN"]:
1653 self.downarrowevent()
1653 self.downarrowevent()
1654 elif keypressed in ["J", "KEY_NPAGE"]:
1654 elif keypressed in ["J", "KEY_NPAGE"]:
1655 self.downarrowshiftevent()
1655 self.downarrowshiftevent()
1656 elif keypressed in ["l", "KEY_RIGHT"]:
1656 elif keypressed in ["l", "KEY_RIGHT"]:
1657 self.rightarrowevent()
1657 self.rightarrowevent()
1658 elif keypressed in ["h", "KEY_LEFT"]:
1658 elif keypressed in ["h", "KEY_LEFT"]:
1659 self.leftarrowevent()
1659 self.leftarrowevent()
1660 elif keypressed in ["H", "KEY_SLEFT"]:
1660 elif keypressed in ["H", "KEY_SLEFT"]:
1661 self.leftarrowshiftevent()
1661 self.leftarrowshiftevent()
1662 elif keypressed in ["q"]:
1662 elif keypressed in ["q"]:
1663 raise error.Abort(_('user quit'))
1663 raise error.Abort(_('user quit'))
1664 elif keypressed in ['a']:
1664 elif keypressed in ['a']:
1665 self.toggleamend(self.opts, test)
1665 self.toggleamend(self.opts, test)
1666 elif keypressed in ["c"]:
1666 elif keypressed in ["c"]:
1667 return True
1667 return True
1668 elif test and keypressed in ['X']:
1668 elif test and keypressed in ['X']:
1669 return True
1669 return True
1670 elif keypressed in ["r"]:
1670 elif keypressed in ["r"]:
1671 if self.reviewcommit():
1671 if self.reviewcommit():
1672 self.opts['review'] = True
1672 self.opts['review'] = True
1673 return True
1673 return True
1674 elif test and keypressed in ['R']:
1674 elif test and keypressed in ['R']:
1675 self.opts['review'] = True
1675 self.opts['review'] = True
1676 return True
1676 return True
1677 elif keypressed in [' '] or (test and keypressed in ["TOGGLE"]):
1677 elif keypressed in [' '] or (test and keypressed in ["TOGGLE"]):
1678 self.toggleapply()
1678 self.toggleapply()
1679 if self.ui.configbool('experimental', 'spacemovesdown'):
1679 if self.ui.configbool('experimental', 'spacemovesdown'):
1680 self.downarrowevent()
1680 self.downarrowevent()
1681 elif keypressed in ['A']:
1681 elif keypressed in ['A']:
1682 self.toggleall()
1682 self.toggleall()
1683 elif keypressed in ['e']:
1683 elif keypressed in ['e']:
1684 self.toggleedit(test=test)
1684 self.toggleedit(test=test)
1685 elif keypressed in ["f"]:
1685 elif keypressed in ["f"]:
1686 self.togglefolded()
1686 self.togglefolded()
1687 elif keypressed in ["F"]:
1687 elif keypressed in ["F"]:
1688 self.togglefolded(foldparent=True)
1688 self.togglefolded(foldparent=True)
1689 elif keypressed in ["m"]:
1689 elif keypressed in ["m"]:
1690 self.commitMessageWindow()
1690 self.commitMessageWindow()
1691 elif keypressed in ["?"]:
1691 elif keypressed in ["?"]:
1692 self.helpwindow()
1692 self.helpwindow()
1693 self.stdscr.clear()
1693 self.stdscr.clear()
1694 self.stdscr.refresh()
1694 self.stdscr.refresh()
1695 elif curses.unctrl(keypressed) in ["^L"]:
1695 elif curses.unctrl(keypressed) in ["^L"]:
1696 # scroll the current line to the top of the screen
1696 # scroll the current line to the top of the screen
1697 self.scrolllines(self.selecteditemstartline)
1697 self.scrolllines(self.selecteditemstartline)
1698
1698
1699 def main(self, stdscr):
1699 def main(self, stdscr):
1700 """
1700 """
1701 method to be wrapped by curses.wrapper() for selecting chunks.
1701 method to be wrapped by curses.wrapper() for selecting chunks.
1702 """
1702 """
1703
1703
1704 origsigwinch = sentinel = object()
1704 origsigwinch = sentinel = object()
1705 if util.safehasattr(signal, 'SIGWINCH'):
1705 if util.safehasattr(signal, 'SIGWINCH'):
1706 origsigwinch = signal.signal(signal.SIGWINCH,
1706 origsigwinch = signal.signal(signal.SIGWINCH,
1707 self.sigwinchhandler)
1707 self.sigwinchhandler)
1708 try:
1708 try:
1709 return self._main(stdscr)
1709 return self._main(stdscr)
1710 finally:
1710 finally:
1711 if origsigwinch is not sentinel:
1711 if origsigwinch is not sentinel:
1712 signal.signal(signal.SIGWINCH, origsigwinch)
1712 signal.signal(signal.SIGWINCH, origsigwinch)
1713
1713
1714 def _main(self, stdscr):
1714 def _main(self, stdscr):
1715 self.stdscr = stdscr
1715 self.stdscr = stdscr
1716 # error during initialization, cannot be printed in the curses
1716 # error during initialization, cannot be printed in the curses
1717 # interface, it should be printed by the calling code
1717 # interface, it should be printed by the calling code
1718 self.initerr = None
1718 self.initerr = None
1719 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1719 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1720
1720
1721 curses.start_color()
1721 curses.start_color()
1722 try:
1722 try:
1723 curses.use_default_colors()
1723 curses.use_default_colors()
1724 except curses.error:
1724 except curses.error:
1725 self.usecolor = False
1725 self.usecolor = False
1726
1726
1727 # available colors: black, blue, cyan, green, magenta, white, yellow
1727 # available colors: black, blue, cyan, green, magenta, white, yellow
1728 # init_pair(color_id, foreground_color, background_color)
1728 # init_pair(color_id, foreground_color, background_color)
1729 self.initcolorpair(None, None, name="normal")
1729 self.initcolorpair(None, None, name="normal")
1730 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA,
1730 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA,
1731 name="selected")
1731 name="selected")
1732 self.initcolorpair(curses.COLOR_RED, None, name="deletion")
1732 self.initcolorpair(curses.COLOR_RED, None, name="deletion")
1733 self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
1733 self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
1734 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
1734 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
1735 # newwin([height, width,] begin_y, begin_x)
1735 # newwin([height, width,] begin_y, begin_x)
1736 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1736 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1737 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1737 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1738
1738
1739 # figure out how much space to allocate for the chunk-pad which is
1739 # figure out how much space to allocate for the chunk-pad which is
1740 # used for displaying the patch
1740 # used for displaying the patch
1741
1741
1742 # stupid hack to prevent getnumlinesdisplayed from failing
1742 # stupid hack to prevent getnumlinesdisplayed from failing
1743 self.chunkpad = curses.newpad(1, self.xscreensize)
1743 self.chunkpad = curses.newpad(1, self.xscreensize)
1744
1744
1745 # add 1 so to account for last line text reaching end of line
1745 # add 1 so to account for last line text reaching end of line
1746 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1746 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1747
1747
1748 try:
1748 try:
1749 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1749 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1750 except curses.error:
1750 except curses.error:
1751 self.initerr = _('this diff is too large to be displayed')
1751 self.initerr = _('this diff is too large to be displayed')
1752 return
1752 return
1753 # initialize selecteditemendline (initial start-line is 0)
1753 # initialize selecteditemendline (initial start-line is 0)
1754 self.selecteditemendline = self.getnumlinesdisplayed(
1754 self.selecteditemendline = self.getnumlinesdisplayed(
1755 self.currentselecteditem, recursechildren=False)
1755 self.currentselecteditem, recursechildren=False)
1756
1756
1757 while True:
1757 while True:
1758 self.updatescreen()
1758 self.updatescreen()
1759 try:
1759 try:
1760 with self.ui.timeblockedsection('crecord'):
1760 with self.ui.timeblockedsection('crecord'):
1761 keypressed = self.statuswin.getkey()
1761 keypressed = self.statuswin.getkey()
1762 if self.errorstr is not None:
1762 if self.errorstr is not None:
1763 self.errorstr = None
1763 self.errorstr = None
1764 continue
1764 continue
1765 except curses.error:
1765 except curses.error:
1766 keypressed = "foobar"
1766 keypressed = "foobar"
1767 if self.handlekeypressed(keypressed):
1767 if self.handlekeypressed(keypressed):
1768 break
1768 break
1769
1769
1770 if self.commenttext != "":
1770 if self.commenttext != "":
1771 whitespaceremoved = re.sub("(?m)^\s.*(\n|$)", "", self.commenttext)
1771 whitespaceremoved = re.sub("(?m)^\s.*(\n|$)", "", self.commenttext)
1772 if whitespaceremoved != "":
1772 if whitespaceremoved != "":
1773 self.opts['message'] = self.commenttext
1773 self.opts['message'] = self.commenttext
General Comments 0
You need to be logged in to leave comments. Login now