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