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