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