##// END OF EJS Templates
patch: always raise PatchError with a message, simplify handling
Patrick Mezard -
r12674:aa2fe1f5 default
parent child Browse files
Show More
@@ -1,573 +1,569
1 # record.py
1 # record.py
2 #
2 #
3 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
3 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.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 '''commands to interactively select changes for commit/qrefresh'''
8 '''commands to interactively select changes for commit/qrefresh'''
9
9
10 from mercurial.i18n import gettext, _
10 from mercurial.i18n import gettext, _
11 from mercurial import cmdutil, commands, extensions, hg, mdiff, patch
11 from mercurial import cmdutil, commands, extensions, hg, mdiff, patch
12 from mercurial import util
12 from mercurial import util
13 import copy, cStringIO, errno, os, re, tempfile
13 import copy, cStringIO, errno, os, re, tempfile
14
14
15 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
15 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
16
16
17 def scanpatch(fp):
17 def scanpatch(fp):
18 """like patch.iterhunks, but yield different events
18 """like patch.iterhunks, but yield different events
19
19
20 - ('file', [header_lines + fromfile + tofile])
20 - ('file', [header_lines + fromfile + tofile])
21 - ('context', [context_lines])
21 - ('context', [context_lines])
22 - ('hunk', [hunk_lines])
22 - ('hunk', [hunk_lines])
23 - ('range', (-start,len, +start,len, diffp))
23 - ('range', (-start,len, +start,len, diffp))
24 """
24 """
25 lr = patch.linereader(fp)
25 lr = patch.linereader(fp)
26
26
27 def scanwhile(first, p):
27 def scanwhile(first, p):
28 """scan lr while predicate holds"""
28 """scan lr while predicate holds"""
29 lines = [first]
29 lines = [first]
30 while True:
30 while True:
31 line = lr.readline()
31 line = lr.readline()
32 if not line:
32 if not line:
33 break
33 break
34 if p(line):
34 if p(line):
35 lines.append(line)
35 lines.append(line)
36 else:
36 else:
37 lr.push(line)
37 lr.push(line)
38 break
38 break
39 return lines
39 return lines
40
40
41 while True:
41 while True:
42 line = lr.readline()
42 line = lr.readline()
43 if not line:
43 if not line:
44 break
44 break
45 if line.startswith('diff --git a/'):
45 if line.startswith('diff --git a/'):
46 def notheader(line):
46 def notheader(line):
47 s = line.split(None, 1)
47 s = line.split(None, 1)
48 return not s or s[0] not in ('---', 'diff')
48 return not s or s[0] not in ('---', 'diff')
49 header = scanwhile(line, notheader)
49 header = scanwhile(line, notheader)
50 fromfile = lr.readline()
50 fromfile = lr.readline()
51 if fromfile.startswith('---'):
51 if fromfile.startswith('---'):
52 tofile = lr.readline()
52 tofile = lr.readline()
53 header += [fromfile, tofile]
53 header += [fromfile, tofile]
54 else:
54 else:
55 lr.push(fromfile)
55 lr.push(fromfile)
56 yield 'file', header
56 yield 'file', header
57 elif line[0] == ' ':
57 elif line[0] == ' ':
58 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
58 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
59 elif line[0] in '-+':
59 elif line[0] in '-+':
60 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
60 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
61 else:
61 else:
62 m = lines_re.match(line)
62 m = lines_re.match(line)
63 if m:
63 if m:
64 yield 'range', m.groups()
64 yield 'range', m.groups()
65 else:
65 else:
66 raise patch.PatchError('unknown patch content: %r' % line)
66 raise patch.PatchError('unknown patch content: %r' % line)
67
67
68 class header(object):
68 class header(object):
69 """patch header
69 """patch header
70
70
71 XXX shoudn't we move this to mercurial/patch.py ?
71 XXX shoudn't we move this to mercurial/patch.py ?
72 """
72 """
73 diff_re = re.compile('diff --git a/(.*) b/(.*)$')
73 diff_re = re.compile('diff --git a/(.*) b/(.*)$')
74 allhunks_re = re.compile('(?:index|new file|deleted file) ')
74 allhunks_re = re.compile('(?:index|new file|deleted file) ')
75 pretty_re = re.compile('(?:new file|deleted file) ')
75 pretty_re = re.compile('(?:new file|deleted file) ')
76 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
76 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
77
77
78 def __init__(self, header):
78 def __init__(self, header):
79 self.header = header
79 self.header = header
80 self.hunks = []
80 self.hunks = []
81
81
82 def binary(self):
82 def binary(self):
83 for h in self.header:
83 for h in self.header:
84 if h.startswith('index '):
84 if h.startswith('index '):
85 return True
85 return True
86
86
87 def pretty(self, fp):
87 def pretty(self, fp):
88 for h in self.header:
88 for h in self.header:
89 if h.startswith('index '):
89 if h.startswith('index '):
90 fp.write(_('this modifies a binary file (all or nothing)\n'))
90 fp.write(_('this modifies a binary file (all or nothing)\n'))
91 break
91 break
92 if self.pretty_re.match(h):
92 if self.pretty_re.match(h):
93 fp.write(h)
93 fp.write(h)
94 if self.binary():
94 if self.binary():
95 fp.write(_('this is a binary file\n'))
95 fp.write(_('this is a binary file\n'))
96 break
96 break
97 if h.startswith('---'):
97 if h.startswith('---'):
98 fp.write(_('%d hunks, %d lines changed\n') %
98 fp.write(_('%d hunks, %d lines changed\n') %
99 (len(self.hunks),
99 (len(self.hunks),
100 sum([max(h.added, h.removed) for h in self.hunks])))
100 sum([max(h.added, h.removed) for h in self.hunks])))
101 break
101 break
102 fp.write(h)
102 fp.write(h)
103
103
104 def write(self, fp):
104 def write(self, fp):
105 fp.write(''.join(self.header))
105 fp.write(''.join(self.header))
106
106
107 def allhunks(self):
107 def allhunks(self):
108 for h in self.header:
108 for h in self.header:
109 if self.allhunks_re.match(h):
109 if self.allhunks_re.match(h):
110 return True
110 return True
111
111
112 def files(self):
112 def files(self):
113 fromfile, tofile = self.diff_re.match(self.header[0]).groups()
113 fromfile, tofile = self.diff_re.match(self.header[0]).groups()
114 if fromfile == tofile:
114 if fromfile == tofile:
115 return [fromfile]
115 return [fromfile]
116 return [fromfile, tofile]
116 return [fromfile, tofile]
117
117
118 def filename(self):
118 def filename(self):
119 return self.files()[-1]
119 return self.files()[-1]
120
120
121 def __repr__(self):
121 def __repr__(self):
122 return '<header %s>' % (' '.join(map(repr, self.files())))
122 return '<header %s>' % (' '.join(map(repr, self.files())))
123
123
124 def special(self):
124 def special(self):
125 for h in self.header:
125 for h in self.header:
126 if self.special_re.match(h):
126 if self.special_re.match(h):
127 return True
127 return True
128
128
129 def countchanges(hunk):
129 def countchanges(hunk):
130 """hunk -> (n+,n-)"""
130 """hunk -> (n+,n-)"""
131 add = len([h for h in hunk if h[0] == '+'])
131 add = len([h for h in hunk if h[0] == '+'])
132 rem = len([h for h in hunk if h[0] == '-'])
132 rem = len([h for h in hunk if h[0] == '-'])
133 return add, rem
133 return add, rem
134
134
135 class hunk(object):
135 class hunk(object):
136 """patch hunk
136 """patch hunk
137
137
138 XXX shouldn't we merge this with patch.hunk ?
138 XXX shouldn't we merge this with patch.hunk ?
139 """
139 """
140 maxcontext = 3
140 maxcontext = 3
141
141
142 def __init__(self, header, fromline, toline, proc, before, hunk, after):
142 def __init__(self, header, fromline, toline, proc, before, hunk, after):
143 def trimcontext(number, lines):
143 def trimcontext(number, lines):
144 delta = len(lines) - self.maxcontext
144 delta = len(lines) - self.maxcontext
145 if False and delta > 0:
145 if False and delta > 0:
146 return number + delta, lines[:self.maxcontext]
146 return number + delta, lines[:self.maxcontext]
147 return number, lines
147 return number, lines
148
148
149 self.header = header
149 self.header = header
150 self.fromline, self.before = trimcontext(fromline, before)
150 self.fromline, self.before = trimcontext(fromline, before)
151 self.toline, self.after = trimcontext(toline, after)
151 self.toline, self.after = trimcontext(toline, after)
152 self.proc = proc
152 self.proc = proc
153 self.hunk = hunk
153 self.hunk = hunk
154 self.added, self.removed = countchanges(self.hunk)
154 self.added, self.removed = countchanges(self.hunk)
155
155
156 def write(self, fp):
156 def write(self, fp):
157 delta = len(self.before) + len(self.after)
157 delta = len(self.before) + len(self.after)
158 if self.after and self.after[-1] == '\\ No newline at end of file\n':
158 if self.after and self.after[-1] == '\\ No newline at end of file\n':
159 delta -= 1
159 delta -= 1
160 fromlen = delta + self.removed
160 fromlen = delta + self.removed
161 tolen = delta + self.added
161 tolen = delta + self.added
162 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
162 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
163 (self.fromline, fromlen, self.toline, tolen,
163 (self.fromline, fromlen, self.toline, tolen,
164 self.proc and (' ' + self.proc)))
164 self.proc and (' ' + self.proc)))
165 fp.write(''.join(self.before + self.hunk + self.after))
165 fp.write(''.join(self.before + self.hunk + self.after))
166
166
167 pretty = write
167 pretty = write
168
168
169 def filename(self):
169 def filename(self):
170 return self.header.filename()
170 return self.header.filename()
171
171
172 def __repr__(self):
172 def __repr__(self):
173 return '<hunk %r@%d>' % (self.filename(), self.fromline)
173 return '<hunk %r@%d>' % (self.filename(), self.fromline)
174
174
175 def parsepatch(fp):
175 def parsepatch(fp):
176 """patch -> [] of hunks """
176 """patch -> [] of hunks """
177 class parser(object):
177 class parser(object):
178 """patch parsing state machine"""
178 """patch parsing state machine"""
179 def __init__(self):
179 def __init__(self):
180 self.fromline = 0
180 self.fromline = 0
181 self.toline = 0
181 self.toline = 0
182 self.proc = ''
182 self.proc = ''
183 self.header = None
183 self.header = None
184 self.context = []
184 self.context = []
185 self.before = []
185 self.before = []
186 self.hunk = []
186 self.hunk = []
187 self.stream = []
187 self.stream = []
188
188
189 def addrange(self, limits):
189 def addrange(self, limits):
190 fromstart, fromend, tostart, toend, proc = limits
190 fromstart, fromend, tostart, toend, proc = limits
191 self.fromline = int(fromstart)
191 self.fromline = int(fromstart)
192 self.toline = int(tostart)
192 self.toline = int(tostart)
193 self.proc = proc
193 self.proc = proc
194
194
195 def addcontext(self, context):
195 def addcontext(self, context):
196 if self.hunk:
196 if self.hunk:
197 h = hunk(self.header, self.fromline, self.toline, self.proc,
197 h = hunk(self.header, self.fromline, self.toline, self.proc,
198 self.before, self.hunk, context)
198 self.before, self.hunk, context)
199 self.header.hunks.append(h)
199 self.header.hunks.append(h)
200 self.stream.append(h)
200 self.stream.append(h)
201 self.fromline += len(self.before) + h.removed
201 self.fromline += len(self.before) + h.removed
202 self.toline += len(self.before) + h.added
202 self.toline += len(self.before) + h.added
203 self.before = []
203 self.before = []
204 self.hunk = []
204 self.hunk = []
205 self.proc = ''
205 self.proc = ''
206 self.context = context
206 self.context = context
207
207
208 def addhunk(self, hunk):
208 def addhunk(self, hunk):
209 if self.context:
209 if self.context:
210 self.before = self.context
210 self.before = self.context
211 self.context = []
211 self.context = []
212 self.hunk = hunk
212 self.hunk = hunk
213
213
214 def newfile(self, hdr):
214 def newfile(self, hdr):
215 self.addcontext([])
215 self.addcontext([])
216 h = header(hdr)
216 h = header(hdr)
217 self.stream.append(h)
217 self.stream.append(h)
218 self.header = h
218 self.header = h
219
219
220 def finished(self):
220 def finished(self):
221 self.addcontext([])
221 self.addcontext([])
222 return self.stream
222 return self.stream
223
223
224 transitions = {
224 transitions = {
225 'file': {'context': addcontext,
225 'file': {'context': addcontext,
226 'file': newfile,
226 'file': newfile,
227 'hunk': addhunk,
227 'hunk': addhunk,
228 'range': addrange},
228 'range': addrange},
229 'context': {'file': newfile,
229 'context': {'file': newfile,
230 'hunk': addhunk,
230 'hunk': addhunk,
231 'range': addrange},
231 'range': addrange},
232 'hunk': {'context': addcontext,
232 'hunk': {'context': addcontext,
233 'file': newfile,
233 'file': newfile,
234 'range': addrange},
234 'range': addrange},
235 'range': {'context': addcontext,
235 'range': {'context': addcontext,
236 'hunk': addhunk},
236 'hunk': addhunk},
237 }
237 }
238
238
239 p = parser()
239 p = parser()
240
240
241 state = 'context'
241 state = 'context'
242 for newstate, data in scanpatch(fp):
242 for newstate, data in scanpatch(fp):
243 try:
243 try:
244 p.transitions[state][newstate](p, data)
244 p.transitions[state][newstate](p, data)
245 except KeyError:
245 except KeyError:
246 raise patch.PatchError('unhandled transition: %s -> %s' %
246 raise patch.PatchError('unhandled transition: %s -> %s' %
247 (state, newstate))
247 (state, newstate))
248 state = newstate
248 state = newstate
249 return p.finished()
249 return p.finished()
250
250
251 def filterpatch(ui, chunks):
251 def filterpatch(ui, chunks):
252 """Interactively filter patch chunks into applied-only chunks"""
252 """Interactively filter patch chunks into applied-only chunks"""
253 chunks = list(chunks)
253 chunks = list(chunks)
254 chunks.reverse()
254 chunks.reverse()
255 seen = set()
255 seen = set()
256 def consumefile():
256 def consumefile():
257 """fetch next portion from chunks until a 'header' is seen
257 """fetch next portion from chunks until a 'header' is seen
258 NB: header == new-file mark
258 NB: header == new-file mark
259 """
259 """
260 consumed = []
260 consumed = []
261 while chunks:
261 while chunks:
262 if isinstance(chunks[-1], header):
262 if isinstance(chunks[-1], header):
263 break
263 break
264 else:
264 else:
265 consumed.append(chunks.pop())
265 consumed.append(chunks.pop())
266 return consumed
266 return consumed
267
267
268 resp_all = [None] # this two are changed from inside prompt,
268 resp_all = [None] # this two are changed from inside prompt,
269 resp_file = [None] # so can't be usual variables
269 resp_file = [None] # so can't be usual variables
270 applied = {} # 'filename' -> [] of chunks
270 applied = {} # 'filename' -> [] of chunks
271 def prompt(query):
271 def prompt(query):
272 """prompt query, and process base inputs
272 """prompt query, and process base inputs
273
273
274 - y/n for the rest of file
274 - y/n for the rest of file
275 - y/n for the rest
275 - y/n for the rest
276 - ? (help)
276 - ? (help)
277 - q (quit)
277 - q (quit)
278
278
279 Returns True/False and sets reps_all and resp_file as
279 Returns True/False and sets reps_all and resp_file as
280 appropriate.
280 appropriate.
281 """
281 """
282 if resp_all[0] is not None:
282 if resp_all[0] is not None:
283 return resp_all[0]
283 return resp_all[0]
284 if resp_file[0] is not None:
284 if resp_file[0] is not None:
285 return resp_file[0]
285 return resp_file[0]
286 while True:
286 while True:
287 resps = _('[Ynsfdaq?]')
287 resps = _('[Ynsfdaq?]')
288 choices = (_('&Yes, record this change'),
288 choices = (_('&Yes, record this change'),
289 _('&No, skip this change'),
289 _('&No, skip this change'),
290 _('&Skip remaining changes to this file'),
290 _('&Skip remaining changes to this file'),
291 _('Record remaining changes to this &file'),
291 _('Record remaining changes to this &file'),
292 _('&Done, skip remaining changes and files'),
292 _('&Done, skip remaining changes and files'),
293 _('Record &all changes to all remaining files'),
293 _('Record &all changes to all remaining files'),
294 _('&Quit, recording no changes'),
294 _('&Quit, recording no changes'),
295 _('&?'))
295 _('&?'))
296 r = ui.promptchoice("%s %s" % (query, resps), choices)
296 r = ui.promptchoice("%s %s" % (query, resps), choices)
297 ui.write("\n")
297 ui.write("\n")
298 if r == 7: # ?
298 if r == 7: # ?
299 doc = gettext(record.__doc__)
299 doc = gettext(record.__doc__)
300 c = doc.find('::') + 2
300 c = doc.find('::') + 2
301 for l in doc[c:].splitlines():
301 for l in doc[c:].splitlines():
302 if l.startswith(' '):
302 if l.startswith(' '):
303 ui.write(l.strip(), '\n')
303 ui.write(l.strip(), '\n')
304 continue
304 continue
305 elif r == 0: # yes
305 elif r == 0: # yes
306 ret = True
306 ret = True
307 elif r == 1: # no
307 elif r == 1: # no
308 ret = False
308 ret = False
309 elif r == 2: # Skip
309 elif r == 2: # Skip
310 ret = resp_file[0] = False
310 ret = resp_file[0] = False
311 elif r == 3: # file (Record remaining)
311 elif r == 3: # file (Record remaining)
312 ret = resp_file[0] = True
312 ret = resp_file[0] = True
313 elif r == 4: # done, skip remaining
313 elif r == 4: # done, skip remaining
314 ret = resp_all[0] = False
314 ret = resp_all[0] = False
315 elif r == 5: # all
315 elif r == 5: # all
316 ret = resp_all[0] = True
316 ret = resp_all[0] = True
317 elif r == 6: # quit
317 elif r == 6: # quit
318 raise util.Abort(_('user quit'))
318 raise util.Abort(_('user quit'))
319 return ret
319 return ret
320 pos, total = 0, len(chunks) - 1
320 pos, total = 0, len(chunks) - 1
321 while chunks:
321 while chunks:
322 pos = total - len(chunks) + 1
322 pos = total - len(chunks) + 1
323 chunk = chunks.pop()
323 chunk = chunks.pop()
324 if isinstance(chunk, header):
324 if isinstance(chunk, header):
325 # new-file mark
325 # new-file mark
326 resp_file = [None]
326 resp_file = [None]
327 fixoffset = 0
327 fixoffset = 0
328 hdr = ''.join(chunk.header)
328 hdr = ''.join(chunk.header)
329 if hdr in seen:
329 if hdr in seen:
330 consumefile()
330 consumefile()
331 continue
331 continue
332 seen.add(hdr)
332 seen.add(hdr)
333 if resp_all[0] is None:
333 if resp_all[0] is None:
334 chunk.pretty(ui)
334 chunk.pretty(ui)
335 r = prompt(_('examine changes to %s?') %
335 r = prompt(_('examine changes to %s?') %
336 _(' and ').join(map(repr, chunk.files())))
336 _(' and ').join(map(repr, chunk.files())))
337 if r:
337 if r:
338 applied[chunk.filename()] = [chunk]
338 applied[chunk.filename()] = [chunk]
339 if chunk.allhunks():
339 if chunk.allhunks():
340 applied[chunk.filename()] += consumefile()
340 applied[chunk.filename()] += consumefile()
341 else:
341 else:
342 consumefile()
342 consumefile()
343 else:
343 else:
344 # new hunk
344 # new hunk
345 if resp_file[0] is None and resp_all[0] is None:
345 if resp_file[0] is None and resp_all[0] is None:
346 chunk.pretty(ui)
346 chunk.pretty(ui)
347 r = total == 1 and prompt(_('record this change to %r?') %
347 r = total == 1 and prompt(_('record this change to %r?') %
348 chunk.filename()) \
348 chunk.filename()) \
349 or prompt(_('record change %d/%d to %r?') %
349 or prompt(_('record change %d/%d to %r?') %
350 (pos, total, chunk.filename()))
350 (pos, total, chunk.filename()))
351 if r:
351 if r:
352 if fixoffset:
352 if fixoffset:
353 chunk = copy.copy(chunk)
353 chunk = copy.copy(chunk)
354 chunk.toline += fixoffset
354 chunk.toline += fixoffset
355 applied[chunk.filename()].append(chunk)
355 applied[chunk.filename()].append(chunk)
356 else:
356 else:
357 fixoffset += chunk.removed - chunk.added
357 fixoffset += chunk.removed - chunk.added
358 return sum([h for h in applied.itervalues()
358 return sum([h for h in applied.itervalues()
359 if h[0].special() or len(h) > 1], [])
359 if h[0].special() or len(h) > 1], [])
360
360
361 def record(ui, repo, *pats, **opts):
361 def record(ui, repo, *pats, **opts):
362 '''interactively select changes to commit
362 '''interactively select changes to commit
363
363
364 If a list of files is omitted, all changes reported by :hg:`status`
364 If a list of files is omitted, all changes reported by :hg:`status`
365 will be candidates for recording.
365 will be candidates for recording.
366
366
367 See :hg:`help dates` for a list of formats valid for -d/--date.
367 See :hg:`help dates` for a list of formats valid for -d/--date.
368
368
369 You will be prompted for whether to record changes to each
369 You will be prompted for whether to record changes to each
370 modified file, and for files with multiple changes, for each
370 modified file, and for files with multiple changes, for each
371 change to use. For each query, the following responses are
371 change to use. For each query, the following responses are
372 possible::
372 possible::
373
373
374 y - record this change
374 y - record this change
375 n - skip this change
375 n - skip this change
376
376
377 s - skip remaining changes to this file
377 s - skip remaining changes to this file
378 f - record remaining changes to this file
378 f - record remaining changes to this file
379
379
380 d - done, skip remaining changes and files
380 d - done, skip remaining changes and files
381 a - record all changes to all remaining files
381 a - record all changes to all remaining files
382 q - quit, recording no changes
382 q - quit, recording no changes
383
383
384 ? - display help
384 ? - display help
385
385
386 This command is not available when committing a merge.'''
386 This command is not available when committing a merge.'''
387
387
388 dorecord(ui, repo, commands.commit, *pats, **opts)
388 dorecord(ui, repo, commands.commit, *pats, **opts)
389
389
390
390
391 def qrecord(ui, repo, patch, *pats, **opts):
391 def qrecord(ui, repo, patch, *pats, **opts):
392 '''interactively record a new patch
392 '''interactively record a new patch
393
393
394 See :hg:`help qnew` & :hg:`help record` for more information and
394 See :hg:`help qnew` & :hg:`help record` for more information and
395 usage.
395 usage.
396 '''
396 '''
397
397
398 try:
398 try:
399 mq = extensions.find('mq')
399 mq = extensions.find('mq')
400 except KeyError:
400 except KeyError:
401 raise util.Abort(_("'mq' extension not loaded"))
401 raise util.Abort(_("'mq' extension not loaded"))
402
402
403 def committomq(ui, repo, *pats, **opts):
403 def committomq(ui, repo, *pats, **opts):
404 mq.new(ui, repo, patch, *pats, **opts)
404 mq.new(ui, repo, patch, *pats, **opts)
405
405
406 opts = opts.copy()
406 opts = opts.copy()
407 opts['force'] = True # always 'qnew -f'
407 opts['force'] = True # always 'qnew -f'
408 dorecord(ui, repo, committomq, *pats, **opts)
408 dorecord(ui, repo, committomq, *pats, **opts)
409
409
410
410
411 def dorecord(ui, repo, commitfunc, *pats, **opts):
411 def dorecord(ui, repo, commitfunc, *pats, **opts):
412 if not ui.interactive():
412 if not ui.interactive():
413 raise util.Abort(_('running non-interactively, use commit instead'))
413 raise util.Abort(_('running non-interactively, use commit instead'))
414
414
415 def recordfunc(ui, repo, message, match, opts):
415 def recordfunc(ui, repo, message, match, opts):
416 """This is generic record driver.
416 """This is generic record driver.
417
417
418 Its job is to interactively filter local changes, and accordingly
418 Its job is to interactively filter local changes, and accordingly
419 prepare working dir into a state, where the job can be delegated to
419 prepare working dir into a state, where the job can be delegated to
420 non-interactive commit command such as 'commit' or 'qrefresh'.
420 non-interactive commit command such as 'commit' or 'qrefresh'.
421
421
422 After the actual job is done by non-interactive command, working dir
422 After the actual job is done by non-interactive command, working dir
423 state is restored to original.
423 state is restored to original.
424
424
425 In the end we'll record interesting changes, and everything else will be
425 In the end we'll record interesting changes, and everything else will be
426 left in place, so the user can continue his work.
426 left in place, so the user can continue his work.
427 """
427 """
428
428
429 merge = len(repo[None].parents()) > 1
429 merge = len(repo[None].parents()) > 1
430 if merge:
430 if merge:
431 raise util.Abort(_('cannot partially commit a merge '
431 raise util.Abort(_('cannot partially commit a merge '
432 '(use hg commit instead)'))
432 '(use hg commit instead)'))
433
433
434 changes = repo.status(match=match)[:3]
434 changes = repo.status(match=match)[:3]
435 diffopts = mdiff.diffopts(git=True, nodates=True)
435 diffopts = mdiff.diffopts(git=True, nodates=True)
436 chunks = patch.diff(repo, changes=changes, opts=diffopts)
436 chunks = patch.diff(repo, changes=changes, opts=diffopts)
437 fp = cStringIO.StringIO()
437 fp = cStringIO.StringIO()
438 fp.write(''.join(chunks))
438 fp.write(''.join(chunks))
439 fp.seek(0)
439 fp.seek(0)
440
440
441 # 1. filter patch, so we have intending-to apply subset of it
441 # 1. filter patch, so we have intending-to apply subset of it
442 chunks = filterpatch(ui, parsepatch(fp))
442 chunks = filterpatch(ui, parsepatch(fp))
443 del fp
443 del fp
444
444
445 contenders = set()
445 contenders = set()
446 for h in chunks:
446 for h in chunks:
447 try:
447 try:
448 contenders.update(set(h.files()))
448 contenders.update(set(h.files()))
449 except AttributeError:
449 except AttributeError:
450 pass
450 pass
451
451
452 changed = changes[0] + changes[1] + changes[2]
452 changed = changes[0] + changes[1] + changes[2]
453 newfiles = [f for f in changed if f in contenders]
453 newfiles = [f for f in changed if f in contenders]
454 if not newfiles:
454 if not newfiles:
455 ui.status(_('no changes to record\n'))
455 ui.status(_('no changes to record\n'))
456 return 0
456 return 0
457
457
458 modified = set(changes[0])
458 modified = set(changes[0])
459
459
460 # 2. backup changed files, so we can restore them in the end
460 # 2. backup changed files, so we can restore them in the end
461 backups = {}
461 backups = {}
462 backupdir = repo.join('record-backups')
462 backupdir = repo.join('record-backups')
463 try:
463 try:
464 os.mkdir(backupdir)
464 os.mkdir(backupdir)
465 except OSError, err:
465 except OSError, err:
466 if err.errno != errno.EEXIST:
466 if err.errno != errno.EEXIST:
467 raise
467 raise
468 try:
468 try:
469 # backup continues
469 # backup continues
470 for f in newfiles:
470 for f in newfiles:
471 if f not in modified:
471 if f not in modified:
472 continue
472 continue
473 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
473 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
474 dir=backupdir)
474 dir=backupdir)
475 os.close(fd)
475 os.close(fd)
476 ui.debug('backup %r as %r\n' % (f, tmpname))
476 ui.debug('backup %r as %r\n' % (f, tmpname))
477 util.copyfile(repo.wjoin(f), tmpname)
477 util.copyfile(repo.wjoin(f), tmpname)
478 backups[f] = tmpname
478 backups[f] = tmpname
479
479
480 fp = cStringIO.StringIO()
480 fp = cStringIO.StringIO()
481 for c in chunks:
481 for c in chunks:
482 if c.filename() in backups:
482 if c.filename() in backups:
483 c.write(fp)
483 c.write(fp)
484 dopatch = fp.tell()
484 dopatch = fp.tell()
485 fp.seek(0)
485 fp.seek(0)
486
486
487 # 3a. apply filtered patch to clean repo (clean)
487 # 3a. apply filtered patch to clean repo (clean)
488 if backups:
488 if backups:
489 hg.revert(repo, repo.dirstate.parents()[0],
489 hg.revert(repo, repo.dirstate.parents()[0],
490 lambda key: key in backups)
490 lambda key: key in backups)
491
491
492 # 3b. (apply)
492 # 3b. (apply)
493 if dopatch:
493 if dopatch:
494 try:
494 try:
495 ui.debug('applying patch\n')
495 ui.debug('applying patch\n')
496 ui.debug(fp.getvalue())
496 ui.debug(fp.getvalue())
497 pfiles = {}
497 pfiles = {}
498 patch.internalpatch(fp, ui, 1, repo.root, files=pfiles,
498 patch.internalpatch(fp, ui, 1, repo.root, files=pfiles,
499 eolmode=None)
499 eolmode=None)
500 cmdutil.updatedir(ui, repo, pfiles)
500 cmdutil.updatedir(ui, repo, pfiles)
501 except patch.PatchError, err:
501 except patch.PatchError, err:
502 s = str(err)
502 raise util.Abort(str(err))
503 if s:
504 raise util.Abort(s)
505 else:
506 raise util.Abort(_('patch failed to apply'))
507 del fp
503 del fp
508
504
509 # 4. We prepared working directory according to filtered patch.
505 # 4. We prepared working directory according to filtered patch.
510 # Now is the time to delegate the job to commit/qrefresh or the like!
506 # Now is the time to delegate the job to commit/qrefresh or the like!
511
507
512 # it is important to first chdir to repo root -- we'll call a
508 # it is important to first chdir to repo root -- we'll call a
513 # highlevel command with list of pathnames relative to repo root
509 # highlevel command with list of pathnames relative to repo root
514 cwd = os.getcwd()
510 cwd = os.getcwd()
515 os.chdir(repo.root)
511 os.chdir(repo.root)
516 try:
512 try:
517 commitfunc(ui, repo, *newfiles, **opts)
513 commitfunc(ui, repo, *newfiles, **opts)
518 finally:
514 finally:
519 os.chdir(cwd)
515 os.chdir(cwd)
520
516
521 return 0
517 return 0
522 finally:
518 finally:
523 # 5. finally restore backed-up files
519 # 5. finally restore backed-up files
524 try:
520 try:
525 for realname, tmpname in backups.iteritems():
521 for realname, tmpname in backups.iteritems():
526 ui.debug('restoring %r to %r\n' % (tmpname, realname))
522 ui.debug('restoring %r to %r\n' % (tmpname, realname))
527 util.copyfile(tmpname, repo.wjoin(realname))
523 util.copyfile(tmpname, repo.wjoin(realname))
528 os.unlink(tmpname)
524 os.unlink(tmpname)
529 os.rmdir(backupdir)
525 os.rmdir(backupdir)
530 except OSError:
526 except OSError:
531 pass
527 pass
532
528
533 # wrap ui.write so diff output can be labeled/colorized
529 # wrap ui.write so diff output can be labeled/colorized
534 def wrapwrite(orig, *args, **kw):
530 def wrapwrite(orig, *args, **kw):
535 label = kw.pop('label', '')
531 label = kw.pop('label', '')
536 for chunk, l in patch.difflabel(lambda: args):
532 for chunk, l in patch.difflabel(lambda: args):
537 orig(chunk, label=label + l)
533 orig(chunk, label=label + l)
538 oldwrite = ui.write
534 oldwrite = ui.write
539 extensions.wrapfunction(ui, 'write', wrapwrite)
535 extensions.wrapfunction(ui, 'write', wrapwrite)
540 try:
536 try:
541 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
537 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
542 finally:
538 finally:
543 ui.write = oldwrite
539 ui.write = oldwrite
544
540
545 cmdtable = {
541 cmdtable = {
546 "record":
542 "record":
547 (record,
543 (record,
548
544
549 # add commit options
545 # add commit options
550 commands.table['^commit|ci'][1],
546 commands.table['^commit|ci'][1],
551
547
552 _('hg record [OPTION]... [FILE]...')),
548 _('hg record [OPTION]... [FILE]...')),
553 }
549 }
554
550
555
551
556 def uisetup(ui):
552 def uisetup(ui):
557 try:
553 try:
558 mq = extensions.find('mq')
554 mq = extensions.find('mq')
559 except KeyError:
555 except KeyError:
560 return
556 return
561
557
562 qcmdtable = {
558 qcmdtable = {
563 "qrecord":
559 "qrecord":
564 (qrecord,
560 (qrecord,
565
561
566 # add qnew options, except '--force'
562 # add qnew options, except '--force'
567 [opt for opt in mq.cmdtable['^qnew'][1] if opt[1] != 'force'],
563 [opt for opt in mq.cmdtable['^qnew'][1] if opt[1] != 'force'],
568
564
569 _('hg qrecord [OPTION]... PATCH [FILE]...')),
565 _('hg qrecord [OPTION]... PATCH [FILE]...')),
570 }
566 }
571
567
572 cmdtable.update(qcmdtable)
568 cmdtable.update(qcmdtable)
573
569
@@ -1,1632 +1,1628
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 cStringIO, email.Parser, os, re
9 import cStringIO, email.Parser, os, re
10 import tempfile, zlib
10 import tempfile, zlib
11
11
12 from i18n import _
12 from i18n import _
13 from node import hex, nullid, short
13 from node import hex, nullid, short
14 import base85, mdiff, util, diffhelpers, copies, encoding
14 import base85, mdiff, util, diffhelpers, copies, encoding
15
15
16 gitre = re.compile('diff --git a/(.*) b/(.*)')
16 gitre = re.compile('diff --git a/(.*) b/(.*)')
17
17
18 class PatchError(Exception):
18 class PatchError(Exception):
19 pass
19 pass
20
20
21 # helper functions
21 # helper functions
22
22
23 def copyfile(src, dst, basedir):
23 def copyfile(src, dst, basedir):
24 abssrc, absdst = [util.canonpath(basedir, basedir, x) for x in [src, dst]]
24 abssrc, absdst = [util.canonpath(basedir, basedir, x) for x in [src, dst]]
25 if os.path.lexists(absdst):
25 if os.path.lexists(absdst):
26 raise util.Abort(_("cannot create %s: destination already exists") %
26 raise util.Abort(_("cannot create %s: destination already exists") %
27 dst)
27 dst)
28
28
29 dstdir = os.path.dirname(absdst)
29 dstdir = os.path.dirname(absdst)
30 if dstdir and not os.path.isdir(dstdir):
30 if dstdir and not os.path.isdir(dstdir):
31 try:
31 try:
32 os.makedirs(dstdir)
32 os.makedirs(dstdir)
33 except IOError:
33 except IOError:
34 raise util.Abort(
34 raise util.Abort(
35 _("cannot create %s: unable to create destination directory")
35 _("cannot create %s: unable to create destination directory")
36 % dst)
36 % dst)
37
37
38 util.copyfile(abssrc, absdst)
38 util.copyfile(abssrc, absdst)
39
39
40 # public functions
40 # public functions
41
41
42 def split(stream):
42 def split(stream):
43 '''return an iterator of individual patches from a stream'''
43 '''return an iterator of individual patches from a stream'''
44 def isheader(line, inheader):
44 def isheader(line, inheader):
45 if inheader and line[0] in (' ', '\t'):
45 if inheader and line[0] in (' ', '\t'):
46 # continuation
46 # continuation
47 return True
47 return True
48 if line[0] in (' ', '-', '+'):
48 if line[0] in (' ', '-', '+'):
49 # diff line - don't check for header pattern in there
49 # diff line - don't check for header pattern in there
50 return False
50 return False
51 l = line.split(': ', 1)
51 l = line.split(': ', 1)
52 return len(l) == 2 and ' ' not in l[0]
52 return len(l) == 2 and ' ' not in l[0]
53
53
54 def chunk(lines):
54 def chunk(lines):
55 return cStringIO.StringIO(''.join(lines))
55 return cStringIO.StringIO(''.join(lines))
56
56
57 def hgsplit(stream, cur):
57 def hgsplit(stream, cur):
58 inheader = True
58 inheader = True
59
59
60 for line in stream:
60 for line in stream:
61 if not line.strip():
61 if not line.strip():
62 inheader = False
62 inheader = False
63 if not inheader and line.startswith('# HG changeset patch'):
63 if not inheader and line.startswith('# HG changeset patch'):
64 yield chunk(cur)
64 yield chunk(cur)
65 cur = []
65 cur = []
66 inheader = True
66 inheader = True
67
67
68 cur.append(line)
68 cur.append(line)
69
69
70 if cur:
70 if cur:
71 yield chunk(cur)
71 yield chunk(cur)
72
72
73 def mboxsplit(stream, cur):
73 def mboxsplit(stream, cur):
74 for line in stream:
74 for line in stream:
75 if line.startswith('From '):
75 if line.startswith('From '):
76 for c in split(chunk(cur[1:])):
76 for c in split(chunk(cur[1:])):
77 yield c
77 yield c
78 cur = []
78 cur = []
79
79
80 cur.append(line)
80 cur.append(line)
81
81
82 if cur:
82 if cur:
83 for c in split(chunk(cur[1:])):
83 for c in split(chunk(cur[1:])):
84 yield c
84 yield c
85
85
86 def mimesplit(stream, cur):
86 def mimesplit(stream, cur):
87 def msgfp(m):
87 def msgfp(m):
88 fp = cStringIO.StringIO()
88 fp = cStringIO.StringIO()
89 g = email.Generator.Generator(fp, mangle_from_=False)
89 g = email.Generator.Generator(fp, mangle_from_=False)
90 g.flatten(m)
90 g.flatten(m)
91 fp.seek(0)
91 fp.seek(0)
92 return fp
92 return fp
93
93
94 for line in stream:
94 for line in stream:
95 cur.append(line)
95 cur.append(line)
96 c = chunk(cur)
96 c = chunk(cur)
97
97
98 m = email.Parser.Parser().parse(c)
98 m = email.Parser.Parser().parse(c)
99 if not m.is_multipart():
99 if not m.is_multipart():
100 yield msgfp(m)
100 yield msgfp(m)
101 else:
101 else:
102 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
102 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
103 for part in m.walk():
103 for part in m.walk():
104 ct = part.get_content_type()
104 ct = part.get_content_type()
105 if ct not in ok_types:
105 if ct not in ok_types:
106 continue
106 continue
107 yield msgfp(part)
107 yield msgfp(part)
108
108
109 def headersplit(stream, cur):
109 def headersplit(stream, cur):
110 inheader = False
110 inheader = False
111
111
112 for line in stream:
112 for line in stream:
113 if not inheader and isheader(line, inheader):
113 if not inheader and isheader(line, inheader):
114 yield chunk(cur)
114 yield chunk(cur)
115 cur = []
115 cur = []
116 inheader = True
116 inheader = True
117 if inheader and not isheader(line, inheader):
117 if inheader and not isheader(line, inheader):
118 inheader = False
118 inheader = False
119
119
120 cur.append(line)
120 cur.append(line)
121
121
122 if cur:
122 if cur:
123 yield chunk(cur)
123 yield chunk(cur)
124
124
125 def remainder(cur):
125 def remainder(cur):
126 yield chunk(cur)
126 yield chunk(cur)
127
127
128 class fiter(object):
128 class fiter(object):
129 def __init__(self, fp):
129 def __init__(self, fp):
130 self.fp = fp
130 self.fp = fp
131
131
132 def __iter__(self):
132 def __iter__(self):
133 return self
133 return self
134
134
135 def next(self):
135 def next(self):
136 l = self.fp.readline()
136 l = self.fp.readline()
137 if not l:
137 if not l:
138 raise StopIteration
138 raise StopIteration
139 return l
139 return l
140
140
141 inheader = False
141 inheader = False
142 cur = []
142 cur = []
143
143
144 mimeheaders = ['content-type']
144 mimeheaders = ['content-type']
145
145
146 if not hasattr(stream, 'next'):
146 if not hasattr(stream, 'next'):
147 # http responses, for example, have readline but not next
147 # http responses, for example, have readline but not next
148 stream = fiter(stream)
148 stream = fiter(stream)
149
149
150 for line in stream:
150 for line in stream:
151 cur.append(line)
151 cur.append(line)
152 if line.startswith('# HG changeset patch'):
152 if line.startswith('# HG changeset patch'):
153 return hgsplit(stream, cur)
153 return hgsplit(stream, cur)
154 elif line.startswith('From '):
154 elif line.startswith('From '):
155 return mboxsplit(stream, cur)
155 return mboxsplit(stream, cur)
156 elif isheader(line, inheader):
156 elif isheader(line, inheader):
157 inheader = True
157 inheader = True
158 if line.split(':', 1)[0].lower() in mimeheaders:
158 if line.split(':', 1)[0].lower() in mimeheaders:
159 # let email parser handle this
159 # let email parser handle this
160 return mimesplit(stream, cur)
160 return mimesplit(stream, cur)
161 elif line.startswith('--- ') and inheader:
161 elif line.startswith('--- ') and inheader:
162 # No evil headers seen by diff start, split by hand
162 # No evil headers seen by diff start, split by hand
163 return headersplit(stream, cur)
163 return headersplit(stream, cur)
164 # Not enough info, keep reading
164 # Not enough info, keep reading
165
165
166 # if we are here, we have a very plain patch
166 # if we are here, we have a very plain patch
167 return remainder(cur)
167 return remainder(cur)
168
168
169 def extract(ui, fileobj):
169 def extract(ui, fileobj):
170 '''extract patch from data read from fileobj.
170 '''extract patch from data read from fileobj.
171
171
172 patch can be a normal patch or contained in an email message.
172 patch can be a normal patch or contained in an email message.
173
173
174 return tuple (filename, message, user, date, branch, node, p1, p2).
174 return tuple (filename, message, user, date, branch, node, p1, p2).
175 Any item in the returned tuple can be None. If filename is None,
175 Any item in the returned tuple can be None. If filename is None,
176 fileobj did not contain a patch. Caller must unlink filename when done.'''
176 fileobj did not contain a patch. Caller must unlink filename when done.'''
177
177
178 # attempt to detect the start of a patch
178 # attempt to detect the start of a patch
179 # (this heuristic is borrowed from quilt)
179 # (this heuristic is borrowed from quilt)
180 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
180 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
181 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
181 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
182 r'---[ \t].*?^\+\+\+[ \t]|'
182 r'---[ \t].*?^\+\+\+[ \t]|'
183 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
183 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
184
184
185 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
185 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
186 tmpfp = os.fdopen(fd, 'w')
186 tmpfp = os.fdopen(fd, 'w')
187 try:
187 try:
188 msg = email.Parser.Parser().parse(fileobj)
188 msg = email.Parser.Parser().parse(fileobj)
189
189
190 subject = msg['Subject']
190 subject = msg['Subject']
191 user = msg['From']
191 user = msg['From']
192 if not subject and not user:
192 if not subject and not user:
193 # Not an email, restore parsed headers if any
193 # Not an email, restore parsed headers if any
194 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
194 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
195
195
196 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
196 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
197 # should try to parse msg['Date']
197 # should try to parse msg['Date']
198 date = None
198 date = None
199 nodeid = None
199 nodeid = None
200 branch = None
200 branch = None
201 parents = []
201 parents = []
202
202
203 if subject:
203 if subject:
204 if subject.startswith('[PATCH'):
204 if subject.startswith('[PATCH'):
205 pend = subject.find(']')
205 pend = subject.find(']')
206 if pend >= 0:
206 if pend >= 0:
207 subject = subject[pend + 1:].lstrip()
207 subject = subject[pend + 1:].lstrip()
208 subject = subject.replace('\n\t', ' ')
208 subject = subject.replace('\n\t', ' ')
209 ui.debug('Subject: %s\n' % subject)
209 ui.debug('Subject: %s\n' % subject)
210 if user:
210 if user:
211 ui.debug('From: %s\n' % user)
211 ui.debug('From: %s\n' % user)
212 diffs_seen = 0
212 diffs_seen = 0
213 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
213 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
214 message = ''
214 message = ''
215 for part in msg.walk():
215 for part in msg.walk():
216 content_type = part.get_content_type()
216 content_type = part.get_content_type()
217 ui.debug('Content-Type: %s\n' % content_type)
217 ui.debug('Content-Type: %s\n' % content_type)
218 if content_type not in ok_types:
218 if content_type not in ok_types:
219 continue
219 continue
220 payload = part.get_payload(decode=True)
220 payload = part.get_payload(decode=True)
221 m = diffre.search(payload)
221 m = diffre.search(payload)
222 if m:
222 if m:
223 hgpatch = False
223 hgpatch = False
224 hgpatchheader = False
224 hgpatchheader = False
225 ignoretext = False
225 ignoretext = False
226
226
227 ui.debug('found patch at byte %d\n' % m.start(0))
227 ui.debug('found patch at byte %d\n' % m.start(0))
228 diffs_seen += 1
228 diffs_seen += 1
229 cfp = cStringIO.StringIO()
229 cfp = cStringIO.StringIO()
230 for line in payload[:m.start(0)].splitlines():
230 for line in payload[:m.start(0)].splitlines():
231 if line.startswith('# HG changeset patch'):
231 if line.startswith('# HG changeset patch'):
232 ui.debug('patch generated by hg export\n')
232 ui.debug('patch generated by hg export\n')
233 hgpatchheader = True
233 hgpatchheader = True
234 # drop earlier commit message content
234 # drop earlier commit message content
235 cfp.seek(0)
235 cfp.seek(0)
236 cfp.truncate()
236 cfp.truncate()
237 subject = None
237 subject = None
238 elif hgpatchheader:
238 elif hgpatchheader:
239 if line.startswith('# User '):
239 if line.startswith('# User '):
240 user = line[7:]
240 user = line[7:]
241 ui.debug('From: %s\n' % user)
241 ui.debug('From: %s\n' % user)
242 elif line.startswith("# Date "):
242 elif line.startswith("# Date "):
243 date = line[7:]
243 date = line[7:]
244 elif line.startswith("# Branch "):
244 elif line.startswith("# Branch "):
245 branch = line[9:]
245 branch = line[9:]
246 elif line.startswith("# Node ID "):
246 elif line.startswith("# Node ID "):
247 nodeid = line[10:]
247 nodeid = line[10:]
248 elif line.startswith("# Parent "):
248 elif line.startswith("# Parent "):
249 parents.append(line[10:])
249 parents.append(line[10:])
250 elif not line.startswith("# "):
250 elif not line.startswith("# "):
251 hgpatchheader = False
251 hgpatchheader = False
252 hgpatch = True
252 hgpatch = True
253 elif line == '---' and gitsendmail:
253 elif line == '---' and gitsendmail:
254 ignoretext = True
254 ignoretext = True
255 if not hgpatchheader and not ignoretext:
255 if not hgpatchheader and not ignoretext:
256 cfp.write(line)
256 cfp.write(line)
257 cfp.write('\n')
257 cfp.write('\n')
258 message = cfp.getvalue()
258 message = cfp.getvalue()
259 if tmpfp:
259 if tmpfp:
260 tmpfp.write(payload)
260 tmpfp.write(payload)
261 if not payload.endswith('\n'):
261 if not payload.endswith('\n'):
262 tmpfp.write('\n')
262 tmpfp.write('\n')
263 elif not diffs_seen and message and content_type == 'text/plain':
263 elif not diffs_seen and message and content_type == 'text/plain':
264 message += '\n' + payload
264 message += '\n' + payload
265 except:
265 except:
266 tmpfp.close()
266 tmpfp.close()
267 os.unlink(tmpname)
267 os.unlink(tmpname)
268 raise
268 raise
269
269
270 if subject and not message.startswith(subject):
270 if subject and not message.startswith(subject):
271 message = '%s\n%s' % (subject, message)
271 message = '%s\n%s' % (subject, message)
272 tmpfp.close()
272 tmpfp.close()
273 if not diffs_seen:
273 if not diffs_seen:
274 os.unlink(tmpname)
274 os.unlink(tmpname)
275 return None, message, user, date, branch, None, None, None
275 return None, message, user, date, branch, None, None, None
276 p1 = parents and parents.pop(0) or None
276 p1 = parents and parents.pop(0) or None
277 p2 = parents and parents.pop(0) or None
277 p2 = parents and parents.pop(0) or None
278 return tmpname, message, user, date, branch, nodeid, p1, p2
278 return tmpname, message, user, date, branch, nodeid, p1, p2
279
279
280 class patchmeta(object):
280 class patchmeta(object):
281 """Patched file metadata
281 """Patched file metadata
282
282
283 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
283 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
284 or COPY. 'path' is patched file path. 'oldpath' is set to the
284 or COPY. 'path' is patched file path. 'oldpath' is set to the
285 origin file when 'op' is either COPY or RENAME, None otherwise. If
285 origin file when 'op' is either COPY or RENAME, None otherwise. If
286 file mode is changed, 'mode' is a tuple (islink, isexec) where
286 file mode is changed, 'mode' is a tuple (islink, isexec) where
287 'islink' is True if the file is a symlink and 'isexec' is True if
287 'islink' is True if the file is a symlink and 'isexec' is True if
288 the file is executable. Otherwise, 'mode' is None.
288 the file is executable. Otherwise, 'mode' is None.
289 """
289 """
290 def __init__(self, path):
290 def __init__(self, path):
291 self.path = path
291 self.path = path
292 self.oldpath = None
292 self.oldpath = None
293 self.mode = None
293 self.mode = None
294 self.op = 'MODIFY'
294 self.op = 'MODIFY'
295 self.binary = False
295 self.binary = False
296
296
297 def setmode(self, mode):
297 def setmode(self, mode):
298 islink = mode & 020000
298 islink = mode & 020000
299 isexec = mode & 0100
299 isexec = mode & 0100
300 self.mode = (islink, isexec)
300 self.mode = (islink, isexec)
301
301
302 def __repr__(self):
302 def __repr__(self):
303 return "<patchmeta %s %r>" % (self.op, self.path)
303 return "<patchmeta %s %r>" % (self.op, self.path)
304
304
305 def readgitpatch(lr):
305 def readgitpatch(lr):
306 """extract git-style metadata about patches from <patchname>"""
306 """extract git-style metadata about patches from <patchname>"""
307
307
308 # Filter patch for git information
308 # Filter patch for git information
309 gp = None
309 gp = None
310 gitpatches = []
310 gitpatches = []
311 for line in lr:
311 for line in lr:
312 line = line.rstrip(' \r\n')
312 line = line.rstrip(' \r\n')
313 if line.startswith('diff --git'):
313 if line.startswith('diff --git'):
314 m = gitre.match(line)
314 m = gitre.match(line)
315 if m:
315 if m:
316 if gp:
316 if gp:
317 gitpatches.append(gp)
317 gitpatches.append(gp)
318 dst = m.group(2)
318 dst = m.group(2)
319 gp = patchmeta(dst)
319 gp = patchmeta(dst)
320 elif gp:
320 elif gp:
321 if line.startswith('--- '):
321 if line.startswith('--- '):
322 gitpatches.append(gp)
322 gitpatches.append(gp)
323 gp = None
323 gp = None
324 continue
324 continue
325 if line.startswith('rename from '):
325 if line.startswith('rename from '):
326 gp.op = 'RENAME'
326 gp.op = 'RENAME'
327 gp.oldpath = line[12:]
327 gp.oldpath = line[12:]
328 elif line.startswith('rename to '):
328 elif line.startswith('rename to '):
329 gp.path = line[10:]
329 gp.path = line[10:]
330 elif line.startswith('copy from '):
330 elif line.startswith('copy from '):
331 gp.op = 'COPY'
331 gp.op = 'COPY'
332 gp.oldpath = line[10:]
332 gp.oldpath = line[10:]
333 elif line.startswith('copy to '):
333 elif line.startswith('copy to '):
334 gp.path = line[8:]
334 gp.path = line[8:]
335 elif line.startswith('deleted file'):
335 elif line.startswith('deleted file'):
336 gp.op = 'DELETE'
336 gp.op = 'DELETE'
337 elif line.startswith('new file mode '):
337 elif line.startswith('new file mode '):
338 gp.op = 'ADD'
338 gp.op = 'ADD'
339 gp.setmode(int(line[-6:], 8))
339 gp.setmode(int(line[-6:], 8))
340 elif line.startswith('new mode '):
340 elif line.startswith('new mode '):
341 gp.setmode(int(line[-6:], 8))
341 gp.setmode(int(line[-6:], 8))
342 elif line.startswith('GIT binary patch'):
342 elif line.startswith('GIT binary patch'):
343 gp.binary = True
343 gp.binary = True
344 if gp:
344 if gp:
345 gitpatches.append(gp)
345 gitpatches.append(gp)
346
346
347 return gitpatches
347 return gitpatches
348
348
349 class linereader(object):
349 class linereader(object):
350 # simple class to allow pushing lines back into the input stream
350 # simple class to allow pushing lines back into the input stream
351 def __init__(self, fp, textmode=False):
351 def __init__(self, fp, textmode=False):
352 self.fp = fp
352 self.fp = fp
353 self.buf = []
353 self.buf = []
354 self.textmode = textmode
354 self.textmode = textmode
355 self.eol = None
355 self.eol = None
356
356
357 def push(self, line):
357 def push(self, line):
358 if line is not None:
358 if line is not None:
359 self.buf.append(line)
359 self.buf.append(line)
360
360
361 def readline(self):
361 def readline(self):
362 if self.buf:
362 if self.buf:
363 l = self.buf[0]
363 l = self.buf[0]
364 del self.buf[0]
364 del self.buf[0]
365 return l
365 return l
366 l = self.fp.readline()
366 l = self.fp.readline()
367 if not self.eol:
367 if not self.eol:
368 if l.endswith('\r\n'):
368 if l.endswith('\r\n'):
369 self.eol = '\r\n'
369 self.eol = '\r\n'
370 elif l.endswith('\n'):
370 elif l.endswith('\n'):
371 self.eol = '\n'
371 self.eol = '\n'
372 if self.textmode and l.endswith('\r\n'):
372 if self.textmode and l.endswith('\r\n'):
373 l = l[:-2] + '\n'
373 l = l[:-2] + '\n'
374 return l
374 return l
375
375
376 def __iter__(self):
376 def __iter__(self):
377 while 1:
377 while 1:
378 l = self.readline()
378 l = self.readline()
379 if not l:
379 if not l:
380 break
380 break
381 yield l
381 yield l
382
382
383 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
383 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
384 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
384 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
385 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
385 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
386 eolmodes = ['strict', 'crlf', 'lf', 'auto']
386 eolmodes = ['strict', 'crlf', 'lf', 'auto']
387
387
388 class patchfile(object):
388 class patchfile(object):
389 def __init__(self, ui, fname, opener, missing=False, eolmode='strict'):
389 def __init__(self, ui, fname, opener, missing=False, eolmode='strict'):
390 self.fname = fname
390 self.fname = fname
391 self.eolmode = eolmode
391 self.eolmode = eolmode
392 self.eol = None
392 self.eol = None
393 self.opener = opener
393 self.opener = opener
394 self.ui = ui
394 self.ui = ui
395 self.lines = []
395 self.lines = []
396 self.exists = False
396 self.exists = False
397 self.missing = missing
397 self.missing = missing
398 if not missing:
398 if not missing:
399 try:
399 try:
400 self.lines = self.readlines(fname)
400 self.lines = self.readlines(fname)
401 self.exists = True
401 self.exists = True
402 except IOError:
402 except IOError:
403 pass
403 pass
404 else:
404 else:
405 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
405 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
406
406
407 self.hash = {}
407 self.hash = {}
408 self.dirty = 0
408 self.dirty = 0
409 self.offset = 0
409 self.offset = 0
410 self.skew = 0
410 self.skew = 0
411 self.rej = []
411 self.rej = []
412 self.fileprinted = False
412 self.fileprinted = False
413 self.printfile(False)
413 self.printfile(False)
414 self.hunks = 0
414 self.hunks = 0
415
415
416 def readlines(self, fname):
416 def readlines(self, fname):
417 if os.path.islink(fname):
417 if os.path.islink(fname):
418 return [os.readlink(fname)]
418 return [os.readlink(fname)]
419 fp = self.opener(fname, 'r')
419 fp = self.opener(fname, 'r')
420 try:
420 try:
421 lr = linereader(fp, self.eolmode != 'strict')
421 lr = linereader(fp, self.eolmode != 'strict')
422 lines = list(lr)
422 lines = list(lr)
423 self.eol = lr.eol
423 self.eol = lr.eol
424 return lines
424 return lines
425 finally:
425 finally:
426 fp.close()
426 fp.close()
427
427
428 def writelines(self, fname, lines):
428 def writelines(self, fname, lines):
429 # Ensure supplied data ends in fname, being a regular file or
429 # Ensure supplied data ends in fname, being a regular file or
430 # a symlink. cmdutil.updatedir will -too magically- take care
430 # a symlink. cmdutil.updatedir will -too magically- take care
431 # of setting it to the proper type afterwards.
431 # of setting it to the proper type afterwards.
432 islink = os.path.islink(fname)
432 islink = os.path.islink(fname)
433 if islink:
433 if islink:
434 fp = cStringIO.StringIO()
434 fp = cStringIO.StringIO()
435 else:
435 else:
436 fp = self.opener(fname, 'w')
436 fp = self.opener(fname, 'w')
437 try:
437 try:
438 if self.eolmode == 'auto':
438 if self.eolmode == 'auto':
439 eol = self.eol
439 eol = self.eol
440 elif self.eolmode == 'crlf':
440 elif self.eolmode == 'crlf':
441 eol = '\r\n'
441 eol = '\r\n'
442 else:
442 else:
443 eol = '\n'
443 eol = '\n'
444
444
445 if self.eolmode != 'strict' and eol and eol != '\n':
445 if self.eolmode != 'strict' and eol and eol != '\n':
446 for l in lines:
446 for l in lines:
447 if l and l[-1] == '\n':
447 if l and l[-1] == '\n':
448 l = l[:-1] + eol
448 l = l[:-1] + eol
449 fp.write(l)
449 fp.write(l)
450 else:
450 else:
451 fp.writelines(lines)
451 fp.writelines(lines)
452 if islink:
452 if islink:
453 self.opener.symlink(fp.getvalue(), fname)
453 self.opener.symlink(fp.getvalue(), fname)
454 finally:
454 finally:
455 fp.close()
455 fp.close()
456
456
457 def unlink(self, fname):
457 def unlink(self, fname):
458 os.unlink(fname)
458 os.unlink(fname)
459
459
460 def printfile(self, warn):
460 def printfile(self, warn):
461 if self.fileprinted:
461 if self.fileprinted:
462 return
462 return
463 if warn or self.ui.verbose:
463 if warn or self.ui.verbose:
464 self.fileprinted = True
464 self.fileprinted = True
465 s = _("patching file %s\n") % self.fname
465 s = _("patching file %s\n") % self.fname
466 if warn:
466 if warn:
467 self.ui.warn(s)
467 self.ui.warn(s)
468 else:
468 else:
469 self.ui.note(s)
469 self.ui.note(s)
470
470
471
471
472 def findlines(self, l, linenum):
472 def findlines(self, l, linenum):
473 # looks through the hash and finds candidate lines. The
473 # looks through the hash and finds candidate lines. The
474 # result is a list of line numbers sorted based on distance
474 # result is a list of line numbers sorted based on distance
475 # from linenum
475 # from linenum
476
476
477 cand = self.hash.get(l, [])
477 cand = self.hash.get(l, [])
478 if len(cand) > 1:
478 if len(cand) > 1:
479 # resort our list of potentials forward then back.
479 # resort our list of potentials forward then back.
480 cand.sort(key=lambda x: abs(x - linenum))
480 cand.sort(key=lambda x: abs(x - linenum))
481 return cand
481 return cand
482
482
483 def hashlines(self):
483 def hashlines(self):
484 self.hash = {}
484 self.hash = {}
485 for x, s in enumerate(self.lines):
485 for x, s in enumerate(self.lines):
486 self.hash.setdefault(s, []).append(x)
486 self.hash.setdefault(s, []).append(x)
487
487
488 def write_rej(self):
488 def write_rej(self):
489 # our rejects are a little different from patch(1). This always
489 # our rejects are a little different from patch(1). This always
490 # creates rejects in the same form as the original patch. A file
490 # creates rejects in the same form as the original patch. A file
491 # header is inserted so that you can run the reject through patch again
491 # header is inserted so that you can run the reject through patch again
492 # without having to type the filename.
492 # without having to type the filename.
493
493
494 if not self.rej:
494 if not self.rej:
495 return
495 return
496
496
497 fname = self.fname + ".rej"
497 fname = self.fname + ".rej"
498 self.ui.warn(
498 self.ui.warn(
499 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
499 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
500 (len(self.rej), self.hunks, fname))
500 (len(self.rej), self.hunks, fname))
501
501
502 def rejlines():
502 def rejlines():
503 base = os.path.basename(self.fname)
503 base = os.path.basename(self.fname)
504 yield "--- %s\n+++ %s\n" % (base, base)
504 yield "--- %s\n+++ %s\n" % (base, base)
505 for x in self.rej:
505 for x in self.rej:
506 for l in x.hunk:
506 for l in x.hunk:
507 yield l
507 yield l
508 if l[-1] != '\n':
508 if l[-1] != '\n':
509 yield "\n\ No newline at end of file\n"
509 yield "\n\ No newline at end of file\n"
510
510
511 self.writelines(fname, rejlines())
511 self.writelines(fname, rejlines())
512
512
513 def apply(self, h):
513 def apply(self, h):
514 if not h.complete():
514 if not h.complete():
515 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
515 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
516 (h.number, h.desc, len(h.a), h.lena, len(h.b),
516 (h.number, h.desc, len(h.a), h.lena, len(h.b),
517 h.lenb))
517 h.lenb))
518
518
519 self.hunks += 1
519 self.hunks += 1
520
520
521 if self.missing:
521 if self.missing:
522 self.rej.append(h)
522 self.rej.append(h)
523 return -1
523 return -1
524
524
525 if self.exists and h.createfile():
525 if self.exists and h.createfile():
526 self.ui.warn(_("file %s already exists\n") % self.fname)
526 self.ui.warn(_("file %s already exists\n") % self.fname)
527 self.rej.append(h)
527 self.rej.append(h)
528 return -1
528 return -1
529
529
530 if isinstance(h, binhunk):
530 if isinstance(h, binhunk):
531 if h.rmfile():
531 if h.rmfile():
532 self.unlink(self.fname)
532 self.unlink(self.fname)
533 else:
533 else:
534 self.lines[:] = h.new()
534 self.lines[:] = h.new()
535 self.offset += len(h.new())
535 self.offset += len(h.new())
536 self.dirty = 1
536 self.dirty = 1
537 return 0
537 return 0
538
538
539 horig = h
539 horig = h
540 if (self.eolmode in ('crlf', 'lf')
540 if (self.eolmode in ('crlf', 'lf')
541 or self.eolmode == 'auto' and self.eol):
541 or self.eolmode == 'auto' and self.eol):
542 # If new eols are going to be normalized, then normalize
542 # If new eols are going to be normalized, then normalize
543 # hunk data before patching. Otherwise, preserve input
543 # hunk data before patching. Otherwise, preserve input
544 # line-endings.
544 # line-endings.
545 h = h.getnormalized()
545 h = h.getnormalized()
546
546
547 # fast case first, no offsets, no fuzz
547 # fast case first, no offsets, no fuzz
548 old = h.old()
548 old = h.old()
549 # patch starts counting at 1 unless we are adding the file
549 # patch starts counting at 1 unless we are adding the file
550 if h.starta == 0:
550 if h.starta == 0:
551 start = 0
551 start = 0
552 else:
552 else:
553 start = h.starta + self.offset - 1
553 start = h.starta + self.offset - 1
554 orig_start = start
554 orig_start = start
555 # if there's skew we want to emit the "(offset %d lines)" even
555 # if there's skew we want to emit the "(offset %d lines)" even
556 # when the hunk cleanly applies at start + skew, so skip the
556 # when the hunk cleanly applies at start + skew, so skip the
557 # fast case code
557 # fast case code
558 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
558 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
559 if h.rmfile():
559 if h.rmfile():
560 self.unlink(self.fname)
560 self.unlink(self.fname)
561 else:
561 else:
562 self.lines[start : start + h.lena] = h.new()
562 self.lines[start : start + h.lena] = h.new()
563 self.offset += h.lenb - h.lena
563 self.offset += h.lenb - h.lena
564 self.dirty = 1
564 self.dirty = 1
565 return 0
565 return 0
566
566
567 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
567 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
568 self.hashlines()
568 self.hashlines()
569 if h.hunk[-1][0] != ' ':
569 if h.hunk[-1][0] != ' ':
570 # if the hunk tried to put something at the bottom of the file
570 # if the hunk tried to put something at the bottom of the file
571 # override the start line and use eof here
571 # override the start line and use eof here
572 search_start = len(self.lines)
572 search_start = len(self.lines)
573 else:
573 else:
574 search_start = orig_start + self.skew
574 search_start = orig_start + self.skew
575
575
576 for fuzzlen in xrange(3):
576 for fuzzlen in xrange(3):
577 for toponly in [True, False]:
577 for toponly in [True, False]:
578 old = h.old(fuzzlen, toponly)
578 old = h.old(fuzzlen, toponly)
579
579
580 cand = self.findlines(old[0][1:], search_start)
580 cand = self.findlines(old[0][1:], search_start)
581 for l in cand:
581 for l in cand:
582 if diffhelpers.testhunk(old, self.lines, l) == 0:
582 if diffhelpers.testhunk(old, self.lines, l) == 0:
583 newlines = h.new(fuzzlen, toponly)
583 newlines = h.new(fuzzlen, toponly)
584 self.lines[l : l + len(old)] = newlines
584 self.lines[l : l + len(old)] = newlines
585 self.offset += len(newlines) - len(old)
585 self.offset += len(newlines) - len(old)
586 self.skew = l - orig_start
586 self.skew = l - orig_start
587 self.dirty = 1
587 self.dirty = 1
588 offset = l - orig_start - fuzzlen
588 offset = l - orig_start - fuzzlen
589 if fuzzlen:
589 if fuzzlen:
590 msg = _("Hunk #%d succeeded at %d "
590 msg = _("Hunk #%d succeeded at %d "
591 "with fuzz %d "
591 "with fuzz %d "
592 "(offset %d lines).\n")
592 "(offset %d lines).\n")
593 self.printfile(True)
593 self.printfile(True)
594 self.ui.warn(msg %
594 self.ui.warn(msg %
595 (h.number, l + 1, fuzzlen, offset))
595 (h.number, l + 1, fuzzlen, offset))
596 else:
596 else:
597 msg = _("Hunk #%d succeeded at %d "
597 msg = _("Hunk #%d succeeded at %d "
598 "(offset %d lines).\n")
598 "(offset %d lines).\n")
599 self.ui.note(msg % (h.number, l + 1, offset))
599 self.ui.note(msg % (h.number, l + 1, offset))
600 return fuzzlen
600 return fuzzlen
601 self.printfile(True)
601 self.printfile(True)
602 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
602 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
603 self.rej.append(horig)
603 self.rej.append(horig)
604 return -1
604 return -1
605
605
606 class hunk(object):
606 class hunk(object):
607 def __init__(self, desc, num, lr, context, create=False, remove=False):
607 def __init__(self, desc, num, lr, context, create=False, remove=False):
608 self.number = num
608 self.number = num
609 self.desc = desc
609 self.desc = desc
610 self.hunk = [desc]
610 self.hunk = [desc]
611 self.a = []
611 self.a = []
612 self.b = []
612 self.b = []
613 self.starta = self.lena = None
613 self.starta = self.lena = None
614 self.startb = self.lenb = None
614 self.startb = self.lenb = None
615 if lr is not None:
615 if lr is not None:
616 if context:
616 if context:
617 self.read_context_hunk(lr)
617 self.read_context_hunk(lr)
618 else:
618 else:
619 self.read_unified_hunk(lr)
619 self.read_unified_hunk(lr)
620 self.create = create
620 self.create = create
621 self.remove = remove and not create
621 self.remove = remove and not create
622
622
623 def getnormalized(self):
623 def getnormalized(self):
624 """Return a copy with line endings normalized to LF."""
624 """Return a copy with line endings normalized to LF."""
625
625
626 def normalize(lines):
626 def normalize(lines):
627 nlines = []
627 nlines = []
628 for line in lines:
628 for line in lines:
629 if line.endswith('\r\n'):
629 if line.endswith('\r\n'):
630 line = line[:-2] + '\n'
630 line = line[:-2] + '\n'
631 nlines.append(line)
631 nlines.append(line)
632 return nlines
632 return nlines
633
633
634 # Dummy object, it is rebuilt manually
634 # Dummy object, it is rebuilt manually
635 nh = hunk(self.desc, self.number, None, None, False, False)
635 nh = hunk(self.desc, self.number, None, None, False, False)
636 nh.number = self.number
636 nh.number = self.number
637 nh.desc = self.desc
637 nh.desc = self.desc
638 nh.hunk = self.hunk
638 nh.hunk = self.hunk
639 nh.a = normalize(self.a)
639 nh.a = normalize(self.a)
640 nh.b = normalize(self.b)
640 nh.b = normalize(self.b)
641 nh.starta = self.starta
641 nh.starta = self.starta
642 nh.startb = self.startb
642 nh.startb = self.startb
643 nh.lena = self.lena
643 nh.lena = self.lena
644 nh.lenb = self.lenb
644 nh.lenb = self.lenb
645 nh.create = self.create
645 nh.create = self.create
646 nh.remove = self.remove
646 nh.remove = self.remove
647 return nh
647 return nh
648
648
649 def read_unified_hunk(self, lr):
649 def read_unified_hunk(self, lr):
650 m = unidesc.match(self.desc)
650 m = unidesc.match(self.desc)
651 if not m:
651 if not m:
652 raise PatchError(_("bad hunk #%d") % self.number)
652 raise PatchError(_("bad hunk #%d") % self.number)
653 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
653 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
654 if self.lena is None:
654 if self.lena is None:
655 self.lena = 1
655 self.lena = 1
656 else:
656 else:
657 self.lena = int(self.lena)
657 self.lena = int(self.lena)
658 if self.lenb is None:
658 if self.lenb is None:
659 self.lenb = 1
659 self.lenb = 1
660 else:
660 else:
661 self.lenb = int(self.lenb)
661 self.lenb = int(self.lenb)
662 self.starta = int(self.starta)
662 self.starta = int(self.starta)
663 self.startb = int(self.startb)
663 self.startb = int(self.startb)
664 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
664 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
665 # if we hit eof before finishing out the hunk, the last line will
665 # if we hit eof before finishing out the hunk, the last line will
666 # be zero length. Lets try to fix it up.
666 # be zero length. Lets try to fix it up.
667 while len(self.hunk[-1]) == 0:
667 while len(self.hunk[-1]) == 0:
668 del self.hunk[-1]
668 del self.hunk[-1]
669 del self.a[-1]
669 del self.a[-1]
670 del self.b[-1]
670 del self.b[-1]
671 self.lena -= 1
671 self.lena -= 1
672 self.lenb -= 1
672 self.lenb -= 1
673
673
674 def read_context_hunk(self, lr):
674 def read_context_hunk(self, lr):
675 self.desc = lr.readline()
675 self.desc = lr.readline()
676 m = contextdesc.match(self.desc)
676 m = contextdesc.match(self.desc)
677 if not m:
677 if not m:
678 raise PatchError(_("bad hunk #%d") % self.number)
678 raise PatchError(_("bad hunk #%d") % self.number)
679 foo, self.starta, foo2, aend, foo3 = m.groups()
679 foo, self.starta, foo2, aend, foo3 = m.groups()
680 self.starta = int(self.starta)
680 self.starta = int(self.starta)
681 if aend is None:
681 if aend is None:
682 aend = self.starta
682 aend = self.starta
683 self.lena = int(aend) - self.starta
683 self.lena = int(aend) - self.starta
684 if self.starta:
684 if self.starta:
685 self.lena += 1
685 self.lena += 1
686 for x in xrange(self.lena):
686 for x in xrange(self.lena):
687 l = lr.readline()
687 l = lr.readline()
688 if l.startswith('---'):
688 if l.startswith('---'):
689 lr.push(l)
689 lr.push(l)
690 break
690 break
691 s = l[2:]
691 s = l[2:]
692 if l.startswith('- ') or l.startswith('! '):
692 if l.startswith('- ') or l.startswith('! '):
693 u = '-' + s
693 u = '-' + s
694 elif l.startswith(' '):
694 elif l.startswith(' '):
695 u = ' ' + s
695 u = ' ' + s
696 else:
696 else:
697 raise PatchError(_("bad hunk #%d old text line %d") %
697 raise PatchError(_("bad hunk #%d old text line %d") %
698 (self.number, x))
698 (self.number, x))
699 self.a.append(u)
699 self.a.append(u)
700 self.hunk.append(u)
700 self.hunk.append(u)
701
701
702 l = lr.readline()
702 l = lr.readline()
703 if l.startswith('\ '):
703 if l.startswith('\ '):
704 s = self.a[-1][:-1]
704 s = self.a[-1][:-1]
705 self.a[-1] = s
705 self.a[-1] = s
706 self.hunk[-1] = s
706 self.hunk[-1] = s
707 l = lr.readline()
707 l = lr.readline()
708 m = contextdesc.match(l)
708 m = contextdesc.match(l)
709 if not m:
709 if not m:
710 raise PatchError(_("bad hunk #%d") % self.number)
710 raise PatchError(_("bad hunk #%d") % self.number)
711 foo, self.startb, foo2, bend, foo3 = m.groups()
711 foo, self.startb, foo2, bend, foo3 = m.groups()
712 self.startb = int(self.startb)
712 self.startb = int(self.startb)
713 if bend is None:
713 if bend is None:
714 bend = self.startb
714 bend = self.startb
715 self.lenb = int(bend) - self.startb
715 self.lenb = int(bend) - self.startb
716 if self.startb:
716 if self.startb:
717 self.lenb += 1
717 self.lenb += 1
718 hunki = 1
718 hunki = 1
719 for x in xrange(self.lenb):
719 for x in xrange(self.lenb):
720 l = lr.readline()
720 l = lr.readline()
721 if l.startswith('\ '):
721 if l.startswith('\ '):
722 s = self.b[-1][:-1]
722 s = self.b[-1][:-1]
723 self.b[-1] = s
723 self.b[-1] = s
724 self.hunk[hunki - 1] = s
724 self.hunk[hunki - 1] = s
725 continue
725 continue
726 if not l:
726 if not l:
727 lr.push(l)
727 lr.push(l)
728 break
728 break
729 s = l[2:]
729 s = l[2:]
730 if l.startswith('+ ') or l.startswith('! '):
730 if l.startswith('+ ') or l.startswith('! '):
731 u = '+' + s
731 u = '+' + s
732 elif l.startswith(' '):
732 elif l.startswith(' '):
733 u = ' ' + s
733 u = ' ' + s
734 elif len(self.b) == 0:
734 elif len(self.b) == 0:
735 # this can happen when the hunk does not add any lines
735 # this can happen when the hunk does not add any lines
736 lr.push(l)
736 lr.push(l)
737 break
737 break
738 else:
738 else:
739 raise PatchError(_("bad hunk #%d old text line %d") %
739 raise PatchError(_("bad hunk #%d old text line %d") %
740 (self.number, x))
740 (self.number, x))
741 self.b.append(s)
741 self.b.append(s)
742 while True:
742 while True:
743 if hunki >= len(self.hunk):
743 if hunki >= len(self.hunk):
744 h = ""
744 h = ""
745 else:
745 else:
746 h = self.hunk[hunki]
746 h = self.hunk[hunki]
747 hunki += 1
747 hunki += 1
748 if h == u:
748 if h == u:
749 break
749 break
750 elif h.startswith('-'):
750 elif h.startswith('-'):
751 continue
751 continue
752 else:
752 else:
753 self.hunk.insert(hunki - 1, u)
753 self.hunk.insert(hunki - 1, u)
754 break
754 break
755
755
756 if not self.a:
756 if not self.a:
757 # this happens when lines were only added to the hunk
757 # this happens when lines were only added to the hunk
758 for x in self.hunk:
758 for x in self.hunk:
759 if x.startswith('-') or x.startswith(' '):
759 if x.startswith('-') or x.startswith(' '):
760 self.a.append(x)
760 self.a.append(x)
761 if not self.b:
761 if not self.b:
762 # this happens when lines were only deleted from the hunk
762 # this happens when lines were only deleted from the hunk
763 for x in self.hunk:
763 for x in self.hunk:
764 if x.startswith('+') or x.startswith(' '):
764 if x.startswith('+') or x.startswith(' '):
765 self.b.append(x[1:])
765 self.b.append(x[1:])
766 # @@ -start,len +start,len @@
766 # @@ -start,len +start,len @@
767 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
767 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
768 self.startb, self.lenb)
768 self.startb, self.lenb)
769 self.hunk[0] = self.desc
769 self.hunk[0] = self.desc
770
770
771 def fix_newline(self):
771 def fix_newline(self):
772 diffhelpers.fix_newline(self.hunk, self.a, self.b)
772 diffhelpers.fix_newline(self.hunk, self.a, self.b)
773
773
774 def complete(self):
774 def complete(self):
775 return len(self.a) == self.lena and len(self.b) == self.lenb
775 return len(self.a) == self.lena and len(self.b) == self.lenb
776
776
777 def createfile(self):
777 def createfile(self):
778 return self.starta == 0 and self.lena == 0 and self.create
778 return self.starta == 0 and self.lena == 0 and self.create
779
779
780 def rmfile(self):
780 def rmfile(self):
781 return self.startb == 0 and self.lenb == 0 and self.remove
781 return self.startb == 0 and self.lenb == 0 and self.remove
782
782
783 def fuzzit(self, l, fuzz, toponly):
783 def fuzzit(self, l, fuzz, toponly):
784 # this removes context lines from the top and bottom of list 'l'. It
784 # this removes context lines from the top and bottom of list 'l'. It
785 # checks the hunk to make sure only context lines are removed, and then
785 # checks the hunk to make sure only context lines are removed, and then
786 # returns a new shortened list of lines.
786 # returns a new shortened list of lines.
787 fuzz = min(fuzz, len(l)-1)
787 fuzz = min(fuzz, len(l)-1)
788 if fuzz:
788 if fuzz:
789 top = 0
789 top = 0
790 bot = 0
790 bot = 0
791 hlen = len(self.hunk)
791 hlen = len(self.hunk)
792 for x in xrange(hlen - 1):
792 for x in xrange(hlen - 1):
793 # the hunk starts with the @@ line, so use x+1
793 # the hunk starts with the @@ line, so use x+1
794 if self.hunk[x + 1][0] == ' ':
794 if self.hunk[x + 1][0] == ' ':
795 top += 1
795 top += 1
796 else:
796 else:
797 break
797 break
798 if not toponly:
798 if not toponly:
799 for x in xrange(hlen - 1):
799 for x in xrange(hlen - 1):
800 if self.hunk[hlen - bot - 1][0] == ' ':
800 if self.hunk[hlen - bot - 1][0] == ' ':
801 bot += 1
801 bot += 1
802 else:
802 else:
803 break
803 break
804
804
805 # top and bot now count context in the hunk
805 # top and bot now count context in the hunk
806 # adjust them if either one is short
806 # adjust them if either one is short
807 context = max(top, bot, 3)
807 context = max(top, bot, 3)
808 if bot < context:
808 if bot < context:
809 bot = max(0, fuzz - (context - bot))
809 bot = max(0, fuzz - (context - bot))
810 else:
810 else:
811 bot = min(fuzz, bot)
811 bot = min(fuzz, bot)
812 if top < context:
812 if top < context:
813 top = max(0, fuzz - (context - top))
813 top = max(0, fuzz - (context - top))
814 else:
814 else:
815 top = min(fuzz, top)
815 top = min(fuzz, top)
816
816
817 return l[top:len(l)-bot]
817 return l[top:len(l)-bot]
818 return l
818 return l
819
819
820 def old(self, fuzz=0, toponly=False):
820 def old(self, fuzz=0, toponly=False):
821 return self.fuzzit(self.a, fuzz, toponly)
821 return self.fuzzit(self.a, fuzz, toponly)
822
822
823 def new(self, fuzz=0, toponly=False):
823 def new(self, fuzz=0, toponly=False):
824 return self.fuzzit(self.b, fuzz, toponly)
824 return self.fuzzit(self.b, fuzz, toponly)
825
825
826 class binhunk:
826 class binhunk:
827 'A binary patch file. Only understands literals so far.'
827 'A binary patch file. Only understands literals so far.'
828 def __init__(self, gitpatch):
828 def __init__(self, gitpatch):
829 self.gitpatch = gitpatch
829 self.gitpatch = gitpatch
830 self.text = None
830 self.text = None
831 self.hunk = ['GIT binary patch\n']
831 self.hunk = ['GIT binary patch\n']
832
832
833 def createfile(self):
833 def createfile(self):
834 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
834 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
835
835
836 def rmfile(self):
836 def rmfile(self):
837 return self.gitpatch.op == 'DELETE'
837 return self.gitpatch.op == 'DELETE'
838
838
839 def complete(self):
839 def complete(self):
840 return self.text is not None
840 return self.text is not None
841
841
842 def new(self):
842 def new(self):
843 return [self.text]
843 return [self.text]
844
844
845 def extract(self, lr):
845 def extract(self, lr):
846 line = lr.readline()
846 line = lr.readline()
847 self.hunk.append(line)
847 self.hunk.append(line)
848 while line and not line.startswith('literal '):
848 while line and not line.startswith('literal '):
849 line = lr.readline()
849 line = lr.readline()
850 self.hunk.append(line)
850 self.hunk.append(line)
851 if not line:
851 if not line:
852 raise PatchError(_('could not extract binary patch'))
852 raise PatchError(_('could not extract binary patch'))
853 size = int(line[8:].rstrip())
853 size = int(line[8:].rstrip())
854 dec = []
854 dec = []
855 line = lr.readline()
855 line = lr.readline()
856 self.hunk.append(line)
856 self.hunk.append(line)
857 while len(line) > 1:
857 while len(line) > 1:
858 l = line[0]
858 l = line[0]
859 if l <= 'Z' and l >= 'A':
859 if l <= 'Z' and l >= 'A':
860 l = ord(l) - ord('A') + 1
860 l = ord(l) - ord('A') + 1
861 else:
861 else:
862 l = ord(l) - ord('a') + 27
862 l = ord(l) - ord('a') + 27
863 dec.append(base85.b85decode(line[1:-1])[:l])
863 dec.append(base85.b85decode(line[1:-1])[:l])
864 line = lr.readline()
864 line = lr.readline()
865 self.hunk.append(line)
865 self.hunk.append(line)
866 text = zlib.decompress(''.join(dec))
866 text = zlib.decompress(''.join(dec))
867 if len(text) != size:
867 if len(text) != size:
868 raise PatchError(_('binary patch is %d bytes, not %d') %
868 raise PatchError(_('binary patch is %d bytes, not %d') %
869 len(text), size)
869 len(text), size)
870 self.text = text
870 self.text = text
871
871
872 def parsefilename(str):
872 def parsefilename(str):
873 # --- filename \t|space stuff
873 # --- filename \t|space stuff
874 s = str[4:].rstrip('\r\n')
874 s = str[4:].rstrip('\r\n')
875 i = s.find('\t')
875 i = s.find('\t')
876 if i < 0:
876 if i < 0:
877 i = s.find(' ')
877 i = s.find(' ')
878 if i < 0:
878 if i < 0:
879 return s
879 return s
880 return s[:i]
880 return s[:i]
881
881
882 def pathstrip(path, strip):
882 def pathstrip(path, strip):
883 pathlen = len(path)
883 pathlen = len(path)
884 i = 0
884 i = 0
885 if strip == 0:
885 if strip == 0:
886 return '', path.rstrip()
886 return '', path.rstrip()
887 count = strip
887 count = strip
888 while count > 0:
888 while count > 0:
889 i = path.find('/', i)
889 i = path.find('/', i)
890 if i == -1:
890 if i == -1:
891 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
891 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
892 (count, strip, path))
892 (count, strip, path))
893 i += 1
893 i += 1
894 # consume '//' in the path
894 # consume '//' in the path
895 while i < pathlen - 1 and path[i] == '/':
895 while i < pathlen - 1 and path[i] == '/':
896 i += 1
896 i += 1
897 count -= 1
897 count -= 1
898 return path[:i].lstrip(), path[i:].rstrip()
898 return path[:i].lstrip(), path[i:].rstrip()
899
899
900 def selectfile(afile_orig, bfile_orig, hunk, strip):
900 def selectfile(afile_orig, bfile_orig, hunk, strip):
901 nulla = afile_orig == "/dev/null"
901 nulla = afile_orig == "/dev/null"
902 nullb = bfile_orig == "/dev/null"
902 nullb = bfile_orig == "/dev/null"
903 abase, afile = pathstrip(afile_orig, strip)
903 abase, afile = pathstrip(afile_orig, strip)
904 gooda = not nulla and os.path.lexists(afile)
904 gooda = not nulla and os.path.lexists(afile)
905 bbase, bfile = pathstrip(bfile_orig, strip)
905 bbase, bfile = pathstrip(bfile_orig, strip)
906 if afile == bfile:
906 if afile == bfile:
907 goodb = gooda
907 goodb = gooda
908 else:
908 else:
909 goodb = not nullb and os.path.lexists(bfile)
909 goodb = not nullb and os.path.lexists(bfile)
910 createfunc = hunk.createfile
910 createfunc = hunk.createfile
911 missing = not goodb and not gooda and not createfunc()
911 missing = not goodb and not gooda and not createfunc()
912
912
913 # some diff programs apparently produce patches where the afile is
913 # some diff programs apparently produce patches where the afile is
914 # not /dev/null, but afile starts with bfile
914 # not /dev/null, but afile starts with bfile
915 abasedir = afile[:afile.rfind('/') + 1]
915 abasedir = afile[:afile.rfind('/') + 1]
916 bbasedir = bfile[:bfile.rfind('/') + 1]
916 bbasedir = bfile[:bfile.rfind('/') + 1]
917 if missing and abasedir == bbasedir and afile.startswith(bfile):
917 if missing and abasedir == bbasedir and afile.startswith(bfile):
918 # this isn't very pretty
918 # this isn't very pretty
919 hunk.create = True
919 hunk.create = True
920 if createfunc():
920 if createfunc():
921 missing = False
921 missing = False
922 else:
922 else:
923 hunk.create = False
923 hunk.create = False
924
924
925 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
925 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
926 # diff is between a file and its backup. In this case, the original
926 # diff is between a file and its backup. In this case, the original
927 # file should be patched (see original mpatch code).
927 # file should be patched (see original mpatch code).
928 isbackup = (abase == bbase and bfile.startswith(afile))
928 isbackup = (abase == bbase and bfile.startswith(afile))
929 fname = None
929 fname = None
930 if not missing:
930 if not missing:
931 if gooda and goodb:
931 if gooda and goodb:
932 fname = isbackup and afile or bfile
932 fname = isbackup and afile or bfile
933 elif gooda:
933 elif gooda:
934 fname = afile
934 fname = afile
935
935
936 if not fname:
936 if not fname:
937 if not nullb:
937 if not nullb:
938 fname = isbackup and afile or bfile
938 fname = isbackup and afile or bfile
939 elif not nulla:
939 elif not nulla:
940 fname = afile
940 fname = afile
941 else:
941 else:
942 raise PatchError(_("undefined source and destination files"))
942 raise PatchError(_("undefined source and destination files"))
943
943
944 return fname, missing
944 return fname, missing
945
945
946 def scangitpatch(lr, firstline):
946 def scangitpatch(lr, firstline):
947 """
947 """
948 Git patches can emit:
948 Git patches can emit:
949 - rename a to b
949 - rename a to b
950 - change b
950 - change b
951 - copy a to c
951 - copy a to c
952 - change c
952 - change c
953
953
954 We cannot apply this sequence as-is, the renamed 'a' could not be
954 We cannot apply this sequence as-is, the renamed 'a' could not be
955 found for it would have been renamed already. And we cannot copy
955 found for it would have been renamed already. And we cannot copy
956 from 'b' instead because 'b' would have been changed already. So
956 from 'b' instead because 'b' would have been changed already. So
957 we scan the git patch for copy and rename commands so we can
957 we scan the git patch for copy and rename commands so we can
958 perform the copies ahead of time.
958 perform the copies ahead of time.
959 """
959 """
960 pos = 0
960 pos = 0
961 try:
961 try:
962 pos = lr.fp.tell()
962 pos = lr.fp.tell()
963 fp = lr.fp
963 fp = lr.fp
964 except IOError:
964 except IOError:
965 fp = cStringIO.StringIO(lr.fp.read())
965 fp = cStringIO.StringIO(lr.fp.read())
966 gitlr = linereader(fp, lr.textmode)
966 gitlr = linereader(fp, lr.textmode)
967 gitlr.push(firstline)
967 gitlr.push(firstline)
968 gitpatches = readgitpatch(gitlr)
968 gitpatches = readgitpatch(gitlr)
969 fp.seek(pos)
969 fp.seek(pos)
970 return gitpatches
970 return gitpatches
971
971
972 def iterhunks(ui, fp, sourcefile=None):
972 def iterhunks(ui, fp, sourcefile=None):
973 """Read a patch and yield the following events:
973 """Read a patch and yield the following events:
974 - ("file", afile, bfile, firsthunk): select a new target file.
974 - ("file", afile, bfile, firsthunk): select a new target file.
975 - ("hunk", hunk): a new hunk is ready to be applied, follows a
975 - ("hunk", hunk): a new hunk is ready to be applied, follows a
976 "file" event.
976 "file" event.
977 - ("git", gitchanges): current diff is in git format, gitchanges
977 - ("git", gitchanges): current diff is in git format, gitchanges
978 maps filenames to gitpatch records. Unique event.
978 maps filenames to gitpatch records. Unique event.
979 """
979 """
980 changed = {}
980 changed = {}
981 current_hunk = None
981 current_hunk = None
982 afile = ""
982 afile = ""
983 bfile = ""
983 bfile = ""
984 state = None
984 state = None
985 hunknum = 0
985 hunknum = 0
986 emitfile = False
986 emitfile = False
987 git = False
987 git = False
988
988
989 # our states
989 # our states
990 BFILE = 1
990 BFILE = 1
991 context = None
991 context = None
992 lr = linereader(fp)
992 lr = linereader(fp)
993 # gitworkdone is True if a git operation (copy, rename, ...) was
993 # gitworkdone is True if a git operation (copy, rename, ...) was
994 # performed already for the current file. Useful when the file
994 # performed already for the current file. Useful when the file
995 # section may have no hunk.
995 # section may have no hunk.
996 gitworkdone = False
996 gitworkdone = False
997
997
998 while True:
998 while True:
999 newfile = newgitfile = False
999 newfile = newgitfile = False
1000 x = lr.readline()
1000 x = lr.readline()
1001 if not x:
1001 if not x:
1002 break
1002 break
1003 if current_hunk:
1003 if current_hunk:
1004 if x.startswith('\ '):
1004 if x.startswith('\ '):
1005 current_hunk.fix_newline()
1005 current_hunk.fix_newline()
1006 yield 'hunk', current_hunk
1006 yield 'hunk', current_hunk
1007 current_hunk = None
1007 current_hunk = None
1008 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
1008 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
1009 ((context is not False) and x.startswith('***************')))):
1009 ((context is not False) and x.startswith('***************')))):
1010 try:
1010 try:
1011 if context is None and x.startswith('***************'):
1011 if context is None and x.startswith('***************'):
1012 context = True
1012 context = True
1013 gpatch = changed.get(bfile)
1013 gpatch = changed.get(bfile)
1014 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1014 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1015 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1015 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1016 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
1016 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
1017 except PatchError, err:
1017 except PatchError, err:
1018 ui.debug(err)
1018 ui.debug(err)
1019 current_hunk = None
1019 current_hunk = None
1020 continue
1020 continue
1021 hunknum += 1
1021 hunknum += 1
1022 if emitfile:
1022 if emitfile:
1023 emitfile = False
1023 emitfile = False
1024 yield 'file', (afile, bfile, current_hunk)
1024 yield 'file', (afile, bfile, current_hunk)
1025 elif state == BFILE and x.startswith('GIT binary patch'):
1025 elif state == BFILE and x.startswith('GIT binary patch'):
1026 current_hunk = binhunk(changed[bfile])
1026 current_hunk = binhunk(changed[bfile])
1027 hunknum += 1
1027 hunknum += 1
1028 if emitfile:
1028 if emitfile:
1029 emitfile = False
1029 emitfile = False
1030 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
1030 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
1031 current_hunk.extract(lr)
1031 current_hunk.extract(lr)
1032 elif x.startswith('diff --git'):
1032 elif x.startswith('diff --git'):
1033 # check for git diff, scanning the whole patch file if needed
1033 # check for git diff, scanning the whole patch file if needed
1034 m = gitre.match(x)
1034 m = gitre.match(x)
1035 gitworkdone = False
1035 gitworkdone = False
1036 if m:
1036 if m:
1037 afile, bfile = m.group(1, 2)
1037 afile, bfile = m.group(1, 2)
1038 if not git:
1038 if not git:
1039 git = True
1039 git = True
1040 gitpatches = scangitpatch(lr, x)
1040 gitpatches = scangitpatch(lr, x)
1041 yield 'git', gitpatches
1041 yield 'git', gitpatches
1042 for gp in gitpatches:
1042 for gp in gitpatches:
1043 changed[gp.path] = gp
1043 changed[gp.path] = gp
1044 # else error?
1044 # else error?
1045 # copy/rename + modify should modify target, not source
1045 # copy/rename + modify should modify target, not source
1046 gp = changed.get(bfile)
1046 gp = changed.get(bfile)
1047 if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
1047 if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
1048 or gp.mode):
1048 or gp.mode):
1049 afile = bfile
1049 afile = bfile
1050 gitworkdone = True
1050 gitworkdone = True
1051 newgitfile = True
1051 newgitfile = True
1052 elif x.startswith('---'):
1052 elif x.startswith('---'):
1053 # check for a unified diff
1053 # check for a unified diff
1054 l2 = lr.readline()
1054 l2 = lr.readline()
1055 if not l2.startswith('+++'):
1055 if not l2.startswith('+++'):
1056 lr.push(l2)
1056 lr.push(l2)
1057 continue
1057 continue
1058 newfile = True
1058 newfile = True
1059 context = False
1059 context = False
1060 afile = parsefilename(x)
1060 afile = parsefilename(x)
1061 bfile = parsefilename(l2)
1061 bfile = parsefilename(l2)
1062 elif x.startswith('***'):
1062 elif x.startswith('***'):
1063 # check for a context diff
1063 # check for a context diff
1064 l2 = lr.readline()
1064 l2 = lr.readline()
1065 if not l2.startswith('---'):
1065 if not l2.startswith('---'):
1066 lr.push(l2)
1066 lr.push(l2)
1067 continue
1067 continue
1068 l3 = lr.readline()
1068 l3 = lr.readline()
1069 lr.push(l3)
1069 lr.push(l3)
1070 if not l3.startswith("***************"):
1070 if not l3.startswith("***************"):
1071 lr.push(l2)
1071 lr.push(l2)
1072 continue
1072 continue
1073 newfile = True
1073 newfile = True
1074 context = True
1074 context = True
1075 afile = parsefilename(x)
1075 afile = parsefilename(x)
1076 bfile = parsefilename(l2)
1076 bfile = parsefilename(l2)
1077
1077
1078 if newfile:
1078 if newfile:
1079 gitworkdone = False
1079 gitworkdone = False
1080
1080
1081 if newgitfile or newfile:
1081 if newgitfile or newfile:
1082 emitfile = True
1082 emitfile = True
1083 state = BFILE
1083 state = BFILE
1084 hunknum = 0
1084 hunknum = 0
1085 if current_hunk:
1085 if current_hunk:
1086 if current_hunk.complete():
1086 if current_hunk.complete():
1087 yield 'hunk', current_hunk
1087 yield 'hunk', current_hunk
1088 else:
1088 else:
1089 raise PatchError(_("malformed patch %s %s") % (afile,
1089 raise PatchError(_("malformed patch %s %s") % (afile,
1090 current_hunk.desc))
1090 current_hunk.desc))
1091
1091
1092 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
1092 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
1093 """Reads a patch from fp and tries to apply it.
1093 """Reads a patch from fp and tries to apply it.
1094
1094
1095 The dict 'changed' is filled in with all of the filenames changed
1095 The dict 'changed' is filled in with all of the filenames changed
1096 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1096 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1097 found and 1 if there was any fuzz.
1097 found and 1 if there was any fuzz.
1098
1098
1099 If 'eolmode' is 'strict', the patch content and patched file are
1099 If 'eolmode' is 'strict', the patch content and patched file are
1100 read in binary mode. Otherwise, line endings are ignored when
1100 read in binary mode. Otherwise, line endings are ignored when
1101 patching then normalized according to 'eolmode'.
1101 patching then normalized according to 'eolmode'.
1102
1102
1103 Callers probably want to call 'cmdutil.updatedir' after this to
1103 Callers probably want to call 'cmdutil.updatedir' after this to
1104 apply certain categories of changes not done by this function.
1104 apply certain categories of changes not done by this function.
1105 """
1105 """
1106 return _applydiff(
1106 return _applydiff(
1107 ui, fp, patchfile, copyfile,
1107 ui, fp, patchfile, copyfile,
1108 changed, strip=strip, sourcefile=sourcefile, eolmode=eolmode)
1108 changed, strip=strip, sourcefile=sourcefile, eolmode=eolmode)
1109
1109
1110
1110
1111 def _applydiff(ui, fp, patcher, copyfn, changed, strip=1,
1111 def _applydiff(ui, fp, patcher, copyfn, changed, strip=1,
1112 sourcefile=None, eolmode='strict'):
1112 sourcefile=None, eolmode='strict'):
1113 rejects = 0
1113 rejects = 0
1114 err = 0
1114 err = 0
1115 current_file = None
1115 current_file = None
1116 cwd = os.getcwd()
1116 cwd = os.getcwd()
1117 opener = util.opener(cwd)
1117 opener = util.opener(cwd)
1118
1118
1119 def closefile():
1119 def closefile():
1120 if not current_file:
1120 if not current_file:
1121 return 0
1121 return 0
1122 if current_file.dirty:
1122 if current_file.dirty:
1123 current_file.writelines(current_file.fname, current_file.lines)
1123 current_file.writelines(current_file.fname, current_file.lines)
1124 current_file.write_rej()
1124 current_file.write_rej()
1125 return len(current_file.rej)
1125 return len(current_file.rej)
1126
1126
1127 for state, values in iterhunks(ui, fp, sourcefile):
1127 for state, values in iterhunks(ui, fp, sourcefile):
1128 if state == 'hunk':
1128 if state == 'hunk':
1129 if not current_file:
1129 if not current_file:
1130 continue
1130 continue
1131 ret = current_file.apply(values)
1131 ret = current_file.apply(values)
1132 if ret >= 0:
1132 if ret >= 0:
1133 changed.setdefault(current_file.fname, None)
1133 changed.setdefault(current_file.fname, None)
1134 if ret > 0:
1134 if ret > 0:
1135 err = 1
1135 err = 1
1136 elif state == 'file':
1136 elif state == 'file':
1137 rejects += closefile()
1137 rejects += closefile()
1138 afile, bfile, first_hunk = values
1138 afile, bfile, first_hunk = values
1139 try:
1139 try:
1140 if sourcefile:
1140 if sourcefile:
1141 current_file = patcher(ui, sourcefile, opener,
1141 current_file = patcher(ui, sourcefile, opener,
1142 eolmode=eolmode)
1142 eolmode=eolmode)
1143 else:
1143 else:
1144 current_file, missing = selectfile(afile, bfile,
1144 current_file, missing = selectfile(afile, bfile,
1145 first_hunk, strip)
1145 first_hunk, strip)
1146 current_file = patcher(ui, current_file, opener,
1146 current_file = patcher(ui, current_file, opener,
1147 missing=missing, eolmode=eolmode)
1147 missing=missing, eolmode=eolmode)
1148 except PatchError, err:
1148 except PatchError, err:
1149 ui.warn(str(err) + '\n')
1149 ui.warn(str(err) + '\n')
1150 current_file = None
1150 current_file = None
1151 rejects += 1
1151 rejects += 1
1152 continue
1152 continue
1153 elif state == 'git':
1153 elif state == 'git':
1154 for gp in values:
1154 for gp in values:
1155 gp.path = pathstrip(gp.path, strip - 1)[1]
1155 gp.path = pathstrip(gp.path, strip - 1)[1]
1156 if gp.oldpath:
1156 if gp.oldpath:
1157 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1157 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1158 # Binary patches really overwrite target files, copying them
1158 # Binary patches really overwrite target files, copying them
1159 # will just make it fails with "target file exists"
1159 # will just make it fails with "target file exists"
1160 if gp.op in ('COPY', 'RENAME') and not gp.binary:
1160 if gp.op in ('COPY', 'RENAME') and not gp.binary:
1161 copyfn(gp.oldpath, gp.path, cwd)
1161 copyfn(gp.oldpath, gp.path, cwd)
1162 changed[gp.path] = gp
1162 changed[gp.path] = gp
1163 else:
1163 else:
1164 raise util.Abort(_('unsupported parser state: %s') % state)
1164 raise util.Abort(_('unsupported parser state: %s') % state)
1165
1165
1166 rejects += closefile()
1166 rejects += closefile()
1167
1167
1168 if rejects:
1168 if rejects:
1169 return -1
1169 return -1
1170 return err
1170 return err
1171
1171
1172 def externalpatch(patcher, patchname, ui, strip, cwd, files):
1172 def externalpatch(patcher, patchname, ui, strip, cwd, files):
1173 """use <patcher> to apply <patchname> to the working directory.
1173 """use <patcher> to apply <patchname> to the working directory.
1174 returns whether patch was applied with fuzz factor."""
1174 returns whether patch was applied with fuzz factor."""
1175
1175
1176 fuzz = False
1176 fuzz = False
1177 args = []
1177 args = []
1178 if cwd:
1178 if cwd:
1179 args.append('-d %s' % util.shellquote(cwd))
1179 args.append('-d %s' % util.shellquote(cwd))
1180 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1180 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1181 util.shellquote(patchname)))
1181 util.shellquote(patchname)))
1182
1182
1183 for line in fp:
1183 for line in fp:
1184 line = line.rstrip()
1184 line = line.rstrip()
1185 ui.note(line + '\n')
1185 ui.note(line + '\n')
1186 if line.startswith('patching file '):
1186 if line.startswith('patching file '):
1187 pf = util.parse_patch_output(line)
1187 pf = util.parse_patch_output(line)
1188 printed_file = False
1188 printed_file = False
1189 files.setdefault(pf, None)
1189 files.setdefault(pf, None)
1190 elif line.find('with fuzz') >= 0:
1190 elif line.find('with fuzz') >= 0:
1191 fuzz = True
1191 fuzz = True
1192 if not printed_file:
1192 if not printed_file:
1193 ui.warn(pf + '\n')
1193 ui.warn(pf + '\n')
1194 printed_file = True
1194 printed_file = True
1195 ui.warn(line + '\n')
1195 ui.warn(line + '\n')
1196 elif line.find('saving rejects to file') >= 0:
1196 elif line.find('saving rejects to file') >= 0:
1197 ui.warn(line + '\n')
1197 ui.warn(line + '\n')
1198 elif line.find('FAILED') >= 0:
1198 elif line.find('FAILED') >= 0:
1199 if not printed_file:
1199 if not printed_file:
1200 ui.warn(pf + '\n')
1200 ui.warn(pf + '\n')
1201 printed_file = True
1201 printed_file = True
1202 ui.warn(line + '\n')
1202 ui.warn(line + '\n')
1203 code = fp.close()
1203 code = fp.close()
1204 if code:
1204 if code:
1205 raise PatchError(_("patch command failed: %s") %
1205 raise PatchError(_("patch command failed: %s") %
1206 util.explain_exit(code)[0])
1206 util.explain_exit(code)[0])
1207 return fuzz
1207 return fuzz
1208
1208
1209 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1209 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1210 """use builtin patch to apply <patchobj> to the working directory.
1210 """use builtin patch to apply <patchobj> to the working directory.
1211 returns whether patch was applied with fuzz factor."""
1211 returns whether patch was applied with fuzz factor."""
1212
1212
1213 if files is None:
1213 if files is None:
1214 files = {}
1214 files = {}
1215 if eolmode is None:
1215 if eolmode is None:
1216 eolmode = ui.config('patch', 'eol', 'strict')
1216 eolmode = ui.config('patch', 'eol', 'strict')
1217 if eolmode.lower() not in eolmodes:
1217 if eolmode.lower() not in eolmodes:
1218 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1218 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1219 eolmode = eolmode.lower()
1219 eolmode = eolmode.lower()
1220
1220
1221 try:
1221 try:
1222 fp = open(patchobj, 'rb')
1222 fp = open(patchobj, 'rb')
1223 except TypeError:
1223 except TypeError:
1224 fp = patchobj
1224 fp = patchobj
1225 if cwd:
1225 if cwd:
1226 curdir = os.getcwd()
1226 curdir = os.getcwd()
1227 os.chdir(cwd)
1227 os.chdir(cwd)
1228 try:
1228 try:
1229 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1229 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1230 finally:
1230 finally:
1231 if cwd:
1231 if cwd:
1232 os.chdir(curdir)
1232 os.chdir(curdir)
1233 if fp != patchobj:
1233 if fp != patchobj:
1234 fp.close()
1234 fp.close()
1235 if ret < 0:
1235 if ret < 0:
1236 raise PatchError
1236 raise PatchError(_('patch failed to apply'))
1237 return ret > 0
1237 return ret > 0
1238
1238
1239 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1239 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1240 """Apply <patchname> to the working directory.
1240 """Apply <patchname> to the working directory.
1241
1241
1242 'eolmode' specifies how end of lines should be handled. It can be:
1242 'eolmode' specifies how end of lines should be handled. It can be:
1243 - 'strict': inputs are read in binary mode, EOLs are preserved
1243 - 'strict': inputs are read in binary mode, EOLs are preserved
1244 - 'crlf': EOLs are ignored when patching and reset to CRLF
1244 - 'crlf': EOLs are ignored when patching and reset to CRLF
1245 - 'lf': EOLs are ignored when patching and reset to LF
1245 - 'lf': EOLs are ignored when patching and reset to LF
1246 - None: get it from user settings, default to 'strict'
1246 - None: get it from user settings, default to 'strict'
1247 'eolmode' is ignored when using an external patcher program.
1247 'eolmode' is ignored when using an external patcher program.
1248
1248
1249 Returns whether patch was applied with fuzz factor.
1249 Returns whether patch was applied with fuzz factor.
1250 """
1250 """
1251 patcher = ui.config('ui', 'patch')
1251 patcher = ui.config('ui', 'patch')
1252 if files is None:
1252 if files is None:
1253 files = {}
1253 files = {}
1254 try:
1254 try:
1255 if patcher:
1255 if patcher:
1256 return externalpatch(patcher, patchname, ui, strip, cwd, files)
1256 return externalpatch(patcher, patchname, ui, strip, cwd, files)
1257 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1257 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1258 except PatchError, err:
1258 except PatchError, err:
1259 s = str(err)
1259 raise util.Abort(str(err))
1260 if s:
1261 raise util.Abort(s)
1262 else:
1263 raise util.Abort(_('patch failed to apply'))
1264
1260
1265 def b85diff(to, tn):
1261 def b85diff(to, tn):
1266 '''print base85-encoded binary diff'''
1262 '''print base85-encoded binary diff'''
1267 def gitindex(text):
1263 def gitindex(text):
1268 if not text:
1264 if not text:
1269 return hex(nullid)
1265 return hex(nullid)
1270 l = len(text)
1266 l = len(text)
1271 s = util.sha1('blob %d\0' % l)
1267 s = util.sha1('blob %d\0' % l)
1272 s.update(text)
1268 s.update(text)
1273 return s.hexdigest()
1269 return s.hexdigest()
1274
1270
1275 def fmtline(line):
1271 def fmtline(line):
1276 l = len(line)
1272 l = len(line)
1277 if l <= 26:
1273 if l <= 26:
1278 l = chr(ord('A') + l - 1)
1274 l = chr(ord('A') + l - 1)
1279 else:
1275 else:
1280 l = chr(l - 26 + ord('a') - 1)
1276 l = chr(l - 26 + ord('a') - 1)
1281 return '%c%s\n' % (l, base85.b85encode(line, True))
1277 return '%c%s\n' % (l, base85.b85encode(line, True))
1282
1278
1283 def chunk(text, csize=52):
1279 def chunk(text, csize=52):
1284 l = len(text)
1280 l = len(text)
1285 i = 0
1281 i = 0
1286 while i < l:
1282 while i < l:
1287 yield text[i:i + csize]
1283 yield text[i:i + csize]
1288 i += csize
1284 i += csize
1289
1285
1290 tohash = gitindex(to)
1286 tohash = gitindex(to)
1291 tnhash = gitindex(tn)
1287 tnhash = gitindex(tn)
1292 if tohash == tnhash:
1288 if tohash == tnhash:
1293 return ""
1289 return ""
1294
1290
1295 # TODO: deltas
1291 # TODO: deltas
1296 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1292 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1297 (tohash, tnhash, len(tn))]
1293 (tohash, tnhash, len(tn))]
1298 for l in chunk(zlib.compress(tn)):
1294 for l in chunk(zlib.compress(tn)):
1299 ret.append(fmtline(l))
1295 ret.append(fmtline(l))
1300 ret.append('\n')
1296 ret.append('\n')
1301 return ''.join(ret)
1297 return ''.join(ret)
1302
1298
1303 class GitDiffRequired(Exception):
1299 class GitDiffRequired(Exception):
1304 pass
1300 pass
1305
1301
1306 def diffopts(ui, opts=None, untrusted=False):
1302 def diffopts(ui, opts=None, untrusted=False):
1307 def get(key, name=None, getter=ui.configbool):
1303 def get(key, name=None, getter=ui.configbool):
1308 return ((opts and opts.get(key)) or
1304 return ((opts and opts.get(key)) or
1309 getter('diff', name or key, None, untrusted=untrusted))
1305 getter('diff', name or key, None, untrusted=untrusted))
1310 return mdiff.diffopts(
1306 return mdiff.diffopts(
1311 text=opts and opts.get('text'),
1307 text=opts and opts.get('text'),
1312 git=get('git'),
1308 git=get('git'),
1313 nodates=get('nodates'),
1309 nodates=get('nodates'),
1314 showfunc=get('show_function', 'showfunc'),
1310 showfunc=get('show_function', 'showfunc'),
1315 ignorews=get('ignore_all_space', 'ignorews'),
1311 ignorews=get('ignore_all_space', 'ignorews'),
1316 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1312 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1317 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1313 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1318 context=get('unified', getter=ui.config))
1314 context=get('unified', getter=ui.config))
1319
1315
1320 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1316 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1321 losedatafn=None, prefix=''):
1317 losedatafn=None, prefix=''):
1322 '''yields diff of changes to files between two nodes, or node and
1318 '''yields diff of changes to files between two nodes, or node and
1323 working directory.
1319 working directory.
1324
1320
1325 if node1 is None, use first dirstate parent instead.
1321 if node1 is None, use first dirstate parent instead.
1326 if node2 is None, compare node1 with working directory.
1322 if node2 is None, compare node1 with working directory.
1327
1323
1328 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1324 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1329 every time some change cannot be represented with the current
1325 every time some change cannot be represented with the current
1330 patch format. Return False to upgrade to git patch format, True to
1326 patch format. Return False to upgrade to git patch format, True to
1331 accept the loss or raise an exception to abort the diff. It is
1327 accept the loss or raise an exception to abort the diff. It is
1332 called with the name of current file being diffed as 'fn'. If set
1328 called with the name of current file being diffed as 'fn'. If set
1333 to None, patches will always be upgraded to git format when
1329 to None, patches will always be upgraded to git format when
1334 necessary.
1330 necessary.
1335
1331
1336 prefix is a filename prefix that is prepended to all filenames on
1332 prefix is a filename prefix that is prepended to all filenames on
1337 display (used for subrepos).
1333 display (used for subrepos).
1338 '''
1334 '''
1339
1335
1340 if opts is None:
1336 if opts is None:
1341 opts = mdiff.defaultopts
1337 opts = mdiff.defaultopts
1342
1338
1343 if not node1 and not node2:
1339 if not node1 and not node2:
1344 node1 = repo.dirstate.parents()[0]
1340 node1 = repo.dirstate.parents()[0]
1345
1341
1346 def lrugetfilectx():
1342 def lrugetfilectx():
1347 cache = {}
1343 cache = {}
1348 order = []
1344 order = []
1349 def getfilectx(f, ctx):
1345 def getfilectx(f, ctx):
1350 fctx = ctx.filectx(f, filelog=cache.get(f))
1346 fctx = ctx.filectx(f, filelog=cache.get(f))
1351 if f not in cache:
1347 if f not in cache:
1352 if len(cache) > 20:
1348 if len(cache) > 20:
1353 del cache[order.pop(0)]
1349 del cache[order.pop(0)]
1354 cache[f] = fctx.filelog()
1350 cache[f] = fctx.filelog()
1355 else:
1351 else:
1356 order.remove(f)
1352 order.remove(f)
1357 order.append(f)
1353 order.append(f)
1358 return fctx
1354 return fctx
1359 return getfilectx
1355 return getfilectx
1360 getfilectx = lrugetfilectx()
1356 getfilectx = lrugetfilectx()
1361
1357
1362 ctx1 = repo[node1]
1358 ctx1 = repo[node1]
1363 ctx2 = repo[node2]
1359 ctx2 = repo[node2]
1364
1360
1365 if not changes:
1361 if not changes:
1366 changes = repo.status(ctx1, ctx2, match=match)
1362 changes = repo.status(ctx1, ctx2, match=match)
1367 modified, added, removed = changes[:3]
1363 modified, added, removed = changes[:3]
1368
1364
1369 if not modified and not added and not removed:
1365 if not modified and not added and not removed:
1370 return []
1366 return []
1371
1367
1372 revs = None
1368 revs = None
1373 if not repo.ui.quiet:
1369 if not repo.ui.quiet:
1374 hexfunc = repo.ui.debugflag and hex or short
1370 hexfunc = repo.ui.debugflag and hex or short
1375 revs = [hexfunc(node) for node in [node1, node2] if node]
1371 revs = [hexfunc(node) for node in [node1, node2] if node]
1376
1372
1377 copy = {}
1373 copy = {}
1378 if opts.git or opts.upgrade:
1374 if opts.git or opts.upgrade:
1379 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1375 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1380
1376
1381 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1377 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1382 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1378 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1383 if opts.upgrade and not opts.git:
1379 if opts.upgrade and not opts.git:
1384 try:
1380 try:
1385 def losedata(fn):
1381 def losedata(fn):
1386 if not losedatafn or not losedatafn(fn=fn):
1382 if not losedatafn or not losedatafn(fn=fn):
1387 raise GitDiffRequired()
1383 raise GitDiffRequired()
1388 # Buffer the whole output until we are sure it can be generated
1384 # Buffer the whole output until we are sure it can be generated
1389 return list(difffn(opts.copy(git=False), losedata))
1385 return list(difffn(opts.copy(git=False), losedata))
1390 except GitDiffRequired:
1386 except GitDiffRequired:
1391 return difffn(opts.copy(git=True), None)
1387 return difffn(opts.copy(git=True), None)
1392 else:
1388 else:
1393 return difffn(opts, None)
1389 return difffn(opts, None)
1394
1390
1395 def difflabel(func, *args, **kw):
1391 def difflabel(func, *args, **kw):
1396 '''yields 2-tuples of (output, label) based on the output of func()'''
1392 '''yields 2-tuples of (output, label) based on the output of func()'''
1397 prefixes = [('diff', 'diff.diffline'),
1393 prefixes = [('diff', 'diff.diffline'),
1398 ('copy', 'diff.extended'),
1394 ('copy', 'diff.extended'),
1399 ('rename', 'diff.extended'),
1395 ('rename', 'diff.extended'),
1400 ('old', 'diff.extended'),
1396 ('old', 'diff.extended'),
1401 ('new', 'diff.extended'),
1397 ('new', 'diff.extended'),
1402 ('deleted', 'diff.extended'),
1398 ('deleted', 'diff.extended'),
1403 ('---', 'diff.file_a'),
1399 ('---', 'diff.file_a'),
1404 ('+++', 'diff.file_b'),
1400 ('+++', 'diff.file_b'),
1405 ('@@', 'diff.hunk'),
1401 ('@@', 'diff.hunk'),
1406 ('-', 'diff.deleted'),
1402 ('-', 'diff.deleted'),
1407 ('+', 'diff.inserted')]
1403 ('+', 'diff.inserted')]
1408
1404
1409 for chunk in func(*args, **kw):
1405 for chunk in func(*args, **kw):
1410 lines = chunk.split('\n')
1406 lines = chunk.split('\n')
1411 for i, line in enumerate(lines):
1407 for i, line in enumerate(lines):
1412 if i != 0:
1408 if i != 0:
1413 yield ('\n', '')
1409 yield ('\n', '')
1414 stripline = line
1410 stripline = line
1415 if line and line[0] in '+-':
1411 if line and line[0] in '+-':
1416 # highlight trailing whitespace, but only in changed lines
1412 # highlight trailing whitespace, but only in changed lines
1417 stripline = line.rstrip()
1413 stripline = line.rstrip()
1418 for prefix, label in prefixes:
1414 for prefix, label in prefixes:
1419 if stripline.startswith(prefix):
1415 if stripline.startswith(prefix):
1420 yield (stripline, label)
1416 yield (stripline, label)
1421 break
1417 break
1422 else:
1418 else:
1423 yield (line, '')
1419 yield (line, '')
1424 if line != stripline:
1420 if line != stripline:
1425 yield (line[len(stripline):], 'diff.trailingwhitespace')
1421 yield (line[len(stripline):], 'diff.trailingwhitespace')
1426
1422
1427 def diffui(*args, **kw):
1423 def diffui(*args, **kw):
1428 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1424 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1429 return difflabel(diff, *args, **kw)
1425 return difflabel(diff, *args, **kw)
1430
1426
1431
1427
1432 def _addmodehdr(header, omode, nmode):
1428 def _addmodehdr(header, omode, nmode):
1433 if omode != nmode:
1429 if omode != nmode:
1434 header.append('old mode %s\n' % omode)
1430 header.append('old mode %s\n' % omode)
1435 header.append('new mode %s\n' % nmode)
1431 header.append('new mode %s\n' % nmode)
1436
1432
1437 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1433 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1438 copy, getfilectx, opts, losedatafn, prefix):
1434 copy, getfilectx, opts, losedatafn, prefix):
1439
1435
1440 def join(f):
1436 def join(f):
1441 return os.path.join(prefix, f)
1437 return os.path.join(prefix, f)
1442
1438
1443 date1 = util.datestr(ctx1.date())
1439 date1 = util.datestr(ctx1.date())
1444 man1 = ctx1.manifest()
1440 man1 = ctx1.manifest()
1445
1441
1446 gone = set()
1442 gone = set()
1447 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1443 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1448
1444
1449 copyto = dict([(v, k) for k, v in copy.items()])
1445 copyto = dict([(v, k) for k, v in copy.items()])
1450
1446
1451 if opts.git:
1447 if opts.git:
1452 revs = None
1448 revs = None
1453
1449
1454 for f in sorted(modified + added + removed):
1450 for f in sorted(modified + added + removed):
1455 to = None
1451 to = None
1456 tn = None
1452 tn = None
1457 dodiff = True
1453 dodiff = True
1458 header = []
1454 header = []
1459 if f in man1:
1455 if f in man1:
1460 to = getfilectx(f, ctx1).data()
1456 to = getfilectx(f, ctx1).data()
1461 if f not in removed:
1457 if f not in removed:
1462 tn = getfilectx(f, ctx2).data()
1458 tn = getfilectx(f, ctx2).data()
1463 a, b = f, f
1459 a, b = f, f
1464 if opts.git or losedatafn:
1460 if opts.git or losedatafn:
1465 if f in added:
1461 if f in added:
1466 mode = gitmode[ctx2.flags(f)]
1462 mode = gitmode[ctx2.flags(f)]
1467 if f in copy or f in copyto:
1463 if f in copy or f in copyto:
1468 if opts.git:
1464 if opts.git:
1469 if f in copy:
1465 if f in copy:
1470 a = copy[f]
1466 a = copy[f]
1471 else:
1467 else:
1472 a = copyto[f]
1468 a = copyto[f]
1473 omode = gitmode[man1.flags(a)]
1469 omode = gitmode[man1.flags(a)]
1474 _addmodehdr(header, omode, mode)
1470 _addmodehdr(header, omode, mode)
1475 if a in removed and a not in gone:
1471 if a in removed and a not in gone:
1476 op = 'rename'
1472 op = 'rename'
1477 gone.add(a)
1473 gone.add(a)
1478 else:
1474 else:
1479 op = 'copy'
1475 op = 'copy'
1480 header.append('%s from %s\n' % (op, join(a)))
1476 header.append('%s from %s\n' % (op, join(a)))
1481 header.append('%s to %s\n' % (op, join(f)))
1477 header.append('%s to %s\n' % (op, join(f)))
1482 to = getfilectx(a, ctx1).data()
1478 to = getfilectx(a, ctx1).data()
1483 else:
1479 else:
1484 losedatafn(f)
1480 losedatafn(f)
1485 else:
1481 else:
1486 if opts.git:
1482 if opts.git:
1487 header.append('new file mode %s\n' % mode)
1483 header.append('new file mode %s\n' % mode)
1488 elif ctx2.flags(f):
1484 elif ctx2.flags(f):
1489 losedatafn(f)
1485 losedatafn(f)
1490 # In theory, if tn was copied or renamed we should check
1486 # In theory, if tn was copied or renamed we should check
1491 # if the source is binary too but the copy record already
1487 # if the source is binary too but the copy record already
1492 # forces git mode.
1488 # forces git mode.
1493 if util.binary(tn):
1489 if util.binary(tn):
1494 if opts.git:
1490 if opts.git:
1495 dodiff = 'binary'
1491 dodiff = 'binary'
1496 else:
1492 else:
1497 losedatafn(f)
1493 losedatafn(f)
1498 if not opts.git and not tn:
1494 if not opts.git and not tn:
1499 # regular diffs cannot represent new empty file
1495 # regular diffs cannot represent new empty file
1500 losedatafn(f)
1496 losedatafn(f)
1501 elif f in removed:
1497 elif f in removed:
1502 if opts.git:
1498 if opts.git:
1503 # have we already reported a copy above?
1499 # have we already reported a copy above?
1504 if ((f in copy and copy[f] in added
1500 if ((f in copy and copy[f] in added
1505 and copyto[copy[f]] == f) or
1501 and copyto[copy[f]] == f) or
1506 (f in copyto and copyto[f] in added
1502 (f in copyto and copyto[f] in added
1507 and copy[copyto[f]] == f)):
1503 and copy[copyto[f]] == f)):
1508 dodiff = False
1504 dodiff = False
1509 else:
1505 else:
1510 header.append('deleted file mode %s\n' %
1506 header.append('deleted file mode %s\n' %
1511 gitmode[man1.flags(f)])
1507 gitmode[man1.flags(f)])
1512 elif not to or util.binary(to):
1508 elif not to or util.binary(to):
1513 # regular diffs cannot represent empty file deletion
1509 # regular diffs cannot represent empty file deletion
1514 losedatafn(f)
1510 losedatafn(f)
1515 else:
1511 else:
1516 oflag = man1.flags(f)
1512 oflag = man1.flags(f)
1517 nflag = ctx2.flags(f)
1513 nflag = ctx2.flags(f)
1518 binary = util.binary(to) or util.binary(tn)
1514 binary = util.binary(to) or util.binary(tn)
1519 if opts.git:
1515 if opts.git:
1520 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1516 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1521 if binary:
1517 if binary:
1522 dodiff = 'binary'
1518 dodiff = 'binary'
1523 elif binary or nflag != oflag:
1519 elif binary or nflag != oflag:
1524 losedatafn(f)
1520 losedatafn(f)
1525 if opts.git:
1521 if opts.git:
1526 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1522 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1527
1523
1528 if dodiff:
1524 if dodiff:
1529 if dodiff == 'binary':
1525 if dodiff == 'binary':
1530 text = b85diff(to, tn)
1526 text = b85diff(to, tn)
1531 else:
1527 else:
1532 text = mdiff.unidiff(to, date1,
1528 text = mdiff.unidiff(to, date1,
1533 # ctx2 date may be dynamic
1529 # ctx2 date may be dynamic
1534 tn, util.datestr(ctx2.date()),
1530 tn, util.datestr(ctx2.date()),
1535 join(a), join(b), revs, opts=opts)
1531 join(a), join(b), revs, opts=opts)
1536 if header and (text or len(header) > 1):
1532 if header and (text or len(header) > 1):
1537 yield ''.join(header)
1533 yield ''.join(header)
1538 if text:
1534 if text:
1539 yield text
1535 yield text
1540
1536
1541 def diffstatdata(lines):
1537 def diffstatdata(lines):
1542 filename, adds, removes = None, 0, 0
1538 filename, adds, removes = None, 0, 0
1543 for line in lines:
1539 for line in lines:
1544 if line.startswith('diff'):
1540 if line.startswith('diff'):
1545 if filename:
1541 if filename:
1546 isbinary = adds == 0 and removes == 0
1542 isbinary = adds == 0 and removes == 0
1547 yield (filename, adds, removes, isbinary)
1543 yield (filename, adds, removes, isbinary)
1548 # set numbers to 0 anyway when starting new file
1544 # set numbers to 0 anyway when starting new file
1549 adds, removes = 0, 0
1545 adds, removes = 0, 0
1550 if line.startswith('diff --git'):
1546 if line.startswith('diff --git'):
1551 filename = gitre.search(line).group(1)
1547 filename = gitre.search(line).group(1)
1552 else:
1548 else:
1553 # format: "diff -r ... -r ... filename"
1549 # format: "diff -r ... -r ... filename"
1554 filename = line.split(None, 5)[-1]
1550 filename = line.split(None, 5)[-1]
1555 elif line.startswith('+') and not line.startswith('+++'):
1551 elif line.startswith('+') and not line.startswith('+++'):
1556 adds += 1
1552 adds += 1
1557 elif line.startswith('-') and not line.startswith('---'):
1553 elif line.startswith('-') and not line.startswith('---'):
1558 removes += 1
1554 removes += 1
1559 if filename:
1555 if filename:
1560 isbinary = adds == 0 and removes == 0
1556 isbinary = adds == 0 and removes == 0
1561 yield (filename, adds, removes, isbinary)
1557 yield (filename, adds, removes, isbinary)
1562
1558
1563 def diffstat(lines, width=80, git=False):
1559 def diffstat(lines, width=80, git=False):
1564 output = []
1560 output = []
1565 stats = list(diffstatdata(lines))
1561 stats = list(diffstatdata(lines))
1566
1562
1567 maxtotal, maxname = 0, 0
1563 maxtotal, maxname = 0, 0
1568 totaladds, totalremoves = 0, 0
1564 totaladds, totalremoves = 0, 0
1569 hasbinary = False
1565 hasbinary = False
1570
1566
1571 sized = [(filename, adds, removes, isbinary, encoding.colwidth(filename))
1567 sized = [(filename, adds, removes, isbinary, encoding.colwidth(filename))
1572 for filename, adds, removes, isbinary in stats]
1568 for filename, adds, removes, isbinary in stats]
1573
1569
1574 for filename, adds, removes, isbinary, namewidth in sized:
1570 for filename, adds, removes, isbinary, namewidth in sized:
1575 totaladds += adds
1571 totaladds += adds
1576 totalremoves += removes
1572 totalremoves += removes
1577 maxname = max(maxname, namewidth)
1573 maxname = max(maxname, namewidth)
1578 maxtotal = max(maxtotal, adds + removes)
1574 maxtotal = max(maxtotal, adds + removes)
1579 if isbinary:
1575 if isbinary:
1580 hasbinary = True
1576 hasbinary = True
1581
1577
1582 countwidth = len(str(maxtotal))
1578 countwidth = len(str(maxtotal))
1583 if hasbinary and countwidth < 3:
1579 if hasbinary and countwidth < 3:
1584 countwidth = 3
1580 countwidth = 3
1585 graphwidth = width - countwidth - maxname - 6
1581 graphwidth = width - countwidth - maxname - 6
1586 if graphwidth < 10:
1582 if graphwidth < 10:
1587 graphwidth = 10
1583 graphwidth = 10
1588
1584
1589 def scale(i):
1585 def scale(i):
1590 if maxtotal <= graphwidth:
1586 if maxtotal <= graphwidth:
1591 return i
1587 return i
1592 # If diffstat runs out of room it doesn't print anything,
1588 # If diffstat runs out of room it doesn't print anything,
1593 # which isn't very useful, so always print at least one + or -
1589 # which isn't very useful, so always print at least one + or -
1594 # if there were at least some changes.
1590 # if there were at least some changes.
1595 return max(i * graphwidth // maxtotal, int(bool(i)))
1591 return max(i * graphwidth // maxtotal, int(bool(i)))
1596
1592
1597 for filename, adds, removes, isbinary, namewidth in sized:
1593 for filename, adds, removes, isbinary, namewidth in sized:
1598 if git and isbinary:
1594 if git and isbinary:
1599 count = 'Bin'
1595 count = 'Bin'
1600 else:
1596 else:
1601 count = adds + removes
1597 count = adds + removes
1602 pluses = '+' * scale(adds)
1598 pluses = '+' * scale(adds)
1603 minuses = '-' * scale(removes)
1599 minuses = '-' * scale(removes)
1604 output.append(' %s%s | %*s %s%s\n' %
1600 output.append(' %s%s | %*s %s%s\n' %
1605 (filename, ' ' * (maxname - namewidth),
1601 (filename, ' ' * (maxname - namewidth),
1606 countwidth, count,
1602 countwidth, count,
1607 pluses, minuses))
1603 pluses, minuses))
1608
1604
1609 if stats:
1605 if stats:
1610 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1606 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1611 % (len(stats), totaladds, totalremoves))
1607 % (len(stats), totaladds, totalremoves))
1612
1608
1613 return ''.join(output)
1609 return ''.join(output)
1614
1610
1615 def diffstatui(*args, **kw):
1611 def diffstatui(*args, **kw):
1616 '''like diffstat(), but yields 2-tuples of (output, label) for
1612 '''like diffstat(), but yields 2-tuples of (output, label) for
1617 ui.write()
1613 ui.write()
1618 '''
1614 '''
1619
1615
1620 for line in diffstat(*args, **kw).splitlines():
1616 for line in diffstat(*args, **kw).splitlines():
1621 if line and line[-1] in '+-':
1617 if line and line[-1] in '+-':
1622 name, graph = line.rsplit(' ', 1)
1618 name, graph = line.rsplit(' ', 1)
1623 yield (name + ' ', '')
1619 yield (name + ' ', '')
1624 m = re.search(r'\++', graph)
1620 m = re.search(r'\++', graph)
1625 if m:
1621 if m:
1626 yield (m.group(0), 'diffstat.inserted')
1622 yield (m.group(0), 'diffstat.inserted')
1627 m = re.search(r'-+', graph)
1623 m = re.search(r'-+', graph)
1628 if m:
1624 if m:
1629 yield (m.group(0), 'diffstat.deleted')
1625 yield (m.group(0), 'diffstat.deleted')
1630 else:
1626 else:
1631 yield (line, '')
1627 yield (line, '')
1632 yield ('\n', '')
1628 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now