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