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