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