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