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