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