##// END OF EJS Templates
patch: rewrite reversehunks (issue5337)...
Jun Wu -
r32979:66117dae default
parent child Browse files
Show More
@@ -1,1683 +1,1731 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 stringio = util.stringio
26 stringio = util.stringio
27
27
28 # This is required for ncurses to display non-ASCII characters in default user
28 # This is required for ncurses to display non-ASCII characters in default user
29 # locale encoding correctly. --immerrr
29 # locale encoding correctly. --immerrr
30 locale.setlocale(locale.LC_ALL, u'')
30 locale.setlocale(locale.LC_ALL, u'')
31
31
32 # patch comments based on the git one
32 # patch comments based on the git one
33 diffhelptext = _("""# To remove '-' lines, make them ' ' lines (context).
33 diffhelptext = _("""# To remove '-' lines, make them ' ' lines (context).
34 # To remove '+' lines, delete them.
34 # To remove '+' lines, delete them.
35 # Lines starting with # will be removed from the patch.
35 # Lines starting with # will be removed from the patch.
36 """)
36 """)
37
37
38 hunkhelptext = _("""#
38 hunkhelptext = _("""#
39 # If the patch applies cleanly, the edited hunk will immediately be
39 # If the patch applies cleanly, the edited hunk will immediately be
40 # added to the record list. If it does not apply cleanly, a rejects file
40 # added to the record list. If it does not apply cleanly, a rejects file
41 # will be generated. You can use that when you try again. If all lines
41 # will be generated. You can use that when you try again. If all lines
42 # of the hunk are removed, then the edit is aborted and the hunk is left
42 # of the hunk are removed, then the edit is aborted and the hunk is left
43 # unchanged.
43 # unchanged.
44 """)
44 """)
45
45
46 patchhelptext = _("""#
46 patchhelptext = _("""#
47 # If the patch applies cleanly, the edited patch will immediately
47 # If the patch applies cleanly, the edited patch will immediately
48 # be finalised. If it does not apply cleanly, rejects files will be
48 # be finalised. If it does not apply cleanly, rejects files will be
49 # generated. You can use those when you try again.
49 # generated. You can use those when you try again.
50 """)
50 """)
51
51
52 try:
52 try:
53 import curses
53 import curses
54 curses.error
54 curses.error
55 except ImportError:
55 except ImportError:
56 # I have no idea if wcurses works with crecord...
56 # I have no idea if wcurses works with crecord...
57 try:
57 try:
58 import wcurses as curses
58 import wcurses as curses
59 curses.error
59 curses.error
60 except ImportError:
60 except ImportError:
61 # wcurses is not shipped on Windows by default, or python is not
61 # wcurses is not shipped on Windows by default, or python is not
62 # compiled with curses
62 # compiled with curses
63 curses = False
63 curses = False
64
64
65 def checkcurses(ui):
65 def checkcurses(ui):
66 """Return True if the user wants to use curses
66 """Return True if the user wants to use curses
67
67
68 This method returns True if curses is found (and that python is built with
68 This method returns True if curses is found (and that python is built with
69 it) and that the user has the correct flag for the ui.
69 it) and that the user has the correct flag for the ui.
70 """
70 """
71 return curses and ui.interface("chunkselector") == "curses"
71 return curses and ui.interface("chunkselector") == "curses"
72
72
73 class patchnode(object):
73 class patchnode(object):
74 """abstract class for patch graph nodes
74 """abstract class for patch graph nodes
75 (i.e. patchroot, header, hunk, hunkline)
75 (i.e. patchroot, header, hunk, hunkline)
76 """
76 """
77
77
78 def firstchild(self):
78 def firstchild(self):
79 raise NotImplementedError("method must be implemented by subclass")
79 raise NotImplementedError("method must be implemented by subclass")
80
80
81 def lastchild(self):
81 def lastchild(self):
82 raise NotImplementedError("method must be implemented by subclass")
82 raise NotImplementedError("method must be implemented by subclass")
83
83
84 def allchildren(self):
84 def allchildren(self):
85 "Return a list of all of the direct children of this node"
85 "Return a list of all of the direct children of this node"
86 raise NotImplementedError("method must be implemented by subclass")
86 raise NotImplementedError("method must be implemented by subclass")
87
87
88 def nextsibling(self):
88 def nextsibling(self):
89 """
89 """
90 Return the closest next item of the same type where there are no items
90 Return the closest next item of the same type where there are no items
91 of different types between the current item and this closest item.
91 of different types between the current item and this closest item.
92 If no such item exists, return None.
92 If no such item exists, return None.
93 """
93 """
94 raise NotImplementedError("method must be implemented by subclass")
94 raise NotImplementedError("method must be implemented by subclass")
95
95
96 def prevsibling(self):
96 def prevsibling(self):
97 """
97 """
98 Return the closest previous item of the same type where there are no
98 Return the closest previous item of the same type where there are no
99 items of different types between the current item and this closest item.
99 items of different types between the current item and this closest item.
100 If no such item exists, return None.
100 If no such item exists, return None.
101 """
101 """
102 raise NotImplementedError("method must be implemented by subclass")
102 raise NotImplementedError("method must be implemented by subclass")
103
103
104 def parentitem(self):
104 def parentitem(self):
105 raise NotImplementedError("method must be implemented by subclass")
105 raise NotImplementedError("method must be implemented by subclass")
106
106
107 def nextitem(self, skipfolded=True):
107 def nextitem(self, skipfolded=True):
108 """
108 """
109 Try to return the next item closest to this item, regardless of item's
109 Try to return the next item closest to this item, regardless of item's
110 type (header, hunk, or hunkline).
110 type (header, hunk, or hunkline).
111
111
112 If skipfolded == True, and the current item is folded, then the child
112 If skipfolded == True, and the current item is folded, then the child
113 items that are hidden due to folding will be skipped when determining
113 items that are hidden due to folding will be skipped when determining
114 the next item.
114 the next item.
115
115
116 If it is not possible to get the next item, return None.
116 If it is not possible to get the next item, return None.
117 """
117 """
118 try:
118 try:
119 itemfolded = self.folded
119 itemfolded = self.folded
120 except AttributeError:
120 except AttributeError:
121 itemfolded = False
121 itemfolded = False
122 if skipfolded and itemfolded:
122 if skipfolded and itemfolded:
123 nextitem = self.nextsibling()
123 nextitem = self.nextsibling()
124 if nextitem is None:
124 if nextitem is None:
125 try:
125 try:
126 nextitem = self.parentitem().nextsibling()
126 nextitem = self.parentitem().nextsibling()
127 except AttributeError:
127 except AttributeError:
128 nextitem = None
128 nextitem = None
129 return nextitem
129 return nextitem
130 else:
130 else:
131 # try child
131 # try child
132 item = self.firstchild()
132 item = self.firstchild()
133 if item is not None:
133 if item is not None:
134 return item
134 return item
135
135
136 # else try next sibling
136 # else try next sibling
137 item = self.nextsibling()
137 item = self.nextsibling()
138 if item is not None:
138 if item is not None:
139 return item
139 return item
140
140
141 try:
141 try:
142 # else try parent's next sibling
142 # else try parent's next sibling
143 item = self.parentitem().nextsibling()
143 item = self.parentitem().nextsibling()
144 if item is not None:
144 if item is not None:
145 return item
145 return item
146
146
147 # else return grandparent's next sibling (or None)
147 # else return grandparent's next sibling (or None)
148 return self.parentitem().parentitem().nextsibling()
148 return self.parentitem().parentitem().nextsibling()
149
149
150 except AttributeError: # parent and/or grandparent was None
150 except AttributeError: # parent and/or grandparent was None
151 return None
151 return None
152
152
153 def previtem(self):
153 def previtem(self):
154 """
154 """
155 Try to return the previous item closest to this item, regardless of
155 Try to return the previous item closest to this item, regardless of
156 item's type (header, hunk, or hunkline).
156 item's type (header, hunk, or hunkline).
157
157
158 If it is not possible to get the previous item, return None.
158 If it is not possible to get the previous item, return None.
159 """
159 """
160 # try previous sibling's last child's last child,
160 # try previous sibling's last child's last child,
161 # else try previous sibling's last child, else try previous sibling
161 # else try previous sibling's last child, else try previous sibling
162 prevsibling = self.prevsibling()
162 prevsibling = self.prevsibling()
163 if prevsibling is not None:
163 if prevsibling is not None:
164 prevsiblinglastchild = prevsibling.lastchild()
164 prevsiblinglastchild = prevsibling.lastchild()
165 if ((prevsiblinglastchild is not None) and
165 if ((prevsiblinglastchild is not None) and
166 not prevsibling.folded):
166 not prevsibling.folded):
167 prevsiblinglclc = prevsiblinglastchild.lastchild()
167 prevsiblinglclc = prevsiblinglastchild.lastchild()
168 if ((prevsiblinglclc is not None) and
168 if ((prevsiblinglclc is not None) and
169 not prevsiblinglastchild.folded):
169 not prevsiblinglastchild.folded):
170 return prevsiblinglclc
170 return prevsiblinglclc
171 else:
171 else:
172 return prevsiblinglastchild
172 return prevsiblinglastchild
173 else:
173 else:
174 return prevsibling
174 return prevsibling
175
175
176 # try parent (or None)
176 # try parent (or None)
177 return self.parentitem()
177 return self.parentitem()
178
178
179 class patch(patchnode, list): # todo: rename patchroot
179 class patch(patchnode, list): # todo: rename patchroot
180 """
180 """
181 list of header objects representing the patch.
181 list of header objects representing the patch.
182 """
182 """
183 def __init__(self, headerlist):
183 def __init__(self, headerlist):
184 self.extend(headerlist)
184 self.extend(headerlist)
185 # add parent patch object reference to each header
185 # add parent patch object reference to each header
186 for header in self:
186 for header in self:
187 header.patch = self
187 header.patch = self
188
188
189 class uiheader(patchnode):
189 class uiheader(patchnode):
190 """patch header
190 """patch header
191
191
192 xxx shouldn't we move this to mercurial/patch.py ?
192 xxx shouldn't we move this to mercurial/patch.py ?
193 """
193 """
194
194
195 def __init__(self, header):
195 def __init__(self, header):
196 self.nonuiheader = header
196 self.nonuiheader = header
197 # flag to indicate whether to apply this chunk
197 # flag to indicate whether to apply this chunk
198 self.applied = True
198 self.applied = True
199 # flag which only affects the status display indicating if a node's
199 # flag which only affects the status display indicating if a node's
200 # children are partially applied (i.e. some applied, some not).
200 # children are partially applied (i.e. some applied, some not).
201 self.partial = False
201 self.partial = False
202
202
203 # flag to indicate whether to display as folded/unfolded to user
203 # flag to indicate whether to display as folded/unfolded to user
204 self.folded = True
204 self.folded = True
205
205
206 # list of all headers in patch
206 # list of all headers in patch
207 self.patch = None
207 self.patch = None
208
208
209 # flag is False if this header was ever unfolded from initial state
209 # flag is False if this header was ever unfolded from initial state
210 self.neverunfolded = True
210 self.neverunfolded = True
211 self.hunks = [uihunk(h, self) for h in self.hunks]
211 self.hunks = [uihunk(h, self) for h in self.hunks]
212
212
213 def prettystr(self):
213 def prettystr(self):
214 x = stringio()
214 x = stringio()
215 self.pretty(x)
215 self.pretty(x)
216 return x.getvalue()
216 return x.getvalue()
217
217
218 def nextsibling(self):
218 def nextsibling(self):
219 numheadersinpatch = len(self.patch)
219 numheadersinpatch = len(self.patch)
220 indexofthisheader = self.patch.index(self)
220 indexofthisheader = self.patch.index(self)
221
221
222 if indexofthisheader < numheadersinpatch - 1:
222 if indexofthisheader < numheadersinpatch - 1:
223 nextheader = self.patch[indexofthisheader + 1]
223 nextheader = self.patch[indexofthisheader + 1]
224 return nextheader
224 return nextheader
225 else:
225 else:
226 return None
226 return None
227
227
228 def prevsibling(self):
228 def prevsibling(self):
229 indexofthisheader = self.patch.index(self)
229 indexofthisheader = self.patch.index(self)
230 if indexofthisheader > 0:
230 if indexofthisheader > 0:
231 previousheader = self.patch[indexofthisheader - 1]
231 previousheader = self.patch[indexofthisheader - 1]
232 return previousheader
232 return previousheader
233 else:
233 else:
234 return None
234 return None
235
235
236 def parentitem(self):
236 def parentitem(self):
237 """
237 """
238 there is no 'real' parent item of a header that can be selected,
238 there is no 'real' parent item of a header that can be selected,
239 so return None.
239 so return None.
240 """
240 """
241 return None
241 return None
242
242
243 def firstchild(self):
243 def firstchild(self):
244 "return the first child of this item, if one exists. otherwise None."
244 "return the first child of this item, if one exists. otherwise None."
245 if len(self.hunks) > 0:
245 if len(self.hunks) > 0:
246 return self.hunks[0]
246 return self.hunks[0]
247 else:
247 else:
248 return None
248 return None
249
249
250 def lastchild(self):
250 def lastchild(self):
251 "return the last child of this item, if one exists. otherwise None."
251 "return the last child of this item, if one exists. otherwise None."
252 if len(self.hunks) > 0:
252 if len(self.hunks) > 0:
253 return self.hunks[-1]
253 return self.hunks[-1]
254 else:
254 else:
255 return None
255 return None
256
256
257 def allchildren(self):
257 def allchildren(self):
258 "return a list of all of the direct children of this node"
258 "return a list of all of the direct children of this node"
259 return self.hunks
259 return self.hunks
260
260
261 def __getattr__(self, name):
261 def __getattr__(self, name):
262 return getattr(self.nonuiheader, name)
262 return getattr(self.nonuiheader, name)
263
263
264 class uihunkline(patchnode):
264 class uihunkline(patchnode):
265 "represents a changed line in a hunk"
265 "represents a changed line in a hunk"
266 def __init__(self, linetext, hunk):
266 def __init__(self, linetext, hunk):
267 self.linetext = linetext
267 self.linetext = linetext
268 self.applied = True
268 self.applied = True
269 # the parent hunk to which this line belongs
269 # the parent hunk to which this line belongs
270 self.hunk = hunk
270 self.hunk = hunk
271 # folding lines currently is not used/needed, but this flag is needed
271 # folding lines currently is not used/needed, but this flag is needed
272 # in the previtem method.
272 # in the previtem method.
273 self.folded = False
273 self.folded = False
274
274
275 def prettystr(self):
275 def prettystr(self):
276 return self.linetext
276 return self.linetext
277
277
278 def nextsibling(self):
278 def nextsibling(self):
279 numlinesinhunk = len(self.hunk.changedlines)
279 numlinesinhunk = len(self.hunk.changedlines)
280 indexofthisline = self.hunk.changedlines.index(self)
280 indexofthisline = self.hunk.changedlines.index(self)
281
281
282 if (indexofthisline < numlinesinhunk - 1):
282 if (indexofthisline < numlinesinhunk - 1):
283 nextline = self.hunk.changedlines[indexofthisline + 1]
283 nextline = self.hunk.changedlines[indexofthisline + 1]
284 return nextline
284 return nextline
285 else:
285 else:
286 return None
286 return None
287
287
288 def prevsibling(self):
288 def prevsibling(self):
289 indexofthisline = self.hunk.changedlines.index(self)
289 indexofthisline = self.hunk.changedlines.index(self)
290 if indexofthisline > 0:
290 if indexofthisline > 0:
291 previousline = self.hunk.changedlines[indexofthisline - 1]
291 previousline = self.hunk.changedlines[indexofthisline - 1]
292 return previousline
292 return previousline
293 else:
293 else:
294 return None
294 return None
295
295
296 def parentitem(self):
296 def parentitem(self):
297 "return the parent to the current item"
297 "return the parent to the current item"
298 return self.hunk
298 return self.hunk
299
299
300 def firstchild(self):
300 def firstchild(self):
301 "return the first child of this item, if one exists. otherwise None."
301 "return the first child of this item, if one exists. otherwise None."
302 # hunk-lines don't have children
302 # hunk-lines don't have children
303 return None
303 return None
304
304
305 def lastchild(self):
305 def lastchild(self):
306 "return the last child of this item, if one exists. otherwise None."
306 "return the last child of this item, if one exists. otherwise None."
307 # hunk-lines don't have children
307 # hunk-lines don't have children
308 return None
308 return None
309
309
310 class uihunk(patchnode):
310 class uihunk(patchnode):
311 """ui patch hunk, wraps a hunk and keep track of ui behavior """
311 """ui patch hunk, wraps a hunk and keep track of ui behavior """
312 maxcontext = 3
312 maxcontext = 3
313
313
314 def __init__(self, hunk, header):
314 def __init__(self, hunk, header):
315 self._hunk = hunk
315 self._hunk = hunk
316 self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
316 self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
317 self.header = header
317 self.header = header
318 # used at end for detecting how many removed lines were un-applied
318 # used at end for detecting how many removed lines were un-applied
319 self.originalremoved = self.removed
319 self.originalremoved = self.removed
320
320
321 # flag to indicate whether to display as folded/unfolded to user
321 # flag to indicate whether to display as folded/unfolded to user
322 self.folded = True
322 self.folded = True
323 # flag to indicate whether to apply this chunk
323 # flag to indicate whether to apply this chunk
324 self.applied = True
324 self.applied = True
325 # flag which only affects the status display indicating if a node's
325 # flag which only affects the status display indicating if a node's
326 # children are partially applied (i.e. some applied, some not).
326 # children are partially applied (i.e. some applied, some not).
327 self.partial = False
327 self.partial = False
328
328
329 def nextsibling(self):
329 def nextsibling(self):
330 numhunksinheader = len(self.header.hunks)
330 numhunksinheader = len(self.header.hunks)
331 indexofthishunk = self.header.hunks.index(self)
331 indexofthishunk = self.header.hunks.index(self)
332
332
333 if (indexofthishunk < numhunksinheader - 1):
333 if (indexofthishunk < numhunksinheader - 1):
334 nexthunk = self.header.hunks[indexofthishunk + 1]
334 nexthunk = self.header.hunks[indexofthishunk + 1]
335 return nexthunk
335 return nexthunk
336 else:
336 else:
337 return None
337 return None
338
338
339 def prevsibling(self):
339 def prevsibling(self):
340 indexofthishunk = self.header.hunks.index(self)
340 indexofthishunk = self.header.hunks.index(self)
341 if indexofthishunk > 0:
341 if indexofthishunk > 0:
342 previoushunk = self.header.hunks[indexofthishunk - 1]
342 previoushunk = self.header.hunks[indexofthishunk - 1]
343 return previoushunk
343 return previoushunk
344 else:
344 else:
345 return None
345 return None
346
346
347 def parentitem(self):
347 def parentitem(self):
348 "return the parent to the current item"
348 "return the parent to the current item"
349 return self.header
349 return self.header
350
350
351 def firstchild(self):
351 def firstchild(self):
352 "return the first child of this item, if one exists. otherwise None."
352 "return the first child of this item, if one exists. otherwise None."
353 if len(self.changedlines) > 0:
353 if len(self.changedlines) > 0:
354 return self.changedlines[0]
354 return self.changedlines[0]
355 else:
355 else:
356 return None
356 return None
357
357
358 def lastchild(self):
358 def lastchild(self):
359 "return the last child of this item, if one exists. otherwise None."
359 "return the last child of this item, if one exists. otherwise None."
360 if len(self.changedlines) > 0:
360 if len(self.changedlines) > 0:
361 return self.changedlines[-1]
361 return self.changedlines[-1]
362 else:
362 else:
363 return None
363 return None
364
364
365 def allchildren(self):
365 def allchildren(self):
366 "return a list of all of the direct children of this node"
366 "return a list of all of the direct children of this node"
367 return self.changedlines
367 return self.changedlines
368
368
369 def countchanges(self):
369 def countchanges(self):
370 """changedlines -> (n+,n-)"""
370 """changedlines -> (n+,n-)"""
371 add = len([l for l in self.changedlines if l.applied
371 add = len([l for l in self.changedlines if l.applied
372 and l.prettystr()[0] == '+'])
372 and l.prettystr()[0] == '+'])
373 rem = len([l for l in self.changedlines if l.applied
373 rem = len([l for l in self.changedlines if l.applied
374 and l.prettystr()[0] == '-'])
374 and l.prettystr()[0] == '-'])
375 return add, rem
375 return add, rem
376
376
377 def getfromtoline(self):
377 def getfromtoline(self):
378 # calculate the number of removed lines converted to context lines
378 # calculate the number of removed lines converted to context lines
379 removedconvertedtocontext = self.originalremoved - self.removed
379 removedconvertedtocontext = self.originalremoved - self.removed
380
380
381 contextlen = (len(self.before) + len(self.after) +
381 contextlen = (len(self.before) + len(self.after) +
382 removedconvertedtocontext)
382 removedconvertedtocontext)
383 if self.after and self.after[-1] == '\\ No newline at end of file\n':
383 if self.after and self.after[-1] == '\\ No newline at end of file\n':
384 contextlen -= 1
384 contextlen -= 1
385 fromlen = contextlen + self.removed
385 fromlen = contextlen + self.removed
386 tolen = contextlen + self.added
386 tolen = contextlen + self.added
387
387
388 # diffutils manual, section "2.2.2.2 detailed description of unified
388 # diffutils manual, section "2.2.2.2 detailed description of unified
389 # format": "an empty hunk is considered to end at the line that
389 # format": "an empty hunk is considered to end at the line that
390 # precedes the hunk."
390 # precedes the hunk."
391 #
391 #
392 # so, if either of hunks is empty, decrease its line start. --immerrr
392 # so, if either of hunks is empty, decrease its line start. --immerrr
393 # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
393 # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
394 fromline, toline = self.fromline, self.toline
394 fromline, toline = self.fromline, self.toline
395 if fromline != 0:
395 if fromline != 0:
396 if fromlen == 0:
396 if fromlen == 0:
397 fromline -= 1
397 fromline -= 1
398 if tolen == 0:
398 if tolen == 0:
399 toline -= 1
399 toline -= 1
400
400
401 fromtoline = '@@ -%d,%d +%d,%d @@%s\n' % (
401 fromtoline = '@@ -%d,%d +%d,%d @@%s\n' % (
402 fromline, fromlen, toline, tolen,
402 fromline, fromlen, toline, tolen,
403 self.proc and (' ' + self.proc))
403 self.proc and (' ' + self.proc))
404 return fromtoline
404 return fromtoline
405
405
406 def write(self, fp):
406 def write(self, fp):
407 # updated self.added/removed, which are used by getfromtoline()
407 # updated self.added/removed, which are used by getfromtoline()
408 self.added, self.removed = self.countchanges()
408 self.added, self.removed = self.countchanges()
409 fp.write(self.getfromtoline())
409 fp.write(self.getfromtoline())
410
410
411 hunklinelist = []
411 hunklinelist = []
412 # add the following to the list: (1) all applied lines, and
412 # add the following to the list: (1) all applied lines, and
413 # (2) all unapplied removal lines (convert these to context lines)
413 # (2) all unapplied removal lines (convert these to context lines)
414 for changedline in self.changedlines:
414 for changedline in self.changedlines:
415 changedlinestr = changedline.prettystr()
415 changedlinestr = changedline.prettystr()
416 if changedline.applied:
416 if changedline.applied:
417 hunklinelist.append(changedlinestr)
417 hunklinelist.append(changedlinestr)
418 elif changedlinestr[0] == "-":
418 elif changedlinestr[0] == "-":
419 hunklinelist.append(" " + changedlinestr[1:])
419 hunklinelist.append(" " + changedlinestr[1:])
420
420
421 fp.write(''.join(self.before + hunklinelist + self.after))
421 fp.write(''.join(self.before + hunklinelist + self.after))
422
422
423 pretty = write
423 pretty = write
424
424
425 def prettystr(self):
425 def prettystr(self):
426 x = stringio()
426 x = stringio()
427 self.pretty(x)
427 self.pretty(x)
428 return x.getvalue()
428 return x.getvalue()
429
429
430 def reversehunk(self):
431 """return a recordhunk which is the reverse of the hunk
432
433 Assuming the displayed patch is diff(A, B) result. The returned hunk is
434 intended to be applied to B, instead of A.
435
436 For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and
437 the user made the following selection:
438
439 0
440 [x] -1 [x]: selected
441 [ ] -2 [ ]: not selected
442 [x] +3
443 [ ] +4
444 [x] +5
445 6
446
447 This function returns a hunk like:
448
449 0
450 -3
451 -4
452 -5
453 +1
454 +4
455 6
456
457 Note "4" was first deleted then added. That's because "4" exists in B
458 side and "-4" must exist between "-3" and "-5" to make the patch
459 applicable to B.
460 """
461 dels = []
462 adds = []
463 for line in self.changedlines:
464 text = line.linetext
465 if line.applied:
466 if text[0] == '+':
467 dels.append(text[1:])
468 elif text[0] == '-':
469 adds.append(text[1:])
470 elif text[0] == '+':
471 dels.append(text[1:])
472 adds.append(text[1:])
473 hunk = ['-%s' % l for l in dels] + ['+%s' % l for l in adds]
474 h = self._hunk
475 return patchmod.recordhunk(h.header, h.toline, h.fromline, h.proc,
476 h.before, hunk, h.after)
477
430 def __getattr__(self, name):
478 def __getattr__(self, name):
431 return getattr(self._hunk, name)
479 return getattr(self._hunk, name)
432
480
433 def __repr__(self):
481 def __repr__(self):
434 return '<hunk %r@%d>' % (self.filename(), self.fromline)
482 return '<hunk %r@%d>' % (self.filename(), self.fromline)
435
483
436 def filterpatch(ui, chunks, chunkselector, operation=None):
484 def filterpatch(ui, chunks, chunkselector, operation=None):
437 """interactively filter patch chunks into applied-only chunks"""
485 """interactively filter patch chunks into applied-only chunks"""
438 chunks = list(chunks)
486 chunks = list(chunks)
439 # convert chunks list into structure suitable for displaying/modifying
487 # convert chunks list into structure suitable for displaying/modifying
440 # with curses. create a list of headers only.
488 # with curses. create a list of headers only.
441 headers = [c for c in chunks if isinstance(c, patchmod.header)]
489 headers = [c for c in chunks if isinstance(c, patchmod.header)]
442
490
443 # if there are no changed files
491 # if there are no changed files
444 if len(headers) == 0:
492 if len(headers) == 0:
445 return [], {}
493 return [], {}
446 uiheaders = [uiheader(h) for h in headers]
494 uiheaders = [uiheader(h) for h in headers]
447 # let user choose headers/hunks/lines, and mark their applied flags
495 # let user choose headers/hunks/lines, and mark their applied flags
448 # accordingly
496 # accordingly
449 ret = chunkselector(ui, uiheaders, operation=operation)
497 ret = chunkselector(ui, uiheaders, operation=operation)
450 appliedhunklist = []
498 appliedhunklist = []
451 for hdr in uiheaders:
499 for hdr in uiheaders:
452 if (hdr.applied and
500 if (hdr.applied and
453 (hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0)):
501 (hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0)):
454 appliedhunklist.append(hdr)
502 appliedhunklist.append(hdr)
455 fixoffset = 0
503 fixoffset = 0
456 for hnk in hdr.hunks:
504 for hnk in hdr.hunks:
457 if hnk.applied:
505 if hnk.applied:
458 appliedhunklist.append(hnk)
506 appliedhunklist.append(hnk)
459 # adjust the 'to'-line offset of the hunk to be correct
507 # adjust the 'to'-line offset of the hunk to be correct
460 # after de-activating some of the other hunks for this file
508 # after de-activating some of the other hunks for this file
461 if fixoffset:
509 if fixoffset:
462 #hnk = copy.copy(hnk) # necessary??
510 #hnk = copy.copy(hnk) # necessary??
463 hnk.toline += fixoffset
511 hnk.toline += fixoffset
464 else:
512 else:
465 fixoffset += hnk.removed - hnk.added
513 fixoffset += hnk.removed - hnk.added
466
514
467 return (appliedhunklist, ret)
515 return (appliedhunklist, ret)
468
516
469 def chunkselector(ui, headerlist, operation=None):
517 def chunkselector(ui, headerlist, operation=None):
470 """
518 """
471 curses interface to get selection of chunks, and mark the applied flags
519 curses interface to get selection of chunks, and mark the applied flags
472 of the chosen chunks.
520 of the chosen chunks.
473 """
521 """
474 ui.write(_('starting interactive selection\n'))
522 ui.write(_('starting interactive selection\n'))
475 chunkselector = curseschunkselector(headerlist, ui, operation)
523 chunkselector = curseschunkselector(headerlist, ui, operation)
476 origsigtstp = sentinel = object()
524 origsigtstp = sentinel = object()
477 if util.safehasattr(signal, 'SIGTSTP'):
525 if util.safehasattr(signal, 'SIGTSTP'):
478 origsigtstp = signal.getsignal(signal.SIGTSTP)
526 origsigtstp = signal.getsignal(signal.SIGTSTP)
479 try:
527 try:
480 curses.wrapper(chunkselector.main)
528 curses.wrapper(chunkselector.main)
481 if chunkselector.initerr is not None:
529 if chunkselector.initerr is not None:
482 raise error.Abort(chunkselector.initerr)
530 raise error.Abort(chunkselector.initerr)
483 # ncurses does not restore signal handler for SIGTSTP
531 # ncurses does not restore signal handler for SIGTSTP
484 finally:
532 finally:
485 if origsigtstp is not sentinel:
533 if origsigtstp is not sentinel:
486 signal.signal(signal.SIGTSTP, origsigtstp)
534 signal.signal(signal.SIGTSTP, origsigtstp)
487 return chunkselector.opts
535 return chunkselector.opts
488
536
489 def testdecorator(testfn, f):
537 def testdecorator(testfn, f):
490 def u(*args, **kwargs):
538 def u(*args, **kwargs):
491 return f(testfn, *args, **kwargs)
539 return f(testfn, *args, **kwargs)
492 return u
540 return u
493
541
494 def testchunkselector(testfn, ui, headerlist, operation=None):
542 def testchunkselector(testfn, ui, headerlist, operation=None):
495 """
543 """
496 test interface to get selection of chunks, and mark the applied flags
544 test interface to get selection of chunks, and mark the applied flags
497 of the chosen chunks.
545 of the chosen chunks.
498 """
546 """
499 chunkselector = curseschunkselector(headerlist, ui, operation)
547 chunkselector = curseschunkselector(headerlist, ui, operation)
500 if testfn and os.path.exists(testfn):
548 if testfn and os.path.exists(testfn):
501 testf = open(testfn)
549 testf = open(testfn)
502 testcommands = map(lambda x: x.rstrip('\n'), testf.readlines())
550 testcommands = map(lambda x: x.rstrip('\n'), testf.readlines())
503 testf.close()
551 testf.close()
504 while True:
552 while True:
505 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
553 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
506 break
554 break
507 return chunkselector.opts
555 return chunkselector.opts
508
556
509 _headermessages = { # {operation: text}
557 _headermessages = { # {operation: text}
510 'revert': _('Select hunks to revert'),
558 'revert': _('Select hunks to revert'),
511 'discard': _('Select hunks to discard'),
559 'discard': _('Select hunks to discard'),
512 None: _('Select hunks to record'),
560 None: _('Select hunks to record'),
513 }
561 }
514
562
515 class curseschunkselector(object):
563 class curseschunkselector(object):
516 def __init__(self, headerlist, ui, operation=None):
564 def __init__(self, headerlist, ui, operation=None):
517 # put the headers into a patch object
565 # put the headers into a patch object
518 self.headerlist = patch(headerlist)
566 self.headerlist = patch(headerlist)
519
567
520 self.ui = ui
568 self.ui = ui
521 self.opts = {}
569 self.opts = {}
522
570
523 self.errorstr = None
571 self.errorstr = None
524 # list of all chunks
572 # list of all chunks
525 self.chunklist = []
573 self.chunklist = []
526 for h in headerlist:
574 for h in headerlist:
527 self.chunklist.append(h)
575 self.chunklist.append(h)
528 self.chunklist.extend(h.hunks)
576 self.chunklist.extend(h.hunks)
529
577
530 # dictionary mapping (fgcolor, bgcolor) pairs to the
578 # dictionary mapping (fgcolor, bgcolor) pairs to the
531 # corresponding curses color-pair value.
579 # corresponding curses color-pair value.
532 self.colorpairs = {}
580 self.colorpairs = {}
533 # maps custom nicknames of color-pairs to curses color-pair values
581 # maps custom nicknames of color-pairs to curses color-pair values
534 self.colorpairnames = {}
582 self.colorpairnames = {}
535
583
536 # the currently selected header, hunk, or hunk-line
584 # the currently selected header, hunk, or hunk-line
537 self.currentselecteditem = self.headerlist[0]
585 self.currentselecteditem = self.headerlist[0]
538
586
539 # updated when printing out patch-display -- the 'lines' here are the
587 # updated when printing out patch-display -- the 'lines' here are the
540 # line positions *in the pad*, not on the screen.
588 # line positions *in the pad*, not on the screen.
541 self.selecteditemstartline = 0
589 self.selecteditemstartline = 0
542 self.selecteditemendline = None
590 self.selecteditemendline = None
543
591
544 # define indentation levels
592 # define indentation levels
545 self.headerindentnumchars = 0
593 self.headerindentnumchars = 0
546 self.hunkindentnumchars = 3
594 self.hunkindentnumchars = 3
547 self.hunklineindentnumchars = 6
595 self.hunklineindentnumchars = 6
548
596
549 # the first line of the pad to print to the screen
597 # the first line of the pad to print to the screen
550 self.firstlineofpadtoprint = 0
598 self.firstlineofpadtoprint = 0
551
599
552 # keeps track of the number of lines in the pad
600 # keeps track of the number of lines in the pad
553 self.numpadlines = None
601 self.numpadlines = None
554
602
555 self.numstatuslines = 1
603 self.numstatuslines = 1
556
604
557 # keep a running count of the number of lines printed to the pad
605 # keep a running count of the number of lines printed to the pad
558 # (used for determining when the selected item begins/ends)
606 # (used for determining when the selected item begins/ends)
559 self.linesprintedtopadsofar = 0
607 self.linesprintedtopadsofar = 0
560
608
561 # the first line of the pad which is visible on the screen
609 # the first line of the pad which is visible on the screen
562 self.firstlineofpadtoprint = 0
610 self.firstlineofpadtoprint = 0
563
611
564 # stores optional text for a commit comment provided by the user
612 # stores optional text for a commit comment provided by the user
565 self.commenttext = ""
613 self.commenttext = ""
566
614
567 # if the last 'toggle all' command caused all changes to be applied
615 # if the last 'toggle all' command caused all changes to be applied
568 self.waslasttoggleallapplied = True
616 self.waslasttoggleallapplied = True
569
617
570 # affects some ui text
618 # affects some ui text
571 if operation not in _headermessages:
619 if operation not in _headermessages:
572 raise error.ProgrammingError('unexpected operation: %s' % operation)
620 raise error.ProgrammingError('unexpected operation: %s' % operation)
573 self.operation = operation
621 self.operation = operation
574
622
575 def uparrowevent(self):
623 def uparrowevent(self):
576 """
624 """
577 try to select the previous item to the current item that has the
625 try to select the previous item to the current item that has the
578 most-indented level. for example, if a hunk is selected, try to select
626 most-indented level. for example, if a hunk is selected, try to select
579 the last hunkline of the hunk prior to the selected hunk. or, if
627 the last hunkline of the hunk prior to the selected hunk. or, if
580 the first hunkline of a hunk is currently selected, then select the
628 the first hunkline of a hunk is currently selected, then select the
581 hunk itself.
629 hunk itself.
582 """
630 """
583 currentitem = self.currentselecteditem
631 currentitem = self.currentselecteditem
584
632
585 nextitem = currentitem.previtem()
633 nextitem = currentitem.previtem()
586
634
587 if nextitem is None:
635 if nextitem is None:
588 # if no parent item (i.e. currentitem is the first header), then
636 # if no parent item (i.e. currentitem is the first header), then
589 # no change...
637 # no change...
590 nextitem = currentitem
638 nextitem = currentitem
591
639
592 self.currentselecteditem = nextitem
640 self.currentselecteditem = nextitem
593
641
594 def uparrowshiftevent(self):
642 def uparrowshiftevent(self):
595 """
643 """
596 select (if possible) the previous item on the same level as the
644 select (if possible) the previous item on the same level as the
597 currently selected item. otherwise, select (if possible) the
645 currently selected item. otherwise, select (if possible) the
598 parent-item of the currently selected item.
646 parent-item of the currently selected item.
599 """
647 """
600 currentitem = self.currentselecteditem
648 currentitem = self.currentselecteditem
601 nextitem = currentitem.prevsibling()
649 nextitem = currentitem.prevsibling()
602 # if there's no previous sibling, try choosing the parent
650 # if there's no previous sibling, try choosing the parent
603 if nextitem is None:
651 if nextitem is None:
604 nextitem = currentitem.parentitem()
652 nextitem = currentitem.parentitem()
605 if nextitem is None:
653 if nextitem is None:
606 # if no parent item (i.e. currentitem is the first header), then
654 # if no parent item (i.e. currentitem is the first header), then
607 # no change...
655 # no change...
608 nextitem = currentitem
656 nextitem = currentitem
609
657
610 self.currentselecteditem = nextitem
658 self.currentselecteditem = nextitem
611
659
612 def downarrowevent(self):
660 def downarrowevent(self):
613 """
661 """
614 try to select the next item to the current item that has the
662 try to select the next item to the current item that has the
615 most-indented level. for example, if a hunk is selected, select
663 most-indented level. for example, if a hunk is selected, select
616 the first hunkline of the selected hunk. or, if the last hunkline of
664 the first hunkline of the selected hunk. or, if the last hunkline of
617 a hunk is currently selected, then select the next hunk, if one exists,
665 a hunk is currently selected, then select the next hunk, if one exists,
618 or if not, the next header if one exists.
666 or if not, the next header if one exists.
619 """
667 """
620 #self.startprintline += 1 #debug
668 #self.startprintline += 1 #debug
621 currentitem = self.currentselecteditem
669 currentitem = self.currentselecteditem
622
670
623 nextitem = currentitem.nextitem()
671 nextitem = currentitem.nextitem()
624 # if there's no next item, keep the selection as-is
672 # if there's no next item, keep the selection as-is
625 if nextitem is None:
673 if nextitem is None:
626 nextitem = currentitem
674 nextitem = currentitem
627
675
628 self.currentselecteditem = nextitem
676 self.currentselecteditem = nextitem
629
677
630 def downarrowshiftevent(self):
678 def downarrowshiftevent(self):
631 """
679 """
632 select (if possible) the next item on the same level as the currently
680 select (if possible) the next item on the same level as the currently
633 selected item. otherwise, select (if possible) the next item on the
681 selected item. otherwise, select (if possible) the next item on the
634 same level as the parent item of the currently selected item.
682 same level as the parent item of the currently selected item.
635 """
683 """
636 currentitem = self.currentselecteditem
684 currentitem = self.currentselecteditem
637 nextitem = currentitem.nextsibling()
685 nextitem = currentitem.nextsibling()
638 # if there's no next sibling, try choosing the parent's nextsibling
686 # if there's no next sibling, try choosing the parent's nextsibling
639 if nextitem is None:
687 if nextitem is None:
640 try:
688 try:
641 nextitem = currentitem.parentitem().nextsibling()
689 nextitem = currentitem.parentitem().nextsibling()
642 except AttributeError:
690 except AttributeError:
643 # parentitem returned None, so nextsibling() can't be called
691 # parentitem returned None, so nextsibling() can't be called
644 nextitem = None
692 nextitem = None
645 if nextitem is None:
693 if nextitem is None:
646 # if parent has no next sibling, then no change...
694 # if parent has no next sibling, then no change...
647 nextitem = currentitem
695 nextitem = currentitem
648
696
649 self.currentselecteditem = nextitem
697 self.currentselecteditem = nextitem
650
698
651 def rightarrowevent(self):
699 def rightarrowevent(self):
652 """
700 """
653 select (if possible) the first of this item's child-items.
701 select (if possible) the first of this item's child-items.
654 """
702 """
655 currentitem = self.currentselecteditem
703 currentitem = self.currentselecteditem
656 nextitem = currentitem.firstchild()
704 nextitem = currentitem.firstchild()
657
705
658 # turn off folding if we want to show a child-item
706 # turn off folding if we want to show a child-item
659 if currentitem.folded:
707 if currentitem.folded:
660 self.togglefolded(currentitem)
708 self.togglefolded(currentitem)
661
709
662 if nextitem is None:
710 if nextitem is None:
663 # if no next item on parent-level, then no change...
711 # if no next item on parent-level, then no change...
664 nextitem = currentitem
712 nextitem = currentitem
665
713
666 self.currentselecteditem = nextitem
714 self.currentselecteditem = nextitem
667
715
668 def leftarrowevent(self):
716 def leftarrowevent(self):
669 """
717 """
670 if the current item can be folded (i.e. it is an unfolded header or
718 if the current item can be folded (i.e. it is an unfolded header or
671 hunk), then fold it. otherwise try select (if possible) the parent
719 hunk), then fold it. otherwise try select (if possible) the parent
672 of this item.
720 of this item.
673 """
721 """
674 currentitem = self.currentselecteditem
722 currentitem = self.currentselecteditem
675
723
676 # try to fold the item
724 # try to fold the item
677 if not isinstance(currentitem, uihunkline):
725 if not isinstance(currentitem, uihunkline):
678 if not currentitem.folded:
726 if not currentitem.folded:
679 self.togglefolded(item=currentitem)
727 self.togglefolded(item=currentitem)
680 return
728 return
681
729
682 # if it can't be folded, try to select the parent item
730 # if it can't be folded, try to select the parent item
683 nextitem = currentitem.parentitem()
731 nextitem = currentitem.parentitem()
684
732
685 if nextitem is None:
733 if nextitem is None:
686 # if no item on parent-level, then no change...
734 # if no item on parent-level, then no change...
687 nextitem = currentitem
735 nextitem = currentitem
688 if not nextitem.folded:
736 if not nextitem.folded:
689 self.togglefolded(item=nextitem)
737 self.togglefolded(item=nextitem)
690
738
691 self.currentselecteditem = nextitem
739 self.currentselecteditem = nextitem
692
740
693 def leftarrowshiftevent(self):
741 def leftarrowshiftevent(self):
694 """
742 """
695 select the header of the current item (or fold current item if the
743 select the header of the current item (or fold current item if the
696 current item is already a header).
744 current item is already a header).
697 """
745 """
698 currentitem = self.currentselecteditem
746 currentitem = self.currentselecteditem
699
747
700 if isinstance(currentitem, uiheader):
748 if isinstance(currentitem, uiheader):
701 if not currentitem.folded:
749 if not currentitem.folded:
702 self.togglefolded(item=currentitem)
750 self.togglefolded(item=currentitem)
703 return
751 return
704
752
705 # select the parent item recursively until we're at a header
753 # select the parent item recursively until we're at a header
706 while True:
754 while True:
707 nextitem = currentitem.parentitem()
755 nextitem = currentitem.parentitem()
708 if nextitem is None:
756 if nextitem is None:
709 break
757 break
710 else:
758 else:
711 currentitem = nextitem
759 currentitem = nextitem
712
760
713 self.currentselecteditem = currentitem
761 self.currentselecteditem = currentitem
714
762
715 def updatescroll(self):
763 def updatescroll(self):
716 "scroll the screen to fully show the currently-selected"
764 "scroll the screen to fully show the currently-selected"
717 selstart = self.selecteditemstartline
765 selstart = self.selecteditemstartline
718 selend = self.selecteditemendline
766 selend = self.selecteditemendline
719
767
720 padstart = self.firstlineofpadtoprint
768 padstart = self.firstlineofpadtoprint
721 padend = padstart + self.yscreensize - self.numstatuslines - 1
769 padend = padstart + self.yscreensize - self.numstatuslines - 1
722 # 'buffered' pad start/end values which scroll with a certain
770 # 'buffered' pad start/end values which scroll with a certain
723 # top/bottom context margin
771 # top/bottom context margin
724 padstartbuffered = padstart + 3
772 padstartbuffered = padstart + 3
725 padendbuffered = padend - 3
773 padendbuffered = padend - 3
726
774
727 if selend > padendbuffered:
775 if selend > padendbuffered:
728 self.scrolllines(selend - padendbuffered)
776 self.scrolllines(selend - padendbuffered)
729 elif selstart < padstartbuffered:
777 elif selstart < padstartbuffered:
730 # negative values scroll in pgup direction
778 # negative values scroll in pgup direction
731 self.scrolllines(selstart - padstartbuffered)
779 self.scrolllines(selstart - padstartbuffered)
732
780
733 def scrolllines(self, numlines):
781 def scrolllines(self, numlines):
734 "scroll the screen up (down) by numlines when numlines >0 (<0)."
782 "scroll the screen up (down) by numlines when numlines >0 (<0)."
735 self.firstlineofpadtoprint += numlines
783 self.firstlineofpadtoprint += numlines
736 if self.firstlineofpadtoprint < 0:
784 if self.firstlineofpadtoprint < 0:
737 self.firstlineofpadtoprint = 0
785 self.firstlineofpadtoprint = 0
738 if self.firstlineofpadtoprint > self.numpadlines - 1:
786 if self.firstlineofpadtoprint > self.numpadlines - 1:
739 self.firstlineofpadtoprint = self.numpadlines - 1
787 self.firstlineofpadtoprint = self.numpadlines - 1
740
788
741 def toggleapply(self, item=None):
789 def toggleapply(self, item=None):
742 """
790 """
743 toggle the applied flag of the specified item. if no item is specified,
791 toggle the applied flag of the specified item. if no item is specified,
744 toggle the flag of the currently selected item.
792 toggle the flag of the currently selected item.
745 """
793 """
746 if item is None:
794 if item is None:
747 item = self.currentselecteditem
795 item = self.currentselecteditem
748
796
749 item.applied = not item.applied
797 item.applied = not item.applied
750
798
751 if isinstance(item, uiheader):
799 if isinstance(item, uiheader):
752 item.partial = False
800 item.partial = False
753 if item.applied:
801 if item.applied:
754 # apply all its hunks
802 # apply all its hunks
755 for hnk in item.hunks:
803 for hnk in item.hunks:
756 hnk.applied = True
804 hnk.applied = True
757 # apply all their hunklines
805 # apply all their hunklines
758 for hunkline in hnk.changedlines:
806 for hunkline in hnk.changedlines:
759 hunkline.applied = True
807 hunkline.applied = True
760 else:
808 else:
761 # un-apply all its hunks
809 # un-apply all its hunks
762 for hnk in item.hunks:
810 for hnk in item.hunks:
763 hnk.applied = False
811 hnk.applied = False
764 hnk.partial = False
812 hnk.partial = False
765 # un-apply all their hunklines
813 # un-apply all their hunklines
766 for hunkline in hnk.changedlines:
814 for hunkline in hnk.changedlines:
767 hunkline.applied = False
815 hunkline.applied = False
768 elif isinstance(item, uihunk):
816 elif isinstance(item, uihunk):
769 item.partial = False
817 item.partial = False
770 # apply all it's hunklines
818 # apply all it's hunklines
771 for hunkline in item.changedlines:
819 for hunkline in item.changedlines:
772 hunkline.applied = item.applied
820 hunkline.applied = item.applied
773
821
774 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
822 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
775 allsiblingsapplied = not (False in siblingappliedstatus)
823 allsiblingsapplied = not (False in siblingappliedstatus)
776 nosiblingsapplied = not (True in siblingappliedstatus)
824 nosiblingsapplied = not (True in siblingappliedstatus)
777
825
778 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
826 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
779 somesiblingspartial = (True in siblingspartialstatus)
827 somesiblingspartial = (True in siblingspartialstatus)
780
828
781 #cases where applied or partial should be removed from header
829 #cases where applied or partial should be removed from header
782
830
783 # if no 'sibling' hunks are applied (including this hunk)
831 # if no 'sibling' hunks are applied (including this hunk)
784 if nosiblingsapplied:
832 if nosiblingsapplied:
785 if not item.header.special():
833 if not item.header.special():
786 item.header.applied = False
834 item.header.applied = False
787 item.header.partial = False
835 item.header.partial = False
788 else: # some/all parent siblings are applied
836 else: # some/all parent siblings are applied
789 item.header.applied = True
837 item.header.applied = True
790 item.header.partial = (somesiblingspartial or
838 item.header.partial = (somesiblingspartial or
791 not allsiblingsapplied)
839 not allsiblingsapplied)
792
840
793 elif isinstance(item, uihunkline):
841 elif isinstance(item, uihunkline):
794 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
842 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
795 allsiblingsapplied = not (False in siblingappliedstatus)
843 allsiblingsapplied = not (False in siblingappliedstatus)
796 nosiblingsapplied = not (True in siblingappliedstatus)
844 nosiblingsapplied = not (True in siblingappliedstatus)
797
845
798 # if no 'sibling' lines are applied
846 # if no 'sibling' lines are applied
799 if nosiblingsapplied:
847 if nosiblingsapplied:
800 item.hunk.applied = False
848 item.hunk.applied = False
801 item.hunk.partial = False
849 item.hunk.partial = False
802 elif allsiblingsapplied:
850 elif allsiblingsapplied:
803 item.hunk.applied = True
851 item.hunk.applied = True
804 item.hunk.partial = False
852 item.hunk.partial = False
805 else: # some siblings applied
853 else: # some siblings applied
806 item.hunk.applied = True
854 item.hunk.applied = True
807 item.hunk.partial = True
855 item.hunk.partial = True
808
856
809 parentsiblingsapplied = [hnk.applied for hnk
857 parentsiblingsapplied = [hnk.applied for hnk
810 in item.hunk.header.hunks]
858 in item.hunk.header.hunks]
811 noparentsiblingsapplied = not (True in parentsiblingsapplied)
859 noparentsiblingsapplied = not (True in parentsiblingsapplied)
812 allparentsiblingsapplied = not (False in parentsiblingsapplied)
860 allparentsiblingsapplied = not (False in parentsiblingsapplied)
813
861
814 parentsiblingspartial = [hnk.partial for hnk
862 parentsiblingspartial = [hnk.partial for hnk
815 in item.hunk.header.hunks]
863 in item.hunk.header.hunks]
816 someparentsiblingspartial = (True in parentsiblingspartial)
864 someparentsiblingspartial = (True in parentsiblingspartial)
817
865
818 # if all parent hunks are not applied, un-apply header
866 # if all parent hunks are not applied, un-apply header
819 if noparentsiblingsapplied:
867 if noparentsiblingsapplied:
820 if not item.hunk.header.special():
868 if not item.hunk.header.special():
821 item.hunk.header.applied = False
869 item.hunk.header.applied = False
822 item.hunk.header.partial = False
870 item.hunk.header.partial = False
823 # set the applied and partial status of the header if needed
871 # set the applied and partial status of the header if needed
824 else: # some/all parent siblings are applied
872 else: # some/all parent siblings are applied
825 item.hunk.header.applied = True
873 item.hunk.header.applied = True
826 item.hunk.header.partial = (someparentsiblingspartial or
874 item.hunk.header.partial = (someparentsiblingspartial or
827 not allparentsiblingsapplied)
875 not allparentsiblingsapplied)
828
876
829 def toggleall(self):
877 def toggleall(self):
830 "toggle the applied flag of all items."
878 "toggle the applied flag of all items."
831 if self.waslasttoggleallapplied: # then unapply them this time
879 if self.waslasttoggleallapplied: # then unapply them this time
832 for item in self.headerlist:
880 for item in self.headerlist:
833 if item.applied:
881 if item.applied:
834 self.toggleapply(item)
882 self.toggleapply(item)
835 else:
883 else:
836 for item in self.headerlist:
884 for item in self.headerlist:
837 if not item.applied:
885 if not item.applied:
838 self.toggleapply(item)
886 self.toggleapply(item)
839 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
887 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
840
888
841 def togglefolded(self, item=None, foldparent=False):
889 def togglefolded(self, item=None, foldparent=False):
842 "toggle folded flag of specified item (defaults to currently selected)"
890 "toggle folded flag of specified item (defaults to currently selected)"
843 if item is None:
891 if item is None:
844 item = self.currentselecteditem
892 item = self.currentselecteditem
845 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
893 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
846 if not isinstance(item, uiheader):
894 if not isinstance(item, uiheader):
847 # we need to select the parent item in this case
895 # we need to select the parent item in this case
848 self.currentselecteditem = item = item.parentitem()
896 self.currentselecteditem = item = item.parentitem()
849 elif item.neverunfolded:
897 elif item.neverunfolded:
850 item.neverunfolded = False
898 item.neverunfolded = False
851
899
852 # also fold any foldable children of the parent/current item
900 # also fold any foldable children of the parent/current item
853 if isinstance(item, uiheader): # the original or 'new' item
901 if isinstance(item, uiheader): # the original or 'new' item
854 for child in item.allchildren():
902 for child in item.allchildren():
855 child.folded = not item.folded
903 child.folded = not item.folded
856
904
857 if isinstance(item, (uiheader, uihunk)):
905 if isinstance(item, (uiheader, uihunk)):
858 item.folded = not item.folded
906 item.folded = not item.folded
859
907
860 def alignstring(self, instr, window):
908 def alignstring(self, instr, window):
861 """
909 """
862 add whitespace to the end of a string in order to make it fill
910 add whitespace to the end of a string in order to make it fill
863 the screen in the x direction. the current cursor position is
911 the screen in the x direction. the current cursor position is
864 taken into account when making this calculation. the string can span
912 taken into account when making this calculation. the string can span
865 multiple lines.
913 multiple lines.
866 """
914 """
867 y, xstart = window.getyx()
915 y, xstart = window.getyx()
868 width = self.xscreensize
916 width = self.xscreensize
869 # turn tabs into spaces
917 # turn tabs into spaces
870 instr = instr.expandtabs(4)
918 instr = instr.expandtabs(4)
871 strwidth = encoding.colwidth(instr)
919 strwidth = encoding.colwidth(instr)
872 numspaces = (width - ((strwidth + xstart) % width) - 1)
920 numspaces = (width - ((strwidth + xstart) % width) - 1)
873 return instr + " " * numspaces + "\n"
921 return instr + " " * numspaces + "\n"
874
922
875 def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None,
923 def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None,
876 pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False):
924 pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False):
877 """
925 """
878 print the string, text, with the specified colors and attributes, to
926 print the string, text, with the specified colors and attributes, to
879 the specified curses window object.
927 the specified curses window object.
880
928
881 the foreground and background colors are of the form
929 the foreground and background colors are of the form
882 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
930 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
883 magenta, red, white, yellow]. if pairname is provided, a color
931 magenta, red, white, yellow]. if pairname is provided, a color
884 pair will be looked up in the self.colorpairnames dictionary.
932 pair will be looked up in the self.colorpairnames dictionary.
885
933
886 attrlist is a list containing text attributes in the form of
934 attrlist is a list containing text attributes in the form of
887 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
935 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
888 underline].
936 underline].
889
937
890 if align == True, whitespace is added to the printed string such that
938 if align == True, whitespace is added to the printed string such that
891 the string stretches to the right border of the window.
939 the string stretches to the right border of the window.
892
940
893 if showwhtspc == True, trailing whitespace of a string is highlighted.
941 if showwhtspc == True, trailing whitespace of a string is highlighted.
894 """
942 """
895 # preprocess the text, converting tabs to spaces
943 # preprocess the text, converting tabs to spaces
896 text = text.expandtabs(4)
944 text = text.expandtabs(4)
897 # strip \n, and convert control characters to ^[char] representation
945 # strip \n, and convert control characters to ^[char] representation
898 text = re.sub(r'[\x00-\x08\x0a-\x1f]',
946 text = re.sub(r'[\x00-\x08\x0a-\x1f]',
899 lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n'))
947 lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n'))
900
948
901 if pair is not None:
949 if pair is not None:
902 colorpair = pair
950 colorpair = pair
903 elif pairname is not None:
951 elif pairname is not None:
904 colorpair = self.colorpairnames[pairname]
952 colorpair = self.colorpairnames[pairname]
905 else:
953 else:
906 if fgcolor is None:
954 if fgcolor is None:
907 fgcolor = -1
955 fgcolor = -1
908 if bgcolor is None:
956 if bgcolor is None:
909 bgcolor = -1
957 bgcolor = -1
910 if (fgcolor, bgcolor) in self.colorpairs:
958 if (fgcolor, bgcolor) in self.colorpairs:
911 colorpair = self.colorpairs[(fgcolor, bgcolor)]
959 colorpair = self.colorpairs[(fgcolor, bgcolor)]
912 else:
960 else:
913 colorpair = self.getcolorpair(fgcolor, bgcolor)
961 colorpair = self.getcolorpair(fgcolor, bgcolor)
914 # add attributes if possible
962 # add attributes if possible
915 if attrlist is None:
963 if attrlist is None:
916 attrlist = []
964 attrlist = []
917 if colorpair < 256:
965 if colorpair < 256:
918 # then it is safe to apply all attributes
966 # then it is safe to apply all attributes
919 for textattr in attrlist:
967 for textattr in attrlist:
920 colorpair |= textattr
968 colorpair |= textattr
921 else:
969 else:
922 # just apply a select few (safe?) attributes
970 # just apply a select few (safe?) attributes
923 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
971 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
924 if textattr in attrlist:
972 if textattr in attrlist:
925 colorpair |= textattr
973 colorpair |= textattr
926
974
927 y, xstart = self.chunkpad.getyx()
975 y, xstart = self.chunkpad.getyx()
928 t = "" # variable for counting lines printed
976 t = "" # variable for counting lines printed
929 # if requested, show trailing whitespace
977 # if requested, show trailing whitespace
930 if showwhtspc:
978 if showwhtspc:
931 origlen = len(text)
979 origlen = len(text)
932 text = text.rstrip(' \n') # tabs have already been expanded
980 text = text.rstrip(' \n') # tabs have already been expanded
933 strippedlen = len(text)
981 strippedlen = len(text)
934 numtrailingspaces = origlen - strippedlen
982 numtrailingspaces = origlen - strippedlen
935
983
936 if towin:
984 if towin:
937 window.addstr(text, colorpair)
985 window.addstr(text, colorpair)
938 t += text
986 t += text
939
987
940 if showwhtspc:
988 if showwhtspc:
941 wscolorpair = colorpair | curses.A_REVERSE
989 wscolorpair = colorpair | curses.A_REVERSE
942 if towin:
990 if towin:
943 for i in range(numtrailingspaces):
991 for i in range(numtrailingspaces):
944 window.addch(curses.ACS_CKBOARD, wscolorpair)
992 window.addch(curses.ACS_CKBOARD, wscolorpair)
945 t += " " * numtrailingspaces
993 t += " " * numtrailingspaces
946
994
947 if align:
995 if align:
948 if towin:
996 if towin:
949 extrawhitespace = self.alignstring("", window)
997 extrawhitespace = self.alignstring("", window)
950 window.addstr(extrawhitespace, colorpair)
998 window.addstr(extrawhitespace, colorpair)
951 else:
999 else:
952 # need to use t, since the x position hasn't incremented
1000 # need to use t, since the x position hasn't incremented
953 extrawhitespace = self.alignstring(t, window)
1001 extrawhitespace = self.alignstring(t, window)
954 t += extrawhitespace
1002 t += extrawhitespace
955
1003
956 # is reset to 0 at the beginning of printitem()
1004 # is reset to 0 at the beginning of printitem()
957
1005
958 linesprinted = (xstart + len(t)) / self.xscreensize
1006 linesprinted = (xstart + len(t)) / self.xscreensize
959 self.linesprintedtopadsofar += linesprinted
1007 self.linesprintedtopadsofar += linesprinted
960 return t
1008 return t
961
1009
962 def _getstatuslinesegments(self):
1010 def _getstatuslinesegments(self):
963 """-> [str]. return segments"""
1011 """-> [str]. return segments"""
964 selected = self.currentselecteditem.applied
1012 selected = self.currentselecteditem.applied
965 segments = [
1013 segments = [
966 _headermessages[self.operation],
1014 _headermessages[self.operation],
967 '-',
1015 '-',
968 _('[x]=selected **=collapsed'),
1016 _('[x]=selected **=collapsed'),
969 _('c: confirm'),
1017 _('c: confirm'),
970 _('q: abort'),
1018 _('q: abort'),
971 _('arrow keys: move/expand/collapse'),
1019 _('arrow keys: move/expand/collapse'),
972 _('space: deselect') if selected else _('space: select'),
1020 _('space: deselect') if selected else _('space: select'),
973 _('?: help'),
1021 _('?: help'),
974 ]
1022 ]
975 return segments
1023 return segments
976
1024
977 def _getstatuslines(self):
1025 def _getstatuslines(self):
978 """() -> [str]. return short help used in the top status window"""
1026 """() -> [str]. return short help used in the top status window"""
979 if self.errorstr is not None:
1027 if self.errorstr is not None:
980 lines = [self.errorstr, _('Press any key to continue')]
1028 lines = [self.errorstr, _('Press any key to continue')]
981 else:
1029 else:
982 # wrap segments to lines
1030 # wrap segments to lines
983 segments = self._getstatuslinesegments()
1031 segments = self._getstatuslinesegments()
984 width = self.xscreensize
1032 width = self.xscreensize
985 lines = []
1033 lines = []
986 lastwidth = width
1034 lastwidth = width
987 for s in segments:
1035 for s in segments:
988 w = encoding.colwidth(s)
1036 w = encoding.colwidth(s)
989 sep = ' ' * (1 + (s and s[0] not in '-['))
1037 sep = ' ' * (1 + (s and s[0] not in '-['))
990 if lastwidth + w + len(sep) >= width:
1038 if lastwidth + w + len(sep) >= width:
991 lines.append(s)
1039 lines.append(s)
992 lastwidth = w
1040 lastwidth = w
993 else:
1041 else:
994 lines[-1] += sep + s
1042 lines[-1] += sep + s
995 lastwidth += w + len(sep)
1043 lastwidth += w + len(sep)
996 if len(lines) != self.numstatuslines:
1044 if len(lines) != self.numstatuslines:
997 self.numstatuslines = len(lines)
1045 self.numstatuslines = len(lines)
998 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1046 self.statuswin.resize(self.numstatuslines, self.xscreensize)
999 return [util.ellipsis(l, self.xscreensize - 1) for l in lines]
1047 return [util.ellipsis(l, self.xscreensize - 1) for l in lines]
1000
1048
1001 def updatescreen(self):
1049 def updatescreen(self):
1002 self.statuswin.erase()
1050 self.statuswin.erase()
1003 self.chunkpad.erase()
1051 self.chunkpad.erase()
1004
1052
1005 printstring = self.printstring
1053 printstring = self.printstring
1006
1054
1007 # print out the status lines at the top
1055 # print out the status lines at the top
1008 try:
1056 try:
1009 for line in self._getstatuslines():
1057 for line in self._getstatuslines():
1010 printstring(self.statuswin, line, pairname="legend")
1058 printstring(self.statuswin, line, pairname="legend")
1011 self.statuswin.refresh()
1059 self.statuswin.refresh()
1012 except curses.error:
1060 except curses.error:
1013 pass
1061 pass
1014 if self.errorstr is not None:
1062 if self.errorstr is not None:
1015 return
1063 return
1016
1064
1017 # print out the patch in the remaining part of the window
1065 # print out the patch in the remaining part of the window
1018 try:
1066 try:
1019 self.printitem()
1067 self.printitem()
1020 self.updatescroll()
1068 self.updatescroll()
1021 self.chunkpad.refresh(self.firstlineofpadtoprint, 0,
1069 self.chunkpad.refresh(self.firstlineofpadtoprint, 0,
1022 self.numstatuslines, 0,
1070 self.numstatuslines, 0,
1023 self.yscreensize - self.numstatuslines,
1071 self.yscreensize - self.numstatuslines,
1024 self.xscreensize)
1072 self.xscreensize)
1025 except curses.error:
1073 except curses.error:
1026 pass
1074 pass
1027
1075
1028 def getstatusprefixstring(self, item):
1076 def getstatusprefixstring(self, item):
1029 """
1077 """
1030 create a string to prefix a line with which indicates whether 'item'
1078 create a string to prefix a line with which indicates whether 'item'
1031 is applied and/or folded.
1079 is applied and/or folded.
1032 """
1080 """
1033
1081
1034 # create checkbox string
1082 # create checkbox string
1035 if item.applied:
1083 if item.applied:
1036 if not isinstance(item, uihunkline) and item.partial:
1084 if not isinstance(item, uihunkline) and item.partial:
1037 checkbox = "[~]"
1085 checkbox = "[~]"
1038 else:
1086 else:
1039 checkbox = "[x]"
1087 checkbox = "[x]"
1040 else:
1088 else:
1041 checkbox = "[ ]"
1089 checkbox = "[ ]"
1042
1090
1043 try:
1091 try:
1044 if item.folded:
1092 if item.folded:
1045 checkbox += "**"
1093 checkbox += "**"
1046 if isinstance(item, uiheader):
1094 if isinstance(item, uiheader):
1047 # one of "m", "a", or "d" (modified, added, deleted)
1095 # one of "m", "a", or "d" (modified, added, deleted)
1048 filestatus = item.changetype
1096 filestatus = item.changetype
1049
1097
1050 checkbox += filestatus + " "
1098 checkbox += filestatus + " "
1051 else:
1099 else:
1052 checkbox += " "
1100 checkbox += " "
1053 if isinstance(item, uiheader):
1101 if isinstance(item, uiheader):
1054 # add two more spaces for headers
1102 # add two more spaces for headers
1055 checkbox += " "
1103 checkbox += " "
1056 except AttributeError: # not foldable
1104 except AttributeError: # not foldable
1057 checkbox += " "
1105 checkbox += " "
1058
1106
1059 return checkbox
1107 return checkbox
1060
1108
1061 def printheader(self, header, selected=False, towin=True,
1109 def printheader(self, header, selected=False, towin=True,
1062 ignorefolding=False):
1110 ignorefolding=False):
1063 """
1111 """
1064 print the header to the pad. if countlines is True, don't print
1112 print the header to the pad. if countlines is True, don't print
1065 anything, but just count the number of lines which would be printed.
1113 anything, but just count the number of lines which would be printed.
1066 """
1114 """
1067
1115
1068 outstr = ""
1116 outstr = ""
1069 text = header.prettystr()
1117 text = header.prettystr()
1070 chunkindex = self.chunklist.index(header)
1118 chunkindex = self.chunklist.index(header)
1071
1119
1072 if chunkindex != 0 and not header.folded:
1120 if chunkindex != 0 and not header.folded:
1073 # add separating line before headers
1121 # add separating line before headers
1074 outstr += self.printstring(self.chunkpad, '_' * self.xscreensize,
1122 outstr += self.printstring(self.chunkpad, '_' * self.xscreensize,
1075 towin=towin, align=False)
1123 towin=towin, align=False)
1076 # select color-pair based on if the header is selected
1124 # select color-pair based on if the header is selected
1077 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1125 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1078 attrlist=[curses.A_BOLD])
1126 attrlist=[curses.A_BOLD])
1079
1127
1080 # print out each line of the chunk, expanding it to screen width
1128 # print out each line of the chunk, expanding it to screen width
1081
1129
1082 # number of characters to indent lines on this level by
1130 # number of characters to indent lines on this level by
1083 indentnumchars = 0
1131 indentnumchars = 0
1084 checkbox = self.getstatusprefixstring(header)
1132 checkbox = self.getstatusprefixstring(header)
1085 if not header.folded or ignorefolding:
1133 if not header.folded or ignorefolding:
1086 textlist = text.split("\n")
1134 textlist = text.split("\n")
1087 linestr = checkbox + textlist[0]
1135 linestr = checkbox + textlist[0]
1088 else:
1136 else:
1089 linestr = checkbox + header.filename()
1137 linestr = checkbox + header.filename()
1090 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1138 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1091 towin=towin)
1139 towin=towin)
1092 if not header.folded or ignorefolding:
1140 if not header.folded or ignorefolding:
1093 if len(textlist) > 1:
1141 if len(textlist) > 1:
1094 for line in textlist[1:]:
1142 for line in textlist[1:]:
1095 linestr = " "*(indentnumchars + len(checkbox)) + line
1143 linestr = " "*(indentnumchars + len(checkbox)) + line
1096 outstr += self.printstring(self.chunkpad, linestr,
1144 outstr += self.printstring(self.chunkpad, linestr,
1097 pair=colorpair, towin=towin)
1145 pair=colorpair, towin=towin)
1098
1146
1099 return outstr
1147 return outstr
1100
1148
1101 def printhunklinesbefore(self, hunk, selected=False, towin=True,
1149 def printhunklinesbefore(self, hunk, selected=False, towin=True,
1102 ignorefolding=False):
1150 ignorefolding=False):
1103 "includes start/end line indicator"
1151 "includes start/end line indicator"
1104 outstr = ""
1152 outstr = ""
1105 # where hunk is in list of siblings
1153 # where hunk is in list of siblings
1106 hunkindex = hunk.header.hunks.index(hunk)
1154 hunkindex = hunk.header.hunks.index(hunk)
1107
1155
1108 if hunkindex != 0:
1156 if hunkindex != 0:
1109 # add separating line before headers
1157 # add separating line before headers
1110 outstr += self.printstring(self.chunkpad, ' '*self.xscreensize,
1158 outstr += self.printstring(self.chunkpad, ' '*self.xscreensize,
1111 towin=towin, align=False)
1159 towin=towin, align=False)
1112
1160
1113 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1161 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1114 attrlist=[curses.A_BOLD])
1162 attrlist=[curses.A_BOLD])
1115
1163
1116 # print out from-to line with checkbox
1164 # print out from-to line with checkbox
1117 checkbox = self.getstatusprefixstring(hunk)
1165 checkbox = self.getstatusprefixstring(hunk)
1118
1166
1119 lineprefix = " "*self.hunkindentnumchars + checkbox
1167 lineprefix = " "*self.hunkindentnumchars + checkbox
1120 frtoline = " " + hunk.getfromtoline().strip("\n")
1168 frtoline = " " + hunk.getfromtoline().strip("\n")
1121
1169
1122 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1170 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1123 align=False) # add uncolored checkbox/indent
1171 align=False) # add uncolored checkbox/indent
1124 outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair,
1172 outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair,
1125 towin=towin)
1173 towin=towin)
1126
1174
1127 if hunk.folded and not ignorefolding:
1175 if hunk.folded and not ignorefolding:
1128 # skip remainder of output
1176 # skip remainder of output
1129 return outstr
1177 return outstr
1130
1178
1131 # print out lines of the chunk preceeding changed-lines
1179 # print out lines of the chunk preceeding changed-lines
1132 for line in hunk.before:
1180 for line in hunk.before:
1133 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1181 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1134 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1182 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1135
1183
1136 return outstr
1184 return outstr
1137
1185
1138 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1186 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1139 outstr = ""
1187 outstr = ""
1140 if hunk.folded and not ignorefolding:
1188 if hunk.folded and not ignorefolding:
1141 return outstr
1189 return outstr
1142
1190
1143 # a bit superfluous, but to avoid hard-coding indent amount
1191 # a bit superfluous, but to avoid hard-coding indent amount
1144 checkbox = self.getstatusprefixstring(hunk)
1192 checkbox = self.getstatusprefixstring(hunk)
1145 for line in hunk.after:
1193 for line in hunk.after:
1146 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1194 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1147 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1195 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1148
1196
1149 return outstr
1197 return outstr
1150
1198
1151 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1199 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1152 outstr = ""
1200 outstr = ""
1153 checkbox = self.getstatusprefixstring(hunkline)
1201 checkbox = self.getstatusprefixstring(hunkline)
1154
1202
1155 linestr = hunkline.prettystr().strip("\n")
1203 linestr = hunkline.prettystr().strip("\n")
1156
1204
1157 # select color-pair based on whether line is an addition/removal
1205 # select color-pair based on whether line is an addition/removal
1158 if selected:
1206 if selected:
1159 colorpair = self.getcolorpair(name="selected")
1207 colorpair = self.getcolorpair(name="selected")
1160 elif linestr.startswith("+"):
1208 elif linestr.startswith("+"):
1161 colorpair = self.getcolorpair(name="addition")
1209 colorpair = self.getcolorpair(name="addition")
1162 elif linestr.startswith("-"):
1210 elif linestr.startswith("-"):
1163 colorpair = self.getcolorpair(name="deletion")
1211 colorpair = self.getcolorpair(name="deletion")
1164 elif linestr.startswith("\\"):
1212 elif linestr.startswith("\\"):
1165 colorpair = self.getcolorpair(name="normal")
1213 colorpair = self.getcolorpair(name="normal")
1166
1214
1167 lineprefix = " "*self.hunklineindentnumchars + checkbox
1215 lineprefix = " "*self.hunklineindentnumchars + checkbox
1168 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1216 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1169 align=False) # add uncolored checkbox/indent
1217 align=False) # add uncolored checkbox/indent
1170 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1218 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1171 towin=towin, showwhtspc=True)
1219 towin=towin, showwhtspc=True)
1172 return outstr
1220 return outstr
1173
1221
1174 def printitem(self, item=None, ignorefolding=False, recursechildren=True,
1222 def printitem(self, item=None, ignorefolding=False, recursechildren=True,
1175 towin=True):
1223 towin=True):
1176 """
1224 """
1177 use __printitem() to print the the specified item.applied.
1225 use __printitem() to print the the specified item.applied.
1178 if item is not specified, then print the entire patch.
1226 if item is not specified, then print the entire patch.
1179 (hiding folded elements, etc. -- see __printitem() docstring)
1227 (hiding folded elements, etc. -- see __printitem() docstring)
1180 """
1228 """
1181
1229
1182 if item is None:
1230 if item is None:
1183 item = self.headerlist
1231 item = self.headerlist
1184 if recursechildren:
1232 if recursechildren:
1185 self.linesprintedtopadsofar = 0
1233 self.linesprintedtopadsofar = 0
1186
1234
1187 outstr = []
1235 outstr = []
1188 self.__printitem(item, ignorefolding, recursechildren, outstr,
1236 self.__printitem(item, ignorefolding, recursechildren, outstr,
1189 towin=towin)
1237 towin=towin)
1190 return ''.join(outstr)
1238 return ''.join(outstr)
1191
1239
1192 def outofdisplayedarea(self):
1240 def outofdisplayedarea(self):
1193 y, _ = self.chunkpad.getyx() # cursor location
1241 y, _ = self.chunkpad.getyx() # cursor location
1194 # * 2 here works but an optimization would be the max number of
1242 # * 2 here works but an optimization would be the max number of
1195 # consecutive non selectable lines
1243 # consecutive non selectable lines
1196 # i.e the max number of context line for any hunk in the patch
1244 # i.e the max number of context line for any hunk in the patch
1197 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1245 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1198 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1246 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1199 return y < miny or y > maxy
1247 return y < miny or y > maxy
1200
1248
1201 def handleselection(self, item, recursechildren):
1249 def handleselection(self, item, recursechildren):
1202 selected = (item is self.currentselecteditem)
1250 selected = (item is self.currentselecteditem)
1203 if selected and recursechildren:
1251 if selected and recursechildren:
1204 # assumes line numbering starting from line 0
1252 # assumes line numbering starting from line 0
1205 self.selecteditemstartline = self.linesprintedtopadsofar
1253 self.selecteditemstartline = self.linesprintedtopadsofar
1206 selecteditemlines = self.getnumlinesdisplayed(item,
1254 selecteditemlines = self.getnumlinesdisplayed(item,
1207 recursechildren=False)
1255 recursechildren=False)
1208 self.selecteditemendline = (self.selecteditemstartline +
1256 self.selecteditemendline = (self.selecteditemstartline +
1209 selecteditemlines - 1)
1257 selecteditemlines - 1)
1210 return selected
1258 return selected
1211
1259
1212 def __printitem(self, item, ignorefolding, recursechildren, outstr,
1260 def __printitem(self, item, ignorefolding, recursechildren, outstr,
1213 towin=True):
1261 towin=True):
1214 """
1262 """
1215 recursive method for printing out patch/header/hunk/hunk-line data to
1263 recursive method for printing out patch/header/hunk/hunk-line data to
1216 screen. also returns a string with all of the content of the displayed
1264 screen. also returns a string with all of the content of the displayed
1217 patch (not including coloring, etc.).
1265 patch (not including coloring, etc.).
1218
1266
1219 if ignorefolding is True, then folded items are printed out.
1267 if ignorefolding is True, then folded items are printed out.
1220
1268
1221 if recursechildren is False, then only print the item without its
1269 if recursechildren is False, then only print the item without its
1222 child items.
1270 child items.
1223 """
1271 """
1224
1272
1225 if towin and self.outofdisplayedarea():
1273 if towin and self.outofdisplayedarea():
1226 return
1274 return
1227
1275
1228 selected = self.handleselection(item, recursechildren)
1276 selected = self.handleselection(item, recursechildren)
1229
1277
1230 # patch object is a list of headers
1278 # patch object is a list of headers
1231 if isinstance(item, patch):
1279 if isinstance(item, patch):
1232 if recursechildren:
1280 if recursechildren:
1233 for hdr in item:
1281 for hdr in item:
1234 self.__printitem(hdr, ignorefolding,
1282 self.__printitem(hdr, ignorefolding,
1235 recursechildren, outstr, towin)
1283 recursechildren, outstr, towin)
1236 # todo: eliminate all isinstance() calls
1284 # todo: eliminate all isinstance() calls
1237 if isinstance(item, uiheader):
1285 if isinstance(item, uiheader):
1238 outstr.append(self.printheader(item, selected, towin=towin,
1286 outstr.append(self.printheader(item, selected, towin=towin,
1239 ignorefolding=ignorefolding))
1287 ignorefolding=ignorefolding))
1240 if recursechildren:
1288 if recursechildren:
1241 for hnk in item.hunks:
1289 for hnk in item.hunks:
1242 self.__printitem(hnk, ignorefolding,
1290 self.__printitem(hnk, ignorefolding,
1243 recursechildren, outstr, towin)
1291 recursechildren, outstr, towin)
1244 elif (isinstance(item, uihunk) and
1292 elif (isinstance(item, uihunk) and
1245 ((not item.header.folded) or ignorefolding)):
1293 ((not item.header.folded) or ignorefolding)):
1246 # print the hunk data which comes before the changed-lines
1294 # print the hunk data which comes before the changed-lines
1247 outstr.append(self.printhunklinesbefore(item, selected, towin=towin,
1295 outstr.append(self.printhunklinesbefore(item, selected, towin=towin,
1248 ignorefolding=ignorefolding))
1296 ignorefolding=ignorefolding))
1249 if recursechildren:
1297 if recursechildren:
1250 for l in item.changedlines:
1298 for l in item.changedlines:
1251 self.__printitem(l, ignorefolding,
1299 self.__printitem(l, ignorefolding,
1252 recursechildren, outstr, towin)
1300 recursechildren, outstr, towin)
1253 outstr.append(self.printhunklinesafter(item, towin=towin,
1301 outstr.append(self.printhunklinesafter(item, towin=towin,
1254 ignorefolding=ignorefolding))
1302 ignorefolding=ignorefolding))
1255 elif (isinstance(item, uihunkline) and
1303 elif (isinstance(item, uihunkline) and
1256 ((not item.hunk.folded) or ignorefolding)):
1304 ((not item.hunk.folded) or ignorefolding)):
1257 outstr.append(self.printhunkchangedline(item, selected,
1305 outstr.append(self.printhunkchangedline(item, selected,
1258 towin=towin))
1306 towin=towin))
1259
1307
1260 return outstr
1308 return outstr
1261
1309
1262 def getnumlinesdisplayed(self, item=None, ignorefolding=False,
1310 def getnumlinesdisplayed(self, item=None, ignorefolding=False,
1263 recursechildren=True):
1311 recursechildren=True):
1264 """
1312 """
1265 return the number of lines which would be displayed if the item were
1313 return the number of lines which would be displayed if the item were
1266 to be printed to the display. the item will not be printed to the
1314 to be printed to the display. the item will not be printed to the
1267 display (pad).
1315 display (pad).
1268 if no item is given, assume the entire patch.
1316 if no item is given, assume the entire patch.
1269 if ignorefolding is True, folded items will be unfolded when counting
1317 if ignorefolding is True, folded items will be unfolded when counting
1270 the number of lines.
1318 the number of lines.
1271 """
1319 """
1272
1320
1273 # temporarily disable printing to windows by printstring
1321 # temporarily disable printing to windows by printstring
1274 patchdisplaystring = self.printitem(item, ignorefolding,
1322 patchdisplaystring = self.printitem(item, ignorefolding,
1275 recursechildren, towin=False)
1323 recursechildren, towin=False)
1276 numlines = len(patchdisplaystring) / self.xscreensize
1324 numlines = len(patchdisplaystring) / self.xscreensize
1277 return numlines
1325 return numlines
1278
1326
1279 def sigwinchhandler(self, n, frame):
1327 def sigwinchhandler(self, n, frame):
1280 "handle window resizing"
1328 "handle window resizing"
1281 try:
1329 try:
1282 curses.endwin()
1330 curses.endwin()
1283 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1331 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1284 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1332 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1285 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1333 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1286 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1334 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1287 except curses.error:
1335 except curses.error:
1288 pass
1336 pass
1289
1337
1290 def getcolorpair(self, fgcolor=None, bgcolor=None, name=None,
1338 def getcolorpair(self, fgcolor=None, bgcolor=None, name=None,
1291 attrlist=None):
1339 attrlist=None):
1292 """
1340 """
1293 get a curses color pair, adding it to self.colorpairs if it is not
1341 get a curses color pair, adding it to self.colorpairs if it is not
1294 already defined. an optional string, name, can be passed as a shortcut
1342 already defined. an optional string, name, can be passed as a shortcut
1295 for referring to the color-pair. by default, if no arguments are
1343 for referring to the color-pair. by default, if no arguments are
1296 specified, the white foreground / black background color-pair is
1344 specified, the white foreground / black background color-pair is
1297 returned.
1345 returned.
1298
1346
1299 it is expected that this function will be used exclusively for
1347 it is expected that this function will be used exclusively for
1300 initializing color pairs, and not curses.init_pair().
1348 initializing color pairs, and not curses.init_pair().
1301
1349
1302 attrlist is used to 'flavor' the returned color-pair. this information
1350 attrlist is used to 'flavor' the returned color-pair. this information
1303 is not stored in self.colorpairs. it contains attribute values like
1351 is not stored in self.colorpairs. it contains attribute values like
1304 curses.A_BOLD.
1352 curses.A_BOLD.
1305 """
1353 """
1306
1354
1307 if (name is not None) and name in self.colorpairnames:
1355 if (name is not None) and name in self.colorpairnames:
1308 # then get the associated color pair and return it
1356 # then get the associated color pair and return it
1309 colorpair = self.colorpairnames[name]
1357 colorpair = self.colorpairnames[name]
1310 else:
1358 else:
1311 if fgcolor is None:
1359 if fgcolor is None:
1312 fgcolor = -1
1360 fgcolor = -1
1313 if bgcolor is None:
1361 if bgcolor is None:
1314 bgcolor = -1
1362 bgcolor = -1
1315 if (fgcolor, bgcolor) in self.colorpairs:
1363 if (fgcolor, bgcolor) in self.colorpairs:
1316 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1364 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1317 else:
1365 else:
1318 pairindex = len(self.colorpairs) + 1
1366 pairindex = len(self.colorpairs) + 1
1319 curses.init_pair(pairindex, fgcolor, bgcolor)
1367 curses.init_pair(pairindex, fgcolor, bgcolor)
1320 colorpair = self.colorpairs[(fgcolor, bgcolor)] = (
1368 colorpair = self.colorpairs[(fgcolor, bgcolor)] = (
1321 curses.color_pair(pairindex))
1369 curses.color_pair(pairindex))
1322 if name is not None:
1370 if name is not None:
1323 self.colorpairnames[name] = curses.color_pair(pairindex)
1371 self.colorpairnames[name] = curses.color_pair(pairindex)
1324
1372
1325 # add attributes if possible
1373 # add attributes if possible
1326 if attrlist is None:
1374 if attrlist is None:
1327 attrlist = []
1375 attrlist = []
1328 if colorpair < 256:
1376 if colorpair < 256:
1329 # then it is safe to apply all attributes
1377 # then it is safe to apply all attributes
1330 for textattr in attrlist:
1378 for textattr in attrlist:
1331 colorpair |= textattr
1379 colorpair |= textattr
1332 else:
1380 else:
1333 # just apply a select few (safe?) attributes
1381 # just apply a select few (safe?) attributes
1334 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1382 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1335 if textattrib in attrlist:
1383 if textattrib in attrlist:
1336 colorpair |= textattrib
1384 colorpair |= textattrib
1337 return colorpair
1385 return colorpair
1338
1386
1339 def initcolorpair(self, *args, **kwargs):
1387 def initcolorpair(self, *args, **kwargs):
1340 "same as getcolorpair."
1388 "same as getcolorpair."
1341 self.getcolorpair(*args, **kwargs)
1389 self.getcolorpair(*args, **kwargs)
1342
1390
1343 def helpwindow(self):
1391 def helpwindow(self):
1344 "print a help window to the screen. exit after any keypress."
1392 "print a help window to the screen. exit after any keypress."
1345 helptext = _(
1393 helptext = _(
1346 """ [press any key to return to the patch-display]
1394 """ [press any key to return to the patch-display]
1347
1395
1348 crecord allows you to interactively choose among the changes you have made,
1396 crecord allows you to interactively choose among the changes you have made,
1349 and confirm only those changes you select for further processing by the command
1397 and confirm only those changes you select for further processing by the command
1350 you are running (commit/shelve/revert), after confirming the selected
1398 you are running (commit/shelve/revert), after confirming the selected
1351 changes, the unselected changes are still present in your working copy, so you
1399 changes, the unselected changes are still present in your working copy, so you
1352 can use crecord multiple times to split large changes into smaller changesets.
1400 can use crecord multiple times to split large changes into smaller changesets.
1353 the following are valid keystrokes:
1401 the following are valid keystrokes:
1354
1402
1355 [space] : (un-)select item ([~]/[x] = partly/fully applied)
1403 [space] : (un-)select item ([~]/[x] = partly/fully applied)
1356 A : (un-)select all items
1404 A : (un-)select all items
1357 up/down-arrow [k/j] : go to previous/next unfolded item
1405 up/down-arrow [k/j] : go to previous/next unfolded item
1358 pgup/pgdn [K/J] : go to previous/next item of same type
1406 pgup/pgdn [K/J] : go to previous/next item of same type
1359 right/left-arrow [l/h] : go to child item / parent item
1407 right/left-arrow [l/h] : go to child item / parent item
1360 shift-left-arrow [H] : go to parent header / fold selected header
1408 shift-left-arrow [H] : go to parent header / fold selected header
1361 f : fold / unfold item, hiding/revealing its children
1409 f : fold / unfold item, hiding/revealing its children
1362 F : fold / unfold parent item and all of its ancestors
1410 F : fold / unfold parent item and all of its ancestors
1363 ctrl-l : scroll the selected line to the top of the screen
1411 ctrl-l : scroll the selected line to the top of the screen
1364 m : edit / resume editing the commit message
1412 m : edit / resume editing the commit message
1365 e : edit the currently selected hunk
1413 e : edit the currently selected hunk
1366 a : toggle amend mode, only with commit -i
1414 a : toggle amend mode, only with commit -i
1367 c : confirm selected changes
1415 c : confirm selected changes
1368 r : review/edit and confirm selected changes
1416 r : review/edit and confirm selected changes
1369 q : quit without confirming (no changes will be made)
1417 q : quit without confirming (no changes will be made)
1370 ? : help (what you're currently reading)""")
1418 ? : help (what you're currently reading)""")
1371
1419
1372 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1420 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1373 helplines = helptext.split("\n")
1421 helplines = helptext.split("\n")
1374 helplines = helplines + [" "]*(
1422 helplines = helplines + [" "]*(
1375 self.yscreensize - self.numstatuslines - len(helplines) - 1)
1423 self.yscreensize - self.numstatuslines - len(helplines) - 1)
1376 try:
1424 try:
1377 for line in helplines:
1425 for line in helplines:
1378 self.printstring(helpwin, line, pairname="legend")
1426 self.printstring(helpwin, line, pairname="legend")
1379 except curses.error:
1427 except curses.error:
1380 pass
1428 pass
1381 helpwin.refresh()
1429 helpwin.refresh()
1382 try:
1430 try:
1383 with self.ui.timeblockedsection('crecord'):
1431 with self.ui.timeblockedsection('crecord'):
1384 helpwin.getkey()
1432 helpwin.getkey()
1385 except curses.error:
1433 except curses.error:
1386 pass
1434 pass
1387
1435
1388 def confirmationwindow(self, windowtext):
1436 def confirmationwindow(self, windowtext):
1389 "display an informational window, then wait for and return a keypress."
1437 "display an informational window, then wait for and return a keypress."
1390
1438
1391 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1439 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1392 try:
1440 try:
1393 lines = windowtext.split("\n")
1441 lines = windowtext.split("\n")
1394 for line in lines:
1442 for line in lines:
1395 self.printstring(confirmwin, line, pairname="selected")
1443 self.printstring(confirmwin, line, pairname="selected")
1396 except curses.error:
1444 except curses.error:
1397 pass
1445 pass
1398 self.stdscr.refresh()
1446 self.stdscr.refresh()
1399 confirmwin.refresh()
1447 confirmwin.refresh()
1400 try:
1448 try:
1401 with self.ui.timeblockedsection('crecord'):
1449 with self.ui.timeblockedsection('crecord'):
1402 response = chr(self.stdscr.getch())
1450 response = chr(self.stdscr.getch())
1403 except ValueError:
1451 except ValueError:
1404 response = None
1452 response = None
1405
1453
1406 return response
1454 return response
1407
1455
1408 def reviewcommit(self):
1456 def reviewcommit(self):
1409 """ask for 'y' to be pressed to confirm selected. return True if
1457 """ask for 'y' to be pressed to confirm selected. return True if
1410 confirmed."""
1458 confirmed."""
1411 confirmtext = _(
1459 confirmtext = _(
1412 """if you answer yes to the following, the your currently chosen patch chunks
1460 """if you answer yes to the following, the your currently chosen patch chunks
1413 will be loaded into an editor. you may modify the patch from the editor, and
1461 will be loaded into an editor. you may modify the patch from the editor, and
1414 save the changes if you wish to change the patch. otherwise, you can just
1462 save the changes if you wish to change the patch. otherwise, you can just
1415 close the editor without saving to accept the current patch as-is.
1463 close the editor without saving to accept the current patch as-is.
1416
1464
1417 note: don't add/remove lines unless you also modify the range information.
1465 note: don't add/remove lines unless you also modify the range information.
1418 failing to follow this rule will result in the commit aborting.
1466 failing to follow this rule will result in the commit aborting.
1419
1467
1420 are you sure you want to review/edit and confirm the selected changes [yn]?
1468 are you sure you want to review/edit and confirm the selected changes [yn]?
1421 """)
1469 """)
1422 with self.ui.timeblockedsection('crecord'):
1470 with self.ui.timeblockedsection('crecord'):
1423 response = self.confirmationwindow(confirmtext)
1471 response = self.confirmationwindow(confirmtext)
1424 if response is None:
1472 if response is None:
1425 response = "n"
1473 response = "n"
1426 if response.lower().startswith("y"):
1474 if response.lower().startswith("y"):
1427 return True
1475 return True
1428 else:
1476 else:
1429 return False
1477 return False
1430
1478
1431 def toggleamend(self, opts, test):
1479 def toggleamend(self, opts, test):
1432 """Toggle the amend flag.
1480 """Toggle the amend flag.
1433
1481
1434 When the amend flag is set, a commit will modify the most recently
1482 When the amend flag is set, a commit will modify the most recently
1435 committed changeset, instead of creating a new changeset. Otherwise, a
1483 committed changeset, instead of creating a new changeset. Otherwise, a
1436 new changeset will be created (the normal commit behavior).
1484 new changeset will be created (the normal commit behavior).
1437 """
1485 """
1438
1486
1439 try:
1487 try:
1440 ver = float(util.version()[:3])
1488 ver = float(util.version()[:3])
1441 except ValueError:
1489 except ValueError:
1442 ver = 1
1490 ver = 1
1443 if ver < 2.19:
1491 if ver < 2.19:
1444 msg = _("The amend option is unavailable with hg versions < 2.2\n\n"
1492 msg = _("The amend option is unavailable with hg versions < 2.2\n\n"
1445 "Press any key to continue.")
1493 "Press any key to continue.")
1446 elif opts.get('amend') is None:
1494 elif opts.get('amend') is None:
1447 opts['amend'] = True
1495 opts['amend'] = True
1448 msg = _("Amend option is turned on -- committing the currently "
1496 msg = _("Amend option is turned on -- committing the currently "
1449 "selected changes will not create a new changeset, but "
1497 "selected changes will not create a new changeset, but "
1450 "instead update the most recently committed changeset.\n\n"
1498 "instead update the most recently committed changeset.\n\n"
1451 "Press any key to continue.")
1499 "Press any key to continue.")
1452 elif opts.get('amend') is True:
1500 elif opts.get('amend') is True:
1453 opts['amend'] = None
1501 opts['amend'] = None
1454 msg = _("Amend option is turned off -- committing the currently "
1502 msg = _("Amend option is turned off -- committing the currently "
1455 "selected changes will create a new changeset.\n\n"
1503 "selected changes will create a new changeset.\n\n"
1456 "Press any key to continue.")
1504 "Press any key to continue.")
1457 if not test:
1505 if not test:
1458 self.confirmationwindow(msg)
1506 self.confirmationwindow(msg)
1459
1507
1460 def recenterdisplayedarea(self):
1508 def recenterdisplayedarea(self):
1461 """
1509 """
1462 once we scrolled with pg up pg down we can be pointing outside of the
1510 once we scrolled with pg up pg down we can be pointing outside of the
1463 display zone. we print the patch with towin=False to compute the
1511 display zone. we print the patch with towin=False to compute the
1464 location of the selected item even though it is outside of the displayed
1512 location of the selected item even though it is outside of the displayed
1465 zone and then update the scroll.
1513 zone and then update the scroll.
1466 """
1514 """
1467 self.printitem(towin=False)
1515 self.printitem(towin=False)
1468 self.updatescroll()
1516 self.updatescroll()
1469
1517
1470 def toggleedit(self, item=None, test=False):
1518 def toggleedit(self, item=None, test=False):
1471 """
1519 """
1472 edit the currently selected chunk
1520 edit the currently selected chunk
1473 """
1521 """
1474 def updateui(self):
1522 def updateui(self):
1475 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1523 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1476 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1524 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1477 self.updatescroll()
1525 self.updatescroll()
1478 self.stdscr.refresh()
1526 self.stdscr.refresh()
1479 self.statuswin.refresh()
1527 self.statuswin.refresh()
1480 self.stdscr.keypad(1)
1528 self.stdscr.keypad(1)
1481
1529
1482 def editpatchwitheditor(self, chunk):
1530 def editpatchwitheditor(self, chunk):
1483 if chunk is None:
1531 if chunk is None:
1484 self.ui.write(_('cannot edit patch for whole file'))
1532 self.ui.write(_('cannot edit patch for whole file'))
1485 self.ui.write("\n")
1533 self.ui.write("\n")
1486 return None
1534 return None
1487 if chunk.header.binary():
1535 if chunk.header.binary():
1488 self.ui.write(_('cannot edit patch for binary file'))
1536 self.ui.write(_('cannot edit patch for binary file'))
1489 self.ui.write("\n")
1537 self.ui.write("\n")
1490 return None
1538 return None
1491
1539
1492 # write the initial patch
1540 # write the initial patch
1493 patch = stringio()
1541 patch = stringio()
1494 patch.write(diffhelptext + hunkhelptext)
1542 patch.write(diffhelptext + hunkhelptext)
1495 chunk.header.write(patch)
1543 chunk.header.write(patch)
1496 chunk.write(patch)
1544 chunk.write(patch)
1497
1545
1498 # start the editor and wait for it to complete
1546 # start the editor and wait for it to complete
1499 try:
1547 try:
1500 patch = self.ui.edit(patch.getvalue(), "",
1548 patch = self.ui.edit(patch.getvalue(), "",
1501 extra={"suffix": ".diff"})
1549 extra={"suffix": ".diff"})
1502 except error.Abort as exc:
1550 except error.Abort as exc:
1503 self.errorstr = str(exc)
1551 self.errorstr = str(exc)
1504 return None
1552 return None
1505
1553
1506 # remove comment lines
1554 # remove comment lines
1507 patch = [line + '\n' for line in patch.splitlines()
1555 patch = [line + '\n' for line in patch.splitlines()
1508 if not line.startswith('#')]
1556 if not line.startswith('#')]
1509 return patchmod.parsepatch(patch)
1557 return patchmod.parsepatch(patch)
1510
1558
1511 if item is None:
1559 if item is None:
1512 item = self.currentselecteditem
1560 item = self.currentselecteditem
1513 if isinstance(item, uiheader):
1561 if isinstance(item, uiheader):
1514 return
1562 return
1515 if isinstance(item, uihunkline):
1563 if isinstance(item, uihunkline):
1516 item = item.parentitem()
1564 item = item.parentitem()
1517 if not isinstance(item, uihunk):
1565 if not isinstance(item, uihunk):
1518 return
1566 return
1519
1567
1520 # To go back to that hunk or its replacement at the end of the edit
1568 # To go back to that hunk or its replacement at the end of the edit
1521 itemindex = item.parentitem().hunks.index(item)
1569 itemindex = item.parentitem().hunks.index(item)
1522
1570
1523 beforeadded, beforeremoved = item.added, item.removed
1571 beforeadded, beforeremoved = item.added, item.removed
1524 newpatches = editpatchwitheditor(self, item)
1572 newpatches = editpatchwitheditor(self, item)
1525 if newpatches is None:
1573 if newpatches is None:
1526 if not test:
1574 if not test:
1527 updateui(self)
1575 updateui(self)
1528 return
1576 return
1529 header = item.header
1577 header = item.header
1530 editedhunkindex = header.hunks.index(item)
1578 editedhunkindex = header.hunks.index(item)
1531 hunksbefore = header.hunks[:editedhunkindex]
1579 hunksbefore = header.hunks[:editedhunkindex]
1532 hunksafter = header.hunks[editedhunkindex + 1:]
1580 hunksafter = header.hunks[editedhunkindex + 1:]
1533 newpatchheader = newpatches[0]
1581 newpatchheader = newpatches[0]
1534 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1582 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1535 newadded = sum([h.added for h in newhunks])
1583 newadded = sum([h.added for h in newhunks])
1536 newremoved = sum([h.removed for h in newhunks])
1584 newremoved = sum([h.removed for h in newhunks])
1537 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1585 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1538
1586
1539 for h in hunksafter:
1587 for h in hunksafter:
1540 h.toline += offset
1588 h.toline += offset
1541 for h in newhunks:
1589 for h in newhunks:
1542 h.folded = False
1590 h.folded = False
1543 header.hunks = hunksbefore + newhunks + hunksafter
1591 header.hunks = hunksbefore + newhunks + hunksafter
1544 if self.emptypatch():
1592 if self.emptypatch():
1545 header.hunks = hunksbefore + [item] + hunksafter
1593 header.hunks = hunksbefore + [item] + hunksafter
1546 self.currentselecteditem = header
1594 self.currentselecteditem = header
1547 if len(header.hunks) > itemindex:
1595 if len(header.hunks) > itemindex:
1548 self.currentselecteditem = header.hunks[itemindex]
1596 self.currentselecteditem = header.hunks[itemindex]
1549
1597
1550 if not test:
1598 if not test:
1551 updateui(self)
1599 updateui(self)
1552
1600
1553 def emptypatch(self):
1601 def emptypatch(self):
1554 item = self.headerlist
1602 item = self.headerlist
1555 if not item:
1603 if not item:
1556 return True
1604 return True
1557 for header in item:
1605 for header in item:
1558 if header.hunks:
1606 if header.hunks:
1559 return False
1607 return False
1560 return True
1608 return True
1561
1609
1562 def handlekeypressed(self, keypressed, test=False):
1610 def handlekeypressed(self, keypressed, test=False):
1563 """
1611 """
1564 Perform actions based on pressed keys.
1612 Perform actions based on pressed keys.
1565
1613
1566 Return true to exit the main loop.
1614 Return true to exit the main loop.
1567 """
1615 """
1568 if keypressed in ["k", "KEY_UP"]:
1616 if keypressed in ["k", "KEY_UP"]:
1569 self.uparrowevent()
1617 self.uparrowevent()
1570 if keypressed in ["K", "KEY_PPAGE"]:
1618 if keypressed in ["K", "KEY_PPAGE"]:
1571 self.uparrowshiftevent()
1619 self.uparrowshiftevent()
1572 elif keypressed in ["j", "KEY_DOWN"]:
1620 elif keypressed in ["j", "KEY_DOWN"]:
1573 self.downarrowevent()
1621 self.downarrowevent()
1574 elif keypressed in ["J", "KEY_NPAGE"]:
1622 elif keypressed in ["J", "KEY_NPAGE"]:
1575 self.downarrowshiftevent()
1623 self.downarrowshiftevent()
1576 elif keypressed in ["l", "KEY_RIGHT"]:
1624 elif keypressed in ["l", "KEY_RIGHT"]:
1577 self.rightarrowevent()
1625 self.rightarrowevent()
1578 elif keypressed in ["h", "KEY_LEFT"]:
1626 elif keypressed in ["h", "KEY_LEFT"]:
1579 self.leftarrowevent()
1627 self.leftarrowevent()
1580 elif keypressed in ["H", "KEY_SLEFT"]:
1628 elif keypressed in ["H", "KEY_SLEFT"]:
1581 self.leftarrowshiftevent()
1629 self.leftarrowshiftevent()
1582 elif keypressed in ["q"]:
1630 elif keypressed in ["q"]:
1583 raise error.Abort(_('user quit'))
1631 raise error.Abort(_('user quit'))
1584 elif keypressed in ['a']:
1632 elif keypressed in ['a']:
1585 self.toggleamend(self.opts, test)
1633 self.toggleamend(self.opts, test)
1586 elif keypressed in ["c"]:
1634 elif keypressed in ["c"]:
1587 return True
1635 return True
1588 elif test and keypressed in ['X']:
1636 elif test and keypressed in ['X']:
1589 return True
1637 return True
1590 elif keypressed in ["r"]:
1638 elif keypressed in ["r"]:
1591 if self.reviewcommit():
1639 if self.reviewcommit():
1592 self.opts['review'] = True
1640 self.opts['review'] = True
1593 return True
1641 return True
1594 elif test and keypressed in ['R']:
1642 elif test and keypressed in ['R']:
1595 self.opts['review'] = True
1643 self.opts['review'] = True
1596 return True
1644 return True
1597 elif keypressed in [' '] or (test and keypressed in ["TOGGLE"]):
1645 elif keypressed in [' '] or (test and keypressed in ["TOGGLE"]):
1598 self.toggleapply()
1646 self.toggleapply()
1599 if self.ui.configbool('experimental', 'spacemovesdown'):
1647 if self.ui.configbool('experimental', 'spacemovesdown'):
1600 self.downarrowevent()
1648 self.downarrowevent()
1601 elif keypressed in ['A']:
1649 elif keypressed in ['A']:
1602 self.toggleall()
1650 self.toggleall()
1603 elif keypressed in ['e']:
1651 elif keypressed in ['e']:
1604 self.toggleedit(test=test)
1652 self.toggleedit(test=test)
1605 elif keypressed in ["f"]:
1653 elif keypressed in ["f"]:
1606 self.togglefolded()
1654 self.togglefolded()
1607 elif keypressed in ["F"]:
1655 elif keypressed in ["F"]:
1608 self.togglefolded(foldparent=True)
1656 self.togglefolded(foldparent=True)
1609 elif keypressed in ["?"]:
1657 elif keypressed in ["?"]:
1610 self.helpwindow()
1658 self.helpwindow()
1611 self.stdscr.clear()
1659 self.stdscr.clear()
1612 self.stdscr.refresh()
1660 self.stdscr.refresh()
1613 elif curses.unctrl(keypressed) in ["^L"]:
1661 elif curses.unctrl(keypressed) in ["^L"]:
1614 # scroll the current line to the top of the screen
1662 # scroll the current line to the top of the screen
1615 self.scrolllines(self.selecteditemstartline)
1663 self.scrolllines(self.selecteditemstartline)
1616
1664
1617 def main(self, stdscr):
1665 def main(self, stdscr):
1618 """
1666 """
1619 method to be wrapped by curses.wrapper() for selecting chunks.
1667 method to be wrapped by curses.wrapper() for selecting chunks.
1620 """
1668 """
1621
1669
1622 origsigwinch = sentinel = object()
1670 origsigwinch = sentinel = object()
1623 if util.safehasattr(signal, 'SIGWINCH'):
1671 if util.safehasattr(signal, 'SIGWINCH'):
1624 origsigwinch = signal.signal(signal.SIGWINCH,
1672 origsigwinch = signal.signal(signal.SIGWINCH,
1625 self.sigwinchhandler)
1673 self.sigwinchhandler)
1626 try:
1674 try:
1627 return self._main(stdscr)
1675 return self._main(stdscr)
1628 finally:
1676 finally:
1629 if origsigwinch is not sentinel:
1677 if origsigwinch is not sentinel:
1630 signal.signal(signal.SIGWINCH, origsigwinch)
1678 signal.signal(signal.SIGWINCH, origsigwinch)
1631
1679
1632 def _main(self, stdscr):
1680 def _main(self, stdscr):
1633 self.stdscr = stdscr
1681 self.stdscr = stdscr
1634 # error during initialization, cannot be printed in the curses
1682 # error during initialization, cannot be printed in the curses
1635 # interface, it should be printed by the calling code
1683 # interface, it should be printed by the calling code
1636 self.initerr = None
1684 self.initerr = None
1637 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1685 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1638
1686
1639 curses.start_color()
1687 curses.start_color()
1640 curses.use_default_colors()
1688 curses.use_default_colors()
1641
1689
1642 # available colors: black, blue, cyan, green, magenta, white, yellow
1690 # available colors: black, blue, cyan, green, magenta, white, yellow
1643 # init_pair(color_id, foreground_color, background_color)
1691 # init_pair(color_id, foreground_color, background_color)
1644 self.initcolorpair(None, None, name="normal")
1692 self.initcolorpair(None, None, name="normal")
1645 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA,
1693 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA,
1646 name="selected")
1694 name="selected")
1647 self.initcolorpair(curses.COLOR_RED, None, name="deletion")
1695 self.initcolorpair(curses.COLOR_RED, None, name="deletion")
1648 self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
1696 self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
1649 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
1697 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
1650 # newwin([height, width,] begin_y, begin_x)
1698 # newwin([height, width,] begin_y, begin_x)
1651 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1699 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1652 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1700 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1653
1701
1654 # figure out how much space to allocate for the chunk-pad which is
1702 # figure out how much space to allocate for the chunk-pad which is
1655 # used for displaying the patch
1703 # used for displaying the patch
1656
1704
1657 # stupid hack to prevent getnumlinesdisplayed from failing
1705 # stupid hack to prevent getnumlinesdisplayed from failing
1658 self.chunkpad = curses.newpad(1, self.xscreensize)
1706 self.chunkpad = curses.newpad(1, self.xscreensize)
1659
1707
1660 # add 1 so to account for last line text reaching end of line
1708 # add 1 so to account for last line text reaching end of line
1661 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1709 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1662
1710
1663 try:
1711 try:
1664 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1712 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1665 except curses.error:
1713 except curses.error:
1666 self.initerr = _('this diff is too large to be displayed')
1714 self.initerr = _('this diff is too large to be displayed')
1667 return
1715 return
1668 # initialize selecteditemendline (initial start-line is 0)
1716 # initialize selecteditemendline (initial start-line is 0)
1669 self.selecteditemendline = self.getnumlinesdisplayed(
1717 self.selecteditemendline = self.getnumlinesdisplayed(
1670 self.currentselecteditem, recursechildren=False)
1718 self.currentselecteditem, recursechildren=False)
1671
1719
1672 while True:
1720 while True:
1673 self.updatescreen()
1721 self.updatescreen()
1674 try:
1722 try:
1675 with self.ui.timeblockedsection('crecord'):
1723 with self.ui.timeblockedsection('crecord'):
1676 keypressed = self.statuswin.getkey()
1724 keypressed = self.statuswin.getkey()
1677 if self.errorstr is not None:
1725 if self.errorstr is not None:
1678 self.errorstr = None
1726 self.errorstr = None
1679 continue
1727 continue
1680 except curses.error:
1728 except curses.error:
1681 keypressed = "foobar"
1729 keypressed = "foobar"
1682 if self.handlekeypressed(keypressed):
1730 if self.handlekeypressed(keypressed):
1683 break
1731 break
@@ -1,2743 +1,2746 b''
1 # patch.py - patch file parsing routines
1 # patch.py - patch file parsing routines
2 #
2 #
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import collections
11 import collections
12 import copy
12 import copy
13 import email
13 import email
14 import errno
14 import errno
15 import hashlib
15 import hashlib
16 import os
16 import os
17 import posixpath
17 import posixpath
18 import re
18 import re
19 import shutil
19 import shutil
20 import tempfile
20 import tempfile
21 import zlib
21 import zlib
22
22
23 from .i18n import _
23 from .i18n import _
24 from .node import (
24 from .node import (
25 hex,
25 hex,
26 short,
26 short,
27 )
27 )
28 from . import (
28 from . import (
29 copies,
29 copies,
30 encoding,
30 encoding,
31 error,
31 error,
32 mail,
32 mail,
33 mdiff,
33 mdiff,
34 pathutil,
34 pathutil,
35 policy,
35 policy,
36 pycompat,
36 pycompat,
37 scmutil,
37 scmutil,
38 similar,
38 similar,
39 util,
39 util,
40 vfs as vfsmod,
40 vfs as vfsmod,
41 )
41 )
42
42
43 diffhelpers = policy.importmod(r'diffhelpers')
43 diffhelpers = policy.importmod(r'diffhelpers')
44 stringio = util.stringio
44 stringio = util.stringio
45
45
46 gitre = re.compile(br'diff --git a/(.*) b/(.*)')
46 gitre = re.compile(br'diff --git a/(.*) b/(.*)')
47 tabsplitter = re.compile(br'(\t+|[^\t]+)')
47 tabsplitter = re.compile(br'(\t+|[^\t]+)')
48
48
49 class PatchError(Exception):
49 class PatchError(Exception):
50 pass
50 pass
51
51
52
52
53 # public functions
53 # public functions
54
54
55 def split(stream):
55 def split(stream):
56 '''return an iterator of individual patches from a stream'''
56 '''return an iterator of individual patches from a stream'''
57 def isheader(line, inheader):
57 def isheader(line, inheader):
58 if inheader and line[0] in (' ', '\t'):
58 if inheader and line[0] in (' ', '\t'):
59 # continuation
59 # continuation
60 return True
60 return True
61 if line[0] in (' ', '-', '+'):
61 if line[0] in (' ', '-', '+'):
62 # diff line - don't check for header pattern in there
62 # diff line - don't check for header pattern in there
63 return False
63 return False
64 l = line.split(': ', 1)
64 l = line.split(': ', 1)
65 return len(l) == 2 and ' ' not in l[0]
65 return len(l) == 2 and ' ' not in l[0]
66
66
67 def chunk(lines):
67 def chunk(lines):
68 return stringio(''.join(lines))
68 return stringio(''.join(lines))
69
69
70 def hgsplit(stream, cur):
70 def hgsplit(stream, cur):
71 inheader = True
71 inheader = True
72
72
73 for line in stream:
73 for line in stream:
74 if not line.strip():
74 if not line.strip():
75 inheader = False
75 inheader = False
76 if not inheader and line.startswith('# HG changeset patch'):
76 if not inheader and line.startswith('# HG changeset patch'):
77 yield chunk(cur)
77 yield chunk(cur)
78 cur = []
78 cur = []
79 inheader = True
79 inheader = True
80
80
81 cur.append(line)
81 cur.append(line)
82
82
83 if cur:
83 if cur:
84 yield chunk(cur)
84 yield chunk(cur)
85
85
86 def mboxsplit(stream, cur):
86 def mboxsplit(stream, cur):
87 for line in stream:
87 for line in stream:
88 if line.startswith('From '):
88 if line.startswith('From '):
89 for c in split(chunk(cur[1:])):
89 for c in split(chunk(cur[1:])):
90 yield c
90 yield c
91 cur = []
91 cur = []
92
92
93 cur.append(line)
93 cur.append(line)
94
94
95 if cur:
95 if cur:
96 for c in split(chunk(cur[1:])):
96 for c in split(chunk(cur[1:])):
97 yield c
97 yield c
98
98
99 def mimesplit(stream, cur):
99 def mimesplit(stream, cur):
100 def msgfp(m):
100 def msgfp(m):
101 fp = stringio()
101 fp = stringio()
102 g = email.Generator.Generator(fp, mangle_from_=False)
102 g = email.Generator.Generator(fp, mangle_from_=False)
103 g.flatten(m)
103 g.flatten(m)
104 fp.seek(0)
104 fp.seek(0)
105 return fp
105 return fp
106
106
107 for line in stream:
107 for line in stream:
108 cur.append(line)
108 cur.append(line)
109 c = chunk(cur)
109 c = chunk(cur)
110
110
111 m = email.Parser.Parser().parse(c)
111 m = email.Parser.Parser().parse(c)
112 if not m.is_multipart():
112 if not m.is_multipart():
113 yield msgfp(m)
113 yield msgfp(m)
114 else:
114 else:
115 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
115 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
116 for part in m.walk():
116 for part in m.walk():
117 ct = part.get_content_type()
117 ct = part.get_content_type()
118 if ct not in ok_types:
118 if ct not in ok_types:
119 continue
119 continue
120 yield msgfp(part)
120 yield msgfp(part)
121
121
122 def headersplit(stream, cur):
122 def headersplit(stream, cur):
123 inheader = False
123 inheader = False
124
124
125 for line in stream:
125 for line in stream:
126 if not inheader and isheader(line, inheader):
126 if not inheader and isheader(line, inheader):
127 yield chunk(cur)
127 yield chunk(cur)
128 cur = []
128 cur = []
129 inheader = True
129 inheader = True
130 if inheader and not isheader(line, inheader):
130 if inheader and not isheader(line, inheader):
131 inheader = False
131 inheader = False
132
132
133 cur.append(line)
133 cur.append(line)
134
134
135 if cur:
135 if cur:
136 yield chunk(cur)
136 yield chunk(cur)
137
137
138 def remainder(cur):
138 def remainder(cur):
139 yield chunk(cur)
139 yield chunk(cur)
140
140
141 class fiter(object):
141 class fiter(object):
142 def __init__(self, fp):
142 def __init__(self, fp):
143 self.fp = fp
143 self.fp = fp
144
144
145 def __iter__(self):
145 def __iter__(self):
146 return self
146 return self
147
147
148 def next(self):
148 def next(self):
149 l = self.fp.readline()
149 l = self.fp.readline()
150 if not l:
150 if not l:
151 raise StopIteration
151 raise StopIteration
152 return l
152 return l
153
153
154 inheader = False
154 inheader = False
155 cur = []
155 cur = []
156
156
157 mimeheaders = ['content-type']
157 mimeheaders = ['content-type']
158
158
159 if not util.safehasattr(stream, 'next'):
159 if not util.safehasattr(stream, 'next'):
160 # http responses, for example, have readline but not next
160 # http responses, for example, have readline but not next
161 stream = fiter(stream)
161 stream = fiter(stream)
162
162
163 for line in stream:
163 for line in stream:
164 cur.append(line)
164 cur.append(line)
165 if line.startswith('# HG changeset patch'):
165 if line.startswith('# HG changeset patch'):
166 return hgsplit(stream, cur)
166 return hgsplit(stream, cur)
167 elif line.startswith('From '):
167 elif line.startswith('From '):
168 return mboxsplit(stream, cur)
168 return mboxsplit(stream, cur)
169 elif isheader(line, inheader):
169 elif isheader(line, inheader):
170 inheader = True
170 inheader = True
171 if line.split(':', 1)[0].lower() in mimeheaders:
171 if line.split(':', 1)[0].lower() in mimeheaders:
172 # let email parser handle this
172 # let email parser handle this
173 return mimesplit(stream, cur)
173 return mimesplit(stream, cur)
174 elif line.startswith('--- ') and inheader:
174 elif line.startswith('--- ') and inheader:
175 # No evil headers seen by diff start, split by hand
175 # No evil headers seen by diff start, split by hand
176 return headersplit(stream, cur)
176 return headersplit(stream, cur)
177 # Not enough info, keep reading
177 # Not enough info, keep reading
178
178
179 # if we are here, we have a very plain patch
179 # if we are here, we have a very plain patch
180 return remainder(cur)
180 return remainder(cur)
181
181
182 ## Some facility for extensible patch parsing:
182 ## Some facility for extensible patch parsing:
183 # list of pairs ("header to match", "data key")
183 # list of pairs ("header to match", "data key")
184 patchheadermap = [('Date', 'date'),
184 patchheadermap = [('Date', 'date'),
185 ('Branch', 'branch'),
185 ('Branch', 'branch'),
186 ('Node ID', 'nodeid'),
186 ('Node ID', 'nodeid'),
187 ]
187 ]
188
188
189 def extract(ui, fileobj):
189 def extract(ui, fileobj):
190 '''extract patch from data read from fileobj.
190 '''extract patch from data read from fileobj.
191
191
192 patch can be a normal patch or contained in an email message.
192 patch can be a normal patch or contained in an email message.
193
193
194 return a dictionary. Standard keys are:
194 return a dictionary. Standard keys are:
195 - filename,
195 - filename,
196 - message,
196 - message,
197 - user,
197 - user,
198 - date,
198 - date,
199 - branch,
199 - branch,
200 - node,
200 - node,
201 - p1,
201 - p1,
202 - p2.
202 - p2.
203 Any item can be missing from the dictionary. If filename is missing,
203 Any item can be missing from the dictionary. If filename is missing,
204 fileobj did not contain a patch. Caller must unlink filename when done.'''
204 fileobj did not contain a patch. Caller must unlink filename when done.'''
205
205
206 # attempt to detect the start of a patch
206 # attempt to detect the start of a patch
207 # (this heuristic is borrowed from quilt)
207 # (this heuristic is borrowed from quilt)
208 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
208 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
209 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
209 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
210 r'---[ \t].*?^\+\+\+[ \t]|'
210 r'---[ \t].*?^\+\+\+[ \t]|'
211 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
211 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
212
212
213 data = {}
213 data = {}
214 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
214 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
215 tmpfp = os.fdopen(fd, pycompat.sysstr('w'))
215 tmpfp = os.fdopen(fd, pycompat.sysstr('w'))
216 try:
216 try:
217 msg = email.Parser.Parser().parse(fileobj)
217 msg = email.Parser.Parser().parse(fileobj)
218
218
219 subject = msg['Subject'] and mail.headdecode(msg['Subject'])
219 subject = msg['Subject'] and mail.headdecode(msg['Subject'])
220 data['user'] = msg['From'] and mail.headdecode(msg['From'])
220 data['user'] = msg['From'] and mail.headdecode(msg['From'])
221 if not subject and not data['user']:
221 if not subject and not data['user']:
222 # Not an email, restore parsed headers if any
222 # Not an email, restore parsed headers if any
223 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
223 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
224
224
225 # should try to parse msg['Date']
225 # should try to parse msg['Date']
226 parents = []
226 parents = []
227
227
228 if subject:
228 if subject:
229 if subject.startswith('[PATCH'):
229 if subject.startswith('[PATCH'):
230 pend = subject.find(']')
230 pend = subject.find(']')
231 if pend >= 0:
231 if pend >= 0:
232 subject = subject[pend + 1:].lstrip()
232 subject = subject[pend + 1:].lstrip()
233 subject = re.sub(r'\n[ \t]+', ' ', subject)
233 subject = re.sub(r'\n[ \t]+', ' ', subject)
234 ui.debug('Subject: %s\n' % subject)
234 ui.debug('Subject: %s\n' % subject)
235 if data['user']:
235 if data['user']:
236 ui.debug('From: %s\n' % data['user'])
236 ui.debug('From: %s\n' % data['user'])
237 diffs_seen = 0
237 diffs_seen = 0
238 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
238 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
239 message = ''
239 message = ''
240 for part in msg.walk():
240 for part in msg.walk():
241 content_type = part.get_content_type()
241 content_type = part.get_content_type()
242 ui.debug('Content-Type: %s\n' % content_type)
242 ui.debug('Content-Type: %s\n' % content_type)
243 if content_type not in ok_types:
243 if content_type not in ok_types:
244 continue
244 continue
245 payload = part.get_payload(decode=True)
245 payload = part.get_payload(decode=True)
246 m = diffre.search(payload)
246 m = diffre.search(payload)
247 if m:
247 if m:
248 hgpatch = False
248 hgpatch = False
249 hgpatchheader = False
249 hgpatchheader = False
250 ignoretext = False
250 ignoretext = False
251
251
252 ui.debug('found patch at byte %d\n' % m.start(0))
252 ui.debug('found patch at byte %d\n' % m.start(0))
253 diffs_seen += 1
253 diffs_seen += 1
254 cfp = stringio()
254 cfp = stringio()
255 for line in payload[:m.start(0)].splitlines():
255 for line in payload[:m.start(0)].splitlines():
256 if line.startswith('# HG changeset patch') and not hgpatch:
256 if line.startswith('# HG changeset patch') and not hgpatch:
257 ui.debug('patch generated by hg export\n')
257 ui.debug('patch generated by hg export\n')
258 hgpatch = True
258 hgpatch = True
259 hgpatchheader = True
259 hgpatchheader = True
260 # drop earlier commit message content
260 # drop earlier commit message content
261 cfp.seek(0)
261 cfp.seek(0)
262 cfp.truncate()
262 cfp.truncate()
263 subject = None
263 subject = None
264 elif hgpatchheader:
264 elif hgpatchheader:
265 if line.startswith('# User '):
265 if line.startswith('# User '):
266 data['user'] = line[7:]
266 data['user'] = line[7:]
267 ui.debug('From: %s\n' % data['user'])
267 ui.debug('From: %s\n' % data['user'])
268 elif line.startswith("# Parent "):
268 elif line.startswith("# Parent "):
269 parents.append(line[9:].lstrip())
269 parents.append(line[9:].lstrip())
270 elif line.startswith("# "):
270 elif line.startswith("# "):
271 for header, key in patchheadermap:
271 for header, key in patchheadermap:
272 prefix = '# %s ' % header
272 prefix = '# %s ' % header
273 if line.startswith(prefix):
273 if line.startswith(prefix):
274 data[key] = line[len(prefix):]
274 data[key] = line[len(prefix):]
275 else:
275 else:
276 hgpatchheader = False
276 hgpatchheader = False
277 elif line == '---':
277 elif line == '---':
278 ignoretext = True
278 ignoretext = True
279 if not hgpatchheader and not ignoretext:
279 if not hgpatchheader and not ignoretext:
280 cfp.write(line)
280 cfp.write(line)
281 cfp.write('\n')
281 cfp.write('\n')
282 message = cfp.getvalue()
282 message = cfp.getvalue()
283 if tmpfp:
283 if tmpfp:
284 tmpfp.write(payload)
284 tmpfp.write(payload)
285 if not payload.endswith('\n'):
285 if not payload.endswith('\n'):
286 tmpfp.write('\n')
286 tmpfp.write('\n')
287 elif not diffs_seen and message and content_type == 'text/plain':
287 elif not diffs_seen and message and content_type == 'text/plain':
288 message += '\n' + payload
288 message += '\n' + payload
289 except: # re-raises
289 except: # re-raises
290 tmpfp.close()
290 tmpfp.close()
291 os.unlink(tmpname)
291 os.unlink(tmpname)
292 raise
292 raise
293
293
294 if subject and not message.startswith(subject):
294 if subject and not message.startswith(subject):
295 message = '%s\n%s' % (subject, message)
295 message = '%s\n%s' % (subject, message)
296 data['message'] = message
296 data['message'] = message
297 tmpfp.close()
297 tmpfp.close()
298 if parents:
298 if parents:
299 data['p1'] = parents.pop(0)
299 data['p1'] = parents.pop(0)
300 if parents:
300 if parents:
301 data['p2'] = parents.pop(0)
301 data['p2'] = parents.pop(0)
302
302
303 if diffs_seen:
303 if diffs_seen:
304 data['filename'] = tmpname
304 data['filename'] = tmpname
305 else:
305 else:
306 os.unlink(tmpname)
306 os.unlink(tmpname)
307 return data
307 return data
308
308
309 class patchmeta(object):
309 class patchmeta(object):
310 """Patched file metadata
310 """Patched file metadata
311
311
312 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
312 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
313 or COPY. 'path' is patched file path. 'oldpath' is set to the
313 or COPY. 'path' is patched file path. 'oldpath' is set to the
314 origin file when 'op' is either COPY or RENAME, None otherwise. If
314 origin file when 'op' is either COPY or RENAME, None otherwise. If
315 file mode is changed, 'mode' is a tuple (islink, isexec) where
315 file mode is changed, 'mode' is a tuple (islink, isexec) where
316 'islink' is True if the file is a symlink and 'isexec' is True if
316 'islink' is True if the file is a symlink and 'isexec' is True if
317 the file is executable. Otherwise, 'mode' is None.
317 the file is executable. Otherwise, 'mode' is None.
318 """
318 """
319 def __init__(self, path):
319 def __init__(self, path):
320 self.path = path
320 self.path = path
321 self.oldpath = None
321 self.oldpath = None
322 self.mode = None
322 self.mode = None
323 self.op = 'MODIFY'
323 self.op = 'MODIFY'
324 self.binary = False
324 self.binary = False
325
325
326 def setmode(self, mode):
326 def setmode(self, mode):
327 islink = mode & 0o20000
327 islink = mode & 0o20000
328 isexec = mode & 0o100
328 isexec = mode & 0o100
329 self.mode = (islink, isexec)
329 self.mode = (islink, isexec)
330
330
331 def copy(self):
331 def copy(self):
332 other = patchmeta(self.path)
332 other = patchmeta(self.path)
333 other.oldpath = self.oldpath
333 other.oldpath = self.oldpath
334 other.mode = self.mode
334 other.mode = self.mode
335 other.op = self.op
335 other.op = self.op
336 other.binary = self.binary
336 other.binary = self.binary
337 return other
337 return other
338
338
339 def _ispatchinga(self, afile):
339 def _ispatchinga(self, afile):
340 if afile == '/dev/null':
340 if afile == '/dev/null':
341 return self.op == 'ADD'
341 return self.op == 'ADD'
342 return afile == 'a/' + (self.oldpath or self.path)
342 return afile == 'a/' + (self.oldpath or self.path)
343
343
344 def _ispatchingb(self, bfile):
344 def _ispatchingb(self, bfile):
345 if bfile == '/dev/null':
345 if bfile == '/dev/null':
346 return self.op == 'DELETE'
346 return self.op == 'DELETE'
347 return bfile == 'b/' + self.path
347 return bfile == 'b/' + self.path
348
348
349 def ispatching(self, afile, bfile):
349 def ispatching(self, afile, bfile):
350 return self._ispatchinga(afile) and self._ispatchingb(bfile)
350 return self._ispatchinga(afile) and self._ispatchingb(bfile)
351
351
352 def __repr__(self):
352 def __repr__(self):
353 return "<patchmeta %s %r>" % (self.op, self.path)
353 return "<patchmeta %s %r>" % (self.op, self.path)
354
354
355 def readgitpatch(lr):
355 def readgitpatch(lr):
356 """extract git-style metadata about patches from <patchname>"""
356 """extract git-style metadata about patches from <patchname>"""
357
357
358 # Filter patch for git information
358 # Filter patch for git information
359 gp = None
359 gp = None
360 gitpatches = []
360 gitpatches = []
361 for line in lr:
361 for line in lr:
362 line = line.rstrip(' \r\n')
362 line = line.rstrip(' \r\n')
363 if line.startswith('diff --git a/'):
363 if line.startswith('diff --git a/'):
364 m = gitre.match(line)
364 m = gitre.match(line)
365 if m:
365 if m:
366 if gp:
366 if gp:
367 gitpatches.append(gp)
367 gitpatches.append(gp)
368 dst = m.group(2)
368 dst = m.group(2)
369 gp = patchmeta(dst)
369 gp = patchmeta(dst)
370 elif gp:
370 elif gp:
371 if line.startswith('--- '):
371 if line.startswith('--- '):
372 gitpatches.append(gp)
372 gitpatches.append(gp)
373 gp = None
373 gp = None
374 continue
374 continue
375 if line.startswith('rename from '):
375 if line.startswith('rename from '):
376 gp.op = 'RENAME'
376 gp.op = 'RENAME'
377 gp.oldpath = line[12:]
377 gp.oldpath = line[12:]
378 elif line.startswith('rename to '):
378 elif line.startswith('rename to '):
379 gp.path = line[10:]
379 gp.path = line[10:]
380 elif line.startswith('copy from '):
380 elif line.startswith('copy from '):
381 gp.op = 'COPY'
381 gp.op = 'COPY'
382 gp.oldpath = line[10:]
382 gp.oldpath = line[10:]
383 elif line.startswith('copy to '):
383 elif line.startswith('copy to '):
384 gp.path = line[8:]
384 gp.path = line[8:]
385 elif line.startswith('deleted file'):
385 elif line.startswith('deleted file'):
386 gp.op = 'DELETE'
386 gp.op = 'DELETE'
387 elif line.startswith('new file mode '):
387 elif line.startswith('new file mode '):
388 gp.op = 'ADD'
388 gp.op = 'ADD'
389 gp.setmode(int(line[-6:], 8))
389 gp.setmode(int(line[-6:], 8))
390 elif line.startswith('new mode '):
390 elif line.startswith('new mode '):
391 gp.setmode(int(line[-6:], 8))
391 gp.setmode(int(line[-6:], 8))
392 elif line.startswith('GIT binary patch'):
392 elif line.startswith('GIT binary patch'):
393 gp.binary = True
393 gp.binary = True
394 if gp:
394 if gp:
395 gitpatches.append(gp)
395 gitpatches.append(gp)
396
396
397 return gitpatches
397 return gitpatches
398
398
399 class linereader(object):
399 class linereader(object):
400 # simple class to allow pushing lines back into the input stream
400 # simple class to allow pushing lines back into the input stream
401 def __init__(self, fp):
401 def __init__(self, fp):
402 self.fp = fp
402 self.fp = fp
403 self.buf = []
403 self.buf = []
404
404
405 def push(self, line):
405 def push(self, line):
406 if line is not None:
406 if line is not None:
407 self.buf.append(line)
407 self.buf.append(line)
408
408
409 def readline(self):
409 def readline(self):
410 if self.buf:
410 if self.buf:
411 l = self.buf[0]
411 l = self.buf[0]
412 del self.buf[0]
412 del self.buf[0]
413 return l
413 return l
414 return self.fp.readline()
414 return self.fp.readline()
415
415
416 def __iter__(self):
416 def __iter__(self):
417 return iter(self.readline, '')
417 return iter(self.readline, '')
418
418
419 class abstractbackend(object):
419 class abstractbackend(object):
420 def __init__(self, ui):
420 def __init__(self, ui):
421 self.ui = ui
421 self.ui = ui
422
422
423 def getfile(self, fname):
423 def getfile(self, fname):
424 """Return target file data and flags as a (data, (islink,
424 """Return target file data and flags as a (data, (islink,
425 isexec)) tuple. Data is None if file is missing/deleted.
425 isexec)) tuple. Data is None if file is missing/deleted.
426 """
426 """
427 raise NotImplementedError
427 raise NotImplementedError
428
428
429 def setfile(self, fname, data, mode, copysource):
429 def setfile(self, fname, data, mode, copysource):
430 """Write data to target file fname and set its mode. mode is a
430 """Write data to target file fname and set its mode. mode is a
431 (islink, isexec) tuple. If data is None, the file content should
431 (islink, isexec) tuple. If data is None, the file content should
432 be left unchanged. If the file is modified after being copied,
432 be left unchanged. If the file is modified after being copied,
433 copysource is set to the original file name.
433 copysource is set to the original file name.
434 """
434 """
435 raise NotImplementedError
435 raise NotImplementedError
436
436
437 def unlink(self, fname):
437 def unlink(self, fname):
438 """Unlink target file."""
438 """Unlink target file."""
439 raise NotImplementedError
439 raise NotImplementedError
440
440
441 def writerej(self, fname, failed, total, lines):
441 def writerej(self, fname, failed, total, lines):
442 """Write rejected lines for fname. total is the number of hunks
442 """Write rejected lines for fname. total is the number of hunks
443 which failed to apply and total the total number of hunks for this
443 which failed to apply and total the total number of hunks for this
444 files.
444 files.
445 """
445 """
446 pass
446 pass
447
447
448 def exists(self, fname):
448 def exists(self, fname):
449 raise NotImplementedError
449 raise NotImplementedError
450
450
451 class fsbackend(abstractbackend):
451 class fsbackend(abstractbackend):
452 def __init__(self, ui, basedir):
452 def __init__(self, ui, basedir):
453 super(fsbackend, self).__init__(ui)
453 super(fsbackend, self).__init__(ui)
454 self.opener = vfsmod.vfs(basedir)
454 self.opener = vfsmod.vfs(basedir)
455
455
456 def _join(self, f):
456 def _join(self, f):
457 return os.path.join(self.opener.base, f)
457 return os.path.join(self.opener.base, f)
458
458
459 def getfile(self, fname):
459 def getfile(self, fname):
460 if self.opener.islink(fname):
460 if self.opener.islink(fname):
461 return (self.opener.readlink(fname), (True, False))
461 return (self.opener.readlink(fname), (True, False))
462
462
463 isexec = False
463 isexec = False
464 try:
464 try:
465 isexec = self.opener.lstat(fname).st_mode & 0o100 != 0
465 isexec = self.opener.lstat(fname).st_mode & 0o100 != 0
466 except OSError as e:
466 except OSError as e:
467 if e.errno != errno.ENOENT:
467 if e.errno != errno.ENOENT:
468 raise
468 raise
469 try:
469 try:
470 return (self.opener.read(fname), (False, isexec))
470 return (self.opener.read(fname), (False, isexec))
471 except IOError as e:
471 except IOError as e:
472 if e.errno != errno.ENOENT:
472 if e.errno != errno.ENOENT:
473 raise
473 raise
474 return None, None
474 return None, None
475
475
476 def setfile(self, fname, data, mode, copysource):
476 def setfile(self, fname, data, mode, copysource):
477 islink, isexec = mode
477 islink, isexec = mode
478 if data is None:
478 if data is None:
479 self.opener.setflags(fname, islink, isexec)
479 self.opener.setflags(fname, islink, isexec)
480 return
480 return
481 if islink:
481 if islink:
482 self.opener.symlink(data, fname)
482 self.opener.symlink(data, fname)
483 else:
483 else:
484 self.opener.write(fname, data)
484 self.opener.write(fname, data)
485 if isexec:
485 if isexec:
486 self.opener.setflags(fname, False, True)
486 self.opener.setflags(fname, False, True)
487
487
488 def unlink(self, fname):
488 def unlink(self, fname):
489 self.opener.unlinkpath(fname, ignoremissing=True)
489 self.opener.unlinkpath(fname, ignoremissing=True)
490
490
491 def writerej(self, fname, failed, total, lines):
491 def writerej(self, fname, failed, total, lines):
492 fname = fname + ".rej"
492 fname = fname + ".rej"
493 self.ui.warn(
493 self.ui.warn(
494 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
494 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
495 (failed, total, fname))
495 (failed, total, fname))
496 fp = self.opener(fname, 'w')
496 fp = self.opener(fname, 'w')
497 fp.writelines(lines)
497 fp.writelines(lines)
498 fp.close()
498 fp.close()
499
499
500 def exists(self, fname):
500 def exists(self, fname):
501 return self.opener.lexists(fname)
501 return self.opener.lexists(fname)
502
502
503 class workingbackend(fsbackend):
503 class workingbackend(fsbackend):
504 def __init__(self, ui, repo, similarity):
504 def __init__(self, ui, repo, similarity):
505 super(workingbackend, self).__init__(ui, repo.root)
505 super(workingbackend, self).__init__(ui, repo.root)
506 self.repo = repo
506 self.repo = repo
507 self.similarity = similarity
507 self.similarity = similarity
508 self.removed = set()
508 self.removed = set()
509 self.changed = set()
509 self.changed = set()
510 self.copied = []
510 self.copied = []
511
511
512 def _checkknown(self, fname):
512 def _checkknown(self, fname):
513 if self.repo.dirstate[fname] == '?' and self.exists(fname):
513 if self.repo.dirstate[fname] == '?' and self.exists(fname):
514 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
514 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
515
515
516 def setfile(self, fname, data, mode, copysource):
516 def setfile(self, fname, data, mode, copysource):
517 self._checkknown(fname)
517 self._checkknown(fname)
518 super(workingbackend, self).setfile(fname, data, mode, copysource)
518 super(workingbackend, self).setfile(fname, data, mode, copysource)
519 if copysource is not None:
519 if copysource is not None:
520 self.copied.append((copysource, fname))
520 self.copied.append((copysource, fname))
521 self.changed.add(fname)
521 self.changed.add(fname)
522
522
523 def unlink(self, fname):
523 def unlink(self, fname):
524 self._checkknown(fname)
524 self._checkknown(fname)
525 super(workingbackend, self).unlink(fname)
525 super(workingbackend, self).unlink(fname)
526 self.removed.add(fname)
526 self.removed.add(fname)
527 self.changed.add(fname)
527 self.changed.add(fname)
528
528
529 def close(self):
529 def close(self):
530 wctx = self.repo[None]
530 wctx = self.repo[None]
531 changed = set(self.changed)
531 changed = set(self.changed)
532 for src, dst in self.copied:
532 for src, dst in self.copied:
533 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
533 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
534 if self.removed:
534 if self.removed:
535 wctx.forget(sorted(self.removed))
535 wctx.forget(sorted(self.removed))
536 for f in self.removed:
536 for f in self.removed:
537 if f not in self.repo.dirstate:
537 if f not in self.repo.dirstate:
538 # File was deleted and no longer belongs to the
538 # File was deleted and no longer belongs to the
539 # dirstate, it was probably marked added then
539 # dirstate, it was probably marked added then
540 # deleted, and should not be considered by
540 # deleted, and should not be considered by
541 # marktouched().
541 # marktouched().
542 changed.discard(f)
542 changed.discard(f)
543 if changed:
543 if changed:
544 scmutil.marktouched(self.repo, changed, self.similarity)
544 scmutil.marktouched(self.repo, changed, self.similarity)
545 return sorted(self.changed)
545 return sorted(self.changed)
546
546
547 class filestore(object):
547 class filestore(object):
548 def __init__(self, maxsize=None):
548 def __init__(self, maxsize=None):
549 self.opener = None
549 self.opener = None
550 self.files = {}
550 self.files = {}
551 self.created = 0
551 self.created = 0
552 self.maxsize = maxsize
552 self.maxsize = maxsize
553 if self.maxsize is None:
553 if self.maxsize is None:
554 self.maxsize = 4*(2**20)
554 self.maxsize = 4*(2**20)
555 self.size = 0
555 self.size = 0
556 self.data = {}
556 self.data = {}
557
557
558 def setfile(self, fname, data, mode, copied=None):
558 def setfile(self, fname, data, mode, copied=None):
559 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
559 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
560 self.data[fname] = (data, mode, copied)
560 self.data[fname] = (data, mode, copied)
561 self.size += len(data)
561 self.size += len(data)
562 else:
562 else:
563 if self.opener is None:
563 if self.opener is None:
564 root = tempfile.mkdtemp(prefix='hg-patch-')
564 root = tempfile.mkdtemp(prefix='hg-patch-')
565 self.opener = vfsmod.vfs(root)
565 self.opener = vfsmod.vfs(root)
566 # Avoid filename issues with these simple names
566 # Avoid filename issues with these simple names
567 fn = str(self.created)
567 fn = str(self.created)
568 self.opener.write(fn, data)
568 self.opener.write(fn, data)
569 self.created += 1
569 self.created += 1
570 self.files[fname] = (fn, mode, copied)
570 self.files[fname] = (fn, mode, copied)
571
571
572 def getfile(self, fname):
572 def getfile(self, fname):
573 if fname in self.data:
573 if fname in self.data:
574 return self.data[fname]
574 return self.data[fname]
575 if not self.opener or fname not in self.files:
575 if not self.opener or fname not in self.files:
576 return None, None, None
576 return None, None, None
577 fn, mode, copied = self.files[fname]
577 fn, mode, copied = self.files[fname]
578 return self.opener.read(fn), mode, copied
578 return self.opener.read(fn), mode, copied
579
579
580 def close(self):
580 def close(self):
581 if self.opener:
581 if self.opener:
582 shutil.rmtree(self.opener.base)
582 shutil.rmtree(self.opener.base)
583
583
584 class repobackend(abstractbackend):
584 class repobackend(abstractbackend):
585 def __init__(self, ui, repo, ctx, store):
585 def __init__(self, ui, repo, ctx, store):
586 super(repobackend, self).__init__(ui)
586 super(repobackend, self).__init__(ui)
587 self.repo = repo
587 self.repo = repo
588 self.ctx = ctx
588 self.ctx = ctx
589 self.store = store
589 self.store = store
590 self.changed = set()
590 self.changed = set()
591 self.removed = set()
591 self.removed = set()
592 self.copied = {}
592 self.copied = {}
593
593
594 def _checkknown(self, fname):
594 def _checkknown(self, fname):
595 if fname not in self.ctx:
595 if fname not in self.ctx:
596 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
596 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
597
597
598 def getfile(self, fname):
598 def getfile(self, fname):
599 try:
599 try:
600 fctx = self.ctx[fname]
600 fctx = self.ctx[fname]
601 except error.LookupError:
601 except error.LookupError:
602 return None, None
602 return None, None
603 flags = fctx.flags()
603 flags = fctx.flags()
604 return fctx.data(), ('l' in flags, 'x' in flags)
604 return fctx.data(), ('l' in flags, 'x' in flags)
605
605
606 def setfile(self, fname, data, mode, copysource):
606 def setfile(self, fname, data, mode, copysource):
607 if copysource:
607 if copysource:
608 self._checkknown(copysource)
608 self._checkknown(copysource)
609 if data is None:
609 if data is None:
610 data = self.ctx[fname].data()
610 data = self.ctx[fname].data()
611 self.store.setfile(fname, data, mode, copysource)
611 self.store.setfile(fname, data, mode, copysource)
612 self.changed.add(fname)
612 self.changed.add(fname)
613 if copysource:
613 if copysource:
614 self.copied[fname] = copysource
614 self.copied[fname] = copysource
615
615
616 def unlink(self, fname):
616 def unlink(self, fname):
617 self._checkknown(fname)
617 self._checkknown(fname)
618 self.removed.add(fname)
618 self.removed.add(fname)
619
619
620 def exists(self, fname):
620 def exists(self, fname):
621 return fname in self.ctx
621 return fname in self.ctx
622
622
623 def close(self):
623 def close(self):
624 return self.changed | self.removed
624 return self.changed | self.removed
625
625
626 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
626 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
627 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
627 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
628 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
628 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
629 eolmodes = ['strict', 'crlf', 'lf', 'auto']
629 eolmodes = ['strict', 'crlf', 'lf', 'auto']
630
630
631 class patchfile(object):
631 class patchfile(object):
632 def __init__(self, ui, gp, backend, store, eolmode='strict'):
632 def __init__(self, ui, gp, backend, store, eolmode='strict'):
633 self.fname = gp.path
633 self.fname = gp.path
634 self.eolmode = eolmode
634 self.eolmode = eolmode
635 self.eol = None
635 self.eol = None
636 self.backend = backend
636 self.backend = backend
637 self.ui = ui
637 self.ui = ui
638 self.lines = []
638 self.lines = []
639 self.exists = False
639 self.exists = False
640 self.missing = True
640 self.missing = True
641 self.mode = gp.mode
641 self.mode = gp.mode
642 self.copysource = gp.oldpath
642 self.copysource = gp.oldpath
643 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
643 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
644 self.remove = gp.op == 'DELETE'
644 self.remove = gp.op == 'DELETE'
645 if self.copysource is None:
645 if self.copysource is None:
646 data, mode = backend.getfile(self.fname)
646 data, mode = backend.getfile(self.fname)
647 else:
647 else:
648 data, mode = store.getfile(self.copysource)[:2]
648 data, mode = store.getfile(self.copysource)[:2]
649 if data is not None:
649 if data is not None:
650 self.exists = self.copysource is None or backend.exists(self.fname)
650 self.exists = self.copysource is None or backend.exists(self.fname)
651 self.missing = False
651 self.missing = False
652 if data:
652 if data:
653 self.lines = mdiff.splitnewlines(data)
653 self.lines = mdiff.splitnewlines(data)
654 if self.mode is None:
654 if self.mode is None:
655 self.mode = mode
655 self.mode = mode
656 if self.lines:
656 if self.lines:
657 # Normalize line endings
657 # Normalize line endings
658 if self.lines[0].endswith('\r\n'):
658 if self.lines[0].endswith('\r\n'):
659 self.eol = '\r\n'
659 self.eol = '\r\n'
660 elif self.lines[0].endswith('\n'):
660 elif self.lines[0].endswith('\n'):
661 self.eol = '\n'
661 self.eol = '\n'
662 if eolmode != 'strict':
662 if eolmode != 'strict':
663 nlines = []
663 nlines = []
664 for l in self.lines:
664 for l in self.lines:
665 if l.endswith('\r\n'):
665 if l.endswith('\r\n'):
666 l = l[:-2] + '\n'
666 l = l[:-2] + '\n'
667 nlines.append(l)
667 nlines.append(l)
668 self.lines = nlines
668 self.lines = nlines
669 else:
669 else:
670 if self.create:
670 if self.create:
671 self.missing = False
671 self.missing = False
672 if self.mode is None:
672 if self.mode is None:
673 self.mode = (False, False)
673 self.mode = (False, False)
674 if self.missing:
674 if self.missing:
675 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
675 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
676 self.ui.warn(_("(use '--prefix' to apply patch relative to the "
676 self.ui.warn(_("(use '--prefix' to apply patch relative to the "
677 "current directory)\n"))
677 "current directory)\n"))
678
678
679 self.hash = {}
679 self.hash = {}
680 self.dirty = 0
680 self.dirty = 0
681 self.offset = 0
681 self.offset = 0
682 self.skew = 0
682 self.skew = 0
683 self.rej = []
683 self.rej = []
684 self.fileprinted = False
684 self.fileprinted = False
685 self.printfile(False)
685 self.printfile(False)
686 self.hunks = 0
686 self.hunks = 0
687
687
688 def writelines(self, fname, lines, mode):
688 def writelines(self, fname, lines, mode):
689 if self.eolmode == 'auto':
689 if self.eolmode == 'auto':
690 eol = self.eol
690 eol = self.eol
691 elif self.eolmode == 'crlf':
691 elif self.eolmode == 'crlf':
692 eol = '\r\n'
692 eol = '\r\n'
693 else:
693 else:
694 eol = '\n'
694 eol = '\n'
695
695
696 if self.eolmode != 'strict' and eol and eol != '\n':
696 if self.eolmode != 'strict' and eol and eol != '\n':
697 rawlines = []
697 rawlines = []
698 for l in lines:
698 for l in lines:
699 if l and l[-1] == '\n':
699 if l and l[-1] == '\n':
700 l = l[:-1] + eol
700 l = l[:-1] + eol
701 rawlines.append(l)
701 rawlines.append(l)
702 lines = rawlines
702 lines = rawlines
703
703
704 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
704 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
705
705
706 def printfile(self, warn):
706 def printfile(self, warn):
707 if self.fileprinted:
707 if self.fileprinted:
708 return
708 return
709 if warn or self.ui.verbose:
709 if warn or self.ui.verbose:
710 self.fileprinted = True
710 self.fileprinted = True
711 s = _("patching file %s\n") % self.fname
711 s = _("patching file %s\n") % self.fname
712 if warn:
712 if warn:
713 self.ui.warn(s)
713 self.ui.warn(s)
714 else:
714 else:
715 self.ui.note(s)
715 self.ui.note(s)
716
716
717
717
718 def findlines(self, l, linenum):
718 def findlines(self, l, linenum):
719 # looks through the hash and finds candidate lines. The
719 # looks through the hash and finds candidate lines. The
720 # result is a list of line numbers sorted based on distance
720 # result is a list of line numbers sorted based on distance
721 # from linenum
721 # from linenum
722
722
723 cand = self.hash.get(l, [])
723 cand = self.hash.get(l, [])
724 if len(cand) > 1:
724 if len(cand) > 1:
725 # resort our list of potentials forward then back.
725 # resort our list of potentials forward then back.
726 cand.sort(key=lambda x: abs(x - linenum))
726 cand.sort(key=lambda x: abs(x - linenum))
727 return cand
727 return cand
728
728
729 def write_rej(self):
729 def write_rej(self):
730 # our rejects are a little different from patch(1). This always
730 # our rejects are a little different from patch(1). This always
731 # creates rejects in the same form as the original patch. A file
731 # creates rejects in the same form as the original patch. A file
732 # header is inserted so that you can run the reject through patch again
732 # header is inserted so that you can run the reject through patch again
733 # without having to type the filename.
733 # without having to type the filename.
734 if not self.rej:
734 if not self.rej:
735 return
735 return
736 base = os.path.basename(self.fname)
736 base = os.path.basename(self.fname)
737 lines = ["--- %s\n+++ %s\n" % (base, base)]
737 lines = ["--- %s\n+++ %s\n" % (base, base)]
738 for x in self.rej:
738 for x in self.rej:
739 for l in x.hunk:
739 for l in x.hunk:
740 lines.append(l)
740 lines.append(l)
741 if l[-1:] != '\n':
741 if l[-1:] != '\n':
742 lines.append("\n\ No newline at end of file\n")
742 lines.append("\n\ No newline at end of file\n")
743 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
743 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
744
744
745 def apply(self, h):
745 def apply(self, h):
746 if not h.complete():
746 if not h.complete():
747 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
747 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
748 (h.number, h.desc, len(h.a), h.lena, len(h.b),
748 (h.number, h.desc, len(h.a), h.lena, len(h.b),
749 h.lenb))
749 h.lenb))
750
750
751 self.hunks += 1
751 self.hunks += 1
752
752
753 if self.missing:
753 if self.missing:
754 self.rej.append(h)
754 self.rej.append(h)
755 return -1
755 return -1
756
756
757 if self.exists and self.create:
757 if self.exists and self.create:
758 if self.copysource:
758 if self.copysource:
759 self.ui.warn(_("cannot create %s: destination already "
759 self.ui.warn(_("cannot create %s: destination already "
760 "exists\n") % self.fname)
760 "exists\n") % self.fname)
761 else:
761 else:
762 self.ui.warn(_("file %s already exists\n") % self.fname)
762 self.ui.warn(_("file %s already exists\n") % self.fname)
763 self.rej.append(h)
763 self.rej.append(h)
764 return -1
764 return -1
765
765
766 if isinstance(h, binhunk):
766 if isinstance(h, binhunk):
767 if self.remove:
767 if self.remove:
768 self.backend.unlink(self.fname)
768 self.backend.unlink(self.fname)
769 else:
769 else:
770 l = h.new(self.lines)
770 l = h.new(self.lines)
771 self.lines[:] = l
771 self.lines[:] = l
772 self.offset += len(l)
772 self.offset += len(l)
773 self.dirty = True
773 self.dirty = True
774 return 0
774 return 0
775
775
776 horig = h
776 horig = h
777 if (self.eolmode in ('crlf', 'lf')
777 if (self.eolmode in ('crlf', 'lf')
778 or self.eolmode == 'auto' and self.eol):
778 or self.eolmode == 'auto' and self.eol):
779 # If new eols are going to be normalized, then normalize
779 # If new eols are going to be normalized, then normalize
780 # hunk data before patching. Otherwise, preserve input
780 # hunk data before patching. Otherwise, preserve input
781 # line-endings.
781 # line-endings.
782 h = h.getnormalized()
782 h = h.getnormalized()
783
783
784 # fast case first, no offsets, no fuzz
784 # fast case first, no offsets, no fuzz
785 old, oldstart, new, newstart = h.fuzzit(0, False)
785 old, oldstart, new, newstart = h.fuzzit(0, False)
786 oldstart += self.offset
786 oldstart += self.offset
787 orig_start = oldstart
787 orig_start = oldstart
788 # if there's skew we want to emit the "(offset %d lines)" even
788 # if there's skew we want to emit the "(offset %d lines)" even
789 # when the hunk cleanly applies at start + skew, so skip the
789 # when the hunk cleanly applies at start + skew, so skip the
790 # fast case code
790 # fast case code
791 if (self.skew == 0 and
791 if (self.skew == 0 and
792 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
792 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
793 if self.remove:
793 if self.remove:
794 self.backend.unlink(self.fname)
794 self.backend.unlink(self.fname)
795 else:
795 else:
796 self.lines[oldstart:oldstart + len(old)] = new
796 self.lines[oldstart:oldstart + len(old)] = new
797 self.offset += len(new) - len(old)
797 self.offset += len(new) - len(old)
798 self.dirty = True
798 self.dirty = True
799 return 0
799 return 0
800
800
801 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
801 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
802 self.hash = {}
802 self.hash = {}
803 for x, s in enumerate(self.lines):
803 for x, s in enumerate(self.lines):
804 self.hash.setdefault(s, []).append(x)
804 self.hash.setdefault(s, []).append(x)
805
805
806 for fuzzlen in xrange(self.ui.configint("patch", "fuzz", 2) + 1):
806 for fuzzlen in xrange(self.ui.configint("patch", "fuzz", 2) + 1):
807 for toponly in [True, False]:
807 for toponly in [True, False]:
808 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
808 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
809 oldstart = oldstart + self.offset + self.skew
809 oldstart = oldstart + self.offset + self.skew
810 oldstart = min(oldstart, len(self.lines))
810 oldstart = min(oldstart, len(self.lines))
811 if old:
811 if old:
812 cand = self.findlines(old[0][1:], oldstart)
812 cand = self.findlines(old[0][1:], oldstart)
813 else:
813 else:
814 # Only adding lines with no or fuzzed context, just
814 # Only adding lines with no or fuzzed context, just
815 # take the skew in account
815 # take the skew in account
816 cand = [oldstart]
816 cand = [oldstart]
817
817
818 for l in cand:
818 for l in cand:
819 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
819 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
820 self.lines[l : l + len(old)] = new
820 self.lines[l : l + len(old)] = new
821 self.offset += len(new) - len(old)
821 self.offset += len(new) - len(old)
822 self.skew = l - orig_start
822 self.skew = l - orig_start
823 self.dirty = True
823 self.dirty = True
824 offset = l - orig_start - fuzzlen
824 offset = l - orig_start - fuzzlen
825 if fuzzlen:
825 if fuzzlen:
826 msg = _("Hunk #%d succeeded at %d "
826 msg = _("Hunk #%d succeeded at %d "
827 "with fuzz %d "
827 "with fuzz %d "
828 "(offset %d lines).\n")
828 "(offset %d lines).\n")
829 self.printfile(True)
829 self.printfile(True)
830 self.ui.warn(msg %
830 self.ui.warn(msg %
831 (h.number, l + 1, fuzzlen, offset))
831 (h.number, l + 1, fuzzlen, offset))
832 else:
832 else:
833 msg = _("Hunk #%d succeeded at %d "
833 msg = _("Hunk #%d succeeded at %d "
834 "(offset %d lines).\n")
834 "(offset %d lines).\n")
835 self.ui.note(msg % (h.number, l + 1, offset))
835 self.ui.note(msg % (h.number, l + 1, offset))
836 return fuzzlen
836 return fuzzlen
837 self.printfile(True)
837 self.printfile(True)
838 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
838 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
839 self.rej.append(horig)
839 self.rej.append(horig)
840 return -1
840 return -1
841
841
842 def close(self):
842 def close(self):
843 if self.dirty:
843 if self.dirty:
844 self.writelines(self.fname, self.lines, self.mode)
844 self.writelines(self.fname, self.lines, self.mode)
845 self.write_rej()
845 self.write_rej()
846 return len(self.rej)
846 return len(self.rej)
847
847
848 class header(object):
848 class header(object):
849 """patch header
849 """patch header
850 """
850 """
851 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
851 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
852 diff_re = re.compile('diff -r .* (.*)$')
852 diff_re = re.compile('diff -r .* (.*)$')
853 allhunks_re = re.compile('(?:index|deleted file) ')
853 allhunks_re = re.compile('(?:index|deleted file) ')
854 pretty_re = re.compile('(?:new file|deleted file) ')
854 pretty_re = re.compile('(?:new file|deleted file) ')
855 special_re = re.compile('(?:index|deleted|copy|rename) ')
855 special_re = re.compile('(?:index|deleted|copy|rename) ')
856 newfile_re = re.compile('(?:new file)')
856 newfile_re = re.compile('(?:new file)')
857
857
858 def __init__(self, header):
858 def __init__(self, header):
859 self.header = header
859 self.header = header
860 self.hunks = []
860 self.hunks = []
861
861
862 def binary(self):
862 def binary(self):
863 return any(h.startswith('index ') for h in self.header)
863 return any(h.startswith('index ') for h in self.header)
864
864
865 def pretty(self, fp):
865 def pretty(self, fp):
866 for h in self.header:
866 for h in self.header:
867 if h.startswith('index '):
867 if h.startswith('index '):
868 fp.write(_('this modifies a binary file (all or nothing)\n'))
868 fp.write(_('this modifies a binary file (all or nothing)\n'))
869 break
869 break
870 if self.pretty_re.match(h):
870 if self.pretty_re.match(h):
871 fp.write(h)
871 fp.write(h)
872 if self.binary():
872 if self.binary():
873 fp.write(_('this is a binary file\n'))
873 fp.write(_('this is a binary file\n'))
874 break
874 break
875 if h.startswith('---'):
875 if h.startswith('---'):
876 fp.write(_('%d hunks, %d lines changed\n') %
876 fp.write(_('%d hunks, %d lines changed\n') %
877 (len(self.hunks),
877 (len(self.hunks),
878 sum([max(h.added, h.removed) for h in self.hunks])))
878 sum([max(h.added, h.removed) for h in self.hunks])))
879 break
879 break
880 fp.write(h)
880 fp.write(h)
881
881
882 def write(self, fp):
882 def write(self, fp):
883 fp.write(''.join(self.header))
883 fp.write(''.join(self.header))
884
884
885 def allhunks(self):
885 def allhunks(self):
886 return any(self.allhunks_re.match(h) for h in self.header)
886 return any(self.allhunks_re.match(h) for h in self.header)
887
887
888 def files(self):
888 def files(self):
889 match = self.diffgit_re.match(self.header[0])
889 match = self.diffgit_re.match(self.header[0])
890 if match:
890 if match:
891 fromfile, tofile = match.groups()
891 fromfile, tofile = match.groups()
892 if fromfile == tofile:
892 if fromfile == tofile:
893 return [fromfile]
893 return [fromfile]
894 return [fromfile, tofile]
894 return [fromfile, tofile]
895 else:
895 else:
896 return self.diff_re.match(self.header[0]).groups()
896 return self.diff_re.match(self.header[0]).groups()
897
897
898 def filename(self):
898 def filename(self):
899 return self.files()[-1]
899 return self.files()[-1]
900
900
901 def __repr__(self):
901 def __repr__(self):
902 return '<header %s>' % (' '.join(map(repr, self.files())))
902 return '<header %s>' % (' '.join(map(repr, self.files())))
903
903
904 def isnewfile(self):
904 def isnewfile(self):
905 return any(self.newfile_re.match(h) for h in self.header)
905 return any(self.newfile_re.match(h) for h in self.header)
906
906
907 def special(self):
907 def special(self):
908 # Special files are shown only at the header level and not at the hunk
908 # Special files are shown only at the header level and not at the hunk
909 # level for example a file that has been deleted is a special file.
909 # level for example a file that has been deleted is a special file.
910 # The user cannot change the content of the operation, in the case of
910 # The user cannot change the content of the operation, in the case of
911 # the deleted file he has to take the deletion or not take it, he
911 # the deleted file he has to take the deletion or not take it, he
912 # cannot take some of it.
912 # cannot take some of it.
913 # Newly added files are special if they are empty, they are not special
913 # Newly added files are special if they are empty, they are not special
914 # if they have some content as we want to be able to change it
914 # if they have some content as we want to be able to change it
915 nocontent = len(self.header) == 2
915 nocontent = len(self.header) == 2
916 emptynewfile = self.isnewfile() and nocontent
916 emptynewfile = self.isnewfile() and nocontent
917 return emptynewfile or \
917 return emptynewfile or \
918 any(self.special_re.match(h) for h in self.header)
918 any(self.special_re.match(h) for h in self.header)
919
919
920 class recordhunk(object):
920 class recordhunk(object):
921 """patch hunk
921 """patch hunk
922
922
923 XXX shouldn't we merge this with the other hunk class?
923 XXX shouldn't we merge this with the other hunk class?
924 """
924 """
925 maxcontext = 3
925 maxcontext = 3
926
926
927 def __init__(self, header, fromline, toline, proc, before, hunk, after):
927 def __init__(self, header, fromline, toline, proc, before, hunk, after):
928 def trimcontext(number, lines):
928 def trimcontext(number, lines):
929 delta = len(lines) - self.maxcontext
929 delta = len(lines) - self.maxcontext
930 if False and delta > 0:
930 if False and delta > 0:
931 return number + delta, lines[:self.maxcontext]
931 return number + delta, lines[:self.maxcontext]
932 return number, lines
932 return number, lines
933
933
934 self.header = header
934 self.header = header
935 self.fromline, self.before = trimcontext(fromline, before)
935 self.fromline, self.before = trimcontext(fromline, before)
936 self.toline, self.after = trimcontext(toline, after)
936 self.toline, self.after = trimcontext(toline, after)
937 self.proc = proc
937 self.proc = proc
938 self.hunk = hunk
938 self.hunk = hunk
939 self.added, self.removed = self.countchanges(self.hunk)
939 self.added, self.removed = self.countchanges(self.hunk)
940
940
941 def __eq__(self, v):
941 def __eq__(self, v):
942 if not isinstance(v, recordhunk):
942 if not isinstance(v, recordhunk):
943 return False
943 return False
944
944
945 return ((v.hunk == self.hunk) and
945 return ((v.hunk == self.hunk) and
946 (v.proc == self.proc) and
946 (v.proc == self.proc) and
947 (self.fromline == v.fromline) and
947 (self.fromline == v.fromline) and
948 (self.header.files() == v.header.files()))
948 (self.header.files() == v.header.files()))
949
949
950 def __hash__(self):
950 def __hash__(self):
951 return hash((tuple(self.hunk),
951 return hash((tuple(self.hunk),
952 tuple(self.header.files()),
952 tuple(self.header.files()),
953 self.fromline,
953 self.fromline,
954 self.proc))
954 self.proc))
955
955
956 def countchanges(self, hunk):
956 def countchanges(self, hunk):
957 """hunk -> (n+,n-)"""
957 """hunk -> (n+,n-)"""
958 add = len([h for h in hunk if h[0] == '+'])
958 add = len([h for h in hunk if h[0] == '+'])
959 rem = len([h for h in hunk if h[0] == '-'])
959 rem = len([h for h in hunk if h[0] == '-'])
960 return add, rem
960 return add, rem
961
961
962 def reversehunk(self):
963 """return another recordhunk which is the reverse of the hunk
964
965 If this hunk is diff(A, B), the returned hunk is diff(B, A). To do
966 that, swap fromline/toline and +/- signs while keep other things
967 unchanged.
968 """
969 m = {'+': '-', '-': '+'}
970 hunk = ['%s%s' % (m[l[0]], l[1:]) for l in self.hunk]
971 return recordhunk(self.header, self.toline, self.fromline, self.proc,
972 self.before, hunk, self.after)
973
962 def write(self, fp):
974 def write(self, fp):
963 delta = len(self.before) + len(self.after)
975 delta = len(self.before) + len(self.after)
964 if self.after and self.after[-1] == '\\ No newline at end of file\n':
976 if self.after and self.after[-1] == '\\ No newline at end of file\n':
965 delta -= 1
977 delta -= 1
966 fromlen = delta + self.removed
978 fromlen = delta + self.removed
967 tolen = delta + self.added
979 tolen = delta + self.added
968 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
980 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
969 (self.fromline, fromlen, self.toline, tolen,
981 (self.fromline, fromlen, self.toline, tolen,
970 self.proc and (' ' + self.proc)))
982 self.proc and (' ' + self.proc)))
971 fp.write(''.join(self.before + self.hunk + self.after))
983 fp.write(''.join(self.before + self.hunk + self.after))
972
984
973 pretty = write
985 pretty = write
974
986
975 def filename(self):
987 def filename(self):
976 return self.header.filename()
988 return self.header.filename()
977
989
978 def __repr__(self):
990 def __repr__(self):
979 return '<hunk %r@%d>' % (self.filename(), self.fromline)
991 return '<hunk %r@%d>' % (self.filename(), self.fromline)
980
992
981 def filterpatch(ui, headers, operation=None):
993 def filterpatch(ui, headers, operation=None):
982 """Interactively filter patch chunks into applied-only chunks"""
994 """Interactively filter patch chunks into applied-only chunks"""
983 if operation is None:
995 if operation is None:
984 operation = 'record'
996 operation = 'record'
985 messages = {
997 messages = {
986 'multiple': {
998 'multiple': {
987 'discard': _("discard change %d/%d to '%s'?"),
999 'discard': _("discard change %d/%d to '%s'?"),
988 'record': _("record change %d/%d to '%s'?"),
1000 'record': _("record change %d/%d to '%s'?"),
989 'revert': _("revert change %d/%d to '%s'?"),
1001 'revert': _("revert change %d/%d to '%s'?"),
990 }[operation],
1002 }[operation],
991 'single': {
1003 'single': {
992 'discard': _("discard this change to '%s'?"),
1004 'discard': _("discard this change to '%s'?"),
993 'record': _("record this change to '%s'?"),
1005 'record': _("record this change to '%s'?"),
994 'revert': _("revert this change to '%s'?"),
1006 'revert': _("revert this change to '%s'?"),
995 }[operation],
1007 }[operation],
996 'help': {
1008 'help': {
997 'discard': _('[Ynesfdaq?]'
1009 'discard': _('[Ynesfdaq?]'
998 '$$ &Yes, discard this change'
1010 '$$ &Yes, discard this change'
999 '$$ &No, skip this change'
1011 '$$ &No, skip this change'
1000 '$$ &Edit this change manually'
1012 '$$ &Edit this change manually'
1001 '$$ &Skip remaining changes to this file'
1013 '$$ &Skip remaining changes to this file'
1002 '$$ Discard remaining changes to this &file'
1014 '$$ Discard remaining changes to this &file'
1003 '$$ &Done, skip remaining changes and files'
1015 '$$ &Done, skip remaining changes and files'
1004 '$$ Discard &all changes to all remaining files'
1016 '$$ Discard &all changes to all remaining files'
1005 '$$ &Quit, discarding no changes'
1017 '$$ &Quit, discarding no changes'
1006 '$$ &? (display help)'),
1018 '$$ &? (display help)'),
1007 'record': _('[Ynesfdaq?]'
1019 'record': _('[Ynesfdaq?]'
1008 '$$ &Yes, record this change'
1020 '$$ &Yes, record this change'
1009 '$$ &No, skip this change'
1021 '$$ &No, skip this change'
1010 '$$ &Edit this change manually'
1022 '$$ &Edit this change manually'
1011 '$$ &Skip remaining changes to this file'
1023 '$$ &Skip remaining changes to this file'
1012 '$$ Record remaining changes to this &file'
1024 '$$ Record remaining changes to this &file'
1013 '$$ &Done, skip remaining changes and files'
1025 '$$ &Done, skip remaining changes and files'
1014 '$$ Record &all changes to all remaining files'
1026 '$$ Record &all changes to all remaining files'
1015 '$$ &Quit, recording no changes'
1027 '$$ &Quit, recording no changes'
1016 '$$ &? (display help)'),
1028 '$$ &? (display help)'),
1017 'revert': _('[Ynesfdaq?]'
1029 'revert': _('[Ynesfdaq?]'
1018 '$$ &Yes, revert this change'
1030 '$$ &Yes, revert this change'
1019 '$$ &No, skip this change'
1031 '$$ &No, skip this change'
1020 '$$ &Edit this change manually'
1032 '$$ &Edit this change manually'
1021 '$$ &Skip remaining changes to this file'
1033 '$$ &Skip remaining changes to this file'
1022 '$$ Revert remaining changes to this &file'
1034 '$$ Revert remaining changes to this &file'
1023 '$$ &Done, skip remaining changes and files'
1035 '$$ &Done, skip remaining changes and files'
1024 '$$ Revert &all changes to all remaining files'
1036 '$$ Revert &all changes to all remaining files'
1025 '$$ &Quit, reverting no changes'
1037 '$$ &Quit, reverting no changes'
1026 '$$ &? (display help)')
1038 '$$ &? (display help)')
1027 }[operation]
1039 }[operation]
1028 }
1040 }
1029
1041
1030 def prompt(skipfile, skipall, query, chunk):
1042 def prompt(skipfile, skipall, query, chunk):
1031 """prompt query, and process base inputs
1043 """prompt query, and process base inputs
1032
1044
1033 - y/n for the rest of file
1045 - y/n for the rest of file
1034 - y/n for the rest
1046 - y/n for the rest
1035 - ? (help)
1047 - ? (help)
1036 - q (quit)
1048 - q (quit)
1037
1049
1038 Return True/False and possibly updated skipfile and skipall.
1050 Return True/False and possibly updated skipfile and skipall.
1039 """
1051 """
1040 newpatches = None
1052 newpatches = None
1041 if skipall is not None:
1053 if skipall is not None:
1042 return skipall, skipfile, skipall, newpatches
1054 return skipall, skipfile, skipall, newpatches
1043 if skipfile is not None:
1055 if skipfile is not None:
1044 return skipfile, skipfile, skipall, newpatches
1056 return skipfile, skipfile, skipall, newpatches
1045 while True:
1057 while True:
1046 resps = messages['help']
1058 resps = messages['help']
1047 r = ui.promptchoice("%s %s" % (query, resps))
1059 r = ui.promptchoice("%s %s" % (query, resps))
1048 ui.write("\n")
1060 ui.write("\n")
1049 if r == 8: # ?
1061 if r == 8: # ?
1050 for c, t in ui.extractchoices(resps)[1]:
1062 for c, t in ui.extractchoices(resps)[1]:
1051 ui.write('%s - %s\n' % (c, encoding.lower(t)))
1063 ui.write('%s - %s\n' % (c, encoding.lower(t)))
1052 continue
1064 continue
1053 elif r == 0: # yes
1065 elif r == 0: # yes
1054 ret = True
1066 ret = True
1055 elif r == 1: # no
1067 elif r == 1: # no
1056 ret = False
1068 ret = False
1057 elif r == 2: # Edit patch
1069 elif r == 2: # Edit patch
1058 if chunk is None:
1070 if chunk is None:
1059 ui.write(_('cannot edit patch for whole file'))
1071 ui.write(_('cannot edit patch for whole file'))
1060 ui.write("\n")
1072 ui.write("\n")
1061 continue
1073 continue
1062 if chunk.header.binary():
1074 if chunk.header.binary():
1063 ui.write(_('cannot edit patch for binary file'))
1075 ui.write(_('cannot edit patch for binary file'))
1064 ui.write("\n")
1076 ui.write("\n")
1065 continue
1077 continue
1066 # Patch comment based on the Git one (based on comment at end of
1078 # Patch comment based on the Git one (based on comment at end of
1067 # https://mercurial-scm.org/wiki/RecordExtension)
1079 # https://mercurial-scm.org/wiki/RecordExtension)
1068 phelp = '---' + _("""
1080 phelp = '---' + _("""
1069 To remove '-' lines, make them ' ' lines (context).
1081 To remove '-' lines, make them ' ' lines (context).
1070 To remove '+' lines, delete them.
1082 To remove '+' lines, delete them.
1071 Lines starting with # will be removed from the patch.
1083 Lines starting with # will be removed from the patch.
1072
1084
1073 If the patch applies cleanly, the edited hunk will immediately be
1085 If the patch applies cleanly, the edited hunk will immediately be
1074 added to the record list. If it does not apply cleanly, a rejects
1086 added to the record list. If it does not apply cleanly, a rejects
1075 file will be generated: you can use that when you try again. If
1087 file will be generated: you can use that when you try again. If
1076 all lines of the hunk are removed, then the edit is aborted and
1088 all lines of the hunk are removed, then the edit is aborted and
1077 the hunk is left unchanged.
1089 the hunk is left unchanged.
1078 """)
1090 """)
1079 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1091 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1080 suffix=".diff", text=True)
1092 suffix=".diff", text=True)
1081 ncpatchfp = None
1093 ncpatchfp = None
1082 try:
1094 try:
1083 # Write the initial patch
1095 # Write the initial patch
1084 f = os.fdopen(patchfd, pycompat.sysstr("w"))
1096 f = os.fdopen(patchfd, pycompat.sysstr("w"))
1085 chunk.header.write(f)
1097 chunk.header.write(f)
1086 chunk.write(f)
1098 chunk.write(f)
1087 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1099 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1088 f.close()
1100 f.close()
1089 # Start the editor and wait for it to complete
1101 # Start the editor and wait for it to complete
1090 editor = ui.geteditor()
1102 editor = ui.geteditor()
1091 ret = ui.system("%s \"%s\"" % (editor, patchfn),
1103 ret = ui.system("%s \"%s\"" % (editor, patchfn),
1092 environ={'HGUSER': ui.username()},
1104 environ={'HGUSER': ui.username()},
1093 blockedtag='filterpatch')
1105 blockedtag='filterpatch')
1094 if ret != 0:
1106 if ret != 0:
1095 ui.warn(_("editor exited with exit code %d\n") % ret)
1107 ui.warn(_("editor exited with exit code %d\n") % ret)
1096 continue
1108 continue
1097 # Remove comment lines
1109 # Remove comment lines
1098 patchfp = open(patchfn)
1110 patchfp = open(patchfn)
1099 ncpatchfp = stringio()
1111 ncpatchfp = stringio()
1100 for line in util.iterfile(patchfp):
1112 for line in util.iterfile(patchfp):
1101 if not line.startswith('#'):
1113 if not line.startswith('#'):
1102 ncpatchfp.write(line)
1114 ncpatchfp.write(line)
1103 patchfp.close()
1115 patchfp.close()
1104 ncpatchfp.seek(0)
1116 ncpatchfp.seek(0)
1105 newpatches = parsepatch(ncpatchfp)
1117 newpatches = parsepatch(ncpatchfp)
1106 finally:
1118 finally:
1107 os.unlink(patchfn)
1119 os.unlink(patchfn)
1108 del ncpatchfp
1120 del ncpatchfp
1109 # Signal that the chunk shouldn't be applied as-is, but
1121 # Signal that the chunk shouldn't be applied as-is, but
1110 # provide the new patch to be used instead.
1122 # provide the new patch to be used instead.
1111 ret = False
1123 ret = False
1112 elif r == 3: # Skip
1124 elif r == 3: # Skip
1113 ret = skipfile = False
1125 ret = skipfile = False
1114 elif r == 4: # file (Record remaining)
1126 elif r == 4: # file (Record remaining)
1115 ret = skipfile = True
1127 ret = skipfile = True
1116 elif r == 5: # done, skip remaining
1128 elif r == 5: # done, skip remaining
1117 ret = skipall = False
1129 ret = skipall = False
1118 elif r == 6: # all
1130 elif r == 6: # all
1119 ret = skipall = True
1131 ret = skipall = True
1120 elif r == 7: # quit
1132 elif r == 7: # quit
1121 raise error.Abort(_('user quit'))
1133 raise error.Abort(_('user quit'))
1122 return ret, skipfile, skipall, newpatches
1134 return ret, skipfile, skipall, newpatches
1123
1135
1124 seen = set()
1136 seen = set()
1125 applied = {} # 'filename' -> [] of chunks
1137 applied = {} # 'filename' -> [] of chunks
1126 skipfile, skipall = None, None
1138 skipfile, skipall = None, None
1127 pos, total = 1, sum(len(h.hunks) for h in headers)
1139 pos, total = 1, sum(len(h.hunks) for h in headers)
1128 for h in headers:
1140 for h in headers:
1129 pos += len(h.hunks)
1141 pos += len(h.hunks)
1130 skipfile = None
1142 skipfile = None
1131 fixoffset = 0
1143 fixoffset = 0
1132 hdr = ''.join(h.header)
1144 hdr = ''.join(h.header)
1133 if hdr in seen:
1145 if hdr in seen:
1134 continue
1146 continue
1135 seen.add(hdr)
1147 seen.add(hdr)
1136 if skipall is None:
1148 if skipall is None:
1137 h.pretty(ui)
1149 h.pretty(ui)
1138 msg = (_('examine changes to %s?') %
1150 msg = (_('examine changes to %s?') %
1139 _(' and ').join("'%s'" % f for f in h.files()))
1151 _(' and ').join("'%s'" % f for f in h.files()))
1140 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1152 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1141 if not r:
1153 if not r:
1142 continue
1154 continue
1143 applied[h.filename()] = [h]
1155 applied[h.filename()] = [h]
1144 if h.allhunks():
1156 if h.allhunks():
1145 applied[h.filename()] += h.hunks
1157 applied[h.filename()] += h.hunks
1146 continue
1158 continue
1147 for i, chunk in enumerate(h.hunks):
1159 for i, chunk in enumerate(h.hunks):
1148 if skipfile is None and skipall is None:
1160 if skipfile is None and skipall is None:
1149 chunk.pretty(ui)
1161 chunk.pretty(ui)
1150 if total == 1:
1162 if total == 1:
1151 msg = messages['single'] % chunk.filename()
1163 msg = messages['single'] % chunk.filename()
1152 else:
1164 else:
1153 idx = pos - len(h.hunks) + i
1165 idx = pos - len(h.hunks) + i
1154 msg = messages['multiple'] % (idx, total, chunk.filename())
1166 msg = messages['multiple'] % (idx, total, chunk.filename())
1155 r, skipfile, skipall, newpatches = prompt(skipfile,
1167 r, skipfile, skipall, newpatches = prompt(skipfile,
1156 skipall, msg, chunk)
1168 skipall, msg, chunk)
1157 if r:
1169 if r:
1158 if fixoffset:
1170 if fixoffset:
1159 chunk = copy.copy(chunk)
1171 chunk = copy.copy(chunk)
1160 chunk.toline += fixoffset
1172 chunk.toline += fixoffset
1161 applied[chunk.filename()].append(chunk)
1173 applied[chunk.filename()].append(chunk)
1162 elif newpatches is not None:
1174 elif newpatches is not None:
1163 for newpatch in newpatches:
1175 for newpatch in newpatches:
1164 for newhunk in newpatch.hunks:
1176 for newhunk in newpatch.hunks:
1165 if fixoffset:
1177 if fixoffset:
1166 newhunk.toline += fixoffset
1178 newhunk.toline += fixoffset
1167 applied[newhunk.filename()].append(newhunk)
1179 applied[newhunk.filename()].append(newhunk)
1168 else:
1180 else:
1169 fixoffset += chunk.removed - chunk.added
1181 fixoffset += chunk.removed - chunk.added
1170 return (sum([h for h in applied.itervalues()
1182 return (sum([h for h in applied.itervalues()
1171 if h[0].special() or len(h) > 1], []), {})
1183 if h[0].special() or len(h) > 1], []), {})
1172 class hunk(object):
1184 class hunk(object):
1173 def __init__(self, desc, num, lr, context):
1185 def __init__(self, desc, num, lr, context):
1174 self.number = num
1186 self.number = num
1175 self.desc = desc
1187 self.desc = desc
1176 self.hunk = [desc]
1188 self.hunk = [desc]
1177 self.a = []
1189 self.a = []
1178 self.b = []
1190 self.b = []
1179 self.starta = self.lena = None
1191 self.starta = self.lena = None
1180 self.startb = self.lenb = None
1192 self.startb = self.lenb = None
1181 if lr is not None:
1193 if lr is not None:
1182 if context:
1194 if context:
1183 self.read_context_hunk(lr)
1195 self.read_context_hunk(lr)
1184 else:
1196 else:
1185 self.read_unified_hunk(lr)
1197 self.read_unified_hunk(lr)
1186
1198
1187 def getnormalized(self):
1199 def getnormalized(self):
1188 """Return a copy with line endings normalized to LF."""
1200 """Return a copy with line endings normalized to LF."""
1189
1201
1190 def normalize(lines):
1202 def normalize(lines):
1191 nlines = []
1203 nlines = []
1192 for line in lines:
1204 for line in lines:
1193 if line.endswith('\r\n'):
1205 if line.endswith('\r\n'):
1194 line = line[:-2] + '\n'
1206 line = line[:-2] + '\n'
1195 nlines.append(line)
1207 nlines.append(line)
1196 return nlines
1208 return nlines
1197
1209
1198 # Dummy object, it is rebuilt manually
1210 # Dummy object, it is rebuilt manually
1199 nh = hunk(self.desc, self.number, None, None)
1211 nh = hunk(self.desc, self.number, None, None)
1200 nh.number = self.number
1212 nh.number = self.number
1201 nh.desc = self.desc
1213 nh.desc = self.desc
1202 nh.hunk = self.hunk
1214 nh.hunk = self.hunk
1203 nh.a = normalize(self.a)
1215 nh.a = normalize(self.a)
1204 nh.b = normalize(self.b)
1216 nh.b = normalize(self.b)
1205 nh.starta = self.starta
1217 nh.starta = self.starta
1206 nh.startb = self.startb
1218 nh.startb = self.startb
1207 nh.lena = self.lena
1219 nh.lena = self.lena
1208 nh.lenb = self.lenb
1220 nh.lenb = self.lenb
1209 return nh
1221 return nh
1210
1222
1211 def read_unified_hunk(self, lr):
1223 def read_unified_hunk(self, lr):
1212 m = unidesc.match(self.desc)
1224 m = unidesc.match(self.desc)
1213 if not m:
1225 if not m:
1214 raise PatchError(_("bad hunk #%d") % self.number)
1226 raise PatchError(_("bad hunk #%d") % self.number)
1215 self.starta, self.lena, self.startb, self.lenb = m.groups()
1227 self.starta, self.lena, self.startb, self.lenb = m.groups()
1216 if self.lena is None:
1228 if self.lena is None:
1217 self.lena = 1
1229 self.lena = 1
1218 else:
1230 else:
1219 self.lena = int(self.lena)
1231 self.lena = int(self.lena)
1220 if self.lenb is None:
1232 if self.lenb is None:
1221 self.lenb = 1
1233 self.lenb = 1
1222 else:
1234 else:
1223 self.lenb = int(self.lenb)
1235 self.lenb = int(self.lenb)
1224 self.starta = int(self.starta)
1236 self.starta = int(self.starta)
1225 self.startb = int(self.startb)
1237 self.startb = int(self.startb)
1226 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
1238 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
1227 self.b)
1239 self.b)
1228 # if we hit eof before finishing out the hunk, the last line will
1240 # if we hit eof before finishing out the hunk, the last line will
1229 # be zero length. Lets try to fix it up.
1241 # be zero length. Lets try to fix it up.
1230 while len(self.hunk[-1]) == 0:
1242 while len(self.hunk[-1]) == 0:
1231 del self.hunk[-1]
1243 del self.hunk[-1]
1232 del self.a[-1]
1244 del self.a[-1]
1233 del self.b[-1]
1245 del self.b[-1]
1234 self.lena -= 1
1246 self.lena -= 1
1235 self.lenb -= 1
1247 self.lenb -= 1
1236 self._fixnewline(lr)
1248 self._fixnewline(lr)
1237
1249
1238 def read_context_hunk(self, lr):
1250 def read_context_hunk(self, lr):
1239 self.desc = lr.readline()
1251 self.desc = lr.readline()
1240 m = contextdesc.match(self.desc)
1252 m = contextdesc.match(self.desc)
1241 if not m:
1253 if not m:
1242 raise PatchError(_("bad hunk #%d") % self.number)
1254 raise PatchError(_("bad hunk #%d") % self.number)
1243 self.starta, aend = m.groups()
1255 self.starta, aend = m.groups()
1244 self.starta = int(self.starta)
1256 self.starta = int(self.starta)
1245 if aend is None:
1257 if aend is None:
1246 aend = self.starta
1258 aend = self.starta
1247 self.lena = int(aend) - self.starta
1259 self.lena = int(aend) - self.starta
1248 if self.starta:
1260 if self.starta:
1249 self.lena += 1
1261 self.lena += 1
1250 for x in xrange(self.lena):
1262 for x in xrange(self.lena):
1251 l = lr.readline()
1263 l = lr.readline()
1252 if l.startswith('---'):
1264 if l.startswith('---'):
1253 # lines addition, old block is empty
1265 # lines addition, old block is empty
1254 lr.push(l)
1266 lr.push(l)
1255 break
1267 break
1256 s = l[2:]
1268 s = l[2:]
1257 if l.startswith('- ') or l.startswith('! '):
1269 if l.startswith('- ') or l.startswith('! '):
1258 u = '-' + s
1270 u = '-' + s
1259 elif l.startswith(' '):
1271 elif l.startswith(' '):
1260 u = ' ' + s
1272 u = ' ' + s
1261 else:
1273 else:
1262 raise PatchError(_("bad hunk #%d old text line %d") %
1274 raise PatchError(_("bad hunk #%d old text line %d") %
1263 (self.number, x))
1275 (self.number, x))
1264 self.a.append(u)
1276 self.a.append(u)
1265 self.hunk.append(u)
1277 self.hunk.append(u)
1266
1278
1267 l = lr.readline()
1279 l = lr.readline()
1268 if l.startswith('\ '):
1280 if l.startswith('\ '):
1269 s = self.a[-1][:-1]
1281 s = self.a[-1][:-1]
1270 self.a[-1] = s
1282 self.a[-1] = s
1271 self.hunk[-1] = s
1283 self.hunk[-1] = s
1272 l = lr.readline()
1284 l = lr.readline()
1273 m = contextdesc.match(l)
1285 m = contextdesc.match(l)
1274 if not m:
1286 if not m:
1275 raise PatchError(_("bad hunk #%d") % self.number)
1287 raise PatchError(_("bad hunk #%d") % self.number)
1276 self.startb, bend = m.groups()
1288 self.startb, bend = m.groups()
1277 self.startb = int(self.startb)
1289 self.startb = int(self.startb)
1278 if bend is None:
1290 if bend is None:
1279 bend = self.startb
1291 bend = self.startb
1280 self.lenb = int(bend) - self.startb
1292 self.lenb = int(bend) - self.startb
1281 if self.startb:
1293 if self.startb:
1282 self.lenb += 1
1294 self.lenb += 1
1283 hunki = 1
1295 hunki = 1
1284 for x in xrange(self.lenb):
1296 for x in xrange(self.lenb):
1285 l = lr.readline()
1297 l = lr.readline()
1286 if l.startswith('\ '):
1298 if l.startswith('\ '):
1287 # XXX: the only way to hit this is with an invalid line range.
1299 # XXX: the only way to hit this is with an invalid line range.
1288 # The no-eol marker is not counted in the line range, but I
1300 # The no-eol marker is not counted in the line range, but I
1289 # guess there are diff(1) out there which behave differently.
1301 # guess there are diff(1) out there which behave differently.
1290 s = self.b[-1][:-1]
1302 s = self.b[-1][:-1]
1291 self.b[-1] = s
1303 self.b[-1] = s
1292 self.hunk[hunki - 1] = s
1304 self.hunk[hunki - 1] = s
1293 continue
1305 continue
1294 if not l:
1306 if not l:
1295 # line deletions, new block is empty and we hit EOF
1307 # line deletions, new block is empty and we hit EOF
1296 lr.push(l)
1308 lr.push(l)
1297 break
1309 break
1298 s = l[2:]
1310 s = l[2:]
1299 if l.startswith('+ ') or l.startswith('! '):
1311 if l.startswith('+ ') or l.startswith('! '):
1300 u = '+' + s
1312 u = '+' + s
1301 elif l.startswith(' '):
1313 elif l.startswith(' '):
1302 u = ' ' + s
1314 u = ' ' + s
1303 elif len(self.b) == 0:
1315 elif len(self.b) == 0:
1304 # line deletions, new block is empty
1316 # line deletions, new block is empty
1305 lr.push(l)
1317 lr.push(l)
1306 break
1318 break
1307 else:
1319 else:
1308 raise PatchError(_("bad hunk #%d old text line %d") %
1320 raise PatchError(_("bad hunk #%d old text line %d") %
1309 (self.number, x))
1321 (self.number, x))
1310 self.b.append(s)
1322 self.b.append(s)
1311 while True:
1323 while True:
1312 if hunki >= len(self.hunk):
1324 if hunki >= len(self.hunk):
1313 h = ""
1325 h = ""
1314 else:
1326 else:
1315 h = self.hunk[hunki]
1327 h = self.hunk[hunki]
1316 hunki += 1
1328 hunki += 1
1317 if h == u:
1329 if h == u:
1318 break
1330 break
1319 elif h.startswith('-'):
1331 elif h.startswith('-'):
1320 continue
1332 continue
1321 else:
1333 else:
1322 self.hunk.insert(hunki - 1, u)
1334 self.hunk.insert(hunki - 1, u)
1323 break
1335 break
1324
1336
1325 if not self.a:
1337 if not self.a:
1326 # this happens when lines were only added to the hunk
1338 # this happens when lines were only added to the hunk
1327 for x in self.hunk:
1339 for x in self.hunk:
1328 if x.startswith('-') or x.startswith(' '):
1340 if x.startswith('-') or x.startswith(' '):
1329 self.a.append(x)
1341 self.a.append(x)
1330 if not self.b:
1342 if not self.b:
1331 # this happens when lines were only deleted from the hunk
1343 # this happens when lines were only deleted from the hunk
1332 for x in self.hunk:
1344 for x in self.hunk:
1333 if x.startswith('+') or x.startswith(' '):
1345 if x.startswith('+') or x.startswith(' '):
1334 self.b.append(x[1:])
1346 self.b.append(x[1:])
1335 # @@ -start,len +start,len @@
1347 # @@ -start,len +start,len @@
1336 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
1348 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
1337 self.startb, self.lenb)
1349 self.startb, self.lenb)
1338 self.hunk[0] = self.desc
1350 self.hunk[0] = self.desc
1339 self._fixnewline(lr)
1351 self._fixnewline(lr)
1340
1352
1341 def _fixnewline(self, lr):
1353 def _fixnewline(self, lr):
1342 l = lr.readline()
1354 l = lr.readline()
1343 if l.startswith('\ '):
1355 if l.startswith('\ '):
1344 diffhelpers.fix_newline(self.hunk, self.a, self.b)
1356 diffhelpers.fix_newline(self.hunk, self.a, self.b)
1345 else:
1357 else:
1346 lr.push(l)
1358 lr.push(l)
1347
1359
1348 def complete(self):
1360 def complete(self):
1349 return len(self.a) == self.lena and len(self.b) == self.lenb
1361 return len(self.a) == self.lena and len(self.b) == self.lenb
1350
1362
1351 def _fuzzit(self, old, new, fuzz, toponly):
1363 def _fuzzit(self, old, new, fuzz, toponly):
1352 # this removes context lines from the top and bottom of list 'l'. It
1364 # this removes context lines from the top and bottom of list 'l'. It
1353 # checks the hunk to make sure only context lines are removed, and then
1365 # checks the hunk to make sure only context lines are removed, and then
1354 # returns a new shortened list of lines.
1366 # returns a new shortened list of lines.
1355 fuzz = min(fuzz, len(old))
1367 fuzz = min(fuzz, len(old))
1356 if fuzz:
1368 if fuzz:
1357 top = 0
1369 top = 0
1358 bot = 0
1370 bot = 0
1359 hlen = len(self.hunk)
1371 hlen = len(self.hunk)
1360 for x in xrange(hlen - 1):
1372 for x in xrange(hlen - 1):
1361 # the hunk starts with the @@ line, so use x+1
1373 # the hunk starts with the @@ line, so use x+1
1362 if self.hunk[x + 1][0] == ' ':
1374 if self.hunk[x + 1][0] == ' ':
1363 top += 1
1375 top += 1
1364 else:
1376 else:
1365 break
1377 break
1366 if not toponly:
1378 if not toponly:
1367 for x in xrange(hlen - 1):
1379 for x in xrange(hlen - 1):
1368 if self.hunk[hlen - bot - 1][0] == ' ':
1380 if self.hunk[hlen - bot - 1][0] == ' ':
1369 bot += 1
1381 bot += 1
1370 else:
1382 else:
1371 break
1383 break
1372
1384
1373 bot = min(fuzz, bot)
1385 bot = min(fuzz, bot)
1374 top = min(fuzz, top)
1386 top = min(fuzz, top)
1375 return old[top:len(old) - bot], new[top:len(new) - bot], top
1387 return old[top:len(old) - bot], new[top:len(new) - bot], top
1376 return old, new, 0
1388 return old, new, 0
1377
1389
1378 def fuzzit(self, fuzz, toponly):
1390 def fuzzit(self, fuzz, toponly):
1379 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1391 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1380 oldstart = self.starta + top
1392 oldstart = self.starta + top
1381 newstart = self.startb + top
1393 newstart = self.startb + top
1382 # zero length hunk ranges already have their start decremented
1394 # zero length hunk ranges already have their start decremented
1383 if self.lena and oldstart > 0:
1395 if self.lena and oldstart > 0:
1384 oldstart -= 1
1396 oldstart -= 1
1385 if self.lenb and newstart > 0:
1397 if self.lenb and newstart > 0:
1386 newstart -= 1
1398 newstart -= 1
1387 return old, oldstart, new, newstart
1399 return old, oldstart, new, newstart
1388
1400
1389 class binhunk(object):
1401 class binhunk(object):
1390 'A binary patch file.'
1402 'A binary patch file.'
1391 def __init__(self, lr, fname):
1403 def __init__(self, lr, fname):
1392 self.text = None
1404 self.text = None
1393 self.delta = False
1405 self.delta = False
1394 self.hunk = ['GIT binary patch\n']
1406 self.hunk = ['GIT binary patch\n']
1395 self._fname = fname
1407 self._fname = fname
1396 self._read(lr)
1408 self._read(lr)
1397
1409
1398 def complete(self):
1410 def complete(self):
1399 return self.text is not None
1411 return self.text is not None
1400
1412
1401 def new(self, lines):
1413 def new(self, lines):
1402 if self.delta:
1414 if self.delta:
1403 return [applybindelta(self.text, ''.join(lines))]
1415 return [applybindelta(self.text, ''.join(lines))]
1404 return [self.text]
1416 return [self.text]
1405
1417
1406 def _read(self, lr):
1418 def _read(self, lr):
1407 def getline(lr, hunk):
1419 def getline(lr, hunk):
1408 l = lr.readline()
1420 l = lr.readline()
1409 hunk.append(l)
1421 hunk.append(l)
1410 return l.rstrip('\r\n')
1422 return l.rstrip('\r\n')
1411
1423
1412 size = 0
1424 size = 0
1413 while True:
1425 while True:
1414 line = getline(lr, self.hunk)
1426 line = getline(lr, self.hunk)
1415 if not line:
1427 if not line:
1416 raise PatchError(_('could not extract "%s" binary data')
1428 raise PatchError(_('could not extract "%s" binary data')
1417 % self._fname)
1429 % self._fname)
1418 if line.startswith('literal '):
1430 if line.startswith('literal '):
1419 size = int(line[8:].rstrip())
1431 size = int(line[8:].rstrip())
1420 break
1432 break
1421 if line.startswith('delta '):
1433 if line.startswith('delta '):
1422 size = int(line[6:].rstrip())
1434 size = int(line[6:].rstrip())
1423 self.delta = True
1435 self.delta = True
1424 break
1436 break
1425 dec = []
1437 dec = []
1426 line = getline(lr, self.hunk)
1438 line = getline(lr, self.hunk)
1427 while len(line) > 1:
1439 while len(line) > 1:
1428 l = line[0]
1440 l = line[0]
1429 if l <= 'Z' and l >= 'A':
1441 if l <= 'Z' and l >= 'A':
1430 l = ord(l) - ord('A') + 1
1442 l = ord(l) - ord('A') + 1
1431 else:
1443 else:
1432 l = ord(l) - ord('a') + 27
1444 l = ord(l) - ord('a') + 27
1433 try:
1445 try:
1434 dec.append(util.b85decode(line[1:])[:l])
1446 dec.append(util.b85decode(line[1:])[:l])
1435 except ValueError as e:
1447 except ValueError as e:
1436 raise PatchError(_('could not decode "%s" binary patch: %s')
1448 raise PatchError(_('could not decode "%s" binary patch: %s')
1437 % (self._fname, str(e)))
1449 % (self._fname, str(e)))
1438 line = getline(lr, self.hunk)
1450 line = getline(lr, self.hunk)
1439 text = zlib.decompress(''.join(dec))
1451 text = zlib.decompress(''.join(dec))
1440 if len(text) != size:
1452 if len(text) != size:
1441 raise PatchError(_('"%s" length is %d bytes, should be %d')
1453 raise PatchError(_('"%s" length is %d bytes, should be %d')
1442 % (self._fname, len(text), size))
1454 % (self._fname, len(text), size))
1443 self.text = text
1455 self.text = text
1444
1456
1445 def parsefilename(str):
1457 def parsefilename(str):
1446 # --- filename \t|space stuff
1458 # --- filename \t|space stuff
1447 s = str[4:].rstrip('\r\n')
1459 s = str[4:].rstrip('\r\n')
1448 i = s.find('\t')
1460 i = s.find('\t')
1449 if i < 0:
1461 if i < 0:
1450 i = s.find(' ')
1462 i = s.find(' ')
1451 if i < 0:
1463 if i < 0:
1452 return s
1464 return s
1453 return s[:i]
1465 return s[:i]
1454
1466
1455 def reversehunks(hunks):
1467 def reversehunks(hunks):
1456 '''reverse the signs in the hunks given as argument
1468 '''reverse the signs in the hunks given as argument
1457
1469
1458 This function operates on hunks coming out of patch.filterpatch, that is
1470 This function operates on hunks coming out of patch.filterpatch, that is
1459 a list of the form: [header1, hunk1, hunk2, header2...]. Example usage:
1471 a list of the form: [header1, hunk1, hunk2, header2...]. Example usage:
1460
1472
1461 >>> rawpatch = """diff --git a/folder1/g b/folder1/g
1473 >>> rawpatch = """diff --git a/folder1/g b/folder1/g
1462 ... --- a/folder1/g
1474 ... --- a/folder1/g
1463 ... +++ b/folder1/g
1475 ... +++ b/folder1/g
1464 ... @@ -1,7 +1,7 @@
1476 ... @@ -1,7 +1,7 @@
1465 ... +firstline
1477 ... +firstline
1466 ... c
1478 ... c
1467 ... 1
1479 ... 1
1468 ... 2
1480 ... 2
1469 ... + 3
1481 ... + 3
1470 ... -4
1482 ... -4
1471 ... 5
1483 ... 5
1472 ... d
1484 ... d
1473 ... +lastline"""
1485 ... +lastline"""
1474 >>> hunks = parsepatch(rawpatch)
1486 >>> hunks = parsepatch(rawpatch)
1475 >>> hunkscomingfromfilterpatch = []
1487 >>> hunkscomingfromfilterpatch = []
1476 >>> for h in hunks:
1488 >>> for h in hunks:
1477 ... hunkscomingfromfilterpatch.append(h)
1489 ... hunkscomingfromfilterpatch.append(h)
1478 ... hunkscomingfromfilterpatch.extend(h.hunks)
1490 ... hunkscomingfromfilterpatch.extend(h.hunks)
1479
1491
1480 >>> reversedhunks = reversehunks(hunkscomingfromfilterpatch)
1492 >>> reversedhunks = reversehunks(hunkscomingfromfilterpatch)
1481 >>> from . import util
1493 >>> from . import util
1482 >>> fp = util.stringio()
1494 >>> fp = util.stringio()
1483 >>> for c in reversedhunks:
1495 >>> for c in reversedhunks:
1484 ... c.write(fp)
1496 ... c.write(fp)
1485 >>> fp.seek(0)
1497 >>> fp.seek(0)
1486 >>> reversedpatch = fp.read()
1498 >>> reversedpatch = fp.read()
1487 >>> print reversedpatch
1499 >>> print reversedpatch
1488 diff --git a/folder1/g b/folder1/g
1500 diff --git a/folder1/g b/folder1/g
1489 --- a/folder1/g
1501 --- a/folder1/g
1490 +++ b/folder1/g
1502 +++ b/folder1/g
1491 @@ -1,4 +1,3 @@
1503 @@ -1,4 +1,3 @@
1492 -firstline
1504 -firstline
1493 c
1505 c
1494 1
1506 1
1495 2
1507 2
1496 @@ -1,6 +2,6 @@
1508 @@ -2,6 +1,6 @@
1497 c
1509 c
1498 1
1510 1
1499 2
1511 2
1500 - 3
1512 - 3
1501 +4
1513 +4
1502 5
1514 5
1503 d
1515 d
1504 @@ -5,3 +6,2 @@
1516 @@ -6,3 +5,2 @@
1505 5
1517 5
1506 d
1518 d
1507 -lastline
1519 -lastline
1508
1520
1509 '''
1521 '''
1510
1522
1511 from . import crecord as crecordmod
1512 newhunks = []
1523 newhunks = []
1513 for c in hunks:
1524 for c in hunks:
1514 if isinstance(c, crecordmod.uihunk):
1525 if util.safehasattr(c, 'reversehunk'):
1515 # curses hunks encapsulate the record hunk in _hunk
1526 c = c.reversehunk()
1516 c = c._hunk
1517 if isinstance(c, recordhunk):
1518 for j, line in enumerate(c.hunk):
1519 if line.startswith("-"):
1520 c.hunk[j] = "+" + c.hunk[j][1:]
1521 elif line.startswith("+"):
1522 c.hunk[j] = "-" + c.hunk[j][1:]
1523 c.added, c.removed = c.removed, c.added
1524 newhunks.append(c)
1527 newhunks.append(c)
1525 return newhunks
1528 return newhunks
1526
1529
1527 def parsepatch(originalchunks):
1530 def parsepatch(originalchunks):
1528 """patch -> [] of headers -> [] of hunks """
1531 """patch -> [] of headers -> [] of hunks """
1529 class parser(object):
1532 class parser(object):
1530 """patch parsing state machine"""
1533 """patch parsing state machine"""
1531 def __init__(self):
1534 def __init__(self):
1532 self.fromline = 0
1535 self.fromline = 0
1533 self.toline = 0
1536 self.toline = 0
1534 self.proc = ''
1537 self.proc = ''
1535 self.header = None
1538 self.header = None
1536 self.context = []
1539 self.context = []
1537 self.before = []
1540 self.before = []
1538 self.hunk = []
1541 self.hunk = []
1539 self.headers = []
1542 self.headers = []
1540
1543
1541 def addrange(self, limits):
1544 def addrange(self, limits):
1542 fromstart, fromend, tostart, toend, proc = limits
1545 fromstart, fromend, tostart, toend, proc = limits
1543 self.fromline = int(fromstart)
1546 self.fromline = int(fromstart)
1544 self.toline = int(tostart)
1547 self.toline = int(tostart)
1545 self.proc = proc
1548 self.proc = proc
1546
1549
1547 def addcontext(self, context):
1550 def addcontext(self, context):
1548 if self.hunk:
1551 if self.hunk:
1549 h = recordhunk(self.header, self.fromline, self.toline,
1552 h = recordhunk(self.header, self.fromline, self.toline,
1550 self.proc, self.before, self.hunk, context)
1553 self.proc, self.before, self.hunk, context)
1551 self.header.hunks.append(h)
1554 self.header.hunks.append(h)
1552 self.fromline += len(self.before) + h.removed
1555 self.fromline += len(self.before) + h.removed
1553 self.toline += len(self.before) + h.added
1556 self.toline += len(self.before) + h.added
1554 self.before = []
1557 self.before = []
1555 self.hunk = []
1558 self.hunk = []
1556 self.context = context
1559 self.context = context
1557
1560
1558 def addhunk(self, hunk):
1561 def addhunk(self, hunk):
1559 if self.context:
1562 if self.context:
1560 self.before = self.context
1563 self.before = self.context
1561 self.context = []
1564 self.context = []
1562 self.hunk = hunk
1565 self.hunk = hunk
1563
1566
1564 def newfile(self, hdr):
1567 def newfile(self, hdr):
1565 self.addcontext([])
1568 self.addcontext([])
1566 h = header(hdr)
1569 h = header(hdr)
1567 self.headers.append(h)
1570 self.headers.append(h)
1568 self.header = h
1571 self.header = h
1569
1572
1570 def addother(self, line):
1573 def addother(self, line):
1571 pass # 'other' lines are ignored
1574 pass # 'other' lines are ignored
1572
1575
1573 def finished(self):
1576 def finished(self):
1574 self.addcontext([])
1577 self.addcontext([])
1575 return self.headers
1578 return self.headers
1576
1579
1577 transitions = {
1580 transitions = {
1578 'file': {'context': addcontext,
1581 'file': {'context': addcontext,
1579 'file': newfile,
1582 'file': newfile,
1580 'hunk': addhunk,
1583 'hunk': addhunk,
1581 'range': addrange},
1584 'range': addrange},
1582 'context': {'file': newfile,
1585 'context': {'file': newfile,
1583 'hunk': addhunk,
1586 'hunk': addhunk,
1584 'range': addrange,
1587 'range': addrange,
1585 'other': addother},
1588 'other': addother},
1586 'hunk': {'context': addcontext,
1589 'hunk': {'context': addcontext,
1587 'file': newfile,
1590 'file': newfile,
1588 'range': addrange},
1591 'range': addrange},
1589 'range': {'context': addcontext,
1592 'range': {'context': addcontext,
1590 'hunk': addhunk},
1593 'hunk': addhunk},
1591 'other': {'other': addother},
1594 'other': {'other': addother},
1592 }
1595 }
1593
1596
1594 p = parser()
1597 p = parser()
1595 fp = stringio()
1598 fp = stringio()
1596 fp.write(''.join(originalchunks))
1599 fp.write(''.join(originalchunks))
1597 fp.seek(0)
1600 fp.seek(0)
1598
1601
1599 state = 'context'
1602 state = 'context'
1600 for newstate, data in scanpatch(fp):
1603 for newstate, data in scanpatch(fp):
1601 try:
1604 try:
1602 p.transitions[state][newstate](p, data)
1605 p.transitions[state][newstate](p, data)
1603 except KeyError:
1606 except KeyError:
1604 raise PatchError('unhandled transition: %s -> %s' %
1607 raise PatchError('unhandled transition: %s -> %s' %
1605 (state, newstate))
1608 (state, newstate))
1606 state = newstate
1609 state = newstate
1607 del fp
1610 del fp
1608 return p.finished()
1611 return p.finished()
1609
1612
1610 def pathtransform(path, strip, prefix):
1613 def pathtransform(path, strip, prefix):
1611 '''turn a path from a patch into a path suitable for the repository
1614 '''turn a path from a patch into a path suitable for the repository
1612
1615
1613 prefix, if not empty, is expected to be normalized with a / at the end.
1616 prefix, if not empty, is expected to be normalized with a / at the end.
1614
1617
1615 Returns (stripped components, path in repository).
1618 Returns (stripped components, path in repository).
1616
1619
1617 >>> pathtransform('a/b/c', 0, '')
1620 >>> pathtransform('a/b/c', 0, '')
1618 ('', 'a/b/c')
1621 ('', 'a/b/c')
1619 >>> pathtransform(' a/b/c ', 0, '')
1622 >>> pathtransform(' a/b/c ', 0, '')
1620 ('', ' a/b/c')
1623 ('', ' a/b/c')
1621 >>> pathtransform(' a/b/c ', 2, '')
1624 >>> pathtransform(' a/b/c ', 2, '')
1622 ('a/b/', 'c')
1625 ('a/b/', 'c')
1623 >>> pathtransform('a/b/c', 0, 'd/e/')
1626 >>> pathtransform('a/b/c', 0, 'd/e/')
1624 ('', 'd/e/a/b/c')
1627 ('', 'd/e/a/b/c')
1625 >>> pathtransform(' a//b/c ', 2, 'd/e/')
1628 >>> pathtransform(' a//b/c ', 2, 'd/e/')
1626 ('a//b/', 'd/e/c')
1629 ('a//b/', 'd/e/c')
1627 >>> pathtransform('a/b/c', 3, '')
1630 >>> pathtransform('a/b/c', 3, '')
1628 Traceback (most recent call last):
1631 Traceback (most recent call last):
1629 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1632 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1630 '''
1633 '''
1631 pathlen = len(path)
1634 pathlen = len(path)
1632 i = 0
1635 i = 0
1633 if strip == 0:
1636 if strip == 0:
1634 return '', prefix + path.rstrip()
1637 return '', prefix + path.rstrip()
1635 count = strip
1638 count = strip
1636 while count > 0:
1639 while count > 0:
1637 i = path.find('/', i)
1640 i = path.find('/', i)
1638 if i == -1:
1641 if i == -1:
1639 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1642 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1640 (count, strip, path))
1643 (count, strip, path))
1641 i += 1
1644 i += 1
1642 # consume '//' in the path
1645 # consume '//' in the path
1643 while i < pathlen - 1 and path[i] == '/':
1646 while i < pathlen - 1 and path[i] == '/':
1644 i += 1
1647 i += 1
1645 count -= 1
1648 count -= 1
1646 return path[:i].lstrip(), prefix + path[i:].rstrip()
1649 return path[:i].lstrip(), prefix + path[i:].rstrip()
1647
1650
1648 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1651 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1649 nulla = afile_orig == "/dev/null"
1652 nulla = afile_orig == "/dev/null"
1650 nullb = bfile_orig == "/dev/null"
1653 nullb = bfile_orig == "/dev/null"
1651 create = nulla and hunk.starta == 0 and hunk.lena == 0
1654 create = nulla and hunk.starta == 0 and hunk.lena == 0
1652 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1655 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1653 abase, afile = pathtransform(afile_orig, strip, prefix)
1656 abase, afile = pathtransform(afile_orig, strip, prefix)
1654 gooda = not nulla and backend.exists(afile)
1657 gooda = not nulla and backend.exists(afile)
1655 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1658 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1656 if afile == bfile:
1659 if afile == bfile:
1657 goodb = gooda
1660 goodb = gooda
1658 else:
1661 else:
1659 goodb = not nullb and backend.exists(bfile)
1662 goodb = not nullb and backend.exists(bfile)
1660 missing = not goodb and not gooda and not create
1663 missing = not goodb and not gooda and not create
1661
1664
1662 # some diff programs apparently produce patches where the afile is
1665 # some diff programs apparently produce patches where the afile is
1663 # not /dev/null, but afile starts with bfile
1666 # not /dev/null, but afile starts with bfile
1664 abasedir = afile[:afile.rfind('/') + 1]
1667 abasedir = afile[:afile.rfind('/') + 1]
1665 bbasedir = bfile[:bfile.rfind('/') + 1]
1668 bbasedir = bfile[:bfile.rfind('/') + 1]
1666 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1669 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1667 and hunk.starta == 0 and hunk.lena == 0):
1670 and hunk.starta == 0 and hunk.lena == 0):
1668 create = True
1671 create = True
1669 missing = False
1672 missing = False
1670
1673
1671 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1674 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1672 # diff is between a file and its backup. In this case, the original
1675 # diff is between a file and its backup. In this case, the original
1673 # file should be patched (see original mpatch code).
1676 # file should be patched (see original mpatch code).
1674 isbackup = (abase == bbase and bfile.startswith(afile))
1677 isbackup = (abase == bbase and bfile.startswith(afile))
1675 fname = None
1678 fname = None
1676 if not missing:
1679 if not missing:
1677 if gooda and goodb:
1680 if gooda and goodb:
1678 if isbackup:
1681 if isbackup:
1679 fname = afile
1682 fname = afile
1680 else:
1683 else:
1681 fname = bfile
1684 fname = bfile
1682 elif gooda:
1685 elif gooda:
1683 fname = afile
1686 fname = afile
1684
1687
1685 if not fname:
1688 if not fname:
1686 if not nullb:
1689 if not nullb:
1687 if isbackup:
1690 if isbackup:
1688 fname = afile
1691 fname = afile
1689 else:
1692 else:
1690 fname = bfile
1693 fname = bfile
1691 elif not nulla:
1694 elif not nulla:
1692 fname = afile
1695 fname = afile
1693 else:
1696 else:
1694 raise PatchError(_("undefined source and destination files"))
1697 raise PatchError(_("undefined source and destination files"))
1695
1698
1696 gp = patchmeta(fname)
1699 gp = patchmeta(fname)
1697 if create:
1700 if create:
1698 gp.op = 'ADD'
1701 gp.op = 'ADD'
1699 elif remove:
1702 elif remove:
1700 gp.op = 'DELETE'
1703 gp.op = 'DELETE'
1701 return gp
1704 return gp
1702
1705
1703 def scanpatch(fp):
1706 def scanpatch(fp):
1704 """like patch.iterhunks, but yield different events
1707 """like patch.iterhunks, but yield different events
1705
1708
1706 - ('file', [header_lines + fromfile + tofile])
1709 - ('file', [header_lines + fromfile + tofile])
1707 - ('context', [context_lines])
1710 - ('context', [context_lines])
1708 - ('hunk', [hunk_lines])
1711 - ('hunk', [hunk_lines])
1709 - ('range', (-start,len, +start,len, proc))
1712 - ('range', (-start,len, +start,len, proc))
1710 """
1713 """
1711 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1714 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1712 lr = linereader(fp)
1715 lr = linereader(fp)
1713
1716
1714 def scanwhile(first, p):
1717 def scanwhile(first, p):
1715 """scan lr while predicate holds"""
1718 """scan lr while predicate holds"""
1716 lines = [first]
1719 lines = [first]
1717 for line in iter(lr.readline, ''):
1720 for line in iter(lr.readline, ''):
1718 if p(line):
1721 if p(line):
1719 lines.append(line)
1722 lines.append(line)
1720 else:
1723 else:
1721 lr.push(line)
1724 lr.push(line)
1722 break
1725 break
1723 return lines
1726 return lines
1724
1727
1725 for line in iter(lr.readline, ''):
1728 for line in iter(lr.readline, ''):
1726 if line.startswith('diff --git a/') or line.startswith('diff -r '):
1729 if line.startswith('diff --git a/') or line.startswith('diff -r '):
1727 def notheader(line):
1730 def notheader(line):
1728 s = line.split(None, 1)
1731 s = line.split(None, 1)
1729 return not s or s[0] not in ('---', 'diff')
1732 return not s or s[0] not in ('---', 'diff')
1730 header = scanwhile(line, notheader)
1733 header = scanwhile(line, notheader)
1731 fromfile = lr.readline()
1734 fromfile = lr.readline()
1732 if fromfile.startswith('---'):
1735 if fromfile.startswith('---'):
1733 tofile = lr.readline()
1736 tofile = lr.readline()
1734 header += [fromfile, tofile]
1737 header += [fromfile, tofile]
1735 else:
1738 else:
1736 lr.push(fromfile)
1739 lr.push(fromfile)
1737 yield 'file', header
1740 yield 'file', header
1738 elif line[0] == ' ':
1741 elif line[0] == ' ':
1739 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
1742 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
1740 elif line[0] in '-+':
1743 elif line[0] in '-+':
1741 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
1744 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
1742 else:
1745 else:
1743 m = lines_re.match(line)
1746 m = lines_re.match(line)
1744 if m:
1747 if m:
1745 yield 'range', m.groups()
1748 yield 'range', m.groups()
1746 else:
1749 else:
1747 yield 'other', line
1750 yield 'other', line
1748
1751
1749 def scangitpatch(lr, firstline):
1752 def scangitpatch(lr, firstline):
1750 """
1753 """
1751 Git patches can emit:
1754 Git patches can emit:
1752 - rename a to b
1755 - rename a to b
1753 - change b
1756 - change b
1754 - copy a to c
1757 - copy a to c
1755 - change c
1758 - change c
1756
1759
1757 We cannot apply this sequence as-is, the renamed 'a' could not be
1760 We cannot apply this sequence as-is, the renamed 'a' could not be
1758 found for it would have been renamed already. And we cannot copy
1761 found for it would have been renamed already. And we cannot copy
1759 from 'b' instead because 'b' would have been changed already. So
1762 from 'b' instead because 'b' would have been changed already. So
1760 we scan the git patch for copy and rename commands so we can
1763 we scan the git patch for copy and rename commands so we can
1761 perform the copies ahead of time.
1764 perform the copies ahead of time.
1762 """
1765 """
1763 pos = 0
1766 pos = 0
1764 try:
1767 try:
1765 pos = lr.fp.tell()
1768 pos = lr.fp.tell()
1766 fp = lr.fp
1769 fp = lr.fp
1767 except IOError:
1770 except IOError:
1768 fp = stringio(lr.fp.read())
1771 fp = stringio(lr.fp.read())
1769 gitlr = linereader(fp)
1772 gitlr = linereader(fp)
1770 gitlr.push(firstline)
1773 gitlr.push(firstline)
1771 gitpatches = readgitpatch(gitlr)
1774 gitpatches = readgitpatch(gitlr)
1772 fp.seek(pos)
1775 fp.seek(pos)
1773 return gitpatches
1776 return gitpatches
1774
1777
1775 def iterhunks(fp):
1778 def iterhunks(fp):
1776 """Read a patch and yield the following events:
1779 """Read a patch and yield the following events:
1777 - ("file", afile, bfile, firsthunk): select a new target file.
1780 - ("file", afile, bfile, firsthunk): select a new target file.
1778 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1781 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1779 "file" event.
1782 "file" event.
1780 - ("git", gitchanges): current diff is in git format, gitchanges
1783 - ("git", gitchanges): current diff is in git format, gitchanges
1781 maps filenames to gitpatch records. Unique event.
1784 maps filenames to gitpatch records. Unique event.
1782 """
1785 """
1783 afile = ""
1786 afile = ""
1784 bfile = ""
1787 bfile = ""
1785 state = None
1788 state = None
1786 hunknum = 0
1789 hunknum = 0
1787 emitfile = newfile = False
1790 emitfile = newfile = False
1788 gitpatches = None
1791 gitpatches = None
1789
1792
1790 # our states
1793 # our states
1791 BFILE = 1
1794 BFILE = 1
1792 context = None
1795 context = None
1793 lr = linereader(fp)
1796 lr = linereader(fp)
1794
1797
1795 for x in iter(lr.readline, ''):
1798 for x in iter(lr.readline, ''):
1796 if state == BFILE and (
1799 if state == BFILE and (
1797 (not context and x[0] == '@')
1800 (not context and x[0] == '@')
1798 or (context is not False and x.startswith('***************'))
1801 or (context is not False and x.startswith('***************'))
1799 or x.startswith('GIT binary patch')):
1802 or x.startswith('GIT binary patch')):
1800 gp = None
1803 gp = None
1801 if (gitpatches and
1804 if (gitpatches and
1802 gitpatches[-1].ispatching(afile, bfile)):
1805 gitpatches[-1].ispatching(afile, bfile)):
1803 gp = gitpatches.pop()
1806 gp = gitpatches.pop()
1804 if x.startswith('GIT binary patch'):
1807 if x.startswith('GIT binary patch'):
1805 h = binhunk(lr, gp.path)
1808 h = binhunk(lr, gp.path)
1806 else:
1809 else:
1807 if context is None and x.startswith('***************'):
1810 if context is None and x.startswith('***************'):
1808 context = True
1811 context = True
1809 h = hunk(x, hunknum + 1, lr, context)
1812 h = hunk(x, hunknum + 1, lr, context)
1810 hunknum += 1
1813 hunknum += 1
1811 if emitfile:
1814 if emitfile:
1812 emitfile = False
1815 emitfile = False
1813 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1816 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1814 yield 'hunk', h
1817 yield 'hunk', h
1815 elif x.startswith('diff --git a/'):
1818 elif x.startswith('diff --git a/'):
1816 m = gitre.match(x.rstrip(' \r\n'))
1819 m = gitre.match(x.rstrip(' \r\n'))
1817 if not m:
1820 if not m:
1818 continue
1821 continue
1819 if gitpatches is None:
1822 if gitpatches is None:
1820 # scan whole input for git metadata
1823 # scan whole input for git metadata
1821 gitpatches = scangitpatch(lr, x)
1824 gitpatches = scangitpatch(lr, x)
1822 yield 'git', [g.copy() for g in gitpatches
1825 yield 'git', [g.copy() for g in gitpatches
1823 if g.op in ('COPY', 'RENAME')]
1826 if g.op in ('COPY', 'RENAME')]
1824 gitpatches.reverse()
1827 gitpatches.reverse()
1825 afile = 'a/' + m.group(1)
1828 afile = 'a/' + m.group(1)
1826 bfile = 'b/' + m.group(2)
1829 bfile = 'b/' + m.group(2)
1827 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1830 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1828 gp = gitpatches.pop()
1831 gp = gitpatches.pop()
1829 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1832 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1830 if not gitpatches:
1833 if not gitpatches:
1831 raise PatchError(_('failed to synchronize metadata for "%s"')
1834 raise PatchError(_('failed to synchronize metadata for "%s"')
1832 % afile[2:])
1835 % afile[2:])
1833 gp = gitpatches[-1]
1836 gp = gitpatches[-1]
1834 newfile = True
1837 newfile = True
1835 elif x.startswith('---'):
1838 elif x.startswith('---'):
1836 # check for a unified diff
1839 # check for a unified diff
1837 l2 = lr.readline()
1840 l2 = lr.readline()
1838 if not l2.startswith('+++'):
1841 if not l2.startswith('+++'):
1839 lr.push(l2)
1842 lr.push(l2)
1840 continue
1843 continue
1841 newfile = True
1844 newfile = True
1842 context = False
1845 context = False
1843 afile = parsefilename(x)
1846 afile = parsefilename(x)
1844 bfile = parsefilename(l2)
1847 bfile = parsefilename(l2)
1845 elif x.startswith('***'):
1848 elif x.startswith('***'):
1846 # check for a context diff
1849 # check for a context diff
1847 l2 = lr.readline()
1850 l2 = lr.readline()
1848 if not l2.startswith('---'):
1851 if not l2.startswith('---'):
1849 lr.push(l2)
1852 lr.push(l2)
1850 continue
1853 continue
1851 l3 = lr.readline()
1854 l3 = lr.readline()
1852 lr.push(l3)
1855 lr.push(l3)
1853 if not l3.startswith("***************"):
1856 if not l3.startswith("***************"):
1854 lr.push(l2)
1857 lr.push(l2)
1855 continue
1858 continue
1856 newfile = True
1859 newfile = True
1857 context = True
1860 context = True
1858 afile = parsefilename(x)
1861 afile = parsefilename(x)
1859 bfile = parsefilename(l2)
1862 bfile = parsefilename(l2)
1860
1863
1861 if newfile:
1864 if newfile:
1862 newfile = False
1865 newfile = False
1863 emitfile = True
1866 emitfile = True
1864 state = BFILE
1867 state = BFILE
1865 hunknum = 0
1868 hunknum = 0
1866
1869
1867 while gitpatches:
1870 while gitpatches:
1868 gp = gitpatches.pop()
1871 gp = gitpatches.pop()
1869 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1872 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1870
1873
1871 def applybindelta(binchunk, data):
1874 def applybindelta(binchunk, data):
1872 """Apply a binary delta hunk
1875 """Apply a binary delta hunk
1873 The algorithm used is the algorithm from git's patch-delta.c
1876 The algorithm used is the algorithm from git's patch-delta.c
1874 """
1877 """
1875 def deltahead(binchunk):
1878 def deltahead(binchunk):
1876 i = 0
1879 i = 0
1877 for c in binchunk:
1880 for c in binchunk:
1878 i += 1
1881 i += 1
1879 if not (ord(c) & 0x80):
1882 if not (ord(c) & 0x80):
1880 return i
1883 return i
1881 return i
1884 return i
1882 out = ""
1885 out = ""
1883 s = deltahead(binchunk)
1886 s = deltahead(binchunk)
1884 binchunk = binchunk[s:]
1887 binchunk = binchunk[s:]
1885 s = deltahead(binchunk)
1888 s = deltahead(binchunk)
1886 binchunk = binchunk[s:]
1889 binchunk = binchunk[s:]
1887 i = 0
1890 i = 0
1888 while i < len(binchunk):
1891 while i < len(binchunk):
1889 cmd = ord(binchunk[i])
1892 cmd = ord(binchunk[i])
1890 i += 1
1893 i += 1
1891 if (cmd & 0x80):
1894 if (cmd & 0x80):
1892 offset = 0
1895 offset = 0
1893 size = 0
1896 size = 0
1894 if (cmd & 0x01):
1897 if (cmd & 0x01):
1895 offset = ord(binchunk[i])
1898 offset = ord(binchunk[i])
1896 i += 1
1899 i += 1
1897 if (cmd & 0x02):
1900 if (cmd & 0x02):
1898 offset |= ord(binchunk[i]) << 8
1901 offset |= ord(binchunk[i]) << 8
1899 i += 1
1902 i += 1
1900 if (cmd & 0x04):
1903 if (cmd & 0x04):
1901 offset |= ord(binchunk[i]) << 16
1904 offset |= ord(binchunk[i]) << 16
1902 i += 1
1905 i += 1
1903 if (cmd & 0x08):
1906 if (cmd & 0x08):
1904 offset |= ord(binchunk[i]) << 24
1907 offset |= ord(binchunk[i]) << 24
1905 i += 1
1908 i += 1
1906 if (cmd & 0x10):
1909 if (cmd & 0x10):
1907 size = ord(binchunk[i])
1910 size = ord(binchunk[i])
1908 i += 1
1911 i += 1
1909 if (cmd & 0x20):
1912 if (cmd & 0x20):
1910 size |= ord(binchunk[i]) << 8
1913 size |= ord(binchunk[i]) << 8
1911 i += 1
1914 i += 1
1912 if (cmd & 0x40):
1915 if (cmd & 0x40):
1913 size |= ord(binchunk[i]) << 16
1916 size |= ord(binchunk[i]) << 16
1914 i += 1
1917 i += 1
1915 if size == 0:
1918 if size == 0:
1916 size = 0x10000
1919 size = 0x10000
1917 offset_end = offset + size
1920 offset_end = offset + size
1918 out += data[offset:offset_end]
1921 out += data[offset:offset_end]
1919 elif cmd != 0:
1922 elif cmd != 0:
1920 offset_end = i + cmd
1923 offset_end = i + cmd
1921 out += binchunk[i:offset_end]
1924 out += binchunk[i:offset_end]
1922 i += cmd
1925 i += cmd
1923 else:
1926 else:
1924 raise PatchError(_('unexpected delta opcode 0'))
1927 raise PatchError(_('unexpected delta opcode 0'))
1925 return out
1928 return out
1926
1929
1927 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
1930 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
1928 """Reads a patch from fp and tries to apply it.
1931 """Reads a patch from fp and tries to apply it.
1929
1932
1930 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1933 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1931 there was any fuzz.
1934 there was any fuzz.
1932
1935
1933 If 'eolmode' is 'strict', the patch content and patched file are
1936 If 'eolmode' is 'strict', the patch content and patched file are
1934 read in binary mode. Otherwise, line endings are ignored when
1937 read in binary mode. Otherwise, line endings are ignored when
1935 patching then normalized according to 'eolmode'.
1938 patching then normalized according to 'eolmode'.
1936 """
1939 """
1937 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1940 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1938 prefix=prefix, eolmode=eolmode)
1941 prefix=prefix, eolmode=eolmode)
1939
1942
1940 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
1943 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
1941 eolmode='strict'):
1944 eolmode='strict'):
1942
1945
1943 if prefix:
1946 if prefix:
1944 prefix = pathutil.canonpath(backend.repo.root, backend.repo.getcwd(),
1947 prefix = pathutil.canonpath(backend.repo.root, backend.repo.getcwd(),
1945 prefix)
1948 prefix)
1946 if prefix != '':
1949 if prefix != '':
1947 prefix += '/'
1950 prefix += '/'
1948 def pstrip(p):
1951 def pstrip(p):
1949 return pathtransform(p, strip - 1, prefix)[1]
1952 return pathtransform(p, strip - 1, prefix)[1]
1950
1953
1951 rejects = 0
1954 rejects = 0
1952 err = 0
1955 err = 0
1953 current_file = None
1956 current_file = None
1954
1957
1955 for state, values in iterhunks(fp):
1958 for state, values in iterhunks(fp):
1956 if state == 'hunk':
1959 if state == 'hunk':
1957 if not current_file:
1960 if not current_file:
1958 continue
1961 continue
1959 ret = current_file.apply(values)
1962 ret = current_file.apply(values)
1960 if ret > 0:
1963 if ret > 0:
1961 err = 1
1964 err = 1
1962 elif state == 'file':
1965 elif state == 'file':
1963 if current_file:
1966 if current_file:
1964 rejects += current_file.close()
1967 rejects += current_file.close()
1965 current_file = None
1968 current_file = None
1966 afile, bfile, first_hunk, gp = values
1969 afile, bfile, first_hunk, gp = values
1967 if gp:
1970 if gp:
1968 gp.path = pstrip(gp.path)
1971 gp.path = pstrip(gp.path)
1969 if gp.oldpath:
1972 if gp.oldpath:
1970 gp.oldpath = pstrip(gp.oldpath)
1973 gp.oldpath = pstrip(gp.oldpath)
1971 else:
1974 else:
1972 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
1975 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
1973 prefix)
1976 prefix)
1974 if gp.op == 'RENAME':
1977 if gp.op == 'RENAME':
1975 backend.unlink(gp.oldpath)
1978 backend.unlink(gp.oldpath)
1976 if not first_hunk:
1979 if not first_hunk:
1977 if gp.op == 'DELETE':
1980 if gp.op == 'DELETE':
1978 backend.unlink(gp.path)
1981 backend.unlink(gp.path)
1979 continue
1982 continue
1980 data, mode = None, None
1983 data, mode = None, None
1981 if gp.op in ('RENAME', 'COPY'):
1984 if gp.op in ('RENAME', 'COPY'):
1982 data, mode = store.getfile(gp.oldpath)[:2]
1985 data, mode = store.getfile(gp.oldpath)[:2]
1983 if data is None:
1986 if data is None:
1984 # This means that the old path does not exist
1987 # This means that the old path does not exist
1985 raise PatchError(_("source file '%s' does not exist")
1988 raise PatchError(_("source file '%s' does not exist")
1986 % gp.oldpath)
1989 % gp.oldpath)
1987 if gp.mode:
1990 if gp.mode:
1988 mode = gp.mode
1991 mode = gp.mode
1989 if gp.op == 'ADD':
1992 if gp.op == 'ADD':
1990 # Added files without content have no hunk and
1993 # Added files without content have no hunk and
1991 # must be created
1994 # must be created
1992 data = ''
1995 data = ''
1993 if data or mode:
1996 if data or mode:
1994 if (gp.op in ('ADD', 'RENAME', 'COPY')
1997 if (gp.op in ('ADD', 'RENAME', 'COPY')
1995 and backend.exists(gp.path)):
1998 and backend.exists(gp.path)):
1996 raise PatchError(_("cannot create %s: destination "
1999 raise PatchError(_("cannot create %s: destination "
1997 "already exists") % gp.path)
2000 "already exists") % gp.path)
1998 backend.setfile(gp.path, data, mode, gp.oldpath)
2001 backend.setfile(gp.path, data, mode, gp.oldpath)
1999 continue
2002 continue
2000 try:
2003 try:
2001 current_file = patcher(ui, gp, backend, store,
2004 current_file = patcher(ui, gp, backend, store,
2002 eolmode=eolmode)
2005 eolmode=eolmode)
2003 except PatchError as inst:
2006 except PatchError as inst:
2004 ui.warn(str(inst) + '\n')
2007 ui.warn(str(inst) + '\n')
2005 current_file = None
2008 current_file = None
2006 rejects += 1
2009 rejects += 1
2007 continue
2010 continue
2008 elif state == 'git':
2011 elif state == 'git':
2009 for gp in values:
2012 for gp in values:
2010 path = pstrip(gp.oldpath)
2013 path = pstrip(gp.oldpath)
2011 data, mode = backend.getfile(path)
2014 data, mode = backend.getfile(path)
2012 if data is None:
2015 if data is None:
2013 # The error ignored here will trigger a getfile()
2016 # The error ignored here will trigger a getfile()
2014 # error in a place more appropriate for error
2017 # error in a place more appropriate for error
2015 # handling, and will not interrupt the patching
2018 # handling, and will not interrupt the patching
2016 # process.
2019 # process.
2017 pass
2020 pass
2018 else:
2021 else:
2019 store.setfile(path, data, mode)
2022 store.setfile(path, data, mode)
2020 else:
2023 else:
2021 raise error.Abort(_('unsupported parser state: %s') % state)
2024 raise error.Abort(_('unsupported parser state: %s') % state)
2022
2025
2023 if current_file:
2026 if current_file:
2024 rejects += current_file.close()
2027 rejects += current_file.close()
2025
2028
2026 if rejects:
2029 if rejects:
2027 return -1
2030 return -1
2028 return err
2031 return err
2029
2032
2030 def _externalpatch(ui, repo, patcher, patchname, strip, files,
2033 def _externalpatch(ui, repo, patcher, patchname, strip, files,
2031 similarity):
2034 similarity):
2032 """use <patcher> to apply <patchname> to the working directory.
2035 """use <patcher> to apply <patchname> to the working directory.
2033 returns whether patch was applied with fuzz factor."""
2036 returns whether patch was applied with fuzz factor."""
2034
2037
2035 fuzz = False
2038 fuzz = False
2036 args = []
2039 args = []
2037 cwd = repo.root
2040 cwd = repo.root
2038 if cwd:
2041 if cwd:
2039 args.append('-d %s' % util.shellquote(cwd))
2042 args.append('-d %s' % util.shellquote(cwd))
2040 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
2043 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
2041 util.shellquote(patchname)))
2044 util.shellquote(patchname)))
2042 try:
2045 try:
2043 for line in util.iterfile(fp):
2046 for line in util.iterfile(fp):
2044 line = line.rstrip()
2047 line = line.rstrip()
2045 ui.note(line + '\n')
2048 ui.note(line + '\n')
2046 if line.startswith('patching file '):
2049 if line.startswith('patching file '):
2047 pf = util.parsepatchoutput(line)
2050 pf = util.parsepatchoutput(line)
2048 printed_file = False
2051 printed_file = False
2049 files.add(pf)
2052 files.add(pf)
2050 elif line.find('with fuzz') >= 0:
2053 elif line.find('with fuzz') >= 0:
2051 fuzz = True
2054 fuzz = True
2052 if not printed_file:
2055 if not printed_file:
2053 ui.warn(pf + '\n')
2056 ui.warn(pf + '\n')
2054 printed_file = True
2057 printed_file = True
2055 ui.warn(line + '\n')
2058 ui.warn(line + '\n')
2056 elif line.find('saving rejects to file') >= 0:
2059 elif line.find('saving rejects to file') >= 0:
2057 ui.warn(line + '\n')
2060 ui.warn(line + '\n')
2058 elif line.find('FAILED') >= 0:
2061 elif line.find('FAILED') >= 0:
2059 if not printed_file:
2062 if not printed_file:
2060 ui.warn(pf + '\n')
2063 ui.warn(pf + '\n')
2061 printed_file = True
2064 printed_file = True
2062 ui.warn(line + '\n')
2065 ui.warn(line + '\n')
2063 finally:
2066 finally:
2064 if files:
2067 if files:
2065 scmutil.marktouched(repo, files, similarity)
2068 scmutil.marktouched(repo, files, similarity)
2066 code = fp.close()
2069 code = fp.close()
2067 if code:
2070 if code:
2068 raise PatchError(_("patch command failed: %s") %
2071 raise PatchError(_("patch command failed: %s") %
2069 util.explainexit(code)[0])
2072 util.explainexit(code)[0])
2070 return fuzz
2073 return fuzz
2071
2074
2072 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
2075 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
2073 eolmode='strict'):
2076 eolmode='strict'):
2074 if files is None:
2077 if files is None:
2075 files = set()
2078 files = set()
2076 if eolmode is None:
2079 if eolmode is None:
2077 eolmode = ui.config('patch', 'eol', 'strict')
2080 eolmode = ui.config('patch', 'eol', 'strict')
2078 if eolmode.lower() not in eolmodes:
2081 if eolmode.lower() not in eolmodes:
2079 raise error.Abort(_('unsupported line endings type: %s') % eolmode)
2082 raise error.Abort(_('unsupported line endings type: %s') % eolmode)
2080 eolmode = eolmode.lower()
2083 eolmode = eolmode.lower()
2081
2084
2082 store = filestore()
2085 store = filestore()
2083 try:
2086 try:
2084 fp = open(patchobj, 'rb')
2087 fp = open(patchobj, 'rb')
2085 except TypeError:
2088 except TypeError:
2086 fp = patchobj
2089 fp = patchobj
2087 try:
2090 try:
2088 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
2091 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
2089 eolmode=eolmode)
2092 eolmode=eolmode)
2090 finally:
2093 finally:
2091 if fp != patchobj:
2094 if fp != patchobj:
2092 fp.close()
2095 fp.close()
2093 files.update(backend.close())
2096 files.update(backend.close())
2094 store.close()
2097 store.close()
2095 if ret < 0:
2098 if ret < 0:
2096 raise PatchError(_('patch failed to apply'))
2099 raise PatchError(_('patch failed to apply'))
2097 return ret > 0
2100 return ret > 0
2098
2101
2099 def internalpatch(ui, repo, patchobj, strip, prefix='', files=None,
2102 def internalpatch(ui, repo, patchobj, strip, prefix='', files=None,
2100 eolmode='strict', similarity=0):
2103 eolmode='strict', similarity=0):
2101 """use builtin patch to apply <patchobj> to the working directory.
2104 """use builtin patch to apply <patchobj> to the working directory.
2102 returns whether patch was applied with fuzz factor."""
2105 returns whether patch was applied with fuzz factor."""
2103 backend = workingbackend(ui, repo, similarity)
2106 backend = workingbackend(ui, repo, similarity)
2104 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2107 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2105
2108
2106 def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None,
2109 def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None,
2107 eolmode='strict'):
2110 eolmode='strict'):
2108 backend = repobackend(ui, repo, ctx, store)
2111 backend = repobackend(ui, repo, ctx, store)
2109 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2112 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2110
2113
2111 def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict',
2114 def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict',
2112 similarity=0):
2115 similarity=0):
2113 """Apply <patchname> to the working directory.
2116 """Apply <patchname> to the working directory.
2114
2117
2115 'eolmode' specifies how end of lines should be handled. It can be:
2118 'eolmode' specifies how end of lines should be handled. It can be:
2116 - 'strict': inputs are read in binary mode, EOLs are preserved
2119 - 'strict': inputs are read in binary mode, EOLs are preserved
2117 - 'crlf': EOLs are ignored when patching and reset to CRLF
2120 - 'crlf': EOLs are ignored when patching and reset to CRLF
2118 - 'lf': EOLs are ignored when patching and reset to LF
2121 - 'lf': EOLs are ignored when patching and reset to LF
2119 - None: get it from user settings, default to 'strict'
2122 - None: get it from user settings, default to 'strict'
2120 'eolmode' is ignored when using an external patcher program.
2123 'eolmode' is ignored when using an external patcher program.
2121
2124
2122 Returns whether patch was applied with fuzz factor.
2125 Returns whether patch was applied with fuzz factor.
2123 """
2126 """
2124 patcher = ui.config('ui', 'patch')
2127 patcher = ui.config('ui', 'patch')
2125 if files is None:
2128 if files is None:
2126 files = set()
2129 files = set()
2127 if patcher:
2130 if patcher:
2128 return _externalpatch(ui, repo, patcher, patchname, strip,
2131 return _externalpatch(ui, repo, patcher, patchname, strip,
2129 files, similarity)
2132 files, similarity)
2130 return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode,
2133 return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode,
2131 similarity)
2134 similarity)
2132
2135
2133 def changedfiles(ui, repo, patchpath, strip=1):
2136 def changedfiles(ui, repo, patchpath, strip=1):
2134 backend = fsbackend(ui, repo.root)
2137 backend = fsbackend(ui, repo.root)
2135 with open(patchpath, 'rb') as fp:
2138 with open(patchpath, 'rb') as fp:
2136 changed = set()
2139 changed = set()
2137 for state, values in iterhunks(fp):
2140 for state, values in iterhunks(fp):
2138 if state == 'file':
2141 if state == 'file':
2139 afile, bfile, first_hunk, gp = values
2142 afile, bfile, first_hunk, gp = values
2140 if gp:
2143 if gp:
2141 gp.path = pathtransform(gp.path, strip - 1, '')[1]
2144 gp.path = pathtransform(gp.path, strip - 1, '')[1]
2142 if gp.oldpath:
2145 if gp.oldpath:
2143 gp.oldpath = pathtransform(gp.oldpath, strip - 1, '')[1]
2146 gp.oldpath = pathtransform(gp.oldpath, strip - 1, '')[1]
2144 else:
2147 else:
2145 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2148 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2146 '')
2149 '')
2147 changed.add(gp.path)
2150 changed.add(gp.path)
2148 if gp.op == 'RENAME':
2151 if gp.op == 'RENAME':
2149 changed.add(gp.oldpath)
2152 changed.add(gp.oldpath)
2150 elif state not in ('hunk', 'git'):
2153 elif state not in ('hunk', 'git'):
2151 raise error.Abort(_('unsupported parser state: %s') % state)
2154 raise error.Abort(_('unsupported parser state: %s') % state)
2152 return changed
2155 return changed
2153
2156
2154 class GitDiffRequired(Exception):
2157 class GitDiffRequired(Exception):
2155 pass
2158 pass
2156
2159
2157 def diffallopts(ui, opts=None, untrusted=False, section='diff'):
2160 def diffallopts(ui, opts=None, untrusted=False, section='diff'):
2158 '''return diffopts with all features supported and parsed'''
2161 '''return diffopts with all features supported and parsed'''
2159 return difffeatureopts(ui, opts=opts, untrusted=untrusted, section=section,
2162 return difffeatureopts(ui, opts=opts, untrusted=untrusted, section=section,
2160 git=True, whitespace=True, formatchanging=True)
2163 git=True, whitespace=True, formatchanging=True)
2161
2164
2162 diffopts = diffallopts
2165 diffopts = diffallopts
2163
2166
2164 def difffeatureopts(ui, opts=None, untrusted=False, section='diff', git=False,
2167 def difffeatureopts(ui, opts=None, untrusted=False, section='diff', git=False,
2165 whitespace=False, formatchanging=False):
2168 whitespace=False, formatchanging=False):
2166 '''return diffopts with only opted-in features parsed
2169 '''return diffopts with only opted-in features parsed
2167
2170
2168 Features:
2171 Features:
2169 - git: git-style diffs
2172 - git: git-style diffs
2170 - whitespace: whitespace options like ignoreblanklines and ignorews
2173 - whitespace: whitespace options like ignoreblanklines and ignorews
2171 - formatchanging: options that will likely break or cause correctness issues
2174 - formatchanging: options that will likely break or cause correctness issues
2172 with most diff parsers
2175 with most diff parsers
2173 '''
2176 '''
2174 def get(key, name=None, getter=ui.configbool, forceplain=None):
2177 def get(key, name=None, getter=ui.configbool, forceplain=None):
2175 if opts:
2178 if opts:
2176 v = opts.get(key)
2179 v = opts.get(key)
2177 # diffopts flags are either None-default (which is passed
2180 # diffopts flags are either None-default (which is passed
2178 # through unchanged, so we can identify unset values), or
2181 # through unchanged, so we can identify unset values), or
2179 # some other falsey default (eg --unified, which defaults
2182 # some other falsey default (eg --unified, which defaults
2180 # to an empty string). We only want to override the config
2183 # to an empty string). We only want to override the config
2181 # entries from hgrc with command line values if they
2184 # entries from hgrc with command line values if they
2182 # appear to have been set, which is any truthy value,
2185 # appear to have been set, which is any truthy value,
2183 # True, or False.
2186 # True, or False.
2184 if v or isinstance(v, bool):
2187 if v or isinstance(v, bool):
2185 return v
2188 return v
2186 if forceplain is not None and ui.plain():
2189 if forceplain is not None and ui.plain():
2187 return forceplain
2190 return forceplain
2188 return getter(section, name or key, None, untrusted=untrusted)
2191 return getter(section, name or key, None, untrusted=untrusted)
2189
2192
2190 # core options, expected to be understood by every diff parser
2193 # core options, expected to be understood by every diff parser
2191 buildopts = {
2194 buildopts = {
2192 'nodates': get('nodates'),
2195 'nodates': get('nodates'),
2193 'showfunc': get('show_function', 'showfunc'),
2196 'showfunc': get('show_function', 'showfunc'),
2194 'context': get('unified', getter=ui.config),
2197 'context': get('unified', getter=ui.config),
2195 }
2198 }
2196
2199
2197 if git:
2200 if git:
2198 buildopts['git'] = get('git')
2201 buildopts['git'] = get('git')
2199
2202
2200 # since this is in the experimental section, we need to call
2203 # since this is in the experimental section, we need to call
2201 # ui.configbool directory
2204 # ui.configbool directory
2202 buildopts['showsimilarity'] = ui.configbool('experimental',
2205 buildopts['showsimilarity'] = ui.configbool('experimental',
2203 'extendedheader.similarity')
2206 'extendedheader.similarity')
2204
2207
2205 # need to inspect the ui object instead of using get() since we want to
2208 # need to inspect the ui object instead of using get() since we want to
2206 # test for an int
2209 # test for an int
2207 hconf = ui.config('experimental', 'extendedheader.index')
2210 hconf = ui.config('experimental', 'extendedheader.index')
2208 if hconf is not None:
2211 if hconf is not None:
2209 hlen = None
2212 hlen = None
2210 try:
2213 try:
2211 # the hash config could be an integer (for length of hash) or a
2214 # the hash config could be an integer (for length of hash) or a
2212 # word (e.g. short, full, none)
2215 # word (e.g. short, full, none)
2213 hlen = int(hconf)
2216 hlen = int(hconf)
2214 if hlen < 0 or hlen > 40:
2217 if hlen < 0 or hlen > 40:
2215 msg = _("invalid length for extendedheader.index: '%d'\n")
2218 msg = _("invalid length for extendedheader.index: '%d'\n")
2216 ui.warn(msg % hlen)
2219 ui.warn(msg % hlen)
2217 except ValueError:
2220 except ValueError:
2218 # default value
2221 # default value
2219 if hconf == 'short' or hconf == '':
2222 if hconf == 'short' or hconf == '':
2220 hlen = 12
2223 hlen = 12
2221 elif hconf == 'full':
2224 elif hconf == 'full':
2222 hlen = 40
2225 hlen = 40
2223 elif hconf != 'none':
2226 elif hconf != 'none':
2224 msg = _("invalid value for extendedheader.index: '%s'\n")
2227 msg = _("invalid value for extendedheader.index: '%s'\n")
2225 ui.warn(msg % hconf)
2228 ui.warn(msg % hconf)
2226 finally:
2229 finally:
2227 buildopts['index'] = hlen
2230 buildopts['index'] = hlen
2228
2231
2229 if whitespace:
2232 if whitespace:
2230 buildopts['ignorews'] = get('ignore_all_space', 'ignorews')
2233 buildopts['ignorews'] = get('ignore_all_space', 'ignorews')
2231 buildopts['ignorewsamount'] = get('ignore_space_change',
2234 buildopts['ignorewsamount'] = get('ignore_space_change',
2232 'ignorewsamount')
2235 'ignorewsamount')
2233 buildopts['ignoreblanklines'] = get('ignore_blank_lines',
2236 buildopts['ignoreblanklines'] = get('ignore_blank_lines',
2234 'ignoreblanklines')
2237 'ignoreblanklines')
2235 if formatchanging:
2238 if formatchanging:
2236 buildopts['text'] = opts and opts.get('text')
2239 buildopts['text'] = opts and opts.get('text')
2237 binary = None if opts is None else opts.get('binary')
2240 binary = None if opts is None else opts.get('binary')
2238 buildopts['nobinary'] = (not binary if binary is not None
2241 buildopts['nobinary'] = (not binary if binary is not None
2239 else get('nobinary', forceplain=False))
2242 else get('nobinary', forceplain=False))
2240 buildopts['noprefix'] = get('noprefix', forceplain=False)
2243 buildopts['noprefix'] = get('noprefix', forceplain=False)
2241
2244
2242 return mdiff.diffopts(**pycompat.strkwargs(buildopts))
2245 return mdiff.diffopts(**pycompat.strkwargs(buildopts))
2243
2246
2244 def diff(repo, node1=None, node2=None, match=None, changes=None,
2247 def diff(repo, node1=None, node2=None, match=None, changes=None,
2245 opts=None, losedatafn=None, prefix='', relroot='', copy=None):
2248 opts=None, losedatafn=None, prefix='', relroot='', copy=None):
2246 '''yields diff of changes to files between two nodes, or node and
2249 '''yields diff of changes to files between two nodes, or node and
2247 working directory.
2250 working directory.
2248
2251
2249 if node1 is None, use first dirstate parent instead.
2252 if node1 is None, use first dirstate parent instead.
2250 if node2 is None, compare node1 with working directory.
2253 if node2 is None, compare node1 with working directory.
2251
2254
2252 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2255 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2253 every time some change cannot be represented with the current
2256 every time some change cannot be represented with the current
2254 patch format. Return False to upgrade to git patch format, True to
2257 patch format. Return False to upgrade to git patch format, True to
2255 accept the loss or raise an exception to abort the diff. It is
2258 accept the loss or raise an exception to abort the diff. It is
2256 called with the name of current file being diffed as 'fn'. If set
2259 called with the name of current file being diffed as 'fn'. If set
2257 to None, patches will always be upgraded to git format when
2260 to None, patches will always be upgraded to git format when
2258 necessary.
2261 necessary.
2259
2262
2260 prefix is a filename prefix that is prepended to all filenames on
2263 prefix is a filename prefix that is prepended to all filenames on
2261 display (used for subrepos).
2264 display (used for subrepos).
2262
2265
2263 relroot, if not empty, must be normalized with a trailing /. Any match
2266 relroot, if not empty, must be normalized with a trailing /. Any match
2264 patterns that fall outside it will be ignored.
2267 patterns that fall outside it will be ignored.
2265
2268
2266 copy, if not empty, should contain mappings {dst@y: src@x} of copy
2269 copy, if not empty, should contain mappings {dst@y: src@x} of copy
2267 information.'''
2270 information.'''
2268 for header, hunks in diffhunks(repo, node1=node1, node2=node2, match=match,
2271 for header, hunks in diffhunks(repo, node1=node1, node2=node2, match=match,
2269 changes=changes, opts=opts,
2272 changes=changes, opts=opts,
2270 losedatafn=losedatafn, prefix=prefix,
2273 losedatafn=losedatafn, prefix=prefix,
2271 relroot=relroot, copy=copy):
2274 relroot=relroot, copy=copy):
2272 text = ''.join(sum((list(hlines) for hrange, hlines in hunks), []))
2275 text = ''.join(sum((list(hlines) for hrange, hlines in hunks), []))
2273 if header and (text or len(header) > 1):
2276 if header and (text or len(header) > 1):
2274 yield '\n'.join(header) + '\n'
2277 yield '\n'.join(header) + '\n'
2275 if text:
2278 if text:
2276 yield text
2279 yield text
2277
2280
2278 def diffhunks(repo, node1=None, node2=None, match=None, changes=None,
2281 def diffhunks(repo, node1=None, node2=None, match=None, changes=None,
2279 opts=None, losedatafn=None, prefix='', relroot='', copy=None):
2282 opts=None, losedatafn=None, prefix='', relroot='', copy=None):
2280 """Yield diff of changes to files in the form of (`header`, `hunks`) tuples
2283 """Yield diff of changes to files in the form of (`header`, `hunks`) tuples
2281 where `header` is a list of diff headers and `hunks` is an iterable of
2284 where `header` is a list of diff headers and `hunks` is an iterable of
2282 (`hunkrange`, `hunklines`) tuples.
2285 (`hunkrange`, `hunklines`) tuples.
2283
2286
2284 See diff() for the meaning of parameters.
2287 See diff() for the meaning of parameters.
2285 """
2288 """
2286
2289
2287 if opts is None:
2290 if opts is None:
2288 opts = mdiff.defaultopts
2291 opts = mdiff.defaultopts
2289
2292
2290 if not node1 and not node2:
2293 if not node1 and not node2:
2291 node1 = repo.dirstate.p1()
2294 node1 = repo.dirstate.p1()
2292
2295
2293 def lrugetfilectx():
2296 def lrugetfilectx():
2294 cache = {}
2297 cache = {}
2295 order = collections.deque()
2298 order = collections.deque()
2296 def getfilectx(f, ctx):
2299 def getfilectx(f, ctx):
2297 fctx = ctx.filectx(f, filelog=cache.get(f))
2300 fctx = ctx.filectx(f, filelog=cache.get(f))
2298 if f not in cache:
2301 if f not in cache:
2299 if len(cache) > 20:
2302 if len(cache) > 20:
2300 del cache[order.popleft()]
2303 del cache[order.popleft()]
2301 cache[f] = fctx.filelog()
2304 cache[f] = fctx.filelog()
2302 else:
2305 else:
2303 order.remove(f)
2306 order.remove(f)
2304 order.append(f)
2307 order.append(f)
2305 return fctx
2308 return fctx
2306 return getfilectx
2309 return getfilectx
2307 getfilectx = lrugetfilectx()
2310 getfilectx = lrugetfilectx()
2308
2311
2309 ctx1 = repo[node1]
2312 ctx1 = repo[node1]
2310 ctx2 = repo[node2]
2313 ctx2 = repo[node2]
2311
2314
2312 relfiltered = False
2315 relfiltered = False
2313 if relroot != '' and match.always():
2316 if relroot != '' and match.always():
2314 # as a special case, create a new matcher with just the relroot
2317 # as a special case, create a new matcher with just the relroot
2315 pats = [relroot]
2318 pats = [relroot]
2316 match = scmutil.match(ctx2, pats, default='path')
2319 match = scmutil.match(ctx2, pats, default='path')
2317 relfiltered = True
2320 relfiltered = True
2318
2321
2319 if not changes:
2322 if not changes:
2320 changes = repo.status(ctx1, ctx2, match=match)
2323 changes = repo.status(ctx1, ctx2, match=match)
2321 modified, added, removed = changes[:3]
2324 modified, added, removed = changes[:3]
2322
2325
2323 if not modified and not added and not removed:
2326 if not modified and not added and not removed:
2324 return []
2327 return []
2325
2328
2326 if repo.ui.debugflag:
2329 if repo.ui.debugflag:
2327 hexfunc = hex
2330 hexfunc = hex
2328 else:
2331 else:
2329 hexfunc = short
2332 hexfunc = short
2330 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2333 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2331
2334
2332 if copy is None:
2335 if copy is None:
2333 copy = {}
2336 copy = {}
2334 if opts.git or opts.upgrade:
2337 if opts.git or opts.upgrade:
2335 copy = copies.pathcopies(ctx1, ctx2, match=match)
2338 copy = copies.pathcopies(ctx1, ctx2, match=match)
2336
2339
2337 if relroot is not None:
2340 if relroot is not None:
2338 if not relfiltered:
2341 if not relfiltered:
2339 # XXX this would ideally be done in the matcher, but that is
2342 # XXX this would ideally be done in the matcher, but that is
2340 # generally meant to 'or' patterns, not 'and' them. In this case we
2343 # generally meant to 'or' patterns, not 'and' them. In this case we
2341 # need to 'and' all the patterns from the matcher with relroot.
2344 # need to 'and' all the patterns from the matcher with relroot.
2342 def filterrel(l):
2345 def filterrel(l):
2343 return [f for f in l if f.startswith(relroot)]
2346 return [f for f in l if f.startswith(relroot)]
2344 modified = filterrel(modified)
2347 modified = filterrel(modified)
2345 added = filterrel(added)
2348 added = filterrel(added)
2346 removed = filterrel(removed)
2349 removed = filterrel(removed)
2347 relfiltered = True
2350 relfiltered = True
2348 # filter out copies where either side isn't inside the relative root
2351 # filter out copies where either side isn't inside the relative root
2349 copy = dict(((dst, src) for (dst, src) in copy.iteritems()
2352 copy = dict(((dst, src) for (dst, src) in copy.iteritems()
2350 if dst.startswith(relroot)
2353 if dst.startswith(relroot)
2351 and src.startswith(relroot)))
2354 and src.startswith(relroot)))
2352
2355
2353 modifiedset = set(modified)
2356 modifiedset = set(modified)
2354 addedset = set(added)
2357 addedset = set(added)
2355 removedset = set(removed)
2358 removedset = set(removed)
2356 for f in modified:
2359 for f in modified:
2357 if f not in ctx1:
2360 if f not in ctx1:
2358 # Fix up added, since merged-in additions appear as
2361 # Fix up added, since merged-in additions appear as
2359 # modifications during merges
2362 # modifications during merges
2360 modifiedset.remove(f)
2363 modifiedset.remove(f)
2361 addedset.add(f)
2364 addedset.add(f)
2362 for f in removed:
2365 for f in removed:
2363 if f not in ctx1:
2366 if f not in ctx1:
2364 # Merged-in additions that are then removed are reported as removed.
2367 # Merged-in additions that are then removed are reported as removed.
2365 # They are not in ctx1, so We don't want to show them in the diff.
2368 # They are not in ctx1, so We don't want to show them in the diff.
2366 removedset.remove(f)
2369 removedset.remove(f)
2367 modified = sorted(modifiedset)
2370 modified = sorted(modifiedset)
2368 added = sorted(addedset)
2371 added = sorted(addedset)
2369 removed = sorted(removedset)
2372 removed = sorted(removedset)
2370 for dst, src in copy.items():
2373 for dst, src in copy.items():
2371 if src not in ctx1:
2374 if src not in ctx1:
2372 # Files merged in during a merge and then copied/renamed are
2375 # Files merged in during a merge and then copied/renamed are
2373 # reported as copies. We want to show them in the diff as additions.
2376 # reported as copies. We want to show them in the diff as additions.
2374 del copy[dst]
2377 del copy[dst]
2375
2378
2376 def difffn(opts, losedata):
2379 def difffn(opts, losedata):
2377 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2380 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2378 copy, getfilectx, opts, losedata, prefix, relroot)
2381 copy, getfilectx, opts, losedata, prefix, relroot)
2379 if opts.upgrade and not opts.git:
2382 if opts.upgrade and not opts.git:
2380 try:
2383 try:
2381 def losedata(fn):
2384 def losedata(fn):
2382 if not losedatafn or not losedatafn(fn=fn):
2385 if not losedatafn or not losedatafn(fn=fn):
2383 raise GitDiffRequired
2386 raise GitDiffRequired
2384 # Buffer the whole output until we are sure it can be generated
2387 # Buffer the whole output until we are sure it can be generated
2385 return list(difffn(opts.copy(git=False), losedata))
2388 return list(difffn(opts.copy(git=False), losedata))
2386 except GitDiffRequired:
2389 except GitDiffRequired:
2387 return difffn(opts.copy(git=True), None)
2390 return difffn(opts.copy(git=True), None)
2388 else:
2391 else:
2389 return difffn(opts, None)
2392 return difffn(opts, None)
2390
2393
2391 def difflabel(func, *args, **kw):
2394 def difflabel(func, *args, **kw):
2392 '''yields 2-tuples of (output, label) based on the output of func()'''
2395 '''yields 2-tuples of (output, label) based on the output of func()'''
2393 headprefixes = [('diff', 'diff.diffline'),
2396 headprefixes = [('diff', 'diff.diffline'),
2394 ('copy', 'diff.extended'),
2397 ('copy', 'diff.extended'),
2395 ('rename', 'diff.extended'),
2398 ('rename', 'diff.extended'),
2396 ('old', 'diff.extended'),
2399 ('old', 'diff.extended'),
2397 ('new', 'diff.extended'),
2400 ('new', 'diff.extended'),
2398 ('deleted', 'diff.extended'),
2401 ('deleted', 'diff.extended'),
2399 ('index', 'diff.extended'),
2402 ('index', 'diff.extended'),
2400 ('similarity', 'diff.extended'),
2403 ('similarity', 'diff.extended'),
2401 ('---', 'diff.file_a'),
2404 ('---', 'diff.file_a'),
2402 ('+++', 'diff.file_b')]
2405 ('+++', 'diff.file_b')]
2403 textprefixes = [('@', 'diff.hunk'),
2406 textprefixes = [('@', 'diff.hunk'),
2404 ('-', 'diff.deleted'),
2407 ('-', 'diff.deleted'),
2405 ('+', 'diff.inserted')]
2408 ('+', 'diff.inserted')]
2406 head = False
2409 head = False
2407 for chunk in func(*args, **kw):
2410 for chunk in func(*args, **kw):
2408 lines = chunk.split('\n')
2411 lines = chunk.split('\n')
2409 for i, line in enumerate(lines):
2412 for i, line in enumerate(lines):
2410 if i != 0:
2413 if i != 0:
2411 yield ('\n', '')
2414 yield ('\n', '')
2412 if head:
2415 if head:
2413 if line.startswith('@'):
2416 if line.startswith('@'):
2414 head = False
2417 head = False
2415 else:
2418 else:
2416 if line and line[0] not in ' +-@\\':
2419 if line and line[0] not in ' +-@\\':
2417 head = True
2420 head = True
2418 stripline = line
2421 stripline = line
2419 diffline = False
2422 diffline = False
2420 if not head and line and line[0] in '+-':
2423 if not head and line and line[0] in '+-':
2421 # highlight tabs and trailing whitespace, but only in
2424 # highlight tabs and trailing whitespace, but only in
2422 # changed lines
2425 # changed lines
2423 stripline = line.rstrip()
2426 stripline = line.rstrip()
2424 diffline = True
2427 diffline = True
2425
2428
2426 prefixes = textprefixes
2429 prefixes = textprefixes
2427 if head:
2430 if head:
2428 prefixes = headprefixes
2431 prefixes = headprefixes
2429 for prefix, label in prefixes:
2432 for prefix, label in prefixes:
2430 if stripline.startswith(prefix):
2433 if stripline.startswith(prefix):
2431 if diffline:
2434 if diffline:
2432 for token in tabsplitter.findall(stripline):
2435 for token in tabsplitter.findall(stripline):
2433 if '\t' == token[0]:
2436 if '\t' == token[0]:
2434 yield (token, 'diff.tab')
2437 yield (token, 'diff.tab')
2435 else:
2438 else:
2436 yield (token, label)
2439 yield (token, label)
2437 else:
2440 else:
2438 yield (stripline, label)
2441 yield (stripline, label)
2439 break
2442 break
2440 else:
2443 else:
2441 yield (line, '')
2444 yield (line, '')
2442 if line != stripline:
2445 if line != stripline:
2443 yield (line[len(stripline):], 'diff.trailingwhitespace')
2446 yield (line[len(stripline):], 'diff.trailingwhitespace')
2444
2447
2445 def diffui(*args, **kw):
2448 def diffui(*args, **kw):
2446 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2449 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2447 return difflabel(diff, *args, **kw)
2450 return difflabel(diff, *args, **kw)
2448
2451
2449 def _filepairs(modified, added, removed, copy, opts):
2452 def _filepairs(modified, added, removed, copy, opts):
2450 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2453 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2451 before and f2 is the the name after. For added files, f1 will be None,
2454 before and f2 is the the name after. For added files, f1 will be None,
2452 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2455 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2453 or 'rename' (the latter two only if opts.git is set).'''
2456 or 'rename' (the latter two only if opts.git is set).'''
2454 gone = set()
2457 gone = set()
2455
2458
2456 copyto = dict([(v, k) for k, v in copy.items()])
2459 copyto = dict([(v, k) for k, v in copy.items()])
2457
2460
2458 addedset, removedset = set(added), set(removed)
2461 addedset, removedset = set(added), set(removed)
2459
2462
2460 for f in sorted(modified + added + removed):
2463 for f in sorted(modified + added + removed):
2461 copyop = None
2464 copyop = None
2462 f1, f2 = f, f
2465 f1, f2 = f, f
2463 if f in addedset:
2466 if f in addedset:
2464 f1 = None
2467 f1 = None
2465 if f in copy:
2468 if f in copy:
2466 if opts.git:
2469 if opts.git:
2467 f1 = copy[f]
2470 f1 = copy[f]
2468 if f1 in removedset and f1 not in gone:
2471 if f1 in removedset and f1 not in gone:
2469 copyop = 'rename'
2472 copyop = 'rename'
2470 gone.add(f1)
2473 gone.add(f1)
2471 else:
2474 else:
2472 copyop = 'copy'
2475 copyop = 'copy'
2473 elif f in removedset:
2476 elif f in removedset:
2474 f2 = None
2477 f2 = None
2475 if opts.git:
2478 if opts.git:
2476 # have we already reported a copy above?
2479 # have we already reported a copy above?
2477 if (f in copyto and copyto[f] in addedset
2480 if (f in copyto and copyto[f] in addedset
2478 and copy[copyto[f]] == f):
2481 and copy[copyto[f]] == f):
2479 continue
2482 continue
2480 yield f1, f2, copyop
2483 yield f1, f2, copyop
2481
2484
2482 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2485 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2483 copy, getfilectx, opts, losedatafn, prefix, relroot):
2486 copy, getfilectx, opts, losedatafn, prefix, relroot):
2484 '''given input data, generate a diff and yield it in blocks
2487 '''given input data, generate a diff and yield it in blocks
2485
2488
2486 If generating a diff would lose data like flags or binary data and
2489 If generating a diff would lose data like flags or binary data and
2487 losedatafn is not None, it will be called.
2490 losedatafn is not None, it will be called.
2488
2491
2489 relroot is removed and prefix is added to every path in the diff output.
2492 relroot is removed and prefix is added to every path in the diff output.
2490
2493
2491 If relroot is not empty, this function expects every path in modified,
2494 If relroot is not empty, this function expects every path in modified,
2492 added, removed and copy to start with it.'''
2495 added, removed and copy to start with it.'''
2493
2496
2494 def gitindex(text):
2497 def gitindex(text):
2495 if not text:
2498 if not text:
2496 text = ""
2499 text = ""
2497 l = len(text)
2500 l = len(text)
2498 s = hashlib.sha1('blob %d\0' % l)
2501 s = hashlib.sha1('blob %d\0' % l)
2499 s.update(text)
2502 s.update(text)
2500 return s.hexdigest()
2503 return s.hexdigest()
2501
2504
2502 if opts.noprefix:
2505 if opts.noprefix:
2503 aprefix = bprefix = ''
2506 aprefix = bprefix = ''
2504 else:
2507 else:
2505 aprefix = 'a/'
2508 aprefix = 'a/'
2506 bprefix = 'b/'
2509 bprefix = 'b/'
2507
2510
2508 def diffline(f, revs):
2511 def diffline(f, revs):
2509 revinfo = ' '.join(["-r %s" % rev for rev in revs])
2512 revinfo = ' '.join(["-r %s" % rev for rev in revs])
2510 return 'diff %s %s' % (revinfo, f)
2513 return 'diff %s %s' % (revinfo, f)
2511
2514
2512 def isempty(fctx):
2515 def isempty(fctx):
2513 return fctx is None or fctx.size() == 0
2516 return fctx is None or fctx.size() == 0
2514
2517
2515 date1 = util.datestr(ctx1.date())
2518 date1 = util.datestr(ctx1.date())
2516 date2 = util.datestr(ctx2.date())
2519 date2 = util.datestr(ctx2.date())
2517
2520
2518 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
2521 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
2519
2522
2520 if relroot != '' and (repo.ui.configbool('devel', 'all')
2523 if relroot != '' and (repo.ui.configbool('devel', 'all')
2521 or repo.ui.configbool('devel', 'check-relroot')):
2524 or repo.ui.configbool('devel', 'check-relroot')):
2522 for f in modified + added + removed + copy.keys() + copy.values():
2525 for f in modified + added + removed + copy.keys() + copy.values():
2523 if f is not None and not f.startswith(relroot):
2526 if f is not None and not f.startswith(relroot):
2524 raise AssertionError(
2527 raise AssertionError(
2525 "file %s doesn't start with relroot %s" % (f, relroot))
2528 "file %s doesn't start with relroot %s" % (f, relroot))
2526
2529
2527 for f1, f2, copyop in _filepairs(modified, added, removed, copy, opts):
2530 for f1, f2, copyop in _filepairs(modified, added, removed, copy, opts):
2528 content1 = None
2531 content1 = None
2529 content2 = None
2532 content2 = None
2530 fctx1 = None
2533 fctx1 = None
2531 fctx2 = None
2534 fctx2 = None
2532 flag1 = None
2535 flag1 = None
2533 flag2 = None
2536 flag2 = None
2534 if f1:
2537 if f1:
2535 fctx1 = getfilectx(f1, ctx1)
2538 fctx1 = getfilectx(f1, ctx1)
2536 if opts.git or losedatafn:
2539 if opts.git or losedatafn:
2537 flag1 = ctx1.flags(f1)
2540 flag1 = ctx1.flags(f1)
2538 if f2:
2541 if f2:
2539 fctx2 = getfilectx(f2, ctx2)
2542 fctx2 = getfilectx(f2, ctx2)
2540 if opts.git or losedatafn:
2543 if opts.git or losedatafn:
2541 flag2 = ctx2.flags(f2)
2544 flag2 = ctx2.flags(f2)
2542 # if binary is True, output "summary" or "base85", but not "text diff"
2545 # if binary is True, output "summary" or "base85", but not "text diff"
2543 binary = not opts.text and any(f.isbinary()
2546 binary = not opts.text and any(f.isbinary()
2544 for f in [fctx1, fctx2] if f is not None)
2547 for f in [fctx1, fctx2] if f is not None)
2545
2548
2546 if losedatafn and not opts.git:
2549 if losedatafn and not opts.git:
2547 if (binary or
2550 if (binary or
2548 # copy/rename
2551 # copy/rename
2549 f2 in copy or
2552 f2 in copy or
2550 # empty file creation
2553 # empty file creation
2551 (not f1 and isempty(fctx2)) or
2554 (not f1 and isempty(fctx2)) or
2552 # empty file deletion
2555 # empty file deletion
2553 (isempty(fctx1) and not f2) or
2556 (isempty(fctx1) and not f2) or
2554 # create with flags
2557 # create with flags
2555 (not f1 and flag2) or
2558 (not f1 and flag2) or
2556 # change flags
2559 # change flags
2557 (f1 and f2 and flag1 != flag2)):
2560 (f1 and f2 and flag1 != flag2)):
2558 losedatafn(f2 or f1)
2561 losedatafn(f2 or f1)
2559
2562
2560 path1 = f1 or f2
2563 path1 = f1 or f2
2561 path2 = f2 or f1
2564 path2 = f2 or f1
2562 path1 = posixpath.join(prefix, path1[len(relroot):])
2565 path1 = posixpath.join(prefix, path1[len(relroot):])
2563 path2 = posixpath.join(prefix, path2[len(relroot):])
2566 path2 = posixpath.join(prefix, path2[len(relroot):])
2564 header = []
2567 header = []
2565 if opts.git:
2568 if opts.git:
2566 header.append('diff --git %s%s %s%s' %
2569 header.append('diff --git %s%s %s%s' %
2567 (aprefix, path1, bprefix, path2))
2570 (aprefix, path1, bprefix, path2))
2568 if not f1: # added
2571 if not f1: # added
2569 header.append('new file mode %s' % gitmode[flag2])
2572 header.append('new file mode %s' % gitmode[flag2])
2570 elif not f2: # removed
2573 elif not f2: # removed
2571 header.append('deleted file mode %s' % gitmode[flag1])
2574 header.append('deleted file mode %s' % gitmode[flag1])
2572 else: # modified/copied/renamed
2575 else: # modified/copied/renamed
2573 mode1, mode2 = gitmode[flag1], gitmode[flag2]
2576 mode1, mode2 = gitmode[flag1], gitmode[flag2]
2574 if mode1 != mode2:
2577 if mode1 != mode2:
2575 header.append('old mode %s' % mode1)
2578 header.append('old mode %s' % mode1)
2576 header.append('new mode %s' % mode2)
2579 header.append('new mode %s' % mode2)
2577 if copyop is not None:
2580 if copyop is not None:
2578 if opts.showsimilarity:
2581 if opts.showsimilarity:
2579 sim = similar.score(ctx1[path1], ctx2[path2]) * 100
2582 sim = similar.score(ctx1[path1], ctx2[path2]) * 100
2580 header.append('similarity index %d%%' % sim)
2583 header.append('similarity index %d%%' % sim)
2581 header.append('%s from %s' % (copyop, path1))
2584 header.append('%s from %s' % (copyop, path1))
2582 header.append('%s to %s' % (copyop, path2))
2585 header.append('%s to %s' % (copyop, path2))
2583 elif revs and not repo.ui.quiet:
2586 elif revs and not repo.ui.quiet:
2584 header.append(diffline(path1, revs))
2587 header.append(diffline(path1, revs))
2585
2588
2586 # fctx.is | diffopts | what to | is fctx.data()
2589 # fctx.is | diffopts | what to | is fctx.data()
2587 # binary() | text nobinary git index | output? | outputted?
2590 # binary() | text nobinary git index | output? | outputted?
2588 # ------------------------------------|----------------------------
2591 # ------------------------------------|----------------------------
2589 # yes | no no no * | summary | no
2592 # yes | no no no * | summary | no
2590 # yes | no no yes * | base85 | yes
2593 # yes | no no yes * | base85 | yes
2591 # yes | no yes no * | summary | no
2594 # yes | no yes no * | summary | no
2592 # yes | no yes yes 0 | summary | no
2595 # yes | no yes yes 0 | summary | no
2593 # yes | no yes yes >0 | summary | semi [1]
2596 # yes | no yes yes >0 | summary | semi [1]
2594 # yes | yes * * * | text diff | yes
2597 # yes | yes * * * | text diff | yes
2595 # no | * * * * | text diff | yes
2598 # no | * * * * | text diff | yes
2596 # [1]: hash(fctx.data()) is outputted. so fctx.data() cannot be faked
2599 # [1]: hash(fctx.data()) is outputted. so fctx.data() cannot be faked
2597 if binary and (not opts.git or (opts.git and opts.nobinary and not
2600 if binary and (not opts.git or (opts.git and opts.nobinary and not
2598 opts.index)):
2601 opts.index)):
2599 # fast path: no binary content will be displayed, content1 and
2602 # fast path: no binary content will be displayed, content1 and
2600 # content2 are only used for equivalent test. cmp() could have a
2603 # content2 are only used for equivalent test. cmp() could have a
2601 # fast path.
2604 # fast path.
2602 if fctx1 is not None:
2605 if fctx1 is not None:
2603 content1 = b'\0'
2606 content1 = b'\0'
2604 if fctx2 is not None:
2607 if fctx2 is not None:
2605 if fctx1 is not None and not fctx1.cmp(fctx2):
2608 if fctx1 is not None and not fctx1.cmp(fctx2):
2606 content2 = b'\0' # not different
2609 content2 = b'\0' # not different
2607 else:
2610 else:
2608 content2 = b'\0\0'
2611 content2 = b'\0\0'
2609 else:
2612 else:
2610 # normal path: load contents
2613 # normal path: load contents
2611 if fctx1 is not None:
2614 if fctx1 is not None:
2612 content1 = fctx1.data()
2615 content1 = fctx1.data()
2613 if fctx2 is not None:
2616 if fctx2 is not None:
2614 content2 = fctx2.data()
2617 content2 = fctx2.data()
2615
2618
2616 if binary and opts.git and not opts.nobinary:
2619 if binary and opts.git and not opts.nobinary:
2617 text = mdiff.b85diff(content1, content2)
2620 text = mdiff.b85diff(content1, content2)
2618 if text:
2621 if text:
2619 header.append('index %s..%s' %
2622 header.append('index %s..%s' %
2620 (gitindex(content1), gitindex(content2)))
2623 (gitindex(content1), gitindex(content2)))
2621 hunks = (None, [text]),
2624 hunks = (None, [text]),
2622 else:
2625 else:
2623 if opts.git and opts.index > 0:
2626 if opts.git and opts.index > 0:
2624 flag = flag1
2627 flag = flag1
2625 if flag is None:
2628 if flag is None:
2626 flag = flag2
2629 flag = flag2
2627 header.append('index %s..%s %s' %
2630 header.append('index %s..%s %s' %
2628 (gitindex(content1)[0:opts.index],
2631 (gitindex(content1)[0:opts.index],
2629 gitindex(content2)[0:opts.index],
2632 gitindex(content2)[0:opts.index],
2630 gitmode[flag]))
2633 gitmode[flag]))
2631
2634
2632 uheaders, hunks = mdiff.unidiff(content1, date1,
2635 uheaders, hunks = mdiff.unidiff(content1, date1,
2633 content2, date2,
2636 content2, date2,
2634 path1, path2, opts=opts)
2637 path1, path2, opts=opts)
2635 header.extend(uheaders)
2638 header.extend(uheaders)
2636 yield header, hunks
2639 yield header, hunks
2637
2640
2638 def diffstatsum(stats):
2641 def diffstatsum(stats):
2639 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
2642 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
2640 for f, a, r, b in stats:
2643 for f, a, r, b in stats:
2641 maxfile = max(maxfile, encoding.colwidth(f))
2644 maxfile = max(maxfile, encoding.colwidth(f))
2642 maxtotal = max(maxtotal, a + r)
2645 maxtotal = max(maxtotal, a + r)
2643 addtotal += a
2646 addtotal += a
2644 removetotal += r
2647 removetotal += r
2645 binary = binary or b
2648 binary = binary or b
2646
2649
2647 return maxfile, maxtotal, addtotal, removetotal, binary
2650 return maxfile, maxtotal, addtotal, removetotal, binary
2648
2651
2649 def diffstatdata(lines):
2652 def diffstatdata(lines):
2650 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
2653 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
2651
2654
2652 results = []
2655 results = []
2653 filename, adds, removes, isbinary = None, 0, 0, False
2656 filename, adds, removes, isbinary = None, 0, 0, False
2654
2657
2655 def addresult():
2658 def addresult():
2656 if filename:
2659 if filename:
2657 results.append((filename, adds, removes, isbinary))
2660 results.append((filename, adds, removes, isbinary))
2658
2661
2659 # inheader is used to track if a line is in the
2662 # inheader is used to track if a line is in the
2660 # header portion of the diff. This helps properly account
2663 # header portion of the diff. This helps properly account
2661 # for lines that start with '--' or '++'
2664 # for lines that start with '--' or '++'
2662 inheader = False
2665 inheader = False
2663
2666
2664 for line in lines:
2667 for line in lines:
2665 if line.startswith('diff'):
2668 if line.startswith('diff'):
2666 addresult()
2669 addresult()
2667 # starting a new file diff
2670 # starting a new file diff
2668 # set numbers to 0 and reset inheader
2671 # set numbers to 0 and reset inheader
2669 inheader = True
2672 inheader = True
2670 adds, removes, isbinary = 0, 0, False
2673 adds, removes, isbinary = 0, 0, False
2671 if line.startswith('diff --git a/'):
2674 if line.startswith('diff --git a/'):
2672 filename = gitre.search(line).group(2)
2675 filename = gitre.search(line).group(2)
2673 elif line.startswith('diff -r'):
2676 elif line.startswith('diff -r'):
2674 # format: "diff -r ... -r ... filename"
2677 # format: "diff -r ... -r ... filename"
2675 filename = diffre.search(line).group(1)
2678 filename = diffre.search(line).group(1)
2676 elif line.startswith('@@'):
2679 elif line.startswith('@@'):
2677 inheader = False
2680 inheader = False
2678 elif line.startswith('+') and not inheader:
2681 elif line.startswith('+') and not inheader:
2679 adds += 1
2682 adds += 1
2680 elif line.startswith('-') and not inheader:
2683 elif line.startswith('-') and not inheader:
2681 removes += 1
2684 removes += 1
2682 elif (line.startswith('GIT binary patch') or
2685 elif (line.startswith('GIT binary patch') or
2683 line.startswith('Binary file')):
2686 line.startswith('Binary file')):
2684 isbinary = True
2687 isbinary = True
2685 addresult()
2688 addresult()
2686 return results
2689 return results
2687
2690
2688 def diffstat(lines, width=80):
2691 def diffstat(lines, width=80):
2689 output = []
2692 output = []
2690 stats = diffstatdata(lines)
2693 stats = diffstatdata(lines)
2691 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
2694 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
2692
2695
2693 countwidth = len(str(maxtotal))
2696 countwidth = len(str(maxtotal))
2694 if hasbinary and countwidth < 3:
2697 if hasbinary and countwidth < 3:
2695 countwidth = 3
2698 countwidth = 3
2696 graphwidth = width - countwidth - maxname - 6
2699 graphwidth = width - countwidth - maxname - 6
2697 if graphwidth < 10:
2700 if graphwidth < 10:
2698 graphwidth = 10
2701 graphwidth = 10
2699
2702
2700 def scale(i):
2703 def scale(i):
2701 if maxtotal <= graphwidth:
2704 if maxtotal <= graphwidth:
2702 return i
2705 return i
2703 # If diffstat runs out of room it doesn't print anything,
2706 # If diffstat runs out of room it doesn't print anything,
2704 # which isn't very useful, so always print at least one + or -
2707 # which isn't very useful, so always print at least one + or -
2705 # if there were at least some changes.
2708 # if there were at least some changes.
2706 return max(i * graphwidth // maxtotal, int(bool(i)))
2709 return max(i * graphwidth // maxtotal, int(bool(i)))
2707
2710
2708 for filename, adds, removes, isbinary in stats:
2711 for filename, adds, removes, isbinary in stats:
2709 if isbinary:
2712 if isbinary:
2710 count = 'Bin'
2713 count = 'Bin'
2711 else:
2714 else:
2712 count = adds + removes
2715 count = adds + removes
2713 pluses = '+' * scale(adds)
2716 pluses = '+' * scale(adds)
2714 minuses = '-' * scale(removes)
2717 minuses = '-' * scale(removes)
2715 output.append(' %s%s | %*s %s%s\n' %
2718 output.append(' %s%s | %*s %s%s\n' %
2716 (filename, ' ' * (maxname - encoding.colwidth(filename)),
2719 (filename, ' ' * (maxname - encoding.colwidth(filename)),
2717 countwidth, count, pluses, minuses))
2720 countwidth, count, pluses, minuses))
2718
2721
2719 if stats:
2722 if stats:
2720 output.append(_(' %d files changed, %d insertions(+), '
2723 output.append(_(' %d files changed, %d insertions(+), '
2721 '%d deletions(-)\n')
2724 '%d deletions(-)\n')
2722 % (len(stats), totaladds, totalremoves))
2725 % (len(stats), totaladds, totalremoves))
2723
2726
2724 return ''.join(output)
2727 return ''.join(output)
2725
2728
2726 def diffstatui(*args, **kw):
2729 def diffstatui(*args, **kw):
2727 '''like diffstat(), but yields 2-tuples of (output, label) for
2730 '''like diffstat(), but yields 2-tuples of (output, label) for
2728 ui.write()
2731 ui.write()
2729 '''
2732 '''
2730
2733
2731 for line in diffstat(*args, **kw).splitlines():
2734 for line in diffstat(*args, **kw).splitlines():
2732 if line and line[-1] in '+-':
2735 if line and line[-1] in '+-':
2733 name, graph = line.rsplit(' ', 1)
2736 name, graph = line.rsplit(' ', 1)
2734 yield (name + ' ', '')
2737 yield (name + ' ', '')
2735 m = re.search(r'\++', graph)
2738 m = re.search(r'\++', graph)
2736 if m:
2739 if m:
2737 yield (m.group(0), 'diffstat.inserted')
2740 yield (m.group(0), 'diffstat.inserted')
2738 m = re.search(r'-+', graph)
2741 m = re.search(r'-+', graph)
2739 if m:
2742 if m:
2740 yield (m.group(0), 'diffstat.deleted')
2743 yield (m.group(0), 'diffstat.deleted')
2741 else:
2744 else:
2742 yield (line, '')
2745 yield (line, '')
2743 yield ('\n', '')
2746 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now