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