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