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